diff --git a/.github/workflows/windows-smoke.yml b/.github/workflows/windows-smoke.yml index c7f012c..7b48643 100644 --- a/.github/workflows/windows-smoke.yml +++ b/.github/workflows/windows-smoke.yml @@ -69,8 +69,12 @@ jobs: $logPath = "$env:GITHUB_WORKSPACE\cli.log" $errPath = "$env:GITHUB_WORKSPACE\cli.err.log" + # --no-agent is required as of 0.2.1: bare `agentmind-cli` now + # launches Claude Code, which would fail immediately on a + # runner with no stdin/no claude binary. We're testing the + # static-asset / capture-API endpoints here, not the launcher. $proc = Start-Process node ` - -ArgumentList 'bin/cli.js','--port','8189','--no-open' ` + -ArgumentList 'bin/cli.js','--port','8189','--no-agent','--no-open' ` -PassThru ` -NoNewWindow ` -RedirectStandardOutput $logPath ` diff --git a/README.md b/README.md index df22f51..a022bbb 100644 --- a/README.md +++ b/README.md @@ -51,16 +51,16 @@ npm install -g agentmind-cli # Launch your agent through AgentMind — one command, zero config, # no API keys required. -agentmind-cli claude # or: agentmind-cli claude "fix the build" +agentmind-cli # defaults to Claude Code agentmind-cli codex # or: agentmind-cli codex exec "ship the PR" ``` -That's it. AgentMind boots the dashboard, opens it in your browser, -injects the right env / config so your agent talks to the proxy, and -hands the terminal off to the agent's TUI. Every prompt you type shows -up in the dashboard as a new project — live, while the agent is still -streaming. When the agent exits the dashboard keeps running so you can -browse the captured trace. Hit Ctrl+C to stop AgentMind. +That's it. AgentMind boots the dashboard, injects the right env / config +so your agent talks to the proxy, and hands the terminal off to the +agent's TUI. Every prompt you type shows up in the dashboard as a new +project — live, while the agent is still streaming. When the agent +exits the dashboard keeps running so you can browse the captured trace. +Hit Ctrl+C to stop AgentMind. > Both agents work with their **existing logins** — `claude login` for > Claude Code, `codex login` for Codex CLI. No API key wrangling. The @@ -73,38 +73,41 @@ browse the captured trace. Hit Ctrl+C to stop AgentMind. Prefer `npx`? ```bash +npx agentmind-cli # claude by default npx agentmind-cli codex ``` ### Just want the dashboard? ```bash -agentmind-cli # dashboard on http://127.0.0.1:8088 -agentmind-cli --port 9090 # custom port -agentmind-cli --no-open # don't auto-open the browser +agentmind-cli --no-agent # dashboard on http://127.0.0.1:8088, no agent +agentmind-cli --no-agent --port 9090 # custom port +agentmind-cli --no-agent --no-open # don't auto-open the browser ``` Then start your agent yourself — see [Manual setup](#manual-setup) below. **Supported agents** (v0.2): -| Agent | Upstream | Proxy endpoint we capture | Launcher | -| ---------- | ----------------------- | ------------------------- | ------------------ | -| Claude Code| `api.anthropic.com` | `POST /v1/messages` | `agentmind-cli claude` | -| Codex CLI | `api.openai.com` | `POST /v1/responses` | `agentmind-cli codex` | +| Agent | Upstream | Proxy endpoint we capture | Launcher | +| ---------- | ----------------------- | ------------------------- | ----------------------------------- | +| Claude Code| `api.anthropic.com` | `POST /v1/messages` | `agentmind-cli` (or `… claude`) | +| Codex CLI | `api.openai.com` | `POST /v1/responses` | `agentmind-cli codex` | -Both agents land in the same cwd-keyed project (so if you switch between -them in one directory, you see both threads against the same workspace). -The sidebar tags non-default agents with a small `codex` chip so a mixed -inbox stays scannable. +Projects are keyed by `(cwd, agent)` — running both Claude and Codex in +the same directory produces two distinct projects so each one's +conversation chain stays coherent. The sidebar tags every project with +a small agent chip (Claude = peach, Codex = lavender) so a mixed inbox +stays scannable at a glance. Flags (must come BEFORE the subcommand): -| Flag | Default | Notes | -| ---------------- | ------------- | ------------------------------------------ | -| `--port ` | `8088` | Listen port (and the URL we hand the agent) | -| `--data ` | `~/.agentmind` | Where the JSONL projects live | -| `--no-open` | _off_ | Skip the auto-open browser step | +| Flag | Default | Notes | +| ---------------- | ------------- | ------------------------------------------------------ | +| `--port ` | `8088` | Listen port (and the URL we hand the agent) | +| `--data ` | `~/.agentmind` | Where the JSONL projects live | +| `--no-agent` | _off_ | Skip launching an agent — dashboard only | +| `--no-open` | _off_ | Skip the auto-open browser step (`--no-agent` only) | Run `agentmind-cli --help` for the full reference. @@ -145,12 +148,13 @@ All capture is local-first JSONL — never leaves your machine. ``` ~/.agentmind/ └── projects/ - └── .jsonl # projectId = sha256(cwd).slice(0,16) + └── .jsonl # projectId = sha256(cwd, agent).slice(0,16) ``` -One file per cwd — every `claude` run in the same directory, across -days and proxy restarts, appends to the same file. Each line is one -JSON record: +One file per `(cwd, agent)` pair — every `claude` run in the same +directory appends to the Claude file for that cwd, every `codex` run +to the Codex file. Pre-0.2.2 single-cwd files are migrated to the new +scheme on first boot. Each line is one JSON record: | `type` | When written | Purpose | | ------------- | ----------------------------------------- | -------------------------------- | @@ -188,8 +192,8 @@ Adding a third protocol is one `ProtocolAdapter` away — see ## Manual setup -You don't have to use the launchers — `agentmind-cli` (no subcommand) -just runs the dashboard, and you can point any agent at it yourself. +You don't have to use the launchers — `agentmind-cli --no-agent` +runs the dashboard alone, and you can point any agent at it yourself. ### Claude Code diff --git a/bin/cli.js b/bin/cli.js index 294a59e..1a7ca08 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -12,26 +12,36 @@ // a dist/ — i.e. `pnpm dev` was the developer's intent) // Spawns `vite dev` so HMR/typecheck work while iterating. // -// And two invocation shapes: +// And three invocation shapes: // -// a. Dashboard-only: `agentmind-cli [flags...]` -// Boots the proxy and opens the browser. User wires their agent -// manually (or already has it pointed at us). +// a. Default: `agentmind-cli [flags...] [-- agent-args...]` +// Equivalent to `agentmind-cli claude`. Claude Code is the +// assumed agent because it's the one most users came here for — +// AgentMind is a Claude-first product that also happens to +// capture Codex. Any positional args after `--` are forwarded to +// claude verbatim. // -// b. Launcher: `agentmind-cli {claude|codex} [agent-args...]` -// Boots the proxy AND spawns the named agent in the foreground -// with the right env/config overrides so it talks to us. The -// dashboard stays running after the agent exits so the captured -// trace stays browsable until the user hits Ctrl+C. +// b. Explicit launcher: +// `agentmind-cli {claude|codex} [agent-args...]` +// Same flow, but with the agent named on the command line. +// Pick this when you want Codex (default isn't codex) or when +// you want the choice to be obvious to a reader. +// +// c. Dashboard-only: `agentmind-cli --no-agent [flags...]` +// Boots the proxy + UI without launching an agent. Useful when +// you want to browse the captured trace later (your previous +// session already exited) or when you'd rather wire the agent +// up yourself. // // AgentMind flags must come BEFORE the subcommand. Anything after the // subcommand (including `--`-prefixed args) is forwarded verbatim to // the agent. Examples: // -// agentmind-cli # dashboard only -// agentmind-cli --port 9090 # dashboard on custom port -// agentmind-cli codex # dashboard + codex +// agentmind-cli # = agentmind-cli claude +// agentmind-cli --port 9090 # claude on custom port +// agentmind-cli codex # launch codex instead // agentmind-cli codex "fix the build" # passes the prompt through +// agentmind-cli --no-agent # dashboard only // agentmind-cli --port 9090 claude exec --task "ship" import { spawn } from 'node:child_process' @@ -85,6 +95,7 @@ function getFlagValue(args, name, fallback) { const wantsHelp = getFlag(preArgs, '--help') || getFlag(preArgs, '-h') const noOpen = getFlag(preArgs, '--no-open') +const noAgent = getFlag(preArgs, '--no-agent') const forceDev = getFlag(preArgs, '--dev') const portArg = getFlagValue(preArgs, '--port', '8088') const dataArg = getFlagValue(preArgs, '--data', undefined) @@ -94,26 +105,32 @@ if (wantsHelp) { agentmind-cli — a live window into your agent's mind USAGE: - agentmind-cli start dashboard, open browser - agentmind-cli codex [args...] start dashboard + launch codex - agentmind-cli claude [args...] start dashboard + launch claude + agentmind-cli start dashboard + launch claude (default) + agentmind-cli claude [args...] same, explicit + agentmind-cli codex [args...] start dashboard + launch codex + agentmind-cli --no-agent dashboard only (browse past traces) OPTIONS (must come BEFORE the subcommand): --port Listen port (default 8088) --data Persist projects under /projects/.jsonl (default ~/.agentmind) - --no-open Skip auto-opening the browser (dashboard mode only) - --dev Force vite dev mode (developers only, dashboard only) + --no-open Skip auto-opening the browser (--no-agent only) + --no-agent Don't launch an agent; boot the dashboard alone + --dev Force vite dev mode (developers only, --no-agent only) -h, --help Show this message LAUNCHER NOTES: - * \`agentmind-cli codex\` and \`agentmind-cli claude\` inject the right - env / config so the agent talks to AgentMind. No manual setup. + * \`agentmind-cli\` defaults to Claude Code. Use \`codex\` explicitly + to launch Codex CLI instead. + * The launcher injects the right env / config so the agent talks to + AgentMind. No manual setup, no API key wrangling — it reuses your + existing \`claude login\` / \`codex login\`. * Anything after the subcommand is forwarded verbatim to the agent. * When the agent exits the dashboard stays running so the captured trace stays browsable. Press Ctrl+C to stop AgentMind. MANUAL SETUP (if you'd rather start your own agent): + agentmind-cli --no-agent # then in another terminal: Claude Code: ANTHROPIC_BASE_URL=http://127.0.0.1:8088 claude Codex CLI: see README.md "Manual setup" — needs a Codex provider block with requires_openai_auth=true and @@ -135,13 +152,18 @@ const distCli = resolve(projectRoot, 'dist', 'agentmind', 'cli.mjs') const useProd = !forceDev && existsSync(distCli) async function maybeOpenBrowser() { + // Only reachable from the --no-agent path. The default `agentmind-cli` + // invocation now launches Claude itself, so there's no "point your + // agent at us" step to print there. if (noOpen) return await delay(200) process.stdout.write( - `\n► Point your agent at agentmind:\n\n` + - ` Claude Code:\n` + - ` ANTHROPIC_BASE_URL=${url} claude\n\n` + - ` Codex CLI: agentmind-cli codex (one-liner — handles config)\n\n`, + `\n► Dashboard-only mode (no agent launched).\n\n` + + ` Browse past traces at: ${url}\n` + + ` Start a fresh capture session: agentmind-cli claude (or: codex)\n` + + ` Wire your agent manually:\n` + + ` Claude Code: ANTHROPIC_BASE_URL=${url} claude\n` + + ` Codex CLI: see README.md "Manual setup"\n\n`, ) try { const { default: open } = await import('open') @@ -332,24 +354,36 @@ async function runLauncher(agent, extraArgs) { } // ─── Dispatch ──────────────────────────────────────────────────────────── - -if (subcmd) { +// +// Three branches, picked in this order: +// 1. Launcher: subcommand was given OR no subcommand AND no --no-agent. +// The no-subcommand case fills in `claude` as the default agent — +// that's the 0.2.1 UX shift away from the older dashboard-only +// default. If the user really wants the dashboard alone they pass +// --no-agent (case 2). +// 2. Dashboard-only (prod): --no-agent or a non-launcher invocation +// that has dist/ available. +// 3. Dashboard-only (dev): same as (2) but no dist/ → spawn vite. + +const resolvedSubcmd = subcmd ?? (noAgent ? null : 'claude') + +if (resolvedSubcmd) { if (!useProd) { process.stderr.write( - `agentmind-cli: the \`${subcmd}\` launcher requires a built dist/.\n` + + `agentmind-cli: the \`${resolvedSubcmd}\` launcher requires a built dist/.\n` + `If you're hacking on agentmind, run \`pnpm build\` first or use\n` + - `\`agentmind-cli\` (no subcommand) for the vite dev dashboard.\n`, + `\`agentmind-cli --no-agent --dev\` for the vite dev dashboard.\n`, ) process.exit(1) } try { - await runLauncher(subcmd, agentArgs) + await runLauncher(resolvedSubcmd, agentArgs) } catch (err) { process.stderr.write(`agentmind-cli: launcher failed\n${err?.stack ?? err}\n`) process.exit(1) } } else if (useProd) { - // Production dashboard-only: in-process, no child node, no vite. + // Dashboard-only (prod): in-process, no child node, no vite. try { const mod = await import(pathToFileURL(distCli).toString()) await mod.start({ port, host }) diff --git a/package.json b/package.json index e2eb9cb..da691c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentmind-cli", - "version": "0.2.0", + "version": "0.2.1", "description": "A live window into your agent's mind — watch every thought, tool call and reply your coding agent makes, in real time, locally.", "license": "MIT", "type": "module", diff --git a/scripts/smoke-codex.mjs b/scripts/smoke-codex.mjs index 67bf339..1e44f5b 100644 --- a/scripts/smoke-codex.mjs +++ b/scripts/smoke-codex.mjs @@ -275,9 +275,15 @@ log('chatgpt-route stub listening on', CHATGPT_UPSTREAM_PORT) } // 2. Boot agentmind-cli pointed at BOTH stubs. +// `--no-agent` is the 0.2.1 way to ask for dashboard-only mode. Without +// it the CLI would try to launch claude, which (a) we don't want here +// — these tests drive the HTTP proxy directly with stub bodies, not a +// real agent — and (b) would fail immediately on a CI runner that has +// no `claude` binary or no stdin attached. const cli = spawn(process.execPath, [resolve(repo, 'bin', 'cli.js'), '--port', String(PROXY_PORT), '--data', dataDir, + '--no-agent', '--no-open', ], { cwd: repo, @@ -640,6 +646,7 @@ await delay(200) const cli2 = spawn(process.execPath, [resolve(repo, 'bin', 'cli.js'), '--port', String(PROXY_PORT), '--data', dataDir, + '--no-agent', '--no-open', ], { cwd: repo, diff --git a/scripts/smoke-launcher.mjs b/scripts/smoke-launcher.mjs index 7ac566b..978ecc3 100644 --- a/scripts/smoke-launcher.mjs +++ b/scripts/smoke-launcher.mjs @@ -71,11 +71,15 @@ makeShim('claude') const cliEntry = join(repo, 'bin', 'cli.js') const PORT = 18293 -async function runOne(agent) { +async function runOne(agent, opts = {}) { if (existsSync(snapshotPath)) rmSync(snapshotPath) + // When `agent` is `null` we exercise the 0.2.1 default behavior: + // omit the subcommand entirely, expect the CLI to fill in `claude` + // on our behalf and pass no extra positional args through. + const subArgv = agent ? [agent, 'hello'] : [] const child = spawn( process.execPath, - [cliEntry, '--port', String(PORT), '--data', dataDir, '--no-open', agent, 'hello'], + [cliEntry, '--port', String(PORT), '--data', dataDir, '--no-open', ...subArgv], { cwd: repo, env: { @@ -149,6 +153,23 @@ if (claudeSnap.env.ANTHROPIC_BASE_URL !== `http://127.0.0.1:${PORT}`) { } log('claude OK — argv forwarded verbatim, ANTHROPIC_BASE_URL injected') +// 0.2.1: `agentmind-cli` with NO subcommand should resolve to the +// claude launcher (replaces the pre-0.2.1 dashboard-only default). +// We pass `null` to runOne to omit the subcommand and `hello` arg +// entirely, then assert the shim was invoked with the env Claude Code +// expects. +log('testing default (no subcommand) launcher...') +const defaultSnap = await runOne(null) +if (defaultSnap.argv.length !== 0) { + throw new Error(`default argv unexpected: ${JSON.stringify(defaultSnap.argv)}`) +} +if (defaultSnap.env.ANTHROPIC_BASE_URL !== `http://127.0.0.1:${PORT}`) { + throw new Error( + `default mode should launch claude, ANTHROPIC_BASE_URL got: ${defaultSnap.env.ANTHROPIC_BASE_URL}`, + ) +} +log('default OK — bare `agentmind-cli` launched claude shim') + rmSync(tmpBin, { recursive: true, force: true }) rmSync(dataDir, { recursive: true, force: true }) log('all passed')