@every-env/compound-plugin 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +20 -3
- package/docs/plans/2026-02-14-feat-add-gemini-cli-target-provider-plan.md +370 -0
- package/docs/specs/gemini.md +122 -0
- package/package.json +1 -1
- package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
- package/plugins/compound-engineering/CHANGELOG.md +17 -0
- package/plugins/compound-engineering/commands/workflows/plan.md +3 -0
- package/plugins/compound-engineering/commands/workflows/work.md +8 -1
- package/src/commands/convert.ts +14 -25
- package/src/commands/install.ts +28 -26
- package/src/commands/sync.ts +44 -21
- package/src/converters/claude-to-gemini.ts +193 -0
- package/src/converters/claude-to-opencode.ts +16 -0
- package/src/converters/claude-to-pi.ts +205 -0
- package/src/sync/cursor.ts +78 -0
- package/src/sync/droid.ts +21 -0
- package/src/sync/pi.ts +88 -0
- package/src/targets/gemini.ts +68 -0
- package/src/targets/index.ts +18 -0
- package/src/targets/pi.ts +131 -0
- package/src/templates/pi/compat-extension.ts +452 -0
- package/src/types/gemini.ts +29 -0
- package/src/types/pi.ts +40 -0
- package/src/utils/resolve-home.ts +17 -0
- package/tests/cli.test.ts +76 -0
- package/tests/converter.test.ts +29 -0
- package/tests/gemini-converter.test.ts +373 -0
- package/tests/gemini-writer.test.ts +181 -0
- package/tests/pi-converter.test.ts +116 -0
- package/tests/pi-writer.test.ts +99 -0
- package/tests/sync-cursor.test.ts +92 -0
- package/tests/sync-droid.test.ts +57 -0
- package/tests/sync-pi.test.ts +68 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
export const PI_COMPAT_EXTENSION_SOURCE = `import fs from "node:fs"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
|
6
|
+
import { Type } from "@sinclair/typebox"
|
|
7
|
+
|
|
8
|
+
const MAX_BYTES = 50 * 1024
|
|
9
|
+
const DEFAULT_SUBAGENT_TIMEOUT_MS = 10 * 60 * 1000
|
|
10
|
+
const MAX_PARALLEL_SUBAGENTS = 8
|
|
11
|
+
|
|
12
|
+
type SubagentTask = {
|
|
13
|
+
agent: string
|
|
14
|
+
task: string
|
|
15
|
+
cwd?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type SubagentResult = {
|
|
19
|
+
agent: string
|
|
20
|
+
task: string
|
|
21
|
+
cwd: string
|
|
22
|
+
exitCode: number
|
|
23
|
+
output: string
|
|
24
|
+
stderr: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function truncate(value: string): string {
|
|
28
|
+
const input = value ?? ""
|
|
29
|
+
if (Buffer.byteLength(input, "utf8") <= MAX_BYTES) return input
|
|
30
|
+
const head = input.slice(0, MAX_BYTES)
|
|
31
|
+
return head + "\\n\\n[Output truncated to 50KB]"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shellEscape(value: string): string {
|
|
35
|
+
return "'" + value.replace(/'/g, "'\\"'\\"'") + "'"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeName(value: string): string {
|
|
39
|
+
return String(value || "")
|
|
40
|
+
.trim()
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
43
|
+
.replace(/-+/g, "-")
|
|
44
|
+
.replace(/^-+|-+$/g, "")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveBundledMcporterConfigPath(): string | undefined {
|
|
48
|
+
try {
|
|
49
|
+
const extensionDir = path.dirname(fileURLToPath(import.meta.url))
|
|
50
|
+
const candidates = [
|
|
51
|
+
path.join(extensionDir, "..", "pi-resources", "compound-engineering", "mcporter.json"),
|
|
52
|
+
path.join(extensionDir, "..", "compound-engineering", "mcporter.json"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (fs.existsSync(candidate)) return candidate
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// noop: bundled path is best-effort fallback
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveMcporterConfigPath(cwd: string, explicit?: string): string | undefined {
|
|
66
|
+
if (explicit && explicit.trim()) {
|
|
67
|
+
return path.resolve(explicit)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const projectPath = path.join(cwd, ".pi", "compound-engineering", "mcporter.json")
|
|
71
|
+
if (fs.existsSync(projectPath)) return projectPath
|
|
72
|
+
|
|
73
|
+
const globalPath = path.join(os.homedir(), ".pi", "agent", "compound-engineering", "mcporter.json")
|
|
74
|
+
if (fs.existsSync(globalPath)) return globalPath
|
|
75
|
+
|
|
76
|
+
return resolveBundledMcporterConfigPath()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveTaskCwd(baseCwd: string, taskCwd?: string): string {
|
|
80
|
+
if (!taskCwd || !taskCwd.trim()) return baseCwd
|
|
81
|
+
const expanded = taskCwd === "~"
|
|
82
|
+
? os.homedir()
|
|
83
|
+
: taskCwd.startsWith("~" + path.sep)
|
|
84
|
+
? path.join(os.homedir(), taskCwd.slice(2))
|
|
85
|
+
: taskCwd
|
|
86
|
+
return path.resolve(baseCwd, expanded)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function runSingleSubagent(
|
|
90
|
+
pi: ExtensionAPI,
|
|
91
|
+
baseCwd: string,
|
|
92
|
+
task: SubagentTask,
|
|
93
|
+
signal?: AbortSignal,
|
|
94
|
+
timeoutMs = DEFAULT_SUBAGENT_TIMEOUT_MS,
|
|
95
|
+
): Promise<SubagentResult> {
|
|
96
|
+
const agent = normalizeName(task.agent)
|
|
97
|
+
if (!agent) {
|
|
98
|
+
throw new Error("Subagent task is missing a valid agent name")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const taskText = String(task.task ?? "").trim()
|
|
102
|
+
if (!taskText) {
|
|
103
|
+
throw new Error("Subagent task for " + agent + " is empty")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const cwd = resolveTaskCwd(baseCwd, task.cwd)
|
|
107
|
+
const prompt = "/skill:" + agent + " " + taskText
|
|
108
|
+
const script = "cd " + shellEscape(cwd) + " && pi --no-session -p " + shellEscape(prompt)
|
|
109
|
+
const result = await pi.exec("bash", ["-lc", script], { signal, timeout: timeoutMs })
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
agent,
|
|
113
|
+
task: taskText,
|
|
114
|
+
cwd,
|
|
115
|
+
exitCode: result.code,
|
|
116
|
+
output: truncate(result.stdout || ""),
|
|
117
|
+
stderr: truncate(result.stderr || ""),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runParallelSubagents(
|
|
122
|
+
pi: ExtensionAPI,
|
|
123
|
+
baseCwd: string,
|
|
124
|
+
tasks: SubagentTask[],
|
|
125
|
+
signal?: AbortSignal,
|
|
126
|
+
timeoutMs = DEFAULT_SUBAGENT_TIMEOUT_MS,
|
|
127
|
+
maxConcurrency = 4,
|
|
128
|
+
onProgress?: (completed: number, total: number) => void,
|
|
129
|
+
): Promise<SubagentResult[]> {
|
|
130
|
+
const safeConcurrency = Math.max(1, Math.min(maxConcurrency, MAX_PARALLEL_SUBAGENTS, tasks.length))
|
|
131
|
+
const results: SubagentResult[] = new Array(tasks.length)
|
|
132
|
+
|
|
133
|
+
let nextIndex = 0
|
|
134
|
+
let completed = 0
|
|
135
|
+
|
|
136
|
+
const workers = Array.from({ length: safeConcurrency }, async () => {
|
|
137
|
+
while (true) {
|
|
138
|
+
const current = nextIndex
|
|
139
|
+
nextIndex += 1
|
|
140
|
+
if (current >= tasks.length) return
|
|
141
|
+
|
|
142
|
+
results[current] = await runSingleSubagent(pi, baseCwd, tasks[current], signal, timeoutMs)
|
|
143
|
+
completed += 1
|
|
144
|
+
onProgress?.(completed, tasks.length)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
await Promise.all(workers)
|
|
149
|
+
return results
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatSubagentSummary(results: SubagentResult[]): string {
|
|
153
|
+
if (results.length === 0) return "No subagent work was executed."
|
|
154
|
+
|
|
155
|
+
const success = results.filter((result) => result.exitCode === 0).length
|
|
156
|
+
const failed = results.length - success
|
|
157
|
+
const header = failed === 0
|
|
158
|
+
? "Subagent run completed: " + success + "/" + results.length + " succeeded."
|
|
159
|
+
: "Subagent run completed: " + success + "/" + results.length + " succeeded, " + failed + " failed."
|
|
160
|
+
|
|
161
|
+
const lines = results.map((result) => {
|
|
162
|
+
const status = result.exitCode === 0 ? "ok" : "error"
|
|
163
|
+
const body = result.output || result.stderr || "(no output)"
|
|
164
|
+
const preview = body.split("\\n").slice(0, 6).join("\\n")
|
|
165
|
+
return "\\n[" + status + "] " + result.agent + "\\n" + preview
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return header + lines.join("\\n")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default function (pi: ExtensionAPI) {
|
|
172
|
+
pi.registerTool({
|
|
173
|
+
name: "ask_user_question",
|
|
174
|
+
label: "Ask User Question",
|
|
175
|
+
description: "Ask the user a question with optional choices.",
|
|
176
|
+
parameters: Type.Object({
|
|
177
|
+
question: Type.String({ description: "Question shown to the user" }),
|
|
178
|
+
options: Type.Optional(Type.Array(Type.String(), { description: "Selectable options" })),
|
|
179
|
+
allowCustom: Type.Optional(Type.Boolean({ default: true })),
|
|
180
|
+
}),
|
|
181
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
182
|
+
if (!ctx.hasUI) {
|
|
183
|
+
return {
|
|
184
|
+
isError: true,
|
|
185
|
+
content: [{ type: "text", text: "UI is unavailable in this mode." }],
|
|
186
|
+
details: {},
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const options = params.options ?? []
|
|
191
|
+
const allowCustom = params.allowCustom ?? true
|
|
192
|
+
|
|
193
|
+
if (options.length === 0) {
|
|
194
|
+
const answer = await ctx.ui.input(params.question)
|
|
195
|
+
if (!answer) {
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: "User cancelled." }],
|
|
198
|
+
details: { answer: null },
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: "User answered: " + answer }],
|
|
204
|
+
details: { answer, mode: "input" },
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const customLabel = "Other (type custom answer)"
|
|
209
|
+
const selectable = allowCustom ? [...options, customLabel] : options
|
|
210
|
+
const selected = await ctx.ui.select(params.question, selectable)
|
|
211
|
+
|
|
212
|
+
if (!selected) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: "User cancelled." }],
|
|
215
|
+
details: { answer: null },
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (selected === customLabel) {
|
|
220
|
+
const custom = await ctx.ui.input("Your answer")
|
|
221
|
+
if (!custom) {
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: "User cancelled." }],
|
|
224
|
+
details: { answer: null },
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
content: [{ type: "text", text: "User answered: " + custom }],
|
|
230
|
+
details: { answer: custom, mode: "custom" },
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: "text", text: "User selected: " + selected }],
|
|
236
|
+
details: { answer: selected, mode: "select" },
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const subagentTaskSchema = Type.Object({
|
|
242
|
+
agent: Type.String({ description: "Skill/agent name to invoke" }),
|
|
243
|
+
task: Type.String({ description: "Task instructions for that skill" }),
|
|
244
|
+
cwd: Type.Optional(Type.String({ description: "Optional working directory for this task" })),
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
pi.registerTool({
|
|
248
|
+
name: "subagent",
|
|
249
|
+
label: "Subagent",
|
|
250
|
+
description: "Run one or more skill-based subagent tasks. Supports single, parallel, and chained execution.",
|
|
251
|
+
parameters: Type.Object({
|
|
252
|
+
agent: Type.Optional(Type.String({ description: "Single subagent name" })),
|
|
253
|
+
task: Type.Optional(Type.String({ description: "Single subagent task" })),
|
|
254
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for single mode" })),
|
|
255
|
+
tasks: Type.Optional(Type.Array(subagentTaskSchema, { description: "Parallel subagent tasks" })),
|
|
256
|
+
chain: Type.Optional(Type.Array(subagentTaskSchema, { description: "Sequential tasks; supports {previous} placeholder" })),
|
|
257
|
+
maxConcurrency: Type.Optional(Type.Number({ default: 4 })),
|
|
258
|
+
timeoutMs: Type.Optional(Type.Number({ default: DEFAULT_SUBAGENT_TIMEOUT_MS })),
|
|
259
|
+
}),
|
|
260
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
261
|
+
const hasSingle = Boolean(params.agent && params.task)
|
|
262
|
+
const hasTasks = Boolean(params.tasks && params.tasks.length > 0)
|
|
263
|
+
const hasChain = Boolean(params.chain && params.chain.length > 0)
|
|
264
|
+
const modeCount = Number(hasSingle) + Number(hasTasks) + Number(hasChain)
|
|
265
|
+
|
|
266
|
+
if (modeCount !== 1) {
|
|
267
|
+
return {
|
|
268
|
+
isError: true,
|
|
269
|
+
content: [{ type: "text", text: "Provide exactly one mode: single (agent+task), tasks, or chain." }],
|
|
270
|
+
details: {},
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const timeoutMs = Number(params.timeoutMs || DEFAULT_SUBAGENT_TIMEOUT_MS)
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
if (hasSingle) {
|
|
278
|
+
const result = await runSingleSubagent(
|
|
279
|
+
pi,
|
|
280
|
+
ctx.cwd,
|
|
281
|
+
{ agent: params.agent!, task: params.task!, cwd: params.cwd },
|
|
282
|
+
signal,
|
|
283
|
+
timeoutMs,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const body = formatSubagentSummary([result])
|
|
287
|
+
return {
|
|
288
|
+
isError: result.exitCode !== 0,
|
|
289
|
+
content: [{ type: "text", text: body }],
|
|
290
|
+
details: { mode: "single", results: [result] },
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (hasTasks) {
|
|
295
|
+
const tasks = params.tasks as SubagentTask[]
|
|
296
|
+
const maxConcurrency = Number(params.maxConcurrency || 4)
|
|
297
|
+
|
|
298
|
+
const results = await runParallelSubagents(
|
|
299
|
+
pi,
|
|
300
|
+
ctx.cwd,
|
|
301
|
+
tasks,
|
|
302
|
+
signal,
|
|
303
|
+
timeoutMs,
|
|
304
|
+
maxConcurrency,
|
|
305
|
+
(completed, total) => {
|
|
306
|
+
onUpdate?.({
|
|
307
|
+
content: [{ type: "text", text: "Subagent progress: " + completed + "/" + total }],
|
|
308
|
+
details: { mode: "parallel", completed, total },
|
|
309
|
+
})
|
|
310
|
+
},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const body = formatSubagentSummary(results)
|
|
314
|
+
const hasFailure = results.some((result) => result.exitCode !== 0)
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
isError: hasFailure,
|
|
318
|
+
content: [{ type: "text", text: body }],
|
|
319
|
+
details: { mode: "parallel", results },
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const chain = params.chain as SubagentTask[]
|
|
324
|
+
const results: SubagentResult[] = []
|
|
325
|
+
let previous = ""
|
|
326
|
+
|
|
327
|
+
for (const step of chain) {
|
|
328
|
+
const resolvedTask = step.task.replace(/\\{previous\\}/g, previous)
|
|
329
|
+
const result = await runSingleSubagent(
|
|
330
|
+
pi,
|
|
331
|
+
ctx.cwd,
|
|
332
|
+
{ agent: step.agent, task: resolvedTask, cwd: step.cwd },
|
|
333
|
+
signal,
|
|
334
|
+
timeoutMs,
|
|
335
|
+
)
|
|
336
|
+
results.push(result)
|
|
337
|
+
previous = result.output || result.stderr
|
|
338
|
+
|
|
339
|
+
onUpdate?.({
|
|
340
|
+
content: [{ type: "text", text: "Subagent chain progress: " + results.length + "/" + chain.length }],
|
|
341
|
+
details: { mode: "chain", completed: results.length, total: chain.length },
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
if (result.exitCode !== 0) break
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const body = formatSubagentSummary(results)
|
|
348
|
+
const hasFailure = results.some((result) => result.exitCode !== 0)
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
isError: hasFailure,
|
|
352
|
+
content: [{ type: "text", text: body }],
|
|
353
|
+
details: { mode: "chain", results },
|
|
354
|
+
}
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return {
|
|
357
|
+
isError: true,
|
|
358
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
359
|
+
details: {},
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
pi.registerTool({
|
|
366
|
+
name: "mcporter_list",
|
|
367
|
+
label: "MCPorter List",
|
|
368
|
+
description: "List tools on an MCP server through MCPorter.",
|
|
369
|
+
parameters: Type.Object({
|
|
370
|
+
server: Type.String({ description: "Configured MCP server name" }),
|
|
371
|
+
allParameters: Type.Optional(Type.Boolean({ default: false })),
|
|
372
|
+
json: Type.Optional(Type.Boolean({ default: true })),
|
|
373
|
+
configPath: Type.Optional(Type.String({ description: "Optional mcporter config path" })),
|
|
374
|
+
}),
|
|
375
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
376
|
+
const args = ["list", params.server]
|
|
377
|
+
if (params.allParameters) args.push("--all-parameters")
|
|
378
|
+
if (params.json ?? true) args.push("--json")
|
|
379
|
+
|
|
380
|
+
const configPath = resolveMcporterConfigPath(ctx.cwd, params.configPath)
|
|
381
|
+
if (configPath) {
|
|
382
|
+
args.push("--config", configPath)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const result = await pi.exec("mcporter", args, { signal })
|
|
386
|
+
const output = truncate(result.stdout || result.stderr || "")
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
isError: result.code !== 0,
|
|
390
|
+
content: [{ type: "text", text: output || "(no output)" }],
|
|
391
|
+
details: {
|
|
392
|
+
exitCode: result.code,
|
|
393
|
+
command: "mcporter " + args.join(" "),
|
|
394
|
+
configPath,
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
pi.registerTool({
|
|
401
|
+
name: "mcporter_call",
|
|
402
|
+
label: "MCPorter Call",
|
|
403
|
+
description: "Call a specific MCP tool through MCPorter.",
|
|
404
|
+
parameters: Type.Object({
|
|
405
|
+
call: Type.Optional(Type.String({ description: "Function-style call, e.g. linear.list_issues(limit: 5)" })),
|
|
406
|
+
server: Type.Optional(Type.String({ description: "Server name (if call is omitted)" })),
|
|
407
|
+
tool: Type.Optional(Type.String({ description: "Tool name (if call is omitted)" })),
|
|
408
|
+
args: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "JSON arguments object" })),
|
|
409
|
+
configPath: Type.Optional(Type.String({ description: "Optional mcporter config path" })),
|
|
410
|
+
}),
|
|
411
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
412
|
+
const args = ["call"]
|
|
413
|
+
|
|
414
|
+
if (params.call && params.call.trim()) {
|
|
415
|
+
args.push(params.call.trim())
|
|
416
|
+
} else {
|
|
417
|
+
if (!params.server || !params.tool) {
|
|
418
|
+
return {
|
|
419
|
+
isError: true,
|
|
420
|
+
content: [{ type: "text", text: "Provide either call, or server + tool." }],
|
|
421
|
+
details: {},
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
args.push(params.server + "." + params.tool)
|
|
425
|
+
if (params.args) {
|
|
426
|
+
args.push("--args", JSON.stringify(params.args))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
args.push("--output", "json")
|
|
431
|
+
|
|
432
|
+
const configPath = resolveMcporterConfigPath(ctx.cwd, params.configPath)
|
|
433
|
+
if (configPath) {
|
|
434
|
+
args.push("--config", configPath)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const result = await pi.exec("mcporter", args, { signal })
|
|
438
|
+
const output = truncate(result.stdout || result.stderr || "")
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
isError: result.code !== 0,
|
|
442
|
+
content: [{ type: "text", text: output || "(no output)" }],
|
|
443
|
+
details: {
|
|
444
|
+
exitCode: result.code,
|
|
445
|
+
command: "mcporter " + args.join(" "),
|
|
446
|
+
configPath,
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type GeminiSkill = {
|
|
2
|
+
name: string
|
|
3
|
+
content: string // Full SKILL.md with YAML frontmatter
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type GeminiSkillDir = {
|
|
7
|
+
name: string
|
|
8
|
+
sourceDir: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type GeminiCommand = {
|
|
12
|
+
name: string // e.g. "plan" or "workflows/plan"
|
|
13
|
+
content: string // Full TOML content
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type GeminiMcpServer = {
|
|
17
|
+
command?: string
|
|
18
|
+
args?: string[]
|
|
19
|
+
env?: Record<string, string>
|
|
20
|
+
url?: string
|
|
21
|
+
headers?: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type GeminiBundle = {
|
|
25
|
+
generatedSkills: GeminiSkill[] // From agents
|
|
26
|
+
skillDirs: GeminiSkillDir[] // From skills (pass-through)
|
|
27
|
+
commands: GeminiCommand[]
|
|
28
|
+
mcpServers?: Record<string, GeminiMcpServer>
|
|
29
|
+
}
|
package/src/types/pi.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type PiPrompt = {
|
|
2
|
+
name: string
|
|
3
|
+
content: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type PiSkillDir = {
|
|
7
|
+
name: string
|
|
8
|
+
sourceDir: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type PiGeneratedSkill = {
|
|
12
|
+
name: string
|
|
13
|
+
content: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PiExtensionFile = {
|
|
17
|
+
name: string
|
|
18
|
+
content: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PiMcporterServer = {
|
|
22
|
+
description?: string
|
|
23
|
+
baseUrl?: string
|
|
24
|
+
command?: string
|
|
25
|
+
args?: string[]
|
|
26
|
+
env?: Record<string, string>
|
|
27
|
+
headers?: Record<string, string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type PiMcporterConfig = {
|
|
31
|
+
mcpServers: Record<string, PiMcporterServer>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type PiBundle = {
|
|
35
|
+
prompts: PiPrompt[]
|
|
36
|
+
skillDirs: PiSkillDir[]
|
|
37
|
+
generatedSkills: PiGeneratedSkill[]
|
|
38
|
+
extensions: PiExtensionFile[]
|
|
39
|
+
mcporterConfig?: PiMcporterConfig
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os from "os"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
export function expandHome(value: string): string {
|
|
5
|
+
if (value === "~") return os.homedir()
|
|
6
|
+
if (value.startsWith(`~${path.sep}`)) {
|
|
7
|
+
return path.join(os.homedir(), value.slice(2))
|
|
8
|
+
}
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveTargetHome(value: unknown, defaultPath: string): string {
|
|
13
|
+
if (!value) return defaultPath
|
|
14
|
+
const raw = String(value).trim()
|
|
15
|
+
if (!raw) return defaultPath
|
|
16
|
+
return path.resolve(expandHome(raw))
|
|
17
|
+
}
|
package/tests/cli.test.ts
CHANGED
|
@@ -350,4 +350,80 @@ describe("CLI", () => {
|
|
|
350
350
|
expect(await exists(path.join(codexRoot, "skills", "skill-one", "SKILL.md"))).toBe(true)
|
|
351
351
|
expect(await exists(path.join(codexRoot, "AGENTS.md"))).toBe(true)
|
|
352
352
|
})
|
|
353
|
+
|
|
354
|
+
test("convert supports --pi-home for pi output", async () => {
|
|
355
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-pi-home-"))
|
|
356
|
+
const piRoot = path.join(tempRoot, ".pi")
|
|
357
|
+
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
|
358
|
+
|
|
359
|
+
const proc = Bun.spawn([
|
|
360
|
+
"bun",
|
|
361
|
+
"run",
|
|
362
|
+
"src/index.ts",
|
|
363
|
+
"convert",
|
|
364
|
+
fixtureRoot,
|
|
365
|
+
"--to",
|
|
366
|
+
"pi",
|
|
367
|
+
"--pi-home",
|
|
368
|
+
piRoot,
|
|
369
|
+
], {
|
|
370
|
+
cwd: path.join(import.meta.dir, ".."),
|
|
371
|
+
stdout: "pipe",
|
|
372
|
+
stderr: "pipe",
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
const exitCode = await proc.exited
|
|
376
|
+
const stdout = await new Response(proc.stdout).text()
|
|
377
|
+
const stderr = await new Response(proc.stderr).text()
|
|
378
|
+
|
|
379
|
+
if (exitCode !== 0) {
|
|
380
|
+
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
expect(stdout).toContain("Converted compound-engineering")
|
|
384
|
+
expect(stdout).toContain(piRoot)
|
|
385
|
+
expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true)
|
|
386
|
+
expect(await exists(path.join(piRoot, "skills", "repo-research-analyst", "SKILL.md"))).toBe(true)
|
|
387
|
+
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
|
|
388
|
+
expect(await exists(path.join(piRoot, "compound-engineering", "mcporter.json"))).toBe(true)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test("install supports --also with pi output", async () => {
|
|
392
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-also-pi-"))
|
|
393
|
+
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
|
394
|
+
const piRoot = path.join(tempRoot, ".pi")
|
|
395
|
+
|
|
396
|
+
const proc = Bun.spawn([
|
|
397
|
+
"bun",
|
|
398
|
+
"run",
|
|
399
|
+
"src/index.ts",
|
|
400
|
+
"install",
|
|
401
|
+
fixtureRoot,
|
|
402
|
+
"--to",
|
|
403
|
+
"opencode",
|
|
404
|
+
"--also",
|
|
405
|
+
"pi",
|
|
406
|
+
"--pi-home",
|
|
407
|
+
piRoot,
|
|
408
|
+
"--output",
|
|
409
|
+
tempRoot,
|
|
410
|
+
], {
|
|
411
|
+
cwd: path.join(import.meta.dir, ".."),
|
|
412
|
+
stdout: "pipe",
|
|
413
|
+
stderr: "pipe",
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
const exitCode = await proc.exited
|
|
417
|
+
const stdout = await new Response(proc.stdout).text()
|
|
418
|
+
const stderr = await new Response(proc.stderr).text()
|
|
419
|
+
|
|
420
|
+
if (exitCode !== 0) {
|
|
421
|
+
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
expect(stdout).toContain("Installed compound-engineering")
|
|
425
|
+
expect(stdout).toContain(piRoot)
|
|
426
|
+
expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true)
|
|
427
|
+
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
|
|
428
|
+
})
|
|
353
429
|
})
|
package/tests/converter.test.ts
CHANGED
|
@@ -75,6 +75,35 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
75
75
|
expect(modelCommand?.model).toBe("openai/gpt-4o")
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
+
test("resolves bare Claude model aliases to full IDs", () => {
|
|
79
|
+
const plugin: ClaudePlugin = {
|
|
80
|
+
root: "/tmp/plugin",
|
|
81
|
+
manifest: { name: "fixture", version: "1.0.0" },
|
|
82
|
+
agents: [
|
|
83
|
+
{
|
|
84
|
+
name: "cheap-agent",
|
|
85
|
+
description: "Agent using bare alias",
|
|
86
|
+
body: "Test agent.",
|
|
87
|
+
sourcePath: "/tmp/plugin/agents/cheap-agent.md",
|
|
88
|
+
model: "haiku",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
commands: [],
|
|
92
|
+
skills: [],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const bundle = convertClaudeToOpenCode(plugin, {
|
|
96
|
+
agentMode: "subagent",
|
|
97
|
+
inferTemperature: false,
|
|
98
|
+
permissions: "none",
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const agent = bundle.agents.find((a) => a.name === "cheap-agent")
|
|
102
|
+
expect(agent).toBeDefined()
|
|
103
|
+
const parsed = parseFrontmatter(agent!.content)
|
|
104
|
+
expect(parsed.data.model).toBe("anthropic/claude-haiku-4-5")
|
|
105
|
+
})
|
|
106
|
+
|
|
78
107
|
test("converts hooks into plugin file", async () => {
|
|
79
108
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
|
80
109
|
const bundle = convertClaudeToOpenCode(plugin, {
|