From d4ee47b544c3002572f1870a4f461ee195d9251c Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Wed, 18 Feb 2026 19:59:34 -0500 Subject: [PATCH 1/2] feat: add Pi coding agent support Pi (https://github.com/mariozechner/pi) discovers skills from .pi/skills/ (project) and ~/.pi/agent/skills/ (user). Like Claude Code and Cursor, it needs symlinks from its config directory to .agents/skills/. This adds Pi as a supported agent with: - Skills symlinks (.pi/skills -> .agents/skills) - MCP config at .pi/mcp.json (used by pi-mcp-adapter) - User-scope MCP at ~/.pi/agent/mcp.json - Tests for serializers, paths, and skill discovery Co-Authored-By: Claude Opus 4.6 Generated-By: pi 0.52.12 --- src/agents/definitions/pi.ts | 48 ++++++++++++++++++++++++++++++++++++ src/agents/paths.test.ts | 12 +++++++++ src/agents/paths.ts | 2 ++ src/agents/registry.test.ts | 46 ++++++++++++++++++++++++++++++++-- src/agents/registry.ts | 3 ++- 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 src/agents/definitions/pi.ts diff --git a/src/agents/definitions/pi.ts b/src/agents/definitions/pi.ts new file mode 100644 index 0000000..0086bb9 --- /dev/null +++ b/src/agents/definitions/pi.ts @@ -0,0 +1,48 @@ +import { join } from "node:path"; +import { homedir } from "node:os"; +import type { AgentDefinition } from "../types.js"; +import { UnsupportedFeature } from "../errors.js"; + +const pi: AgentDefinition = { + id: "pi", + displayName: "Pi", + configDir: ".pi", + skillsParentDir: ".pi", + userSkillsParentDirs: [join(homedir(), ".pi", "agent")], + mcp: { + filePath: ".pi/mcp.json", + rootKey: "mcpServers", + format: "json", + shared: false, + }, + serializeServer(s) { + if (s.url) { + return [ + s.name, + { + command: "npx", + args: ["-y", "mcp-remote", s.url], + ...(s.headers && { headers: s.headers }), + }, + ]; + } + const env: Record = {}; + if (s.env) { + for (const key of s.env) env[key] = `\${${key}}`; + } + return [ + s.name, + { + command: s.command, + args: s.args ?? [], + ...(Object.keys(env).length > 0 && { env }), + }, + ]; + }, + hooks: undefined, + serializeHooks() { + throw new UnsupportedFeature("pi", "hooks"); + }, +}; + +export default pi; diff --git a/src/agents/paths.test.ts b/src/agents/paths.test.ts index fd432bf..ebffffe 100644 --- a/src/agents/paths.test.ts +++ b/src/agents/paths.test.ts @@ -38,6 +38,12 @@ describe("getUserMcpTarget", () => { expect(t.shared).toBe(true); }); + it("pi targets ~/.pi/agent/mcp.json (not shared)", () => { + const t = getUserMcpTarget("pi"); + expect(t.filePath).toBe(join(home, ".pi", "agent", "mcp.json")); + expect(t.shared).toBe(false); + }); + it("throws for unknown agent", () => { expect(() => getUserMcpTarget("emacs")).toThrow("Unknown agent"); }); @@ -77,4 +83,10 @@ describe("skill discovery paths", () => { expect(agent.skillsParentDir).toBeUndefined(); expect(agent.userSkillsParentDirs).toBeUndefined(); }); + + it("pi needs project and user symlinks", () => { + const agent = getAgent("pi")!; + expect(agent.skillsParentDir).toBe(".pi"); + expect(agent.userSkillsParentDirs).toEqual([join(home, ".pi", "agent")]); + }); }); diff --git a/src/agents/paths.ts b/src/agents/paths.ts index a69e48c..ffa17e7 100644 --- a/src/agents/paths.ts +++ b/src/agents/paths.ts @@ -25,6 +25,8 @@ export function getUserMcpTarget(agentId: string): UserMcpTarget { return { filePath: vscodeMcpPath(), shared: false }; case "opencode": return { filePath: join(home, ".config", "opencode", "opencode.json"), shared: true }; + case "pi": + return { filePath: join(home, ".pi", "agent", "mcp.json"), shared: false }; default: throw new Error(`Unknown agent for user-scope MCP: ${agentId}`); } diff --git a/src/agents/registry.test.ts b/src/agents/registry.test.ts index d237ce4..f06d14f 100644 --- a/src/agents/registry.test.ts +++ b/src/agents/registry.test.ts @@ -22,14 +22,15 @@ const STDIO_NO_ENV: McpDeclaration = { }; describe("allAgentIds", () => { - it("returns all 5 agents", () => { + it("returns all 6 agents", () => { const ids = allAgentIds(); expect(ids).toContain("claude"); expect(ids).toContain("cursor"); expect(ids).toContain("codex"); expect(ids).toContain("vscode"); expect(ids).toContain("opencode"); - expect(ids).toHaveLength(5); + expect(ids).toContain("pi"); + expect(ids).toHaveLength(6); }); }); @@ -174,3 +175,44 @@ describe("opencode serializer", () => { expect(agent.skillsParentDir).toBeUndefined(); }); }); + +describe("pi serializer", () => { + const agent = getAgent("pi")!; + + it("serializes stdio server", () => { + const [name, config] = agent.serializeServer(STDIO_SERVER); + expect(name).toBe("github"); + expect(config).toEqual({ + command: "npx", + args: ["-y", "@modelcontextprotocol/server-github"], + env: { GITHUB_TOKEN: "${GITHUB_TOKEN}" }, + }); + }); + + it("serializes http server via mcp-remote proxy", () => { + const [name, config] = agent.serializeServer(HTTP_SERVER); + expect(name).toBe("remote-api"); + expect(config).toEqual({ + command: "npx", + args: ["-y", "mcp-remote", "https://mcp.example.com/sse"], + headers: { Authorization: "Bearer tok" }, + }); + }); + + it("omits env when empty", () => { + const [, config] = agent.serializeServer(STDIO_NO_ENV); + expect(config).toEqual({ command: "mcp-server", args: [] }); + expect(config).not.toHaveProperty("env"); + }); + + it("needs project and user symlinks for skills", () => { + expect(agent.skillsParentDir).toBe(".pi"); + expect(agent.userSkillsParentDirs).toHaveLength(1); + expect(agent.userSkillsParentDirs![0]).toMatch(/\.pi[/\\]agent$/); + }); + + it("does not support hooks", () => { + expect(agent.hooks).toBeUndefined(); + expect(() => agent.serializeHooks([])).toThrow(); + }); +}); diff --git a/src/agents/registry.ts b/src/agents/registry.ts index 8bffe05..25cdee9 100644 --- a/src/agents/registry.ts +++ b/src/agents/registry.ts @@ -4,8 +4,9 @@ import cursor from "./definitions/cursor.js"; import codex from "./definitions/codex.js"; import vscode from "./definitions/vscode.js"; import opencode from "./definitions/opencode.js"; +import pi from "./definitions/pi.js"; -const ALL_AGENTS: AgentDefinition[] = [claude, cursor, codex, vscode, opencode]; +const ALL_AGENTS: AgentDefinition[] = [claude, cursor, codex, vscode, opencode, pi]; const AGENT_REGISTRY = new Map( ALL_AGENTS.map((a) => [a.id, a]), From e4626db037025f73e773a5b66f97e890793b91d5 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Wed, 18 Feb 2026 20:05:25 -0500 Subject: [PATCH 2/2] address review: use httpServer/envRecord helpers, fix HTTP serialization - Use httpServer() for HTTP servers (Pi handles URLs natively, no mcp-remote) - Use envRecord() for env var mapping (consistent with other agents) - Update test to match native HTTP serialization --- src/agents/definitions/pi.ts | 26 ++++---------------------- src/agents/registry.test.ts | 5 ++--- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/agents/definitions/pi.ts b/src/agents/definitions/pi.ts index 0086bb9..94ed140 100644 --- a/src/agents/definitions/pi.ts +++ b/src/agents/definitions/pi.ts @@ -2,6 +2,7 @@ import { join } from "node:path"; import { homedir } from "node:os"; import type { AgentDefinition } from "../types.js"; import { UnsupportedFeature } from "../errors.js"; +import { envRecord, httpServer } from "./helpers.js"; const pi: AgentDefinition = { id: "pi", @@ -16,28 +17,9 @@ const pi: AgentDefinition = { shared: false, }, serializeServer(s) { - if (s.url) { - return [ - s.name, - { - command: "npx", - args: ["-y", "mcp-remote", s.url], - ...(s.headers && { headers: s.headers }), - }, - ]; - } - const env: Record = {}; - if (s.env) { - for (const key of s.env) env[key] = `\${${key}}`; - } - return [ - s.name, - { - command: s.command, - args: s.args ?? [], - ...(Object.keys(env).length > 0 && { env }), - }, - ]; + if (s.url) return httpServer(s); + const env = envRecord(s.env, (k) => `\${${k}}`); + return [s.name, { command: s.command, args: s.args ?? [], ...(env && { env }) }]; }, hooks: undefined, serializeHooks() { diff --git a/src/agents/registry.test.ts b/src/agents/registry.test.ts index f06d14f..10e901a 100644 --- a/src/agents/registry.test.ts +++ b/src/agents/registry.test.ts @@ -189,12 +189,11 @@ describe("pi serializer", () => { }); }); - it("serializes http server via mcp-remote proxy", () => { + it("serializes http server natively", () => { const [name, config] = agent.serializeServer(HTTP_SERVER); expect(name).toBe("remote-api"); expect(config).toEqual({ - command: "npx", - args: ["-y", "mcp-remote", "https://mcp.example.com/sse"], + url: "https://mcp.example.com/sse", headers: { Authorization: "Bearer tok" }, }); });