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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

Reach for it whenever you don't want to explain the whole job again: an agent hits its usage limit, you switch tools mid-task, you pick up older work, or you want a clean record of what happened.

Works with **Amp**, **Claude Code**, **Codex**, **Copilot CLI**, **Cursor**, **Cline**, **Kimi**, **Antigravity**, **OpenCode**, **Pi Agent**, **ZCode**, and **DeepSeek Harness**.
Works with **Amp**, **Claude Code**, **Codex**, **Copilot CLI**, **Cursor**, **Cline**, **Kimi**, **Antigravity**, **OpenCode**, **Pi Agent**, **ZCode**, **Grok Build**, and **DeepSeek Harness**.

<div align="center">

Expand Down Expand Up @@ -56,7 +56,7 @@ herdr plugin install wilbeibi/herdr-catchup

## Usage

Agents: amp · `claude` · `codex` · `copilot` · `cursor` · `cline` · `kimi` · `agy` (Antigravity) · `opencode` · `pi-agent` · `zcode` · `deepseek` (dsh)
Agents: amp · `claude` · `codex` · `copilot` · `cursor` · `cline` · `kimi` · `agy` (Antigravity) · `opencode` · `pi-agent` · `zcode` · `deepseek` (dsh) · `grok`

Omit `<agent>` and catchup uses whichever agent has the newest session in this directory. Inside a live session, that's usually the session you're in.

Expand Down
4 changes: 2 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: catchup
description: Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.
description: Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.
---

# catchup
Expand All @@ -27,7 +27,7 @@ catchup fork <agent> # native resume, full state
catchup fork <agent> --into <other> # seed a different agent with the transcript
```

Agents: `amp`, `codex`, `claude`, `agy` (Antigravity), `cline`, `copilot`, `cursor`, `deepseek` (dsh), `kimi`, `opencode`, `pi-agent`, `zcode`.
Agents: `amp`, `codex`, `claude`, `agy` (Antigravity), `cline`, `copilot`, `cursor`, `deepseek` (dsh), `grok`, `kimi`, `opencode`, `pi-agent`, `zcode`.

## Before loading a transcript

Expand Down
1 change: 1 addition & 0 deletions internal/cli/agentlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func TestProseNamesEveryAgent(t *testing.T) {
session.ProviderCopilot: "Copilot CLI",
session.ProviderCursor: "Cursor",
session.ProviderDeepSeek: "DeepSeek Harness",
session.ProviderGrok: "Grok Build",
session.ProviderKimi: "Kimi",
session.ProviderOpenCode: "OpenCode",
session.ProviderPiAgent: "Pi Agent",
Expand Down
16 changes: 15 additions & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/wilbeibi/catchup/internal/copilot"
"github.com/wilbeibi/catchup/internal/cursor"
"github.com/wilbeibi/catchup/internal/deepseek"
"github.com/wilbeibi/catchup/internal/grok"
"github.com/wilbeibi/catchup/internal/kimi"
"github.com/wilbeibi/catchup/internal/opencode"
"github.com/wilbeibi/catchup/internal/piagent"
Expand All @@ -44,7 +45,7 @@ const helpText = `Usage: catchup [agent[/<rank>]] [flags] read a past ses
catchup install-skill [agent]

