feat(pi): native pi integration — extension, config pi, skills sync --agent pi
#96
Reference in New Issue
Block a user
Delete Branch "feat/pi-extension"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Native pi integration — no MCP client, no Claude
Adds
mcpctl config pi, a self-contained pi extension, andskills sync --agent pi.pi has no MCP client, so the extension talks JSON-RPC directly to mcplocal's
/projects/<name>/mcpendpoint and registers each project tool as a native pi tool.mcplocal's gate state is bound to
mcp-session-id, so the extension holds onepersistent session per project and re-lists tools after every call — that way
calling
begin_sessionself-ungates the rest with no push channel.Skills land in
~/.pi/agent/skills/with their own sync state(
~/.mcpctl/skills-state-pi.json). Nothing touches~/.claude/.Review fixes (3rd commit)
src/pi-ext/was covered by no tsconfig and no eslint config. It ships as source— embedded into the CLI, then written into
~/.pi/agent/extensions/— so the CLIbuild never compiled it and nothing checked it against pi's API. Pointing
tscatthe published
@earendil-works/pi-coding-agenttypes found the command surface inert:/mcpctldid nothing.ctx.ui.selecttakesstring[]and returns the chosenstring; it was called with
{value,label}objects, so the menu rendered five[object Object]rows and no branch ever matched.~/.claude/skills— in an integration whose stated goalis not depending on
~/.claude— because it omitted--agent pi. It also ranexecSyncwithstdio: "inherit"(raw output over pi's TUI) and interpolated theproject name into a shell string. Now
execFilewith captured output.Promise<void>handler (silently dropped).content[].typetypedstringinstead of the"text"literal;callToolassertedPromise<unknown>.still reported active, so its calls were forwarded to the first tool.
registerWithPirewrote settings.json on no-op runs, destroying//comments(parsing strips them).
Guards, so this can't recur
src/pi-ext/tsconfig.jsonchecks against the real published pi types (devdependency, not a shim — a shim drifting from the API is the exact failure guarded
against). Wired into
pnpm typecheck.src/pi-ext/*.tslike every other source file.regenerating silently shipped old code.
Also: the branch had added
config piwithout regenerating shell completions (thecommitted-completions test was failing), and the doc advertised
mcpctl pi sync-skills,which does not exist.
Verification
572 tests pass, typecheck clean, RPM/DEB built. Smoke-tested
config pi --pi-diragainst an isolated pi home: pre-existing settings preserved, installed extension is
the fixed source, re-run is idempotent.
Notes for the reviewer
node_modules(existing tree is 528M). Dropping itmeans going back to shipping an untypechecked user-facing surface.
select(356 here) — usable API,unusable UX. Worth filtering before anyone leans on it.
mcpctl_pat_token through to the extension. Only needed againstan authenticated
mcplocal serve; the local daemon does not authenticate/projects/*. Documented rather than built.The pi extension shipped in `src/pi-ext/` was covered by no tsconfig and no eslint config, so nothing ever checked it against pi's API. Pointing tsc at the published @earendil-works/pi-coding-agent types found the command surface to be inert. Fixes: - `/mcpctl` did nothing. `ctx.ui.select` takes `string[]` and returns the chosen string; it was called with `{value,label}` objects, so the menu rendered five `[object Object]` rows and `choice === "status"` never matched any branch. Labels are now plain strings mapped back to actions. - The headless branch returned a status string from a handler typed `Promise<void>`; pi drops it. Reports via notify instead. - "Sync skills" omitted `--agent pi`, writing into ~/.claude/skills — in an integration whose stated purpose is to not depend on ~/.claude — and said so in its own success message. It also ran execSync with `stdio: "inherit"`, painting raw output over pi's TUI, and interpolated the project name into a shell string. Now execFile with `--agent pi` and captured output. - Tool results typed `content[].type` as `string`; pi's AgentToolResult wants the `"text"` literal. - `callTool` asserted `Promise<unknown>` to `ToolCallResult`. - Sanitising MCP tool names to `[a-z0-9_]` can collide (`docs.search` vs `docs-search`). The colliding tool was silently never registered but still reported active, so its calls were forwarded to the first tool. Names are now disambiguated and tracked with the MCP tool they forward to. - `registerWithPi` rewrote settings.json even when nothing changed. Since parsing strips `//` comments, a no-op run destroyed them. Guards, so this class of bug can't return: - `src/pi-ext/tsconfig.json` checks the extension against the real published pi types (dev dependency, not a shim — a shim drifting from the published API is the exact failure being guarded). Wired into `pnpm typecheck`. - eslint now covers `src/pi-ext/*.ts` like every other source file. - A test fails if the embedded copy in `config/pi-extension.ts` is stale; editing the sources without regenerating silently shipped old code. Also: the branch added `config pi` without regenerating shell completions (the committed-completions test was failing), and the doc advertised `mcpctl pi sync-skills`, which does not exist. Both corrected, plus a note on the session-token vs `mcpctl_pat_` bearer difference that would bite against an authenticated `mcplocal serve`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjBmcpd keeps `name` and `description` as columns, and a skill's `content` is often just the body — `propose-learnings` starts straight at its `#` heading. The sync wrote `full.content` verbatim, so the SKILL.md landed with no YAML frontmatter and the host rejected it. Observed live: [Skill warning] ~/.prime/agent/skills/propose-learnings/SKILL.md description is required Claude Code and pi require the same keys, so this affected every target; it only surfaced now because prime-agent prints the warning at startup. `ensureSkillFrontmatter` synthesises the block from the columns when it is absent, and fills in only a missing `name`/`description` when the author already supplied a header — an existing complete header is never rewritten. Values are emitted as JSON strings (valid YAML double-quoted scalars) because descriptions routinely contain `:` and `#`, which break a bare scalar. Also: `--force` now re-fetches skills whose server content is unchanged. Without it a skill already on disk could never be repaired by a client-side fix — the content hash still matched, so it was skipped forever, which is exactly what happened on the first attempt to repair the file above. Three existing assertions compared SKILL.md to the raw server content; they now assert the body survives rather than exact bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB341 of 356 projects on the shared mcpd were smoke-test leftovers, plus 11 never-expiring `smoke-*` mcptokens sitting on the real `mcpctl-development` and `sre` projects. Enough to make the project picker unusable. Two causes, both silent: - `delete project X --force` — `--force` is valid on `create project` but NOT on `delete`, so every cleanup call exited non-zero and deleted nothing. project-llm-ref's afterAll looked correct and had never worked, which is why there were 84 each of smoke-proj-{ok,orphan,none}-*. - mcptoken.smoke had no afterAll at all; its cleanup was an `it()` at the end of the file, so it was skipped whenever an earlier assertion failed. Cleanup now lives in `afterAll` (runs on failure too) and is no longer gated on the health probe: the project is created through mcpd, which can be up when the gateway probe is not, and deleting a project that was never created is a no-op. Adds `pnpm smoke:clean` for the case afterAll cannot cover — a killed process (CI timeout, Ctrl-C, OOM). Dry-run by default, `--yes` to apply. Only touches `smoke-*` names and protects the shared `smoke-data` / `smoke-aws-docs` fixture, which five suites depend on and which must survive a concurrent run. Verified end to end: created a throwaway smoke project, confirmed the dry run lists without deleting, then `--yes` removed it and left all 15 real projects untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjBView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.