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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- mcp-name: io.github.aytzey/showagent -->
<h1 align="center">showagent</h1>

<p align="center"><b>showagent</b> — every AI coding session on your machine, in one TUI.<br>
Expand Down Expand Up @@ -85,6 +86,7 @@ showagent resume latest # reopen the most recent session, any agent
showagent convert latest --to claude --dry-run
# preview exactly what a hand-off would carry/drop
showagent info latest # exact resume command + storage location
showagent mcp # serve session history to MCP-capable agents (stdio)
showagent update # install the latest GitHub release
showagent --help # full CLI help
```
Expand Down Expand Up @@ -145,6 +147,41 @@ Exit codes: `0` success, `1` error (including "no sessions found"), `2` usage.
When stdout is not a terminal, plain `showagent` prints the `list` table, so
pipes just work.

## Use it from inside your agent (MCP)

`showagent mcp` runs a stdio MCP server, so the agent you are talking to can
search every past coding session on your machine — from **any** agent — and
pull one in as context or convert it to continue right there. Ask Claude Code
"have I solved this rate-limit bug before?" and it can find the Codex session
where you did, read the transcript, and hand you the command to resume it —
or rewrite it as a native Claude Code session and keep going. Your session
history stops being per-tool memory and becomes shared memory.

```sh
# Claude Code
claude mcp add showagent -- showagent mcp

# Codex (~/.codex/config.toml)
[mcp_servers.showagent]
command = "showagent"
args = ["mcp"]
```

Tools:

| Tool | What it does |
|---|---|
| `list_sessions` | Search sessions across all agents — filter by provider, workspace substring, or free text over workspace + first/last user message (default 25, max 100 results) |
| `get_transcript` | Read a session's user/assistant turns; `max_turns` keeps the most recent N (default 50) so long sessions don't flood context |
| `branch_session` | Fork a full local copy of a session, same agent; returns the new id, file, and resume command |
| `convert_session` | Rewrite a session into another agent's native format; returns the new id, file, and resume command |
| `resume_command` | The exact shell command (and cwd) that resumes a session — returned as a string, **never executed** |

The MCP surface is deliberately non-destructive: there is **no delete tool**,
and the server never executes an agent CLI. Deleting sessions stays exclusive
to the TUI, where it takes two key presses with a human watching. Branch and
convert only ever write new files — originals are never modified.

## How it compares

Great tools exist for *running* agents in parallel — showagent is about the
Expand Down
25 changes: 24 additions & 1 deletion cmd/showagent/main.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"runtime/debug"
"strconv"
"strings"
"time"

"github.com/charmbracelet/x/term"

"github.com/aytzey/showagent/internal/mcpserver"
"github.com/aytzey/showagent/internal/session"
"github.com/aytzey/showagent/internal/tui"
)

// version is stamped by the release build via -ldflags "-X main.version=...".
var version = "dev"

const usageLine = "usage: showagent [list [--json] | resume <id|latest> [--yolo] | convert <id|latest> --to <provider> [--dry-run] | info <id|latest> | update | setup]"
const usageLine = "usage: showagent [list [--json] | resume <id|latest> [--yolo] | convert <id|latest> --to <provider> [--dry-run] | info <id|latest> | mcp | update | setup]"

func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
Expand Down Expand Up @@ -47,6 +50,8 @@ func run(args []string, stdout, stderr io.Writer) int {
return runConvert(args[1:], stdout, stderr)
case "info":
return runInfo(args[1:], stdout, stderr)
case "mcp":
return runMCP(args[1:], stderr)
case "update":
return runUpdate(args[1:], stdout, stderr)
case "setup":
Expand Down Expand Up @@ -318,6 +323,22 @@ func resolveSession(rows []session.Row, id string) (session.Row, error) {
return session.Row{}, fmt.Errorf("session %q not found; run 'showagent list' to see session ids", id)
}

// runMCP serves the session store to MCP clients over stdio until the client
// disconnects or the process is interrupted. It deliberately offers no delete
// tool and never executes an agent CLI; see internal/mcpserver.
func runMCP(args []string, stderr io.Writer) int {
if len(args) != 0 {
return usageError(stderr, fmt.Sprintf("mcp takes no arguments, got %q", args[0]))
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := mcpserver.Run(ctx, versionString()); err != nil {
_, _ = fmt.Fprintf(stderr, "showagent: %v\n", err)
return 1
}
return 0
}

func runSetup(stdout, stderr io.Writer) int {
results, err := session.EnsureCompoundEngineeringPlugin()
if err != nil {
Expand Down Expand Up @@ -412,6 +433,8 @@ Usage:
preview or write a native session for another agent
showagent info <id|latest> [--yolo]
print the exact resume command and storage location
showagent mcp serve session history to MCP clients over stdio
(search, transcripts, branch, convert; no delete)
showagent update install the latest GitHub release
showagent setup install the compound-engineering plugin for supported agents

Expand Down
12 changes: 11 additions & 1 deletion cmd/showagent/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TestHelpFlag(t *testing.T) {
if code != 0 {
t.Fatalf("%s exit = %d, want 0", flag, code)
}
for _, want := range []string{"Usage:", "list", "resume", "convert", "info", "update", "setup", "CODEX_HOME", "CLAUDE_HOME", "JCODE_HOME", "--yolo", "--json", "--dry-run"} {
for _, want := range []string{"Usage:", "list", "resume", "convert", "info", "mcp", "update", "setup", "CODEX_HOME", "CLAUDE_HOME", "JCODE_HOME", "--yolo", "--json", "--dry-run"} {
if !strings.Contains(stdout, want) {
t.Fatalf("%s output missing %q:\n%s", flag, want, stdout)
}
Expand Down Expand Up @@ -114,6 +114,16 @@ func TestUnknownArgumentExitsTwo(t *testing.T) {
}
}

func TestMCPRejectsArguments(t *testing.T) {
code, _, stderr := runCLI(t, "mcp", "--http")
if code != 2 {
t.Fatalf("exit = %d, want 2", code)
}
if !strings.Contains(stderr, "mcp takes no arguments") {
t.Fatalf("stderr missing mcp usage hint:\n%s", stderr)
}
}

func TestUnknownListFlagExitsTwo(t *testing.T) {
code, _, stderr := runCLI(t, "list", "--nope")
if code != 2 {
Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
charm.land/bubbletea/v2 v2.0.8
charm.land/lipgloss/v2 v2.0.5
github.com/charmbracelet/x/term v0.2.2
github.com/modelcontextprotocol/go-sdk v1.6.1
golang.org/x/sys v0.47.0
)

Expand All @@ -19,11 +20,16 @@ require (
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.3 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.21.0 // indirect
)
18 changes: 18 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,41 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.3 h1:juByESSS32nVD81vr6tHmKmA/8zde7gE+x5CLxrzXPU=
github.com/sahilm/fuzzy v0.1.3/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
Loading