Agents: amp, codex, claude, agy (Antigravity), cline, copilot, cursor, deepseek (dsh),
kimi, opencode, pi-agent, zcode
grok, kimi, opencode, pi-agent, zcode
Omit the agent to use whichever has the newest session here. Bare ` + "`catchup`" + `
prints that session in full, as Markdown. The flags refine three things:
which session, how much of it, and as what.
Expand Down Expand Up @@ -385,6 +386,8 @@ func selectProvider(name string) (session.Provider, error) {
return cursor.New(), nil
case session.ProviderDeepSeek:
return deepseek.New(), nil
case session.ProviderGrok:
return grok.New(), nil
case session.ProviderKimi:
return kimi.New(), nil
case session.ProviderOpenCode:
Expand Down Expand Up @@ -935,6 +938,10 @@ func intoCommand(target, prompt, model string) (string, []string, error) {
// -i starts interactive and auto-executes the prompt; a bare -p is
// non-interactive and exits when the answer lands.
return "copilot", append(modelArgs("--model", model), "-i", prompt), nil
case session.ProviderGrok:
// The TUI takes its opening prompt as a positional argument (a bare -p
// runs headless and exits); options must precede it.
return "grok", append(modelArgs("-m", model), prompt), nil
case session.ProviderKimi:
// Kimi rejects positional arguments and its -p flag is
// non-interactive print mode, so there is no way to start an
Expand Down Expand Up @@ -1035,6 +1042,13 @@ func forkCommand(src session.Source, model string) (string, []string, error) {
// Copilot has no fork; --resume is its native resume, and it takes
// the id inline (a bare --resume opens the session picker).
return "copilot", append([]string{"--resume=" + src.Ref.SessionID}, modelArgs("--model", model)...), nil
case session.ProviderGrok:
if src.Ref.SessionID == "" {
return "", nil, fmt.Errorf("fork grok: missing session id")
}
// --fork-session branches into a new id instead of appending to the
// original, which is what catchup's native fork means.
return "grok", append([]string{"--resume", src.Ref.SessionID, "--fork-session"}, modelArgs("-m", model)...), nil
case session.ProviderCursor:
if src.Ref.SessionID == "" {
return "", nil, fmt.Errorf("fork cursor: missing session id")
Expand Down
43 changes: 42 additions & 1 deletion internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,43 @@ func piAgentRoot(t *testing.T) session.Roots {
return session.Roots{PiAgent: root}
}

// grokRoot writes one Grok session under a percent-encoded cwd group and
// returns a Roots pointing at it. summary.json is the index entry; the
// transcript is chat_history.jsonl.
func grokRoot(t *testing.T) session.Roots {
t.Helper()
root := t.TempDir()
dir := filepath.Join(root, "sessions", "%2Fhome%2Fu%2Fsrc%2Fproj", "gr-1")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
summary := `{"info":{"id":"gr-1","cwd":"/home/u/src/proj"},"generated_title":"grok session","last_active_at":"2026-07-18T15:20:00Z"}`
chat, err := os.ReadFile("../grok/testdata/chat_history.jsonl")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "summary.json"), []byte(summary), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "chat_history.jsonl"), []byte(chat), 0o644); err != nil {
t.Fatal(err)
}
return session.Roots{Grok: root}
}

func TestRunReadsGrokSession(t *testing.T) {
roots := grokRoot(t)
out := run(t, roots, "grok", "--id", "gr-1", "--since-compact")
for _, want := range []string{"agent: grok", "session: gr-1", "title: grok session", "support grok", "the compaction summary", "I will read the log."} {
if !strings.Contains(out, want) {
t.Errorf("grok read missing %q:\n%s", want, out)
}
}
if strings.Contains(out, "injected context") {
t.Error("--since-compact kept an obsolete compaction marker")
}
}

func runWithCwd(t *testing.T, roots session.Roots, cwd string, args ...string) string {
t.Helper()
var out, errOut bytes.Buffer
Expand Down Expand Up @@ -732,6 +769,8 @@ func TestForkCommand(t *testing.T) {
// which would swallow a separated id as an unrelated argument.
{"copilot", session.Source{Ref: session.Ref{Provider: session.ProviderCopilot, SessionID: "gh1"}}, "", "copilot --resume=gh1"},
{"copilot with model", session.Source{Ref: session.Ref{Provider: session.ProviderCopilot, SessionID: "gh1"}}, "gpt-5.4", "copilot --resume=gh1 --model gpt-5.4"},
{"grok", session.Source{Ref: session.Ref{Provider: session.ProviderGrok, SessionID: "gr1"}}, "", "grok --resume gr1 --fork-session"},
{"grok with model", session.Source{Ref: session.Ref{Provider: session.ProviderGrok, SessionID: "gr1"}}, "grok-4.7", "grok --resume gr1 --fork-session -m grok-4.7"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -811,9 +850,10 @@ func runBoth(t *testing.T, roots session.Roots, cwd string, args ...string) (str
func TestRunListAcrossAgents(t *testing.T) {
roots := codexRoot(t)
roots.Claude = claudeRoot(t).Claude
roots.Grok = grokRoot(t).Grok

out, _ := runBoth(t, roots, "", "--list")
for _, want := range []string{"codex/1", "claude/1", "claude/2"} {
for _, want := range []string{"codex/1", "claude/1", "claude/2", "grok/1"} {
if !strings.Contains(out, want) {
t.Errorf("cross-agent listing missing %q:\n%s", want, out)
}
Expand Down Expand Up @@ -1004,6 +1044,7 @@ func TestIntoCommandModelPlacement(t *testing.T) {
{session.ProviderAgy, "--model M -i PROMPT"},
{session.ProviderOpenCode, "--model M --prompt PROMPT"},
{session.ProviderCopilot, "--model M -i PROMPT"},
{session.ProviderGrok, "-m M PROMPT"},
}
for _, tt := range tests {
t.Run(tt.target, func(t *testing.T) {
Expand Down
Loading