From 8b2930c1459c3f07a628e65da8e3fd4b1583a9bd Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 20 Mar 2026 19:36:17 +0100 Subject: [PATCH] feat(agents): Add Pi as a supported agent Add Pi (https://github.com/badlogic/pi-mono) to the agent registry. Pi reads .agents/skills/ natively and does not support MCP or hooks. Make McpConfigSpec optional on AgentDefinition so agents without MCP support are modeled honestly at the type level rather than using dummy specs with runtime throws. The mcp-writer loops skip agents with mcp: undefined via guard clauses. Co-Authored-By: Claude --- README.md | 3 +-- docs/public/llms.txt | 3 ++- specs/SPEC.md | 5 +++-- src/agents/definitions/pi.ts | 21 +++++++++++++++++++++ src/agents/mcp-writer.ts | 4 ++++ src/agents/paths.test.ts | 10 ++++++++++ src/agents/registry.test.ts | 33 +++++++++++++++++++++++++++++---- src/agents/registry.ts | 3 ++- src/agents/types.ts | 4 ++-- 9 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 src/agents/definitions/pi.ts diff --git a/README.md b/README.md index d658f439..28f922c3 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,7 @@ agents = ["claude", "cursor"] | `codex` | `.codex` | `.codex/config.toml` | -- | | `vscode` | `.vscode` | `.vscode/mcp.json` | `.claude/settings.json` | | `opencode` | `.claude` | `opencode.json` | -- | - -[Pi](https://github.com/badlogic/pi-mono) reads `.agents/skills/` natively and needs no configuration. +| `pi` | `.pi` | -- | -- | ## Documentation diff --git a/docs/public/llms.txt b/docs/public/llms.txt index cca0b9bd..55cc792b 100644 --- a/docs/public/llms.txt +++ b/docs/public/llms.txt @@ -126,7 +126,7 @@ command = "notify-done" |-------|------|----------|---------|-------------| | `version` | integer | Yes | -- | Schema version. Always `1`. | | `defaultRepositorySource` | string | No | `github` | Host used for shorthand `owner/repo` skill sources. Valid values: `github`, `gitlab`. | -| `agents` | string[] | No | `[]` | Agent tool IDs: `claude`, `cursor`, `codex`, `vscode`, `opencode`. Creates symlinks and config files for each. | +| `agents` | string[] | No | `[]` | Agent tool IDs: `claude`, `cursor`, `codex`, `vscode`, `opencode`, `pi`. Creates symlinks and config files for each. | ### Skills @@ -385,6 +385,7 @@ Check project health: gitignore setup, installed skills, symlinks, and legacy co | `codex` | Codex | `.codex` | (reads `.agents/skills/` natively) | `.codex/config.toml` | Not supported | | `vscode` | VS Code Copilot | `.vscode` | (reads `.agents/skills/` natively) | `.vscode/mcp.json` | `.claude/settings.json` | | `opencode` | OpenCode | `.claude` | (reads `.agents/skills/` natively) | `opencode.json` | Not supported | +| `pi` | Pi | `.pi` | (reads `.agents/skills/` natively) | Not supported | Not supported | Claude and Cursor use symlinks from their config directory to `.agents/skills/`. Codex, VS Code, OpenCode, and Pi read `.agents/skills/` directly. diff --git a/specs/SPEC.md b/specs/SPEC.md index 1b639d11..95eb7d9a 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -79,7 +79,7 @@ headers = { X-Api-Key = "${API_KEY}" } |-------|----------|-------------| | `version` | Yes | Schema version. Always `1`. | | `defaultRepositorySource` | No | Host used for shorthand `owner/repo` skill sources. Valid values: `github`, `gitlab`. Defaults to `github`. | -| `agents` | No | Array of agent tool IDs. Valid: `claude`, `cursor`, `codex`, `vscode`, `opencode`. Defaults to `[]`. When set, dotagents creates skills symlinks and MCP config files for each agent. | +| `agents` | No | Array of agent tool IDs. Valid: `claude`, `cursor`, `codex`, `vscode`, `opencode`, `pi`. Defaults to `[]`. When set, dotagents creates skills symlinks and MCP config files for each agent. | | `project` | No | Project metadata. | | `symlinks` | No | Symlink configuration (legacy — prefer `agents` for new projects). | | `skills` | No | Skill dependencies (array of tables). | @@ -184,6 +184,7 @@ Hook declarations. Each entry defines a hook that dotagents will configure for a | `codex` | Codex | `.codex` | `.codex/config.toml` | TOML (shared) | | `vscode` | VS Code Copilot | `.vscode` | `.vscode/mcp.json` | JSON | | `opencode` | OpenCode | `.claude` | `opencode.json` | JSON (shared) | +| `pi` | Pi | `.pi` | -- | -- | Each agent has its own MCP config format. dotagents translates the universal `[[mcp]]` declarations into the format each tool expects during `install` and `sync`. @@ -671,7 +672,7 @@ dotagents/ mcp.ts agents/ types.ts # McpDeclaration, AgentDefinition interfaces - registry.ts # Agent registry (claude, cursor, codex, vscode, opencode) + registry.ts # Agent registry (claude, cursor, codex, vscode, opencode, pi) definitions/ # Per-agent definitions (claude.ts, cursor.ts, etc.) mcp-writer.ts # MCP config file generation per agent hook-writer.ts # Hook config file generation per agent diff --git a/src/agents/definitions/pi.ts b/src/agents/definitions/pi.ts new file mode 100644 index 00000000..76421ed9 --- /dev/null +++ b/src/agents/definitions/pi.ts @@ -0,0 +1,21 @@ +import type { AgentDefinition } from "../types.js"; +import { UnsupportedFeature } from "../errors.js"; + +const pi: AgentDefinition = { + id: "pi", + displayName: "Pi", + configDir: ".pi", + // reads .agents/skills/ natively at both project and user scope + skillsParentDir: undefined, + userSkillsParentDirs: undefined, + mcp: undefined, + serializeServer() { + throw new UnsupportedFeature("pi", "MCP"); + }, + hooks: undefined, + serializeHooks() { + throw new UnsupportedFeature("pi", "hooks"); + }, +}; + +export default pi; diff --git a/src/agents/mcp-writer.ts b/src/agents/mcp-writer.ts index dd04848c..fa4eedcb 100644 --- a/src/agents/mcp-writer.ts +++ b/src/agents/mcp-writer.ts @@ -57,6 +57,8 @@ export async function writeMcpConfigs( if (!agent) {continue;} const { mcp } = agent; + if (!mcp) {continue;} + const { filePath, shared } = resolveTarget(id, mcp); if (seen.has(filePath)) {continue;} seen.add(filePath); @@ -96,6 +98,8 @@ export async function verifyMcpConfigs( if (!agent) {continue;} const { mcp } = agent; + if (!mcp) {continue;} + const { filePath } = resolveTarget(id, mcp); if (seen.has(filePath)) {continue;} seen.add(filePath); diff --git a/src/agents/paths.test.ts b/src/agents/paths.test.ts index e2a03ab4..b03855c7 100644 --- a/src/agents/paths.test.ts +++ b/src/agents/paths.test.ts @@ -41,6 +41,10 @@ describe("getUserMcpTarget", () => { it("throws for unknown agent", () => { expect(() => getUserMcpTarget("emacs")).toThrow("Unknown agent"); }); + + it("throws for pi (no MCP support)", () => { + expect(() => getUserMcpTarget("pi")).toThrow("Unknown agent"); + }); }); describe("skill discovery paths", () => { @@ -77,4 +81,10 @@ describe("skill discovery paths", () => { expect(agent.skillsParentDir).toBeUndefined(); expect(agent.userSkillsParentDirs).toBeUndefined(); }); + + it("pi reads .agents/skills/ natively", () => { + const agent = getAgent("pi")!; + expect(agent.skillsParentDir).toBeUndefined(); + expect(agent.userSkillsParentDirs).toBeUndefined(); + }); }); diff --git a/src/agents/registry.test.ts b/src/agents/registry.test.ts index 6801b231..fef6c5fb 100644 --- a/src/agents/registry.test.ts +++ b/src/agents/registry.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; import { getAgent, allAgentIds } from "./registry.js"; +import { UnsupportedFeature } from "./errors.js"; import type { McpDeclaration } from "./types.js"; const STDIO_SERVER: McpDeclaration = { @@ -29,7 +30,8 @@ describe("allAgentIds", () => { expect(ids).toContain("codex"); expect(ids).toContain("vscode"); expect(ids).toContain("opencode"); - expect(ids).toHaveLength(5); + expect(ids).toContain("pi"); + expect(ids).toHaveLength(6); }); }); @@ -110,8 +112,8 @@ describe("codex serializer", () => { }); it("has toml format and shared flag", () => { - expect(agent.mcp.format).toBe("toml"); - expect(agent.mcp.shared).toBe(true); + expect(agent.mcp!.format).toBe("toml"); + expect(agent.mcp!.shared).toBe(true); }); }); @@ -179,7 +181,30 @@ describe("opencode serializer", () => { }); it("shares config and reads .agents/ natively", () => { - expect(agent.mcp.shared).toBe(true); + expect(agent.mcp!.shared).toBe(true); expect(agent.skillsParentDir).toBeUndefined(); }); }); + +describe("pi agent", () => { + const agent = getAgent("pi")!; + + it("does not support MCP", () => { + expect(agent.mcp).toBeUndefined(); + expect(() => agent.serializeServer(STDIO_SERVER)).toThrow(UnsupportedFeature); + }); + + it("does not support hooks", () => { + expect(agent.hooks).toBeUndefined(); + expect(() => agent.serializeHooks([])).toThrow(UnsupportedFeature); + }); + + it("reads .agents/skills/ natively", () => { + expect(agent.skillsParentDir).toBeUndefined(); + expect(agent.userSkillsParentDirs).toBeUndefined(); + }); + + it("uses .pi config directory", () => { + expect(agent.configDir).toBe(".pi"); + }); +}); diff --git a/src/agents/registry.ts b/src/agents/registry.ts index 8bffe056..25cdee92 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]), diff --git a/src/agents/types.ts b/src/agents/types.ts index 0b3b9082..3acb56a8 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -91,8 +91,8 @@ export interface AgentDefinition { * Undefined if the agent reads ~/.agents/skills/ natively (no symlink needed). */ userSkillsParentDirs?: string[]; - /** MCP config file specification */ - mcp: McpConfigSpec; + /** MCP config file specification (undefined if agent doesn't support MCP) */ + mcp?: McpConfigSpec; /** Transforms universal MCP declaration to agent-specific format */ serializeServer: McpSerializer; /** Hook config file specification (undefined if agent doesn't support hooks) */