@every-env/compound-plugin 0.5.2 → 0.8.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/.cursor-plugin/marketplace.json +25 -0
- package/CHANGELOG.md +47 -0
- package/README.md +29 -6
- package/bun.lock +1 -0
- package/docs/brainstorms/2026-02-14-copilot-converter-target-brainstorm.md +117 -0
- package/docs/brainstorms/2026-02-17-copilot-skill-naming-brainstorm.md +30 -0
- package/docs/plans/2026-02-14-feat-add-copilot-converter-target-plan.md +328 -0
- package/docs/plans/2026-02-14-feat-add-gemini-cli-target-provider-plan.md +370 -0
- package/docs/specs/copilot.md +122 -0
- package/docs/specs/gemini.md +122 -0
- package/package.json +1 -1
- package/plugins/coding-tutor/.cursor-plugin/plugin.json +21 -0
- package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
- package/plugins/compound-engineering/.cursor-plugin/plugin.json +31 -0
- package/plugins/compound-engineering/.mcp.json +8 -0
- package/plugins/compound-engineering/CHANGELOG.md +27 -0
- package/plugins/compound-engineering/commands/lfg.md +3 -3
- package/plugins/compound-engineering/commands/slfg.md +2 -2
- package/plugins/compound-engineering/commands/workflows/plan.md +18 -1
- package/plugins/compound-engineering/commands/workflows/work.md +8 -1
- package/src/commands/convert.ts +14 -25
- package/src/commands/install.ts +27 -25
- package/src/commands/sync.ts +44 -21
- package/src/converters/{claude-to-cursor.ts → claude-to-copilot.ts} +93 -49
- 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/copilot.ts +100 -0
- package/src/sync/droid.ts +21 -0
- package/src/sync/pi.ts +88 -0
- package/src/targets/copilot.ts +48 -0
- package/src/targets/gemini.ts +68 -0
- package/src/targets/index.ts +25 -7
- package/src/targets/pi.ts +131 -0
- package/src/templates/pi/compat-extension.ts +452 -0
- package/src/types/copilot.ts +31 -0
- package/src/types/gemini.ts +29 -0
- package/src/types/pi.ts +40 -0
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/resolve-home.ts +17 -0
- package/tests/cli.test.ts +76 -0
- package/tests/converter.test.ts +29 -0
- package/tests/copilot-converter.test.ts +467 -0
- package/tests/copilot-writer.test.ts +189 -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-copilot.test.ts +148 -0
- package/tests/sync-droid.test.ts +57 -0
- package/tests/sync-pi.test.ts +68 -0
- package/src/targets/cursor.ts +0 -48
- package/src/types/cursor.ts +0 -29
- package/tests/cursor-converter.test.ts +0 -347
- package/tests/cursor-writer.test.ts +0 -137
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
|
4
|
+
import type { ClaudeMcpServer } from "../types/claude"
|
|
5
|
+
import { forceSymlink, isValidSkillName } from "../utils/symlink"
|
|
6
|
+
|
|
7
|
+
type CopilotMcpServer = {
|
|
8
|
+
type: string
|
|
9
|
+
command?: string
|
|
10
|
+
args?: string[]
|
|
11
|
+
url?: string
|
|
12
|
+
tools: string[]
|
|
13
|
+
env?: Record<string, string>
|
|
14
|
+
headers?: Record<string, string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type CopilotMcpConfig = {
|
|
18
|
+
mcpServers: Record<string, CopilotMcpServer>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function syncToCopilot(
|
|
22
|
+
config: ClaudeHomeConfig,
|
|
23
|
+
outputRoot: string,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const skillsDir = path.join(outputRoot, "skills")
|
|
26
|
+
await fs.mkdir(skillsDir, { recursive: true })
|
|
27
|
+
|
|
28
|
+
for (const skill of config.skills) {
|
|
29
|
+
if (!isValidSkillName(skill.name)) {
|
|
30
|
+
console.warn(`Skipping skill with invalid name: ${skill.name}`)
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
const target = path.join(skillsDir, skill.name)
|
|
34
|
+
await forceSymlink(skill.sourceDir, target)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (Object.keys(config.mcpServers).length > 0) {
|
|
38
|
+
const mcpPath = path.join(outputRoot, "copilot-mcp-config.json")
|
|
39
|
+
const existing = await readJsonSafe(mcpPath)
|
|
40
|
+
const converted = convertMcpForCopilot(config.mcpServers)
|
|
41
|
+
const merged: CopilotMcpConfig = {
|
|
42
|
+
mcpServers: {
|
|
43
|
+
...(existing.mcpServers ?? {}),
|
|
44
|
+
...converted,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
await fs.writeFile(mcpPath, JSON.stringify(merged, null, 2), { mode: 0o600 })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function readJsonSafe(filePath: string): Promise<Partial<CopilotMcpConfig>> {
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.readFile(filePath, "utf-8")
|
|
54
|
+
return JSON.parse(content) as Partial<CopilotMcpConfig>
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
57
|
+
return {}
|
|
58
|
+
}
|
|
59
|
+
throw err
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function convertMcpForCopilot(
|
|
64
|
+
servers: Record<string, ClaudeMcpServer>,
|
|
65
|
+
): Record<string, CopilotMcpServer> {
|
|
66
|
+
const result: Record<string, CopilotMcpServer> = {}
|
|
67
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
68
|
+
const entry: CopilotMcpServer = {
|
|
69
|
+
type: server.command ? "local" : "sse",
|
|
70
|
+
tools: ["*"],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (server.command) {
|
|
74
|
+
entry.command = server.command
|
|
75
|
+
if (server.args && server.args.length > 0) entry.args = server.args
|
|
76
|
+
} else if (server.url) {
|
|
77
|
+
entry.url = server.url
|
|
78
|
+
if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
82
|
+
entry.env = prefixEnvVars(server.env)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
result[name] = entry
|
|
86
|
+
}
|
|
87
|
+
return result
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function prefixEnvVars(env: Record<string, string>): Record<string, string> {
|
|
91
|
+
const result: Record<string, string> = {}
|
|
92
|
+
for (const [key, value] of Object.entries(env)) {
|
|
93
|
+
if (key.startsWith("COPILOT_MCP_")) {
|
|
94
|
+
result[key] = value
|
|
95
|
+
} else {
|
|
96
|
+
result[`COPILOT_MCP_${key}`] = value
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return result
|
|
100
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
|
4
|
+
import { forceSymlink, isValidSkillName } from "../utils/symlink"
|
|
5
|
+
|
|
6
|
+
export async function syncToDroid(
|
|
7
|
+
config: ClaudeHomeConfig,
|
|
8
|
+
outputRoot: string,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const skillsDir = path.join(outputRoot, "skills")
|
|
11
|
+
await fs.mkdir(skillsDir, { recursive: true })
|
|
12
|
+
|
|
13
|
+
for (const skill of config.skills) {
|
|
14
|
+
if (!isValidSkillName(skill.name)) {
|
|
15
|
+
console.warn(`Skipping skill with invalid name: ${skill.name}`)
|
|
16
|
+
continue
|
|
17
|
+
}
|
|
18
|
+
const target = path.join(skillsDir, skill.name)
|
|
19
|
+
await forceSymlink(skill.sourceDir, target)
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/sync/pi.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
|
4
|
+
import type { ClaudeMcpServer } from "../types/claude"
|
|
5
|
+
import { forceSymlink, isValidSkillName } from "../utils/symlink"
|
|
6
|
+
|
|
7
|
+
type McporterServer = {
|
|
8
|
+
baseUrl?: string
|
|
9
|
+
command?: string
|
|
10
|
+
args?: string[]
|
|
11
|
+
env?: Record<string, string>
|
|
12
|
+
headers?: Record<string, string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type McporterConfig = {
|
|
16
|
+
mcpServers: Record<string, McporterServer>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function syncToPi(
|
|
20
|
+
config: ClaudeHomeConfig,
|
|
21
|
+
outputRoot: string,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const skillsDir = path.join(outputRoot, "skills")
|
|
24
|
+
const mcporterPath = path.join(outputRoot, "compound-engineering", "mcporter.json")
|
|
25
|
+
|
|
26
|
+
await fs.mkdir(skillsDir, { recursive: true })
|
|
27
|
+
|
|
28
|
+
for (const skill of config.skills) {
|
|
29
|
+
if (!isValidSkillName(skill.name)) {
|
|
30
|
+
console.warn(`Skipping skill with invalid name: ${skill.name}`)
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
const target = path.join(skillsDir, skill.name)
|
|
34
|
+
await forceSymlink(skill.sourceDir, target)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (Object.keys(config.mcpServers).length > 0) {
|
|
38
|
+
await fs.mkdir(path.dirname(mcporterPath), { recursive: true })
|
|
39
|
+
|
|
40
|
+
const existing = await readJsonSafe(mcporterPath)
|
|
41
|
+
const converted = convertMcpToMcporter(config.mcpServers)
|
|
42
|
+
const merged: McporterConfig = {
|
|
43
|
+
mcpServers: {
|
|
44
|
+
...(existing.mcpServers ?? {}),
|
|
45
|
+
...converted.mcpServers,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await fs.writeFile(mcporterPath, JSON.stringify(merged, null, 2), { mode: 0o600 })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readJsonSafe(filePath: string): Promise<Partial<McporterConfig>> {
|
|
54
|
+
try {
|
|
55
|
+
const content = await fs.readFile(filePath, "utf-8")
|
|
56
|
+
return JSON.parse(content) as Partial<McporterConfig>
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
59
|
+
return {}
|
|
60
|
+
}
|
|
61
|
+
throw err
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function convertMcpToMcporter(servers: Record<string, ClaudeMcpServer>): McporterConfig {
|
|
66
|
+
const mcpServers: Record<string, McporterServer> = {}
|
|
67
|
+
|
|
68
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
69
|
+
if (server.command) {
|
|
70
|
+
mcpServers[name] = {
|
|
71
|
+
command: server.command,
|
|
72
|
+
args: server.args,
|
|
73
|
+
env: server.env,
|
|
74
|
+
headers: server.headers,
|
|
75
|
+
}
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (server.url) {
|
|
80
|
+
mcpServers[name] = {
|
|
81
|
+
baseUrl: server.url,
|
|
82
|
+
headers: server.headers,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { mcpServers }
|
|
88
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files"
|
|
3
|
+
import type { CopilotBundle } from "../types/copilot"
|
|
4
|
+
|
|
5
|
+
export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBundle): Promise<void> {
|
|
6
|
+
const paths = resolveCopilotPaths(outputRoot)
|
|
7
|
+
await ensureDir(paths.githubDir)
|
|
8
|
+
|
|
9
|
+
if (bundle.agents.length > 0) {
|
|
10
|
+
const agentsDir = path.join(paths.githubDir, "agents")
|
|
11
|
+
for (const agent of bundle.agents) {
|
|
12
|
+
await writeText(path.join(agentsDir, `${agent.name}.agent.md`), agent.content + "\n")
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (bundle.generatedSkills.length > 0) {
|
|
17
|
+
const skillsDir = path.join(paths.githubDir, "skills")
|
|
18
|
+
for (const skill of bundle.generatedSkills) {
|
|
19
|
+
await writeText(path.join(skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (bundle.skillDirs.length > 0) {
|
|
24
|
+
const skillsDir = path.join(paths.githubDir, "skills")
|
|
25
|
+
for (const skill of bundle.skillDirs) {
|
|
26
|
+
await copyDir(skill.sourceDir, path.join(skillsDir, skill.name))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (bundle.mcpConfig && Object.keys(bundle.mcpConfig).length > 0) {
|
|
31
|
+
const mcpPath = path.join(paths.githubDir, "copilot-mcp-config.json")
|
|
32
|
+
const backupPath = await backupFile(mcpPath)
|
|
33
|
+
if (backupPath) {
|
|
34
|
+
console.log(`Backed up existing copilot-mcp-config.json to ${backupPath}`)
|
|
35
|
+
}
|
|
36
|
+
await writeJson(mcpPath, { mcpServers: bundle.mcpConfig })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveCopilotPaths(outputRoot: string) {
|
|
41
|
+
const base = path.basename(outputRoot)
|
|
42
|
+
// If already pointing at .github, write directly into it
|
|
43
|
+
if (base === ".github") {
|
|
44
|
+
return { githubDir: outputRoot }
|
|
45
|
+
}
|
|
46
|
+
// Otherwise nest under .github
|
|
47
|
+
return { githubDir: path.join(outputRoot, ".github") }
|
|
48
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files"
|
|
3
|
+
import type { GeminiBundle } from "../types/gemini"
|
|
4
|
+
|
|
5
|
+
export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle): Promise<void> {
|
|
6
|
+
const paths = resolveGeminiPaths(outputRoot)
|
|
7
|
+
await ensureDir(paths.geminiDir)
|
|
8
|
+
|
|
9
|
+
if (bundle.generatedSkills.length > 0) {
|
|
10
|
+
for (const skill of bundle.generatedSkills) {
|
|
11
|
+
await writeText(path.join(paths.skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (bundle.skillDirs.length > 0) {
|
|
16
|
+
for (const skill of bundle.skillDirs) {
|
|
17
|
+
await copyDir(skill.sourceDir, path.join(paths.skillsDir, skill.name))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (bundle.commands.length > 0) {
|
|
22
|
+
for (const command of bundle.commands) {
|
|
23
|
+
await writeText(path.join(paths.commandsDir, `${command.name}.toml`), command.content + "\n")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (bundle.mcpServers && Object.keys(bundle.mcpServers).length > 0) {
|
|
28
|
+
const settingsPath = path.join(paths.geminiDir, "settings.json")
|
|
29
|
+
const backupPath = await backupFile(settingsPath)
|
|
30
|
+
if (backupPath) {
|
|
31
|
+
console.log(`Backed up existing settings.json to ${backupPath}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Merge mcpServers into existing settings if present
|
|
35
|
+
let existingSettings: Record<string, unknown> = {}
|
|
36
|
+
if (await pathExists(settingsPath)) {
|
|
37
|
+
try {
|
|
38
|
+
existingSettings = await readJson<Record<string, unknown>>(settingsPath)
|
|
39
|
+
} catch {
|
|
40
|
+
console.warn("Warning: existing settings.json could not be parsed and will be replaced.")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const existingMcp = (existingSettings.mcpServers && typeof existingSettings.mcpServers === "object")
|
|
45
|
+
? existingSettings.mcpServers as Record<string, unknown>
|
|
46
|
+
: {}
|
|
47
|
+
const merged = { ...existingSettings, mcpServers: { ...existingMcp, ...bundle.mcpServers } }
|
|
48
|
+
await writeJson(settingsPath, merged)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveGeminiPaths(outputRoot: string) {
|
|
53
|
+
const base = path.basename(outputRoot)
|
|
54
|
+
// If already pointing at .gemini, write directly into it
|
|
55
|
+
if (base === ".gemini") {
|
|
56
|
+
return {
|
|
57
|
+
geminiDir: outputRoot,
|
|
58
|
+
skillsDir: path.join(outputRoot, "skills"),
|
|
59
|
+
commandsDir: path.join(outputRoot, "commands"),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Otherwise nest under .gemini
|
|
63
|
+
return {
|
|
64
|
+
geminiDir: path.join(outputRoot, ".gemini"),
|
|
65
|
+
skillsDir: path.join(outputRoot, ".gemini", "skills"),
|
|
66
|
+
commandsDir: path.join(outputRoot, ".gemini", "commands"),
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/targets/index.ts
CHANGED
|
@@ -2,15 +2,21 @@ import type { ClaudePlugin } from "../types/claude"
|
|
|
2
2
|
import type { OpenCodeBundle } from "../types/opencode"
|
|
3
3
|
import type { CodexBundle } from "../types/codex"
|
|
4
4
|
import type { DroidBundle } from "../types/droid"
|
|
5
|
-
import type {
|
|
5
|
+
import type { PiBundle } from "../types/pi"
|
|
6
|
+
import type { CopilotBundle } from "../types/copilot"
|
|
7
|
+
import type { GeminiBundle } from "../types/gemini"
|
|
6
8
|
import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
|
|
7
9
|
import { convertClaudeToCodex } from "../converters/claude-to-codex"
|
|
8
10
|
import { convertClaudeToDroid } from "../converters/claude-to-droid"
|
|
9
|
-
import {
|
|
11
|
+
import { convertClaudeToPi } from "../converters/claude-to-pi"
|
|
12
|
+
import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
|
|
13
|
+
import { convertClaudeToGemini } from "../converters/claude-to-gemini"
|
|
10
14
|
import { writeOpenCodeBundle } from "./opencode"
|
|
11
15
|
import { writeCodexBundle } from "./codex"
|
|
12
16
|
import { writeDroidBundle } from "./droid"
|
|
13
|
-
import {
|
|
17
|
+
import { writePiBundle } from "./pi"
|
|
18
|
+
import { writeCopilotBundle } from "./copilot"
|
|
19
|
+
import { writeGeminiBundle } from "./gemini"
|
|
14
20
|
|
|
15
21
|
export type TargetHandler<TBundle = unknown> = {
|
|
16
22
|
name: string
|
|
@@ -38,10 +44,22 @@ export const targets: Record<string, TargetHandler> = {
|
|
|
38
44
|
convert: convertClaudeToDroid as TargetHandler<DroidBundle>["convert"],
|
|
39
45
|
write: writeDroidBundle as TargetHandler<DroidBundle>["write"],
|
|
40
46
|
},
|
|
41
|
-
|
|
42
|
-
name: "
|
|
47
|
+
pi: {
|
|
48
|
+
name: "pi",
|
|
43
49
|
implemented: true,
|
|
44
|
-
convert:
|
|
45
|
-
write:
|
|
50
|
+
convert: convertClaudeToPi as TargetHandler<PiBundle>["convert"],
|
|
51
|
+
write: writePiBundle as TargetHandler<PiBundle>["write"],
|
|
52
|
+
},
|
|
53
|
+
copilot: {
|
|
54
|
+
name: "copilot",
|
|
55
|
+
implemented: true,
|
|
56
|
+
convert: convertClaudeToCopilot as TargetHandler<CopilotBundle>["convert"],
|
|
57
|
+
write: writeCopilotBundle as TargetHandler<CopilotBundle>["write"],
|
|
58
|
+
},
|
|
59
|
+
gemini: {
|
|
60
|
+
name: "gemini",
|
|
61
|
+
implemented: true,
|
|
62
|
+
convert: convertClaudeToGemini as TargetHandler<GeminiBundle>["convert"],
|
|
63
|
+
write: writeGeminiBundle as TargetHandler<GeminiBundle>["write"],
|
|
46
64
|
},
|
|
47
65
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import {
|
|
3
|
+
backupFile,
|
|
4
|
+
copyDir,
|
|
5
|
+
ensureDir,
|
|
6
|
+
pathExists,
|
|
7
|
+
readText,
|
|
8
|
+
writeJson,
|
|
9
|
+
writeText,
|
|
10
|
+
} from "../utils/files"
|
|
11
|
+
import type { PiBundle } from "../types/pi"
|
|
12
|
+
|
|
13
|
+
const PI_AGENTS_BLOCK_START = "<!-- BEGIN COMPOUND PI TOOL MAP -->"
|
|
14
|
+
const PI_AGENTS_BLOCK_END = "<!-- END COMPOUND PI TOOL MAP -->"
|
|
15
|
+
|
|
16
|
+
const PI_AGENTS_BLOCK_BODY = `## Compound Engineering (Pi compatibility)
|
|
17
|
+
|
|
18
|
+
This block is managed by compound-plugin.
|
|
19
|
+
|
|
20
|
+
Compatibility notes:
|
|
21
|
+
- Claude Task(agent, args) maps to the subagent extension tool
|
|
22
|
+
- For parallel agent runs, batch multiple subagent calls with multi_tool_use.parallel
|
|
23
|
+
- AskUserQuestion maps to the ask_user_question extension tool
|
|
24
|
+
- MCP access uses MCPorter via mcporter_list and mcporter_call extension tools
|
|
25
|
+
- MCPorter config path: .pi/compound-engineering/mcporter.json (project) or ~/.pi/agent/compound-engineering/mcporter.json (global)
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promise<void> {
|
|
29
|
+
const paths = resolvePiPaths(outputRoot)
|
|
30
|
+
|
|
31
|
+
await ensureDir(paths.skillsDir)
|
|
32
|
+
await ensureDir(paths.promptsDir)
|
|
33
|
+
await ensureDir(paths.extensionsDir)
|
|
34
|
+
|
|
35
|
+
for (const prompt of bundle.prompts) {
|
|
36
|
+
await writeText(path.join(paths.promptsDir, `${prompt.name}.md`), prompt.content + "\n")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const skill of bundle.skillDirs) {
|
|
40
|
+
await copyDir(skill.sourceDir, path.join(paths.skillsDir, skill.name))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const skill of bundle.generatedSkills) {
|
|
44
|
+
await writeText(path.join(paths.skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const extension of bundle.extensions) {
|
|
48
|
+
await writeText(path.join(paths.extensionsDir, extension.name), extension.content + "\n")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (bundle.mcporterConfig) {
|
|
52
|
+
const backupPath = await backupFile(paths.mcporterConfigPath)
|
|
53
|
+
if (backupPath) {
|
|
54
|
+
console.log(`Backed up existing MCPorter config to ${backupPath}`)
|
|
55
|
+
}
|
|
56
|
+
await writeJson(paths.mcporterConfigPath, bundle.mcporterConfig)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await ensurePiAgentsBlock(paths.agentsPath)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolvePiPaths(outputRoot: string) {
|
|
63
|
+
const base = path.basename(outputRoot)
|
|
64
|
+
|
|
65
|
+
// Global install root: ~/.pi/agent
|
|
66
|
+
if (base === "agent") {
|
|
67
|
+
return {
|
|
68
|
+
skillsDir: path.join(outputRoot, "skills"),
|
|
69
|
+
promptsDir: path.join(outputRoot, "prompts"),
|
|
70
|
+
extensionsDir: path.join(outputRoot, "extensions"),
|
|
71
|
+
mcporterConfigPath: path.join(outputRoot, "compound-engineering", "mcporter.json"),
|
|
72
|
+
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Project local .pi directory
|
|
77
|
+
if (base === ".pi") {
|
|
78
|
+
return {
|
|
79
|
+
skillsDir: path.join(outputRoot, "skills"),
|
|
80
|
+
promptsDir: path.join(outputRoot, "prompts"),
|
|
81
|
+
extensionsDir: path.join(outputRoot, "extensions"),
|
|
82
|
+
mcporterConfigPath: path.join(outputRoot, "compound-engineering", "mcporter.json"),
|
|
83
|
+
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Custom output root -> nest under .pi
|
|
88
|
+
return {
|
|
89
|
+
skillsDir: path.join(outputRoot, ".pi", "skills"),
|
|
90
|
+
promptsDir: path.join(outputRoot, ".pi", "prompts"),
|
|
91
|
+
extensionsDir: path.join(outputRoot, ".pi", "extensions"),
|
|
92
|
+
mcporterConfigPath: path.join(outputRoot, ".pi", "compound-engineering", "mcporter.json"),
|
|
93
|
+
agentsPath: path.join(outputRoot, "AGENTS.md"),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function ensurePiAgentsBlock(filePath: string): Promise<void> {
|
|
98
|
+
const block = buildPiAgentsBlock()
|
|
99
|
+
|
|
100
|
+
if (!(await pathExists(filePath))) {
|
|
101
|
+
await writeText(filePath, block + "\n")
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const existing = await readText(filePath)
|
|
106
|
+
const updated = upsertBlock(existing, block)
|
|
107
|
+
if (updated !== existing) {
|
|
108
|
+
await writeText(filePath, updated)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildPiAgentsBlock(): string {
|
|
113
|
+
return [PI_AGENTS_BLOCK_START, PI_AGENTS_BLOCK_BODY.trim(), PI_AGENTS_BLOCK_END].join("\n")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function upsertBlock(existing: string, block: string): string {
|
|
117
|
+
const startIndex = existing.indexOf(PI_AGENTS_BLOCK_START)
|
|
118
|
+
const endIndex = existing.indexOf(PI_AGENTS_BLOCK_END)
|
|
119
|
+
|
|
120
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
121
|
+
const before = existing.slice(0, startIndex).trimEnd()
|
|
122
|
+
const after = existing.slice(endIndex + PI_AGENTS_BLOCK_END.length).trimStart()
|
|
123
|
+
return [before, block, after].filter(Boolean).join("\n\n") + "\n"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (existing.trim().length === 0) {
|
|
127
|
+
return block + "\n"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return existing.trimEnd() + "\n\n" + block + "\n"
|
|
131
|
+
}
|