Skip to content

Commit 25fa591

Browse files
Merge pull request #181 from gvkhosla/feat/pi-target
feat: add first-class Pi target with MCPorter + subagent compatibility
2 parents 87e98b2 + e84fef7 commit 25fa591

14 files changed

Lines changed: 1358 additions & 18 deletions

File tree

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ A Claude Code plugin marketplace featuring the **Compound Engineering Plugin**
1212
/plugin install compound-engineering
1313
```
1414

15-
## OpenCode, Codex, Droid & Cursor (experimental) Install
15+
## OpenCode, Codex, Droid, Cursor & Pi (experimental) Install
1616

17-
This repo includes a Bun/TypeScript CLI that converts Claude Code plugins to OpenCode, Codex, Factory Droid, and Cursor.
17+
This repo includes a Bun/TypeScript CLI that converts Claude Code plugins to OpenCode, Codex, Factory Droid, Cursor, and Pi.
1818

1919
```bash
2020
# convert the compound-engineering plugin into OpenCode format
@@ -28,6 +28,9 @@ bunx @every-env/compound-plugin install compound-engineering --to droid
2828

2929
# convert to Cursor format
3030
bunx @every-env/compound-plugin install compound-engineering --to cursor
31+
32+
# convert to Pi format
33+
bunx @every-env/compound-plugin install compound-engineering --to pi
3134
```
3235

3336
Local dev:
@@ -40,19 +43,23 @@ OpenCode output is written to `~/.config/opencode` by default, with `opencode.js
4043
Codex output is written to `~/.codex/prompts` and `~/.codex/skills`, with each Claude command converted into both a prompt and a skill (the prompt instructs Codex to load the corresponding skill). Generated Codex skill descriptions are truncated to 1024 characters (Codex limit).
4144
Droid output is written to `~/.factory/` with commands, droids (agents), and skills. Claude tool names are mapped to Factory equivalents (`Bash``Execute`, `Write``Create`, etc.) and namespace prefixes are stripped from commands.
4245
Cursor output is written to `.cursor/` with rules (`.mdc`), commands, skills, and `mcp.json`. Agents become "Agent Requested" rules (`alwaysApply: false`) so Cursor's AI activates them on demand. Works with both the Cursor IDE and Cursor CLI (`cursor-agent`) — they share the same `.cursor/` config directory.
46+
Pi output is written to `~/.pi/agent/` by default with prompts, skills, extensions, and `compound-engineering/mcporter.json` for MCPorter interoperability.
4347

4448
All provider targets are experimental and may change as the formats evolve.
4549

4650
## Sync Personal Config
4751

48-
Sync your personal Claude Code config (`~/.claude/`) to OpenCode or Codex:
52+
Sync your personal Claude Code config (`~/.claude/`) to OpenCode, Codex, or Pi:
4953

5054
```bash
5155
# Sync skills and MCP servers to OpenCode
5256
bunx @every-env/compound-plugin sync --target opencode
5357

5458
# Sync to Codex
5559
bunx @every-env/compound-plugin sync --target codex
60+
61+
# Sync to Pi
62+
bunx @every-env/compound-plugin sync --target pi
5663
```
5764

5865
This syncs:

