diff --git a/src/agents/definitions/pi.ts b/src/agents/definitions/pi.ts new file mode 100644 index 0000000..94ed140 --- /dev/null +++ b/src/agents/definitions/pi.ts @@ -0,0 +1,30 @@ +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", + 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 httpServer(s); + const env = envRecord(s.env, (k) => `\${${k}}`); + return [s.name, { command: s.command, args: s.args ?? [], ...(env && { 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..10e901a 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,43 @@ 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 natively", () => { + const [name, config] = agent.serializeServer(HTTP_SERVER); + expect(name).toBe("remote-api"); + expect(config).toEqual({ + url: "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]),