refactor(prime-agent): extract the /mcpctl switcher to typechecked source; pi --dry-run; docs
Some checks failed
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m24s
CI/CD / typecheck (pull_request) Successful in 3m20s
CI/CD / smoke (pull_request) Failing after 2m1s
CI/CD / build (pull_request) Successful in 2m37s
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m24s
CI/CD / typecheck (pull_request) Successful in 3m20s
CI/CD / smoke (pull_request) Failing after 2m1s
CI/CD / build (pull_request) Successful in 2m37s
CI/CD / publish (pull_request) Has been skipped
The prime-agent switcher existed only as a 275-line string literal inside
prime-agent-extension.ts, so nothing typechecked or linted it — the exact gap
that let a wrong ctx.ui.select() option shape ship in the pi extension. It now
lives at src/prime-agent-ext/mcpctl-switch.ts with a generator, a tsconfig
checking it against the real @earendil-works/pi-coding-agent types, eslint
coverage and an embed-freshness test, matching pi and opencode.
The extraction was verified byte-identical before any edit, so the behaviour
shipped today is exactly what was captured. Linting it then found six problems
in code nothing had ever checked: object-truthiness null guards, a nullable
string conditional and a missing return type. All behaviour-preserving to fix,
but exactly the class of thing that ships silently when nothing is looking.
Also:
- `config pi` gains --dry-run, the last agent without it.
- The SessionStart hook installer now drops untagged duplicates of its own
exact command — rows left behind before the marker existed, or by a suite
that used to write into a real ~/.claude. Invisible in the UI; they just run
the sync twice per session. A hook the user wrote is never touched, even one
calling `mcpctl skills sync` with different flags.
- docs/claude-integration.md and docs/prime-agent-extension.md, the two
integrations that had no page.
prime-agent deliberately keeps its per-project MCP entry name rather than the
constant `mcpctl` claude and opencode now use: its switcher already unmounts the
previous project, so it never accumulates entries, and re-keying auth.json from
mcp:<project> to mcp:mcpctl would give up per-project token caching and needs a
migration. Documented as its own change rather than folded in here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
This commit is contained in:
101
docs/prime-agent-extension.md
Normal file
101
docs/prime-agent-extension.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# mcpctl × prime-agent
|
||||
|
||||
## What `mcpctl config prime-agent --project X` wires up
|
||||
|
||||
| Piece | Where | Purpose |
|
||||
|-------|-------|---------|
|
||||
| Proxy MCP | `~/.prime/agent/settings.json` | `{ type: "http", url: "<gateway>/projects/X/mcp" }` |
|
||||
| Bearer credential | `~/.prime/agent/auth.json` | `mcp:X` → a minted mcpctl PAT |
|
||||
| Skills | `~/.prime/agent/skills/` | the project's `SKILL.md` bundles |
|
||||
| `/mcpctl` switcher | `~/.prime/agent/extensions/mcpctl-switch.ts` | switch projects, and the active-project indicator |
|
||||
|
||||
Unlike Claude Code (a stdio bridge, no token) and pi (native tools over
|
||||
JSON-RPC), prime-agent talks to the **HTTP gateway**, so the switch is really
|
||||
three things: a credential, a settings entry, and a reload.
|
||||
|
||||
## Credentials are the fragile part
|
||||
|
||||
`config prime-agent` mints an `mcptoken` per project, stores it under
|
||||
`mcp:<project>`, and retires the one it replaced. Three rules make that safe:
|
||||
|
||||
- **A key being present proves nothing.** A revoked token would short-circuit
|
||||
provisioning and leave prime-agent unable to reach the gateway while the
|
||||
command reported success. mcptokens are shown once, so the stored token's
|
||||
16-char `tokenPrefix` is compared against the project's *active* tokens rather
|
||||
than sending the secret.
|
||||
- **No credential means the switch fails.** Non-zero exit, `settings.json`
|
||||
untouched, so the previously active project keeps working instead of being
|
||||
replaced by a mount that 401s — and the `/mcpctl` switcher, which reads that
|
||||
exit code, reports failure rather than success over a project with no tools.
|
||||
- **Only the token we replaced is revoked.** Sweeping every `prime-agent` token
|
||||
for a project would kill the one another machine is using. Anything else that
|
||||
looks orphaned is reported, not deleted.
|
||||
|
||||
This plumbing is shared with `config opencode`, parameterised by agent rather
|
||||
than copied.
|
||||
|
||||
## The `/mcpctl` switcher
|
||||
|
||||
`pi.registerCommand('mcpctl', …)` opens a picker, shells out to
|
||||
`mcpctl config prime-agent --project X --skip-extension --skip-marker`, then
|
||||
calls `ctx.reload()` — which re-reads `settings.json` and `auth.json` and
|
||||
rebuilds the MCP map, so the switch lands without restarting the app.
|
||||
|
||||
`--skip-extension` stops it rewriting the very file it is running from;
|
||||
`--skip-marker` stops it re-scoping whatever repository prime-agent was started
|
||||
in, which Claude Code's own skills sync would then pick up.
|
||||
|
||||
Above 20 projects it asks for a filter first: prime-agent's selector is an
|
||||
arrow-key list with no search, and real installs run to hundreds of projects.
|
||||
(opencode's dialog filters as you type, so its switcher needs no such prompt.)
|
||||
|
||||
## The active-project indicator
|
||||
|
||||
Published with `ctx.ui.setStatus('mcpctl', …)`, which prime-agent renders in its
|
||||
tray line next to the model name.
|
||||
|
||||
Two quirks worth knowing:
|
||||
|
||||
- prime-agent emits `session_start` **only from `reload()`**, never at startup —
|
||||
so the indicator is also published on `turn_start`, the earliest moment with a
|
||||
real UI context bound.
|
||||
- `resetExtensionUI()` clears extension statuses *after* `session_start`, so the
|
||||
indicator set there is wiped before anyone sees it. It is re-published on a
|
||||
short retry schedule to land after that reset.
|
||||
|
||||
> The rendering itself only exists in prime-agent from
|
||||
> `prime-agent-extension-status.patch` (upstream PR pending). On an unpatched
|
||||
> build `setStatus` silently does nothing and no indicator appears.
|
||||
|
||||
## The extension is real source now
|
||||
|
||||
It used to exist **only** as a string literal inside
|
||||
`src/cli/src/config/prime-agent-extension.ts` — so nothing typechecked or linted
|
||||
it, which is precisely the gap that let a wrong `ctx.ui.select()` option shape
|
||||
ship in the pi extension.
|
||||
|
||||
It now lives at `src/prime-agent-ext/mcpctl-switch.ts`, checked against the real
|
||||
`@earendil-works/pi-coding-agent` types:
|
||||
|
||||
```bash
|
||||
pnpm typecheck:prime-agent-ext
|
||||
npx tsx scripts/generate-prime-agent-extension.ts # after editing it
|
||||
```
|
||||
|
||||
A test fails if the embed goes stale. Extracting it found six lint problems in
|
||||
code that had never been linted — all null-guard and return-type issues rather
|
||||
than live bugs, but exactly the class of thing that ships silently when nothing
|
||||
is looking.
|
||||
|
||||
## What it deliberately does *not* do
|
||||
|
||||
The MCP entry is still **named after the project**, not the constant `mcpctl`
|
||||
that `config claude` and `config opencode` now use. prime-agent's switcher
|
||||
already unmounts the previous project, so it never accumulates entries the way
|
||||
`config claude` did — the bug the constant name fixes does not exist here.
|
||||
|
||||
The remaining difference is tool-prefix stability: switching changes tool names,
|
||||
so the model can hold stale ones. Moving prime-agent to a constant name would
|
||||
also re-key `auth.json` from `mcp:<project>` to `mcp:mcpctl`, giving up
|
||||
per-project token caching and needing a migration. Worth doing, but as its own
|
||||
change rather than folded into a parity pass.
|
||||
Reference in New Issue
Block a user