src/commands/convert.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default defineCommand({
2222
to: {
2323
type: "string",
2424
default: "opencode",
25-
description: "Target format (opencode | codex | droid | cursor)",
25+
description: "Target format (opencode | codex | droid | cursor | pi)",
2626
},
2727
output: {
2828
type: "string",
@@ -34,6 +34,11 @@ export default defineCommand({
3434
alias: "codex-home",
3535
description: "Write Codex output to this .codex root (ex: ~/.codex)",
3636
},
37+
piHome: {
38+
type: "string",
39+
alias: "pi-home",
40+
description: "Write Pi output to this Pi root (ex: ~/.pi/agent or ./.pi)",
41+
},
3742
also: {
3843
type: "string",
3944
description: "Comma-separated extra targets to generate (ex: codex)",
@@ -73,14 +78,15 @@ export default defineCommand({
7378
const plugin = await loadClaudePlugin(String(args.source))
7479
const outputRoot = resolveOutputRoot(args.output)
7580
const codexHome = resolveCodexRoot(args.codexHome)
81+
const piHome = resolvePiRoot(args.piHome)
7682

7783
const options = {
7884
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
7985
inferTemperature: Boolean(args.inferTemperature),
8086
permissions: permissions as PermissionMode,
8187
}
8288

83-
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome)
89+
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome)
8490
const bundle = target.convert(plugin, options)
8591
if (!bundle) {
8692
throw new Error(`Target ${targetName} did not return a bundle.`)
@@ -106,7 +112,7 @@ export default defineCommand({
106112
console.warn(`Skipping ${extra}: no output returned.`)
107113
continue
108114
}
109-
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome)
115+
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, piHome)
110116
await handler.write(extraRoot, extraBundle)
111117
console.log(`Converted ${plugin.manifest.name} to ${extra} at ${extraRoot}`)
112118
}
@@ -137,6 +143,18 @@ function resolveCodexRoot(value: unknown): string {
137143
return resolveCodexHome(value) ?? path.join(os.homedir(), ".codex")
138144
}
139145

146+
function resolvePiHome(value: unknown): string | null {
147+
if (!value) return null
148+
const raw = String(value).trim()
149+
if (!raw) return null
150+
const expanded = expandHome(raw)
151+
return path.resolve(expanded)
152+
}
153+
154+
function resolvePiRoot(value: unknown): string {
155+
return resolvePiHome(value) ?? path.join(os.homedir(), ".pi", "agent")
156+
}
157+
140158
function expandHome(value: string): string {
141159
if (value === "~") return os.homedir()
142160
if (value.startsWith(`~${path.sep}`)) {
@@ -153,8 +171,9 @@ function resolveOutputRoot(value: unknown): string {
153171
return process.cwd()
154172
}
155173

156-
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string): string {
174+
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string, piHome: string): string {
157175
if (targetName === "codex") return codexHome
176+
if (targetName === "pi") return piHome
158177
if (targetName === "droid") return path.join(os.homedir(), ".factory")
159178
if (targetName === "cursor") return path.join(outputRoot, ".cursor")
160179
return outputRoot

src/commands/install.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default defineCommand({
2424
to: {
2525
type: "string",
2626
default: "opencode",
27-
description: "Target format (opencode | codex | droid | cursor)",
27+
description: "Target format (opencode | codex | droid | cursor | pi)",
2828
},
2929
output: {
3030
type: "string",
@@ -36,6 +36,11 @@ export default defineCommand({
3636
alias: "codex-home",
3737
description: "Write Codex output to this .codex root (ex: ~/.codex)",
3838
},
39+
piHome: {
40+
type: "string",
41+
alias: "pi-home",
42+
description: "Write Pi output to this Pi root (ex: ~/.pi/agent or ./.pi)",
43+
},
3944
also: {
4045
type: "string",
4146
description: "Comma-separated extra targets to generate (ex: codex)",
@@ -77,6 +82,7 @@ export default defineCommand({
7782
const plugin = await loadClaudePlugin(resolvedPlugin.path)
7883
const outputRoot = resolveOutputRoot(args.output)
7984
const codexHome = resolveCodexRoot(args.codexHome)
85+
const piHome = resolvePiRoot(args.piHome)
8086

8187
const options = {
8288
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
@@ -89,7 +95,7 @@ export default defineCommand({
8995
throw new Error(`Target ${targetName} did not return a bundle.`)
9096
}
9197
const hasExplicitOutput = Boolean(args.output && String(args.output).trim())
92-
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, hasExplicitOutput)
98+
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome, hasExplicitOutput)
9399
await target.write(primaryOutputRoot, bundle)
94100
console.log(`Installed ${plugin.manifest.name} to ${primaryOutputRoot}`)
95101

@@ -110,7 +116,7 @@ export default defineCommand({
110116
console.warn(`Skipping ${extra}: no output returned.`)
111117
continue
112118
}
113-
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, hasExplicitOutput)
119+
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, piHome, hasExplicitOutput)
114120
await handler.write(extraRoot, extraBundle)
115121
console.log(`Installed ${plugin.manifest.name} to ${extraRoot}`)
116122
}
@@ -164,6 +170,18 @@ function resolveCodexRoot(value: unknown): string {
164170
return resolveCodexHome(value) ?? path.join(os.homedir(), ".codex")
165171
}
166172

173+
function resolvePiHome(value: unknown): string | null {
174+
if (!value) return null
175+
const raw = String(value).trim()
176+
if (!raw) return null
177+
const expanded = expandHome(raw)
178+
return path.resolve(expanded)
179+
}
180+
181+
function resolvePiRoot(value: unknown): string {
182+
return resolvePiHome(value) ?? path.join(os.homedir(), ".pi", "agent")
183+
}
184+
167185
function expandHome(value: string): string {
168186
if (value === "~") return os.homedir()
169187
if (value.startsWith(`~${path.sep}`)) {
@@ -182,8 +200,15 @@ function resolveOutputRoot(value: unknown): string {
182200
return path.join(os.homedir(), ".config", "opencode")
183201
}
184202

185-
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string, hasExplicitOutput: boolean): string {
203+
function resolveTargetOutputRoot(
204+
targetName: string,
205+
outputRoot: string,
206+
codexHome: string,
207+
piHome: string,
208+
hasExplicitOutput: boolean,
209+
): string {
186210
if (targetName === "codex") return codexHome
211+
if (targetName === "pi") return piHome
187212
if (targetName === "droid") return path.join(os.homedir(), ".factory")
188213
if (targetName === "cursor") {
189214
const base = hasExplicitOutput ? outputRoot : process.cwd()

src/commands/sync.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import path from "path"
44
import { loadClaudeHome } from "../parsers/claude-home"
55
import { syncToOpenCode } from "../sync/opencode"
66
import { syncToCodex } from "../sync/codex"
7+
import { syncToPi } from "../sync/pi"
78

8-
function isValidTarget(value: string): value is "opencode" | "codex" {
9-
return value === "opencode" || value === "codex"
9+
function isValidTarget(value: string): value is "opencode" | "codex" | "pi" {
10+
return value === "opencode" || value === "codex" || value === "pi"
1011
}
1112

1213
/** Check if any MCP servers have env vars that might contain secrets */
@@ -26,13 +27,13 @@ function hasPotentialSecrets(mcpServers: Record<string, unknown>): boolean {
2627
export default defineCommand({
2728
meta: {
2829
name: "sync",
29-
description: "Sync Claude Code config (~/.claude/) to OpenCode or Codex",
30+
description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, or Pi",
3031
},
3132
args: {
3233
target: {
3334
type: "string",
3435
required: true,
35-
description: "Target: opencode | codex",
36+
description: "Target: opencode | codex | pi",
3637
},
3738
claudeHome: {
3839
type: "string",
@@ -42,7 +43,7 @@ export default defineCommand({
4243
},
4344
async run({ args }) {
4445
if (!isValidTarget(args.target)) {
45-
throw new Error(`Unknown target: ${args.target}. Use 'opencode' or 'codex'.`)
46+
throw new Error(`Unknown target: ${args.target}. Use 'opencode', 'codex', or 'pi'.`)
4647
}
4748

4849
const claudeHome = expandHome(args.claudeHome ?? path.join(os.homedir(), ".claude"))
@@ -63,12 +64,16 @@ export default defineCommand({
6364
const outputRoot =
6465
args.target === "opencode"
6566
? path.join(os.homedir(), ".config", "opencode")
66-
: path.join(os.homedir(), ".codex")
67+
: args.target === "codex"
68+
? path.join(os.homedir(), ".codex")
69+
: path.join(os.homedir(), ".pi", "agent")
6770

6871
if (args.target === "opencode") {
6972
await syncToOpenCode(config, outputRoot)
70-
} else {
73+
} else if (args.target === "codex") {
7174
await syncToCodex(config, outputRoot)
75+
} else {
76+
await syncToPi(config, outputRoot)
7277
}
7378

7479
console.log(`✓ Synced to ${args.target}: ${outputRoot}`)

0 commit comments

Comments
 (0)