feat: add first-class pi target with mcporter/subagent compatibility
This commit is contained in:
@@ -22,7 +22,7 @@ export default defineCommand({
|
||||
to: {
|
||||
type: "string",
|
||||
default: "opencode",
|
||||
description: "Target format (opencode | codex | droid | cursor)",
|
||||
description: "Target format (opencode | codex | droid | cursor | pi)",
|
||||
},
|
||||
output: {
|
||||
type: "string",
|
||||
@@ -34,6 +34,11 @@ export default defineCommand({
|
||||
alias: "codex-home",
|
||||
description: "Write Codex output to this .codex root (ex: ~/.codex)",
|
||||
},
|
||||
piHome: {
|
||||
type: "string",
|
||||
alias: "pi-home",
|
||||
description: "Write Pi output to this Pi root (ex: ~/.pi/agent or ./.pi)",
|
||||
},
|
||||
also: {
|
||||
type: "string",
|
||||
description: "Comma-separated extra targets to generate (ex: codex)",
|
||||
@@ -73,6 +78,7 @@ export default defineCommand({
|
||||
const plugin = await loadClaudePlugin(String(args.source))
|
||||
const outputRoot = resolveOutputRoot(args.output)
|
||||
const codexHome = resolveCodexRoot(args.codexHome)
|
||||
const piHome = resolvePiRoot(args.piHome)
|
||||
|
||||
const options = {
|
||||
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
|
||||
@@ -80,7 +86,7 @@ export default defineCommand({
|
||||
permissions: permissions as PermissionMode,
|
||||
}
|
||||
|
||||
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome)
|
||||
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome)
|
||||
const bundle = target.convert(plugin, options)
|
||||
if (!bundle) {
|
||||
throw new Error(`Target ${targetName} did not return a bundle.`)
|
||||
@@ -106,7 +112,7 @@ export default defineCommand({
|
||||
console.warn(`Skipping ${extra}: no output returned.`)
|
||||
continue
|
||||
}
|
||||
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome)
|
||||
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, piHome)
|
||||
await handler.write(extraRoot, extraBundle)
|
||||
console.log(`Converted ${plugin.manifest.name} to ${extra} at ${extraRoot}`)
|
||||
}
|
||||
@@ -137,6 +143,18 @@ function resolveCodexRoot(value: unknown): string {
|
||||
return resolveCodexHome(value) ?? path.join(os.homedir(), ".codex")
|
||||
}
|
||||
|
||||
function resolvePiHome(value: unknown): string | null {
|
||||
if (!value) return null
|
||||
const raw = String(value).trim()
|
||||
if (!raw) return null
|
||||
const expanded = expandHome(raw)
|
||||
return path.resolve(expanded)
|
||||
}
|
||||
|
||||
function resolvePiRoot(value: unknown): string {
|
||||
return resolvePiHome(value) ?? path.join(os.homedir(), ".pi", "agent")
|
||||
}
|
||||
|
||||
function expandHome(value: string): string {
|
||||
if (value === "~") return os.homedir()
|
||||
if (value.startsWith(`~${path.sep}`)) {
|
||||
@@ -153,8 +171,9 @@ function resolveOutputRoot(value: unknown): string {
|
||||
return process.cwd()
|
||||
}
|
||||
|
||||
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string): string {
|
||||
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string, piHome: string): string {
|
||||
if (targetName === "codex") return codexHome
|
||||
if (targetName === "pi") return piHome
|
||||
if (targetName === "droid") return path.join(os.homedir(), ".factory")
|
||||
if (targetName === "cursor") return path.join(outputRoot, ".cursor")
|
||||
return outputRoot
|
||||
|
||||
@@ -24,7 +24,7 @@ export default defineCommand({
|
||||
to: {
|
||||
type: "string",
|
||||
default: "opencode",
|
||||
description: "Target format (opencode | codex | droid | cursor)",
|
||||
description: "Target format (opencode | codex | droid | cursor | pi)",
|
||||
},
|
||||
output: {
|
||||
type: "string",
|
||||
@@ -36,6 +36,11 @@ export default defineCommand({
|
||||
alias: "codex-home",
|
||||
description: "Write Codex output to this .codex root (ex: ~/.codex)",
|
||||
},
|
||||
piHome: {
|
||||
type: "string",
|
||||
alias: "pi-home",
|
||||
description: "Write Pi output to this Pi root (ex: ~/.pi/agent or ./.pi)",
|
||||
},
|
||||
also: {
|
||||
type: "string",
|
||||
description: "Comma-separated extra targets to generate (ex: codex)",
|
||||
@@ -77,6 +82,7 @@ export default defineCommand({
|
||||
const plugin = await loadClaudePlugin(resolvedPlugin.path)
|
||||
const outputRoot = resolveOutputRoot(args.output)
|
||||
const codexHome = resolveCodexRoot(args.codexHome)
|
||||
const piHome = resolvePiRoot(args.piHome)
|
||||
|
||||
const options = {
|
||||
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
|
||||
@@ -89,7 +95,7 @@ export default defineCommand({
|
||||
throw new Error(`Target ${targetName} did not return a bundle.`)
|
||||
}
|
||||
const hasExplicitOutput = Boolean(args.output && String(args.output).trim())
|
||||
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, hasExplicitOutput)
|
||||
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome, hasExplicitOutput)
|
||||
await target.write(primaryOutputRoot, bundle)
|
||||
console.log(`Installed ${plugin.manifest.name} to ${primaryOutputRoot}`)
|
||||
|
||||
@@ -110,7 +116,7 @@ export default defineCommand({
|
||||
console.warn(`Skipping ${extra}: no output returned.`)
|
||||
continue
|
||||
}
|
||||
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, hasExplicitOutput)
|
||||
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, piHome, hasExplicitOutput)
|
||||
await handler.write(extraRoot, extraBundle)
|
||||
console.log(`Installed ${plugin.manifest.name} to ${extraRoot}`)
|
||||
}
|
||||
@@ -164,6 +170,18 @@ function resolveCodexRoot(value: unknown): string {
|
||||
return resolveCodexHome(value) ?? path.join(os.homedir(), ".codex")
|
||||
}
|
||||
|
||||
function resolvePiHome(value: unknown): string | null {
|
||||
if (!value) return null
|
||||
const raw = String(value).trim()
|
||||
if (!raw) return null
|
||||
const expanded = expandHome(raw)
|
||||
return path.resolve(expanded)
|
||||
}
|
||||
|
||||
function resolvePiRoot(value: unknown): string {
|
||||
return resolvePiHome(value) ?? path.join(os.homedir(), ".pi", "agent")
|
||||
}
|
||||
|
||||
function expandHome(value: string): string {
|
||||
if (value === "~") return os.homedir()
|
||||
if (value.startsWith(`~${path.sep}`)) {
|
||||
@@ -182,8 +200,15 @@ function resolveOutputRoot(value: unknown): string {
|
||||
return path.join(os.homedir(), ".config", "opencode")
|
||||
}
|
||||
|
||||
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string, hasExplicitOutput: boolean): string {
|
||||
function resolveTargetOutputRoot(
|
||||
targetName: string,
|
||||
outputRoot: string,
|
||||
codexHome: string,
|
||||
piHome: string,
|
||||
hasExplicitOutput: boolean,
|
||||
): string {
|
||||
if (targetName === "codex") return codexHome
|
||||
if (targetName === "pi") return piHome
|
||||
if (targetName === "droid") return path.join(os.homedir(), ".factory")
|
||||
if (targetName === "cursor") {
|
||||
const base = hasExplicitOutput ? outputRoot : process.cwd()
|
||||
|
||||
@@ -4,9 +4,10 @@ import path from "path"
|
||||
import { loadClaudeHome } from "../parsers/claude-home"
|
||||
import { syncToOpenCode } from "../sync/opencode"
|
||||
import { syncToCodex } from "../sync/codex"
|
||||
import { syncToPi } from "../sync/pi"
|
||||
|
||||
function isValidTarget(value: string): value is "opencode" | "codex" {
|
||||
return value === "opencode" || value === "codex"
|
||||
function isValidTarget(value: string): value is "opencode" | "codex" | "pi" {
|
||||
return value === "opencode" || value === "codex" || value === "pi"
|
||||
}
|
||||
|
||||
/** Check if any MCP servers have env vars that might contain secrets */
|
||||
@@ -26,13 +27,13 @@ function hasPotentialSecrets(mcpServers: Record<string, unknown>): boolean {
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: "sync",
|
||||
description: "Sync Claude Code config (~/.claude/) to OpenCode or Codex",
|
||||
description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, or Pi",
|
||||
},
|
||||
args: {
|
||||
target: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "Target: opencode | codex",
|
||||
description: "Target: opencode | codex | pi",
|
||||
},
|
||||
claudeHome: {
|
||||
type: "string",
|
||||
@@ -42,7 +43,7 @@ export default defineCommand({
|
||||
},
|
||||
async run({ args }) {
|
||||
if (!isValidTarget(args.target)) {
|
||||
throw new Error(`Unknown target: ${args.target}. Use 'opencode' or 'codex'.`)
|
||||
throw new Error(`Unknown target: ${args.target}. Use 'opencode', 'codex', or 'pi'.`)
|
||||
}
|
||||
|
||||
const claudeHome = expandHome(args.claudeHome ?? path.join(os.homedir(), ".claude"))
|
||||
@@ -63,12 +64,16 @@ export default defineCommand({
|
||||
const outputRoot =
|
||||
args.target === "opencode"
|
||||
? path.join(os.homedir(), ".config", "opencode")
|
||||
: path.join(os.homedir(), ".codex")
|
||||
: args.target === "codex"
|
||||
? path.join(os.homedir(), ".codex")
|
||||
: path.join(os.homedir(), ".pi", "agent")
|
||||
|
||||
if (args.target === "opencode") {
|
||||
await syncToOpenCode(config, outputRoot)
|
||||
} else {
|
||||
} else if (args.target === "codex") {
|
||||
await syncToCodex(config, outputRoot)
|
||||
} else {
|
||||
await syncToPi(config, outputRoot)
|
||||
}
|
||||
|
||||
console.log(`✓ Synced to ${args.target}: ${outputRoot}`)
|
||||
|
||||
205
src/converters/claude-to-pi.ts
Normal file
205
src/converters/claude-to-pi.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
||||
import type {
|
||||
PiBundle,
|
||||
PiGeneratedSkill,
|
||||
PiMcporterConfig,
|
||||
PiMcporterServer,
|
||||
} from "../types/pi"
|
||||
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
||||
import { PI_COMPAT_EXTENSION_SOURCE } from "../templates/pi/compat-extension"
|
||||
|
||||
export type ClaudeToPiOptions = ClaudeToOpenCodeOptions
|
||||
|
||||
const PI_DESCRIPTION_MAX_LENGTH = 1024
|
||||
|
||||
export function convertClaudeToPi(
|
||||
plugin: ClaudePlugin,
|
||||
_options: ClaudeToPiOptions,
|
||||
): PiBundle {
|
||||
const promptNames = new Set<string>()
|
||||
const usedSkillNames = new Set<string>(plugin.skills.map((skill) => normalizeName(skill.name)))
|
||||
|
||||
const prompts = plugin.commands
|
||||
.filter((command) => !command.disableModelInvocation)
|
||||
.map((command) => convertPrompt(command, promptNames))
|
||||
|
||||
const generatedSkills = plugin.agents.map((agent) => convertAgent(agent, usedSkillNames))
|
||||
|
||||
const extensions = [
|
||||
{
|
||||
name: "compound-engineering-compat.ts",
|
||||
content: PI_COMPAT_EXTENSION_SOURCE,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
prompts,
|
||||
skillDirs: plugin.skills.map((skill) => ({
|
||||
name: skill.name,
|
||||
sourceDir: skill.sourceDir,
|
||||
})),
|
||||
generatedSkills,
|
||||
extensions,
|
||||
mcporterConfig: plugin.mcpServers ? convertMcpToMcporter(plugin.mcpServers) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function convertPrompt(command: ClaudeCommand, usedNames: Set<string>) {
|
||||
const name = uniqueName(normalizeName(command.name), usedNames)
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
description: command.description,
|
||||
"argument-hint": command.argumentHint,
|
||||
}
|
||||
|
||||
let body = transformContentForPi(command.body)
|
||||
body = appendCompatibilityNoteIfNeeded(body)
|
||||
|
||||
return {
|
||||
name,
|
||||
content: formatFrontmatter(frontmatter, body.trim()),
|
||||
}
|
||||
}
|
||||
|
||||
function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): PiGeneratedSkill {
|
||||
const name = uniqueName(normalizeName(agent.name), usedNames)
|
||||
const description = sanitizeDescription(
|
||||
agent.description ?? `Converted from Claude agent ${agent.name}`,
|
||||
)
|
||||
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
name,
|
||||
description,
|
||||
}
|
||||
|
||||
const sections: string[] = []
|
||||
if (agent.capabilities && agent.capabilities.length > 0) {
|
||||
sections.push(`## Capabilities\n${agent.capabilities.map((capability) => `- ${capability}`).join("\n")}`)
|
||||
}
|
||||
|
||||
const body = [
|
||||
...sections,
|
||||
agent.body.trim().length > 0
|
||||
? agent.body.trim()
|
||||
: `Instructions converted from the ${agent.name} agent.`,
|
||||
].join("\n\n")
|
||||
|
||||
return {
|
||||
name,
|
||||
content: formatFrontmatter(frontmatter, body),
|
||||
}
|
||||
}
|
||||
|
||||
function transformContentForPi(body: string): string {
|
||||
let result = body
|
||||
|
||||
// Task repo-research-analyst(feature_description)
|
||||
// -> Run subagent with agent="repo-research-analyst" and task="feature_description"
|
||||
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
|
||||
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
||||
const skillName = normalizeName(agentName)
|
||||
const trimmedArgs = args.trim().replace(/\s+/g, " ")
|
||||
return `${prefix}Run subagent with agent=\"${skillName}\" and task=\"${trimmedArgs}\".`
|
||||
})
|
||||
|
||||
// Claude-specific tool references
|
||||
result = result.replace(/\bAskUserQuestion\b/g, "ask_user_question")
|
||||
result = result.replace(/\bTodoWrite\b/g, "file-based todos (todos/ + /skill:file-todos)")
|
||||
result = result.replace(/\bTodoRead\b/g, "file-based todos (todos/ + /skill:file-todos)")
|
||||
|
||||
// /command-name or /workflows:command-name -> /workflows-command-name
|
||||
const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
|
||||
result = result.replace(slashCommandPattern, (match, commandName: string) => {
|
||||
if (commandName.includes("/")) return match
|
||||
if (["dev", "tmp", "etc", "usr", "var", "bin", "home"].includes(commandName)) {
|
||||
return match
|
||||
}
|
||||
|
||||
if (commandName.startsWith("skill:")) {
|
||||
const skillName = commandName.slice("skill:".length)
|
||||
return `/skill:${normalizeName(skillName)}`
|
||||
}
|
||||
|
||||
const withoutPrefix = commandName.startsWith("prompts:")
|
||||
? commandName.slice("prompts:".length)
|
||||
: commandName
|
||||
|
||||
return `/${normalizeName(withoutPrefix)}`
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function appendCompatibilityNoteIfNeeded(body: string): string {
|
||||
if (!/\bmcp\b/i.test(body)) return body
|
||||
|
||||
const note = [
|
||||
"",
|
||||
"## Pi + MCPorter note",
|
||||
"For MCP access in Pi, use MCPorter via the generated tools:",
|
||||
"- `mcporter_list` to inspect available MCP tools",
|
||||
"- `mcporter_call` to invoke a tool",
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
return body + note
|
||||
}
|
||||
|
||||
function convertMcpToMcporter(servers: Record<string, ClaudeMcpServer>): PiMcporterConfig {
|
||||
const mcpServers: Record<string, PiMcporterServer> = {}
|
||||
|
||||
for (const [name, server] of Object.entries(servers)) {
|
||||
if (server.command) {
|
||||
mcpServers[name] = {
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env,
|
||||
headers: server.headers,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (server.url) {
|
||||
mcpServers[name] = {
|
||||
baseUrl: server.url,
|
||||
headers: server.headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { mcpServers }
|
||||
}
|
||||
|
||||
function normalizeName(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return "item"
|
||||
const normalized = trimmed
|
||||
.toLowerCase()
|
||||
.replace(/[\\/]+/g, "-")
|
||||
.replace(/[:\s]+/g, "-")
|
||||
.replace(/[^a-z0-9_-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
return normalized || "item"
|
||||
}
|
||||
|
||||
function sanitizeDescription(value: string, maxLength = PI_DESCRIPTION_MAX_LENGTH): string {
|
||||
const normalized = value.replace(/\s+/g, " ").trim()
|
||||
if (normalized.length <= maxLength) return normalized
|
||||
const ellipsis = "..."
|
||||
return normalized.slice(0, Math.max(0, maxLength - ellipsis.length)).trimEnd() + ellipsis
|
||||
}
|
||||
|
||||
function uniqueName(base: string, used: Set<string>): string {
|
||||
if (!used.has(base)) {
|
||||
used.add(base)
|
||||
return base
|
||||
}
|
||||
let index = 2
|
||||
while (used.has(`${base}-${index}`)) {
|
||||
index += 1
|
||||
}
|
||||
const name = `${base}-${index}`
|
||||
used.add(name)
|
||||
return name
|
||||
}
|
||||
88
src/sync/pi.ts
Normal file
88
src/sync/pi.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
||||
import type { ClaudeMcpServer } from "../types/claude"
|
||||
import { forceSymlink, isValidSkillName } from "../utils/symlink"
|
||||
|
||||
type McporterServer = {
|
||||
baseUrl?: string
|
||||
command?: string
|
||||
args?: string[]
|
||||
env?: Record<string, string>
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
type McporterConfig = {
|
||||
mcpServers: Record<string, McporterServer>
|
||||
}
|
||||
|
||||
export async function syncToPi(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
const skillsDir = path.join(outputRoot, "skills")
|
||||
const mcporterPath = path.join(outputRoot, "compound-engineering", "mcporter.json")
|
||||
|
||||
await fs.mkdir(skillsDir, { recursive: true })
|
||||
|
||||
for (const skill of config.skills) {
|
||||
if (!isValidSkillName(skill.name)) {
|
||||
console.warn(`Skipping skill with invalid name: ${skill.name}`)
|
||||
continue
|
||||
}
|
||||
const target = path.join(skillsDir, skill.name)
|
||||
await forceSymlink(skill.sourceDir, target)
|
||||
}
|
||||
|
||||
if (Object.keys(config.mcpServers).length > 0) {
|
||||
await fs.mkdir(path.dirname(mcporterPath), { recursive: true })
|
||||
|
||||
const existing = await readJsonSafe(mcporterPath)
|
||||
const converted = convertMcpToMcporter(config.mcpServers)
|
||||
const merged: McporterConfig = {
|
||||
mcpServers: {
|
||||
...(existing.mcpServers ?? {}),
|
||||
...converted.mcpServers,
|
||||
},
|
||||
}
|
||||
|
||||
await fs.writeFile(mcporterPath, JSON.stringify(merged, null, 2), { mode: 0o600 })
|
||||
}
|
||||
}
|
||||
|
||||
async function readJsonSafe(filePath: string): Promise<Partial<McporterConfig>> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
return JSON.parse(content) as Partial<McporterConfig>
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return {}
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function convertMcpToMcporter(servers: Record<string, ClaudeMcpServer>): McporterConfig {
|
||||
const mcpServers: Record<string, McporterServer> = {}
|
||||
|
||||
for (const [name, server] of Object.entries(servers)) {
|
||||
if (server.command) {
|
||||
mcpServers[name] = {
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env,
|
||||
headers: server.headers,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (server.url) {
|
||||
mcpServers[name] = {
|
||||
baseUrl: server.url,
|
||||
headers: server.headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { mcpServers }
|
||||
}
|
||||
@@ -3,14 +3,17 @@ import type { OpenCodeBundle } from "../types/opencode"
|
||||
import type { CodexBundle } from "../types/codex"
|
||||
import type { DroidBundle } from "../types/droid"
|
||||
import type { CursorBundle } from "../types/cursor"
|
||||
import type { PiBundle } from "../types/pi"
|
||||
import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
|
||||
import { convertClaudeToCodex } from "../converters/claude-to-codex"
|
||||
import { convertClaudeToDroid } from "../converters/claude-to-droid"
|
||||
import { convertClaudeToCursor } from "../converters/claude-to-cursor"
|
||||
import { convertClaudeToPi } from "../converters/claude-to-pi"
|
||||
import { writeOpenCodeBundle } from "./opencode"
|
||||
import { writeCodexBundle } from "./codex"
|
||||
import { writeDroidBundle } from "./droid"
|
||||
import { writeCursorBundle } from "./cursor"
|
||||
import { writePiBundle } from "./pi"
|
||||
|
||||
export type TargetHandler<TBundle = unknown> = {
|
||||
name: string
|
||||
@@ -44,4 +47,10 @@ export const targets: Record<string, TargetHandler> = {
|
||||
convert: convertClaudeToCursor as TargetHandler<CursorBundle>["convert"],
|
||||
write: writeCursorBundle as TargetHandler<CursorBundle>["write"],
|
||||
},
|
||||
pi: {
|
||||
name: "pi",
|
||||
implemented: true,
|
||||
convert: convertClaudeToPi as TargetHandler<PiBundle>["convert"],
|
||||
write: writePiBundle as TargetHandler<PiBundle>["write"],
|
||||
},
|
||||
}
|
||||
|
||||
131
src/targets/pi.ts
Normal file
131
src/targets/pi.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import path from "path"
|
||||
import {
|
||||
backupFile,
|
||||
copyDir,
|
||||
ensureDir,
|
||||
pathExists,
|
||||
readText,
|
||||
writeJson,
|
||||
writeText,
|
||||
} from "../utils/files"
|
||||
import type { PiBundle } from "../types/pi"
|
||||
|
||||
const PI_AGENTS_BLOCK_START = "<!-- BEGIN COMPOUND PI TOOL MAP -->"
|
||||
const PI_AGENTS_BLOCK_END = "<!-- END COMPOUND PI TOOL MAP -->"
|
||||
|
||||
const PI_AGENTS_BLOCK_BODY = `## Compound Engineering (Pi compatibility)
|
||||
|
||||
This block is managed by compound-plugin.
|
||||
|
||||
Compatibility notes:
|
||||
- Claude Task(agent, args) maps to the subagent extension tool
|
||||
- For parallel agent runs, batch multiple subagent calls with multi_tool_use.parallel
|
||||
- AskUserQuestion maps to the ask_user_question extension tool
|
||||
- MCP access uses MCPorter via mcporter_list and mcporter_call extension tools
|
||||
- MCPorter config path: .pi/compound-engineering/mcporter.json (project) or ~/.pi/agent/compound-engineering/mcporter.json (global)
|
||||
`
|
||||
|
||||
export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promise<void> {
|
||||
const paths = resolvePiPaths(outputRoot)
|
||||
|
||||
await ensureDir(paths.skillsDir)
|
||||
await ensureDir(paths.promptsDir)
|
||||
await ensureDir(paths.extensionsDir)
|
||||
|
||||
for (const prompt of bundle.prompts) {
|
||||
await writeText(path.join(paths.promptsDir, `${prompt.name}.md`), prompt.content + "\n")
|
||||
}
|
||||
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copyDir(skill.sourceDir, path.join(paths.skillsDir, skill.name))
|
||||
}
|
||||
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(paths.skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
|
||||
for (const extension of bundle.extensions) {
|
||||
await writeText(path.join(paths.extensionsDir, extension.name), extension.content + "\n")
|
||||
}
|
||||
|
||||
if (bundle.mcporterConfig) {
|
||||
const backupPath = await backupFile(paths.mcporterConfigPath)
|
||||
if (backupPath) {
|
||||
console.log(`Backed up existing MCPorter config to ${backupPath}`)
|
||||
}
|
||||
await writeJson(paths.mcporterConfigPath, bundle.mcporterConfig)
|
||||
}
|
||||
|
||||
await ensurePiAgentsBlock(paths.agentsPath)
|
||||
}
|
||||
|
||||
function resolvePiPaths(outputRoot: string) {
|
||||
const base = path.basename(outputRoot)
|
||||
|
||||
// Global install root: ~/.pi/agent
|
||||
if (base === "agent") {
|
||||
return {
|
||||
skillsDir: path.join(outputRoot, "skills"),
|
||||
promptsDir: path.join(outputRoot, "prompts"),
|
||||
extensionsDir: path.join(outputRoot, "extensions"),
|
||||
mcporterConfigPath: path.join(outputRoot, "compound-engineering", "mcporter.json"),
|
||||
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
||||
}
|
||||
}
|
||||
|
||||
// Project local .pi directory
|
||||
if (base === ".pi") {
|
||||
return {
|
||||
skillsDir: path.join(outputRoot, "skills"),
|
||||
promptsDir: path.join(outputRoot, "prompts"),
|
||||
extensionsDir: path.join(outputRoot, "extensions"),
|
||||
mcporterConfigPath: path.join(outputRoot, "compound-engineering", "mcporter.json"),
|
||||
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
||||
}
|
||||
}
|
||||
|
||||
// Custom output root -> nest under .pi
|
||||
return {
|
||||
skillsDir: path.join(outputRoot, ".pi", "skills"),
|
||||
promptsDir: path.join(outputRoot, ".pi", "prompts"),
|
||||
extensionsDir: path.join(outputRoot, ".pi", "extensions"),
|
||||
mcporterConfigPath: path.join(outputRoot, ".pi", "compound-engineering", "mcporter.json"),
|
||||
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePiAgentsBlock(filePath: string): Promise<void> {
|
||||
const block = buildPiAgentsBlock()
|
||||
|
||||
if (!(await pathExists(filePath))) {
|
||||
await writeText(filePath, block + "\n")
|
||||
return
|
||||
}
|
||||
|
||||
const existing = await readText(filePath)
|
||||
const updated = upsertBlock(existing, block)
|
||||
if (updated !== existing) {
|
||||
await writeText(filePath, updated)
|
||||
}
|
||||
}
|
||||
|
||||
function buildPiAgentsBlock(): string {
|
||||
return [PI_AGENTS_BLOCK_START, PI_AGENTS_BLOCK_BODY.trim(), PI_AGENTS_BLOCK_END].join("\n")
|
||||
}
|
||||
|
||||
function upsertBlock(existing: string, block: string): string {
|
||||
const startIndex = existing.indexOf(PI_AGENTS_BLOCK_START)
|
||||
const endIndex = existing.indexOf(PI_AGENTS_BLOCK_END)
|
||||
|
||||
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
||||
const before = existing.slice(0, startIndex).trimEnd()
|
||||
const after = existing.slice(endIndex + PI_AGENTS_BLOCK_END.length).trimStart()
|
||||
return [before, block, after].filter(Boolean).join("\n\n") + "\n"
|
||||
}
|
||||
|
||||
if (existing.trim().length === 0) {
|
||||
return block + "\n"
|
||||
}
|
||||
|
||||
return existing.trimEnd() + "\n\n" + block + "\n"
|
||||
}
|
||||
452
src/templates/pi/compat-extension.ts
Normal file
452
src/templates/pi/compat-extension.ts
Normal file
@@ -0,0 +1,452 @@
|
||||
export const PI_COMPAT_EXTENSION_SOURCE = `import fs from "node:fs"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
||||
import { Type } from "@sinclair/typebox"
|
||||
|
||||
const MAX_BYTES = 50 * 1024
|
||||
const DEFAULT_SUBAGENT_TIMEOUT_MS = 10 * 60 * 1000
|
||||
const MAX_PARALLEL_SUBAGENTS = 8
|
||||
|
||||
type SubagentTask = {
|
||||
agent: string
|
||||
task: string
|
||||
cwd?: string
|
||||
}
|
||||
|
||||
type SubagentResult = {
|
||||
agent: string
|
||||
task: string
|
||||
cwd: string
|
||||
exitCode: number
|
||||
output: string
|
||||
stderr: string
|
||||
}
|
||||
|
||||
function truncate(value: string): string {
|
||||
const input = value ?? ""
|
||||
if (Buffer.byteLength(input, "utf8") <= MAX_BYTES) return input
|
||||
const head = input.slice(0, MAX_BYTES)
|
||||
return head + "\\n\\n[Output truncated to 50KB]"
|
||||
}
|
||||
|
||||
function shellEscape(value: string): string {
|
||||
return "'" + value.replace(/'/g, "'\\"'\\"'") + "'"
|
||||
}
|
||||
|
||||
function normalizeName(value: string): string {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
}
|
||||
|
||||
function resolveBundledMcporterConfigPath(): string | undefined {
|
||||
try {
|
||||
const extensionDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const candidates = [
|
||||
path.join(extensionDir, "..", "pi-resources", "compound-engineering", "mcporter.json"),
|
||||
path.join(extensionDir, "..", "compound-engineering", "mcporter.json"),
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) return candidate
|
||||
}
|
||||
} catch {
|
||||
// noop: bundled path is best-effort fallback
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function resolveMcporterConfigPath(cwd: string, explicit?: string): string | undefined {
|
||||
if (explicit && explicit.trim()) {
|
||||
return path.resolve(explicit)
|
||||
}
|
||||
|
||||
const projectPath = path.join(cwd, ".pi", "compound-engineering", "mcporter.json")
|
||||
if (fs.existsSync(projectPath)) return projectPath
|
||||
|
||||
const globalPath = path.join(os.homedir(), ".pi", "agent", "compound-engineering", "mcporter.json")
|
||||
if (fs.existsSync(globalPath)) return globalPath
|
||||
|
||||
return resolveBundledMcporterConfigPath()
|
||||
}
|
||||
|
||||
function resolveTaskCwd(baseCwd: string, taskCwd?: string): string {
|
||||
if (!taskCwd || !taskCwd.trim()) return baseCwd
|
||||
const expanded = taskCwd === "~"
|
||||
? os.homedir()
|
||||
: taskCwd.startsWith("~" + path.sep)
|
||||
? path.join(os.homedir(), taskCwd.slice(2))
|
||||
: taskCwd
|
||||
return path.resolve(baseCwd, expanded)
|
||||
}
|
||||
|
||||
async function runSingleSubagent(
|
||||
pi: ExtensionAPI,
|
||||
baseCwd: string,
|
||||
task: SubagentTask,
|
||||
signal?: AbortSignal,
|
||||
timeoutMs = DEFAULT_SUBAGENT_TIMEOUT_MS,
|
||||
): Promise<SubagentResult> {
|
||||
const agent = normalizeName(task.agent)
|
||||
if (!agent) {
|
||||
throw new Error("Subagent task is missing a valid agent name")
|
||||
}
|
||||
|
||||
const taskText = String(task.task ?? "").trim()
|
||||
if (!taskText) {
|
||||
throw new Error("Subagent task for " + agent + " is empty")
|
||||
}
|
||||
|
||||
const cwd = resolveTaskCwd(baseCwd, task.cwd)
|
||||
const prompt = "/skill:" + agent + " " + taskText
|
||||
const script = "cd " + shellEscape(cwd) + " && pi --no-session -p " + shellEscape(prompt)
|
||||
const result = await pi.exec("bash", ["-lc", script], { signal, timeout: timeoutMs })
|
||||
|
||||
return {
|
||||
agent,
|
||||
task: taskText,
|
||||
cwd,
|
||||
exitCode: result.code,
|
||||
output: truncate(result.stdout || ""),
|
||||
stderr: truncate(result.stderr || ""),
|
||||
}
|
||||
}
|
||||
|
||||
async function runParallelSubagents(
|
||||
pi: ExtensionAPI,
|
||||
baseCwd: string,
|
||||
tasks: SubagentTask[],
|
||||
signal?: AbortSignal,
|
||||
timeoutMs = DEFAULT_SUBAGENT_TIMEOUT_MS,
|
||||
maxConcurrency = 4,
|
||||
onProgress?: (completed: number, total: number) => void,
|
||||
): Promise<SubagentResult[]> {
|
||||
const safeConcurrency = Math.max(1, Math.min(maxConcurrency, MAX_PARALLEL_SUBAGENTS, tasks.length))
|
||||
const results: SubagentResult[] = new Array(tasks.length)
|
||||
|
||||
let nextIndex = 0
|
||||
let completed = 0
|
||||
|
||||
const workers = Array.from({ length: safeConcurrency }, async () => {
|
||||
while (true) {
|
||||
const current = nextIndex
|
||||
nextIndex += 1
|
||||
if (current >= tasks.length) return
|
||||
|
||||
results[current] = await runSingleSubagent(pi, baseCwd, tasks[current], signal, timeoutMs)
|
||||
completed += 1
|
||||
onProgress?.(completed, tasks.length)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(workers)
|
||||
return results
|
||||
}
|
||||
|
||||
function formatSubagentSummary(results: SubagentResult[]): string {
|
||||
if (results.length === 0) return "No subagent work was executed."
|
||||
|
||||
const success = results.filter((result) => result.exitCode === 0).length
|
||||
const failed = results.length - success
|
||||
const header = failed === 0
|
||||
? "Subagent run completed: " + success + "/" + results.length + " succeeded."
|
||||
: "Subagent run completed: " + success + "/" + results.length + " succeeded, " + failed + " failed."
|
||||
|
||||
const lines = results.map((result) => {
|
||||
const status = result.exitCode === 0 ? "ok" : "error"
|
||||
const body = result.output || result.stderr || "(no output)"
|
||||
const preview = body.split("\\n").slice(0, 6).join("\\n")
|
||||
return "\\n[" + status + "] " + result.agent + "\\n" + preview
|
||||
})
|
||||
|
||||
return header + lines.join("\\n")
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "ask_user_question",
|
||||
label: "Ask User Question",
|
||||
description: "Ask the user a question with optional choices.",
|
||||
parameters: Type.Object({
|
||||
question: Type.String({ description: "Question shown to the user" }),
|
||||
options: Type.Optional(Type.Array(Type.String(), { description: "Selectable options" })),
|
||||
allowCustom: Type.Optional(Type.Boolean({ default: true })),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
if (!ctx.hasUI) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: "UI is unavailable in this mode." }],
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
|
||||
const options = params.options ?? []
|
||||
const allowCustom = params.allowCustom ?? true
|
||||
|
||||
if (options.length === 0) {
|
||||
const answer = await ctx.ui.input(params.question)
|
||||
if (!answer) {
|
||||
return {
|
||||
content: [{ type: "text", text: "User cancelled." }],
|
||||
details: { answer: null },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "User answered: " + answer }],
|
||||
details: { answer, mode: "input" },
|
||||
}
|
||||
}
|
||||
|
||||
const customLabel = "Other (type custom answer)"
|
||||
const selectable = allowCustom ? [...options, customLabel] : options
|
||||
const selected = await ctx.ui.select(params.question, selectable)
|
||||
|
||||
if (!selected) {
|
||||
return {
|
||||
content: [{ type: "text", text: "User cancelled." }],
|
||||
details: { answer: null },
|
||||
}
|
||||
}
|
||||
|
||||
if (selected === customLabel) {
|
||||
const custom = await ctx.ui.input("Your answer")
|
||||
if (!custom) {
|
||||
return {
|
||||
content: [{ type: "text", text: "User cancelled." }],
|
||||
details: { answer: null },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "User answered: " + custom }],
|
||||
details: { answer: custom, mode: "custom" },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "User selected: " + selected }],
|
||||
details: { answer: selected, mode: "select" },
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const subagentTaskSchema = Type.Object({
|
||||
agent: Type.String({ description: "Skill/agent name to invoke" }),
|
||||
task: Type.String({ description: "Task instructions for that skill" }),
|
||||
cwd: Type.Optional(Type.String({ description: "Optional working directory for this task" })),
|
||||
})
|
||||
|
||||
pi.registerTool({
|
||||
name: "subagent",
|
||||
label: "Subagent",
|
||||
description: "Run one or more skill-based subagent tasks. Supports single, parallel, and chained execution.",
|
||||
parameters: Type.Object({
|
||||
agent: Type.Optional(Type.String({ description: "Single subagent name" })),
|
||||
task: Type.Optional(Type.String({ description: "Single subagent task" })),
|
||||
cwd: Type.Optional(Type.String({ description: "Working directory for single mode" })),
|
||||
tasks: Type.Optional(Type.Array(subagentTaskSchema, { description: "Parallel subagent tasks" })),
|
||||
chain: Type.Optional(Type.Array(subagentTaskSchema, { description: "Sequential tasks; supports {previous} placeholder" })),
|
||||
maxConcurrency: Type.Optional(Type.Number({ default: 4 })),
|
||||
timeoutMs: Type.Optional(Type.Number({ default: DEFAULT_SUBAGENT_TIMEOUT_MS })),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
||||
const hasSingle = Boolean(params.agent && params.task)
|
||||
const hasTasks = Boolean(params.tasks && params.tasks.length > 0)
|
||||
const hasChain = Boolean(params.chain && params.chain.length > 0)
|
||||
const modeCount = Number(hasSingle) + Number(hasTasks) + Number(hasChain)
|
||||
|
||||
if (modeCount !== 1) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: "Provide exactly one mode: single (agent+task), tasks, or chain." }],
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutMs = Number(params.timeoutMs || DEFAULT_SUBAGENT_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
if (hasSingle) {
|
||||
const result = await runSingleSubagent(
|
||||
pi,
|
||||
ctx.cwd,
|
||||
{ agent: params.agent!, task: params.task!, cwd: params.cwd },
|
||||
signal,
|
||||
timeoutMs,
|
||||
)
|
||||
|
||||
const body = formatSubagentSummary([result])
|
||||
return {
|
||||
isError: result.exitCode !== 0,
|
||||
content: [{ type: "text", text: body }],
|
||||
details: { mode: "single", results: [result] },
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTasks) {
|
||||
const tasks = params.tasks as SubagentTask[]
|
||||
const maxConcurrency = Number(params.maxConcurrency || 4)
|
||||
|
||||
const results = await runParallelSubagents(
|
||||
pi,
|
||||
ctx.cwd,
|
||||
tasks,
|
||||
signal,
|
||||
timeoutMs,
|
||||
maxConcurrency,
|
||||
(completed, total) => {
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: "Subagent progress: " + completed + "/" + total }],
|
||||
details: { mode: "parallel", completed, total },
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const body = formatSubagentSummary(results)
|
||||
const hasFailure = results.some((result) => result.exitCode !== 0)
|
||||
|
||||
return {
|
||||
isError: hasFailure,
|
||||
content: [{ type: "text", text: body }],
|
||||
details: { mode: "parallel", results },
|
||||
}
|
||||
}
|
||||
|
||||
const chain = params.chain as SubagentTask[]
|
||||
const results: SubagentResult[] = []
|
||||
let previous = ""
|
||||
|
||||
for (const step of chain) {
|
||||
const resolvedTask = step.task.replace(/\\{previous\\}/g, previous)
|
||||
const result = await runSingleSubagent(
|
||||
pi,
|
||||
ctx.cwd,
|
||||
{ agent: step.agent, task: resolvedTask, cwd: step.cwd },
|
||||
signal,
|
||||
timeoutMs,
|
||||
)
|
||||
results.push(result)
|
||||
previous = result.output || result.stderr
|
||||
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: "Subagent chain progress: " + results.length + "/" + chain.length }],
|
||||
details: { mode: "chain", completed: results.length, total: chain.length },
|
||||
})
|
||||
|
||||
if (result.exitCode !== 0) break
|
||||
}
|
||||
|
||||
const body = formatSubagentSummary(results)
|
||||
const hasFailure = results.some((result) => result.exitCode !== 0)
|
||||
|
||||
return {
|
||||
isError: hasFailure,
|
||||
content: [{ type: "text", text: body }],
|
||||
details: { mode: "chain", results },
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
pi.registerTool({
|
||||
name: "mcporter_list",
|
||||
label: "MCPorter List",
|
||||
description: "List tools on an MCP server through MCPorter.",
|
||||
parameters: Type.Object({
|
||||
server: Type.String({ description: "Configured MCP server name" }),
|
||||
allParameters: Type.Optional(Type.Boolean({ default: false })),
|
||||
json: Type.Optional(Type.Boolean({ default: true })),
|
||||
configPath: Type.Optional(Type.String({ description: "Optional mcporter config path" })),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
||||
const args = ["list", params.server]
|
||||
if (params.allParameters) args.push("--all-parameters")
|
||||
if (params.json ?? true) args.push("--json")
|
||||
|
||||
const configPath = resolveMcporterConfigPath(ctx.cwd, params.configPath)
|
||||
if (configPath) {
|
||||
args.push("--config", configPath)
|
||||
}
|
||||
|
||||
const result = await pi.exec("mcporter", args, { signal })
|
||||
const output = truncate(result.stdout || result.stderr || "")
|
||||
|
||||
return {
|
||||
isError: result.code !== 0,
|
||||
content: [{ type: "text", text: output || "(no output)" }],
|
||||
details: {
|
||||
exitCode: result.code,
|
||||
command: "mcporter " + args.join(" "),
|
||||
configPath,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
pi.registerTool({
|
||||
name: "mcporter_call",
|
||||
label: "MCPorter Call",
|
||||
description: "Call a specific MCP tool through MCPorter.",
|
||||
parameters: Type.Object({
|
||||
call: Type.Optional(Type.String({ description: "Function-style call, e.g. linear.list_issues(limit: 5)" })),
|
||||
server: Type.Optional(Type.String({ description: "Server name (if call is omitted)" })),
|
||||
tool: Type.Optional(Type.String({ description: "Tool name (if call is omitted)" })),
|
||||
args: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "JSON arguments object" })),
|
||||
configPath: Type.Optional(Type.String({ description: "Optional mcporter config path" })),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
||||
const args = ["call"]
|
||||
|
||||
if (params.call && params.call.trim()) {
|
||||
args.push(params.call.trim())
|
||||
} else {
|
||||
if (!params.server || !params.tool) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: "Provide either call, or server + tool." }],
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
args.push(params.server + "." + params.tool)
|
||||
if (params.args) {
|
||||
args.push("--args", JSON.stringify(params.args))
|
||||
}
|
||||
}
|
||||
|
||||
args.push("--output", "json")
|
||||
|
||||
const configPath = resolveMcporterConfigPath(ctx.cwd, params.configPath)
|
||||
if (configPath) {
|
||||
args.push("--config", configPath)
|
||||
}
|
||||
|
||||
const result = await pi.exec("mcporter", args, { signal })
|
||||
const output = truncate(result.stdout || result.stderr || "")
|
||||
|
||||
return {
|
||||
isError: result.code !== 0,
|
||||
content: [{ type: "text", text: output || "(no output)" }],
|
||||
details: {
|
||||
exitCode: result.code,
|
||||
command: "mcporter " + args.join(" "),
|
||||
configPath,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
`
|
||||
40
src/types/pi.ts
Normal file
40
src/types/pi.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export type PiPrompt = {
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type PiSkillDir = {
|
||||
name: string
|
||||
sourceDir: string
|
||||
}
|
||||
|
||||
export type PiGeneratedSkill = {
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type PiExtensionFile = {
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type PiMcporterServer = {
|
||||
description?: string
|
||||
baseUrl?: string
|
||||
command?: string
|
||||
args?: string[]
|
||||
env?: Record<string, string>
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export type PiMcporterConfig = {
|
||||
mcpServers: Record<string, PiMcporterServer>
|
||||
}
|
||||
|
||||
export type PiBundle = {
|
||||
prompts: PiPrompt[]
|
||||
skillDirs: PiSkillDir[]
|
||||
generatedSkills: PiGeneratedSkill[]
|
||||
extensions: PiExtensionFile[]
|
||||
mcporterConfig?: PiMcporterConfig
|
||||
}
|
||||
Reference in New Issue
Block a user