Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/ccsession/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: ccsession
description: Recover, inspect, and hand off to local agent sessions with the ccsession CLI. Use when the user wants to find prior context, locate a historical Claude Code/OpenCode/Grok/Codex session, compare candidate sessions, preview a past conversation, or resume work that happened in another agent session.
description: Recover, inspect, and hand off to local agent sessions with the ccsession CLI. Use when the user wants to find prior context, locate a historical Claude Code/OpenCode/Grok/Codex/Pi session, compare candidate sessions, preview a past conversation, or resume work that happened in another agent session.
---

# ccsession
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
![ccsession demo](./docs/assets/ccsession_demo.gif)

`ccsession` lists local agent sessions (Claude Code by default, with optional
OpenCode, Grok, and Codex backends), lets you fuzzy-find across all of your
OpenCode, Grok, Codex, and Pi backends), lets you fuzzy-find across all of your
projects with a live preview pane, and resumes the one you pick in its original
working directory.

Expand Down Expand Up @@ -34,6 +34,7 @@ working directory.
| [`opencode`](https://opencode.ai) | listing & resuming OpenCode sessions (only with `--source=opencode`) |
| `grok` (Grok Build TUI) | listing & resuming Grok sessions (only with `--source=grok`) |
| `codex` (Codex CLI) | listing & resuming Codex sessions (only with `--source=codex`) |
| [`pi`](https://pi.dev) (pi coding agent) | listing & resuming Pi sessions (only with `--source=pi`) |

`ccsession` depends on newer `fzf` actions such as `transform`, `rebind`,
`unbind`, `disable-search`, and `change-nth`. The newest of those,
Expand Down Expand Up @@ -88,7 +89,7 @@ The formula lives in
[`sorafujitani/homebrew-tap`](https://github.com/sorafujitani/homebrew-tap)
and GoReleaser refreshes it on every tagged release. `fzf` is installed as a
dependency; the `claude` CLI must be installed separately. `opencode`, `grok`,
and `codex` are needed only with their matching `--source` backends — they back
`codex`, and `pi` are needed only with their matching `--source` backends — they back
optional features (unlike `fzf`, which is always required), so they are
intentionally left out of the formula's `depends_on`.

Expand All @@ -98,6 +99,7 @@ intentionally left out of the formula's `depends_on`.
ccsession # list -> fzf -> resume
ccsession --grok # use Grok sessions from ~/.grok/sessions
ccsession --codex # use Codex sessions from ~/.codex/sessions
ccsession --pi # use Pi sessions from ~/.pi/agent/sessions
ccsession list [--grep Q] [--regex] # emit TSV rows to stdout
ccsession list --json --grep Q --limit 5 # emit structured rows for agents
ccsession preview [--query Q] [--regex] <id> # render the preview pane (Q highlighted)
Expand Down Expand Up @@ -242,7 +244,8 @@ ccsession exits with an error instead of starting the picker.
## How it works

1. `ccsession list` reads the selected backend (`~/.claude/projects/*/` by
default, or `--source=opencode` / `--source=grok` / `--source=codex`) and prints one TSV row
default, or `--source=opencode` / `--source=grok` / `--source=codex` /
`--source=pi`) and prints one TSV row
per session (`id`, `locator`, `epoch`, relative time, cwd basename, label).
`ccsession list --json --limit N` emits the same candidates as a JSON array
for agent integrations.
Expand All @@ -257,9 +260,11 @@ ccsession exits with an error instead of starting the picker.
`cwd`, `chdir`s into it, and `execve`s the selected agent's resume command
so the resumed process fully replaces the picker.

Backend-specific homes can be overridden with `GROK_HOME` for Grok and
`CODEX_HOME` for Codex. Codex defaults to `~/.codex`, reading sessions from
its `sessions` subdirectory.
Backend-specific homes can be overridden with `GROK_HOME` for Grok,
`CODEX_HOME` for Codex, and `PI_CODING_AGENT_SESSION_DIR` for Pi. Codex
defaults to `~/.codex`, reading sessions from its `sessions` subdirectory; Pi
reads sessions from `~/.pi/agent/sessions`, and `PI_CODING_AGENT_SESSION_DIR`
points directly at that sessions directory.

## Development

Expand Down
16 changes: 15 additions & 1 deletion cmd/ccsession/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ USAGE:
non-flag argument).

GLOBAL FLAGS:
--source <s> session backend: claude (default) | all | opencode | grok | codex. Inherited
--source <s> session backend: claude (default) | all | opencode | grok | codex | pi. Inherited
by the picker's reload/preview/resume re-invocations via
CCSESSION_SOURCE.
--all shorthand for --source=all
--opencode shorthand for --source=opencode
--grok shorthand for --source=grok
--codex shorthand for --source=codex
--pi shorthand for --source=pi
--exclude-dir <s> hide sessions whose cwd contains <s> (case-insensitive).
Applied to every list call, including grep/dir/fuzzy
reloads, so the matching directories never appear in
Expand Down Expand Up @@ -216,6 +217,7 @@ type globalFlags struct {
opencode bool
grok bool
codex bool
pi bool
binds config.Keybindings
}

Expand Down Expand Up @@ -250,6 +252,12 @@ func applySource(gf globalFlags) error {
}
name = "codex"
}
if gf.pi {
if name != "" && name != "pi" {
return fmt.Errorf("--pi conflicts with --source=%s", name)
}
name = "pi"
}
if name == "" {
name = os.Getenv(source.EnvVar)
}
Expand Down Expand Up @@ -301,6 +309,12 @@ next:
i++
continue next
}
// --pi is sugar for --source=pi and takes no value.
if a == "--pi" {
gf.pi = true
i++
continue next
}
for name, p := range dst {
if a == name {
if i+1 >= len(args) {
Expand Down
12 changes: 12 additions & 0 deletions cmd/ccsession/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ func TestParseGlobalFlags(t *testing.T) {
wantGF: globalFlags{codex: true},
wantRest: []string{"list"},
},
{
name: "pi sugar takes no value",
args: []string{"--pi", "list"},
wantGF: globalFlags{pi: true},
wantRest: []string{"list"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -173,26 +179,32 @@ func TestApplySource(t *testing.T) {
{name: "opencode sugar", gf: globalFlags{opencode: true}, wantEnv: "opencode"},
{name: "grok sugar", gf: globalFlags{grok: true}, wantEnv: "grok"},
{name: "codex sugar", gf: globalFlags{codex: true}, wantEnv: "codex"},
{name: "pi sugar", gf: globalFlags{pi: true}, wantEnv: "pi"},
{name: "source flag", gf: globalFlags{source: "opencode"}, wantEnv: "opencode"},
{name: "source flag all", gf: globalFlags{source: "all"}, wantEnv: "all"},
{name: "source flag grok", gf: globalFlags{source: "grok"}, wantEnv: "grok"},
{name: "source flag codex", gf: globalFlags{source: "codex"}, wantEnv: "codex"},
{name: "source flag pi", gf: globalFlags{source: "pi"}, wantEnv: "pi"},
{name: "all sugar agrees with source", gf: globalFlags{all: true, source: "all"}, wantEnv: "all"},
{name: "sugar agrees with source", gf: globalFlags{opencode: true, source: "opencode"}, wantEnv: "opencode"},
{name: "codex sugar agrees with source", gf: globalFlags{codex: true, source: "codex"}, wantEnv: "codex"},
{name: "pi sugar agrees with source", gf: globalFlags{pi: true, source: "pi"}, wantEnv: "pi"},
{name: "all sugar contradicts source", gf: globalFlags{all: true, source: "claude"}, wantErr: true},
{name: "all sugar conflicts with backend sugar", gf: globalFlags{all: true, codex: true}, wantErr: true},
{name: "sugar contradicts source", gf: globalFlags{opencode: true, source: "claude"}, wantErr: true},
{name: "grok sugar contradicts source", gf: globalFlags{grok: true, source: "opencode"}, wantErr: true},
{name: "codex sugar contradicts source", gf: globalFlags{codex: true, source: "grok"}, wantErr: true},
{name: "pi sugar contradicts source", gf: globalFlags{pi: true, source: "codex"}, wantErr: true},
{name: "backend sugars conflict", gf: globalFlags{opencode: true, grok: true}, wantErr: true},
{name: "codex backend sugar conflicts", gf: globalFlags{grok: true, codex: true}, wantErr: true},
{name: "pi backend sugar conflicts", gf: globalFlags{codex: true, pi: true}, wantErr: true},
{name: "unknown source flag", gf: globalFlags{source: "bogus"}, wantErr: true},
{name: "inherited env is validated", env: "bogus", wantErr: true},
{name: "inherited valid env survives", env: "opencode", wantEnv: "opencode"},
{name: "inherited all env survives", env: "all", wantEnv: "all"},
{name: "inherited grok env survives", env: "grok", wantEnv: "grok"},
{name: "inherited codex env survives", env: "codex", wantEnv: "codex"},
{name: "inherited pi env survives", env: "pi", wantEnv: "pi"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
Loading
Loading