@every-env/compound-plugin 0.9.0 → 2.34.2
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 +3 -3
- package/.github/workflows/publish.yml +20 -10
- package/.releaserc.json +31 -0
- package/AGENTS.md +6 -1
- package/CHANGELOG.md +76 -0
- package/CLAUDE.md +16 -3
- package/README.md +83 -16
- package/bun.lock +977 -0
- package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
- package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
- package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
- package/docs/plans/2026-03-01-fix-setup-skill-non-claude-llm-fallback-plan.md +140 -0
- package/docs/plans/2026-03-03-feat-sync-claude-mcp-all-supported-providers-plan.md +639 -0
- package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
- package/docs/solutions/adding-converter-target-providers.md +693 -0
- package/docs/solutions/plugin-versioning-requirements.md +7 -3
- package/docs/specs/windsurf.md +477 -0
- package/package.json +10 -4
- package/plans/landing-page-launchkit-refresh.md +2 -2
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +82 -1
- package/plugins/compound-engineering/CLAUDE.md +14 -7
- package/plugins/compound-engineering/README.md +10 -7
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
- package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
- package/plugins/compound-engineering/commands/ce/compound.md +240 -0
- package/plugins/compound-engineering/commands/ce/plan.md +636 -0
- package/plugins/compound-engineering/commands/ce/review.md +525 -0
- package/plugins/compound-engineering/commands/ce/work.md +470 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
- package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
- package/plugins/compound-engineering/commands/feature-video.md +15 -6
- package/plugins/compound-engineering/commands/heal-skill.md +1 -1
- package/plugins/compound-engineering/commands/lfg.md +3 -3
- package/plugins/compound-engineering/commands/slfg.md +3 -3
- package/plugins/compound-engineering/commands/test-xcode.md +2 -2
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
- package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
- package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
- package/plugins/compound-engineering/commands/workflows/review.md +4 -522
- package/plugins/compound-engineering/commands/workflows/work.md +4 -448
- package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-workflow.md +6 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md +6 -0
- package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
- package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/setup/SKILL.md +8 -2
- package/src/commands/convert.ts +101 -24
- package/src/commands/install.ts +102 -45
- package/src/commands/sync.ts +43 -62
- package/src/converters/claude-to-openclaw.ts +240 -0
- package/src/converters/claude-to-opencode.ts +12 -10
- package/src/converters/claude-to-qwen.ts +238 -0
- package/src/converters/claude-to-windsurf.ts +205 -0
- package/src/index.ts +2 -1
- package/src/parsers/claude-home.ts +55 -3
- package/src/sync/codex.ts +38 -62
- package/src/sync/commands.ts +198 -0
- package/src/sync/copilot.ts +14 -36
- package/src/sync/droid.ts +50 -9
- package/src/sync/gemini.ts +135 -0
- package/src/sync/json-config.ts +47 -0
- package/src/sync/kiro.ts +49 -0
- package/src/sync/mcp-transports.ts +19 -0
- package/src/sync/openclaw.ts +18 -0
- package/src/sync/opencode.ts +10 -30
- package/src/sync/pi.ts +12 -36
- package/src/sync/qwen.ts +66 -0
- package/src/sync/registry.ts +141 -0
- package/src/sync/skills.ts +21 -0
- package/src/sync/windsurf.ts +59 -0
- package/src/targets/index.ts +60 -1
- package/src/targets/openclaw.ts +96 -0
- package/src/targets/opencode.ts +76 -10
- package/src/targets/qwen.ts +64 -0
- package/src/targets/windsurf.ts +104 -0
- package/src/types/kiro.ts +3 -1
- package/src/types/openclaw.ts +52 -0
- package/src/types/opencode.ts +7 -8
- package/src/types/qwen.ts +51 -0
- package/src/types/windsurf.ts +35 -0
- package/src/utils/codex-agents.ts +1 -1
- package/src/utils/detect-tools.ts +37 -0
- package/src/utils/files.ts +14 -0
- package/src/utils/resolve-output.ts +50 -0
- package/src/utils/secrets.ts +24 -0
- package/src/utils/symlink.ts +4 -6
- package/tests/claude-home.test.ts +46 -0
- package/tests/cli.test.ts +180 -0
- package/tests/converter.test.ts +43 -10
- package/tests/detect-tools.test.ts +119 -0
- package/tests/openclaw-converter.test.ts +200 -0
- package/tests/opencode-writer.test.ts +142 -5
- package/tests/qwen-converter.test.ts +238 -0
- package/tests/resolve-output.test.ts +131 -0
- package/tests/sync-codex.test.ts +64 -0
- package/tests/sync-copilot.test.ts +60 -4
- package/tests/sync-droid.test.ts +44 -4
- package/tests/sync-gemini.test.ts +160 -0
- package/tests/sync-kiro.test.ts +83 -0
- package/tests/sync-openclaw.test.ts +51 -0
- package/tests/sync-qwen.test.ts +75 -0
- package/tests/sync-windsurf.test.ts +89 -0
- package/tests/windsurf-converter.test.ts +573 -0
- package/tests/windsurf-writer.test.ts +359 -0
- package/docs/css/docs.css +0 -675
- package/docs/css/style.css +0 -2886
- package/docs/index.html +0 -1046
- package/docs/js/main.js +0 -225
- package/docs/pages/agents.html +0 -649
- package/docs/pages/changelog.html +0 -534
- package/docs/pages/commands.html +0 -523
- package/docs/pages/getting-started.html +0 -582
- package/docs/pages/mcp-servers.html +0 -409
- package/docs/pages/skills.html +0 -611
package/tests/cli.test.ts
CHANGED
|
@@ -426,4 +426,184 @@ describe("CLI", () => {
|
|
|
426
426
|
expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true)
|
|
427
427
|
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
|
|
428
428
|
})
|
|
429
|
+
|
|
430
|
+
test("install --to opencode uses permissions:none by default", async () => {
|
|
431
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-perms-none-"))
|
|
432
|
+
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
|
433
|
+
|
|
434
|
+
const proc = Bun.spawn([
|
|
435
|
+
"bun",
|
|
436
|
+
"run",
|
|
437
|
+
"src/index.ts",
|
|
438
|
+
"install",
|
|
439
|
+
fixtureRoot,
|
|
440
|
+
"--to",
|
|
441
|
+
"opencode",
|
|
442
|
+
"--output",
|
|
443
|
+
tempRoot,
|
|
444
|
+
], {
|
|
445
|
+
cwd: path.join(import.meta.dir, ".."),
|
|
446
|
+
stdout: "pipe",
|
|
447
|
+
stderr: "pipe",
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const exitCode = await proc.exited
|
|
451
|
+
const stdout = await new Response(proc.stdout).text()
|
|
452
|
+
const stderr = await new Response(proc.stderr).text()
|
|
453
|
+
|
|
454
|
+
if (exitCode !== 0) {
|
|
455
|
+
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
expect(stdout).toContain("Installed compound-engineering")
|
|
459
|
+
|
|
460
|
+
const opencodeJsonPath = path.join(tempRoot, "opencode.json")
|
|
461
|
+
const content = await fs.readFile(opencodeJsonPath, "utf-8")
|
|
462
|
+
const json = JSON.parse(content)
|
|
463
|
+
|
|
464
|
+
expect(json).not.toHaveProperty("permission")
|
|
465
|
+
expect(json).not.toHaveProperty("tools")
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test("install --to opencode --permissions broad writes permission block", async () => {
|
|
469
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-perms-broad-"))
|
|
470
|
+
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
|
471
|
+
|
|
472
|
+
const proc = Bun.spawn([
|
|
473
|
+
"bun",
|
|
474
|
+
"run",
|
|
475
|
+
"src/index.ts",
|
|
476
|
+
"install",
|
|
477
|
+
fixtureRoot,
|
|
478
|
+
"--to",
|
|
479
|
+
"opencode",
|
|
480
|
+
"--permissions",
|
|
481
|
+
"broad",
|
|
482
|
+
"--output",
|
|
483
|
+
tempRoot,
|
|
484
|
+
], {
|
|
485
|
+
cwd: path.join(import.meta.dir, ".."),
|
|
486
|
+
stdout: "pipe",
|
|
487
|
+
stderr: "pipe",
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const exitCode = await proc.exited
|
|
491
|
+
const stdout = await new Response(proc.stdout).text()
|
|
492
|
+
const stderr = await new Response(proc.stderr).text()
|
|
493
|
+
|
|
494
|
+
if (exitCode !== 0) {
|
|
495
|
+
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
expect(stdout).toContain("Installed compound-engineering")
|
|
499
|
+
|
|
500
|
+
const opencodeJsonPath = path.join(tempRoot, "opencode.json")
|
|
501
|
+
const content = await fs.readFile(opencodeJsonPath, "utf-8")
|
|
502
|
+
const json = JSON.parse(content)
|
|
503
|
+
|
|
504
|
+
expect(json).toHaveProperty("permission")
|
|
505
|
+
expect(json.permission).not.toBeNull()
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
test("sync --target all detects new sync targets and ignores stale cursor directories", async () => {
|
|
509
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "cli-sync-home-"))
|
|
510
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "cli-sync-cwd-"))
|
|
511
|
+
const repoRoot = path.join(import.meta.dir, "..")
|
|
512
|
+
const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
|
|
513
|
+
const claudeSkillsDir = path.join(tempHome, ".claude", "skills", "skill-one")
|
|
514
|
+
const claudeCommandsDir = path.join(tempHome, ".claude", "commands", "workflows")
|
|
515
|
+
|
|
516
|
+
await fs.mkdir(path.dirname(claudeSkillsDir), { recursive: true })
|
|
517
|
+
await fs.cp(fixtureSkillDir, claudeSkillsDir, { recursive: true })
|
|
518
|
+
await fs.mkdir(claudeCommandsDir, { recursive: true })
|
|
519
|
+
await fs.writeFile(
|
|
520
|
+
path.join(claudeCommandsDir, "plan.md"),
|
|
521
|
+
[
|
|
522
|
+
"---",
|
|
523
|
+
"name: workflows:plan",
|
|
524
|
+
"description: Plan work",
|
|
525
|
+
"argument-hint: \"[goal]\"",
|
|
526
|
+
"---",
|
|
527
|
+
"",
|
|
528
|
+
"Plan the work.",
|
|
529
|
+
].join("\n"),
|
|
530
|
+
)
|
|
531
|
+
await fs.writeFile(
|
|
532
|
+
path.join(tempHome, ".claude", "settings.json"),
|
|
533
|
+
JSON.stringify({
|
|
534
|
+
mcpServers: {
|
|
535
|
+
local: { command: "echo", args: ["hello"] },
|
|
536
|
+
remote: { url: "https://example.com/mcp" },
|
|
537
|
+
legacy: { type: "sse", url: "https://example.com/sse" },
|
|
538
|
+
},
|
|
539
|
+
}, null, 2),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
await fs.mkdir(path.join(tempHome, ".config", "opencode"), { recursive: true })
|
|
543
|
+
await fs.mkdir(path.join(tempHome, ".codex"), { recursive: true })
|
|
544
|
+
await fs.mkdir(path.join(tempHome, ".pi"), { recursive: true })
|
|
545
|
+
await fs.mkdir(path.join(tempHome, ".factory"), { recursive: true })
|
|
546
|
+
await fs.mkdir(path.join(tempHome, ".copilot"), { recursive: true })
|
|
547
|
+
await fs.mkdir(path.join(tempHome, ".gemini"), { recursive: true })
|
|
548
|
+
await fs.mkdir(path.join(tempHome, ".codeium", "windsurf"), { recursive: true })
|
|
549
|
+
await fs.mkdir(path.join(tempHome, ".kiro"), { recursive: true })
|
|
550
|
+
await fs.mkdir(path.join(tempHome, ".qwen"), { recursive: true })
|
|
551
|
+
await fs.mkdir(path.join(tempHome, ".openclaw"), { recursive: true })
|
|
552
|
+
await fs.mkdir(path.join(tempCwd, ".cursor"), { recursive: true })
|
|
553
|
+
|
|
554
|
+
const proc = Bun.spawn([
|
|
555
|
+
"bun",
|
|
556
|
+
"run",
|
|
557
|
+
path.join(repoRoot, "src", "index.ts"),
|
|
558
|
+
"sync",
|
|
559
|
+
"--target",
|
|
560
|
+
"all",
|
|
561
|
+
], {
|
|
562
|
+
cwd: tempCwd,
|
|
563
|
+
stdout: "pipe",
|
|
564
|
+
stderr: "pipe",
|
|
565
|
+
env: {
|
|
566
|
+
...process.env,
|
|
567
|
+
HOME: tempHome,
|
|
568
|
+
},
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
const exitCode = await proc.exited
|
|
572
|
+
const stdout = await new Response(proc.stdout).text()
|
|
573
|
+
const stderr = await new Response(proc.stderr).text()
|
|
574
|
+
|
|
575
|
+
if (exitCode !== 0) {
|
|
576
|
+
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
expect(stdout).toContain("Synced to codex")
|
|
580
|
+
expect(stdout).toContain("Synced to opencode")
|
|
581
|
+
expect(stdout).toContain("Synced to pi")
|
|
582
|
+
expect(stdout).toContain("Synced to droid")
|
|
583
|
+
expect(stdout).toContain("Synced to windsurf")
|
|
584
|
+
expect(stdout).toContain("Synced to kiro")
|
|
585
|
+
expect(stdout).toContain("Synced to qwen")
|
|
586
|
+
expect(stdout).toContain("Synced to openclaw")
|
|
587
|
+
expect(stdout).toContain("Synced to copilot")
|
|
588
|
+
expect(stdout).toContain("Synced to gemini")
|
|
589
|
+
expect(stdout).not.toContain("cursor")
|
|
590
|
+
|
|
591
|
+
expect(await exists(path.join(tempHome, ".config", "opencode", "commands", "workflows:plan.md"))).toBe(true)
|
|
592
|
+
expect(await exists(path.join(tempHome, ".codex", "config.toml"))).toBe(true)
|
|
593
|
+
expect(await exists(path.join(tempHome, ".codex", "prompts", "workflows-plan.md"))).toBe(true)
|
|
594
|
+
expect(await exists(path.join(tempHome, ".codex", "skills", "workflows-plan", "SKILL.md"))).toBe(true)
|
|
595
|
+
expect(await exists(path.join(tempHome, ".pi", "agent", "prompts", "workflows-plan.md"))).toBe(true)
|
|
596
|
+
expect(await exists(path.join(tempHome, ".factory", "commands", "plan.md"))).toBe(true)
|
|
597
|
+
expect(await exists(path.join(tempHome, ".codeium", "windsurf", "mcp_config.json"))).toBe(true)
|
|
598
|
+
expect(await exists(path.join(tempHome, ".codeium", "windsurf", "global_workflows", "workflows-plan.md"))).toBe(true)
|
|
599
|
+
expect(await exists(path.join(tempHome, ".kiro", "settings", "mcp.json"))).toBe(true)
|
|
600
|
+
expect(await exists(path.join(tempHome, ".kiro", "skills", "workflows-plan", "SKILL.md"))).toBe(true)
|
|
601
|
+
expect(await exists(path.join(tempHome, ".qwen", "settings.json"))).toBe(true)
|
|
602
|
+
expect(await exists(path.join(tempHome, ".qwen", "commands", "workflows", "plan.md"))).toBe(true)
|
|
603
|
+
expect(await exists(path.join(tempHome, ".copilot", "mcp-config.json"))).toBe(true)
|
|
604
|
+
expect(await exists(path.join(tempHome, ".copilot", "skills", "workflows-plan", "SKILL.md"))).toBe(true)
|
|
605
|
+
expect(await exists(path.join(tempHome, ".gemini", "settings.json"))).toBe(true)
|
|
606
|
+
expect(await exists(path.join(tempHome, ".gemini", "commands", "workflows", "plan.toml"))).toBe(true)
|
|
607
|
+
expect(await exists(path.join(tempHome, ".openclaw", "skills", "skill-one"))).toBe(true)
|
|
608
|
+
})
|
|
429
609
|
})
|
package/tests/converter.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ClaudePlugin } from "../src/types/claude"
|
|
|
8
8
|
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
|
9
9
|
|
|
10
10
|
describe("convertClaudeToOpenCode", () => {
|
|
11
|
-
test("
|
|
11
|
+
test("from-command mode: map allowedTools to global permission block", async () => {
|
|
12
12
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
|
13
13
|
const bundle = convertClaudeToOpenCode(plugin, {
|
|
14
14
|
agentMode: "subagent",
|
|
@@ -16,8 +16,9 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
16
16
|
permissions: "from-commands",
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
expect(bundle.config.command
|
|
20
|
-
expect(bundle.
|
|
19
|
+
expect(bundle.config.command).toBeUndefined()
|
|
20
|
+
expect(bundle.commandFiles.find((f) => f.name === "workflows:review")).toBeDefined()
|
|
21
|
+
expect(bundle.commandFiles.find((f) => f.name === "plan_review")).toBeDefined()
|
|
21
22
|
|
|
22
23
|
const permission = bundle.config.permission as Record<string, string | Record<string, string>>
|
|
23
24
|
expect(Object.keys(permission).sort()).toEqual([
|
|
@@ -71,8 +72,10 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
71
72
|
expect(parsed.data.model).toBe("anthropic/claude-sonnet-4-20250514")
|
|
72
73
|
expect(parsed.data.temperature).toBe(0.1)
|
|
73
74
|
|
|
74
|
-
const modelCommand = bundle.
|
|
75
|
-
expect(modelCommand
|
|
75
|
+
const modelCommand = bundle.commandFiles.find((f) => f.name === "workflows:work")
|
|
76
|
+
expect(modelCommand).toBeDefined()
|
|
77
|
+
const commandParsed = parseFrontmatter(modelCommand!.content)
|
|
78
|
+
expect(commandParsed.data.model).toBe("openai/gpt-4o")
|
|
76
79
|
})
|
|
77
80
|
|
|
78
81
|
test("resolves bare Claude model aliases to full IDs", () => {
|
|
@@ -199,7 +202,7 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
199
202
|
expect(parsed.data.mode).toBe("primary")
|
|
200
203
|
})
|
|
201
204
|
|
|
202
|
-
test("excludes commands with disable-model-invocation from
|
|
205
|
+
test("excludes commands with disable-model-invocation from commandFiles", async () => {
|
|
203
206
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
|
204
207
|
const bundle = convertClaudeToOpenCode(plugin, {
|
|
205
208
|
agentMode: "subagent",
|
|
@@ -208,10 +211,10 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
208
211
|
})
|
|
209
212
|
|
|
210
213
|
// deploy-docs has disable-model-invocation: true, should be excluded
|
|
211
|
-
expect(bundle.
|
|
214
|
+
expect(bundle.commandFiles.find((f) => f.name === "deploy-docs")).toBeUndefined()
|
|
212
215
|
|
|
213
216
|
// Normal commands should still be present
|
|
214
|
-
expect(bundle.
|
|
217
|
+
expect(bundle.commandFiles.find((f) => f.name === "workflows:review")).toBeDefined()
|
|
215
218
|
})
|
|
216
219
|
|
|
217
220
|
test("rewrites .claude/ paths to .opencode/ in command bodies", () => {
|
|
@@ -240,10 +243,11 @@ Run \`/compound-engineering-setup\` to create a settings file.`,
|
|
|
240
243
|
permissions: "none",
|
|
241
244
|
})
|
|
242
245
|
|
|
243
|
-
const
|
|
246
|
+
const commandFile = bundle.commandFiles.find((f) => f.name === "review")
|
|
247
|
+
expect(commandFile).toBeDefined()
|
|
244
248
|
|
|
245
249
|
// Tool-agnostic path in project root — no rewriting needed
|
|
246
|
-
expect(
|
|
250
|
+
expect(commandFile!.content).toContain("compound-engineering.local.md")
|
|
247
251
|
})
|
|
248
252
|
|
|
249
253
|
test("rewrites .claude/ paths in agent bodies", () => {
|
|
@@ -273,4 +277,33 @@ Run \`/compound-engineering-setup\` to create a settings file.`,
|
|
|
273
277
|
// Tool-agnostic path in project root — no rewriting needed
|
|
274
278
|
expect(agentFile!.content).toContain("compound-engineering.local.md")
|
|
275
279
|
})
|
|
280
|
+
|
|
281
|
+
test("command .md files include description in frontmatter", () => {
|
|
282
|
+
const plugin: ClaudePlugin = {
|
|
283
|
+
root: "/tmp/plugin",
|
|
284
|
+
manifest: { name: "fixture", version: "1.0.0" },
|
|
285
|
+
agents: [],
|
|
286
|
+
commands: [
|
|
287
|
+
{
|
|
288
|
+
name: "test-cmd",
|
|
289
|
+
description: "Test description",
|
|
290
|
+
body: "Do the thing",
|
|
291
|
+
sourcePath: "/tmp/plugin/commands/test-cmd.md",
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
skills: [],
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const bundle = convertClaudeToOpenCode(plugin, {
|
|
298
|
+
agentMode: "subagent",
|
|
299
|
+
inferTemperature: false,
|
|
300
|
+
permissions: "none",
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const commandFile = bundle.commandFiles.find((f) => f.name === "test-cmd")
|
|
304
|
+
expect(commandFile).toBeDefined()
|
|
305
|
+
const parsed = parseFrontmatter(commandFile!.content)
|
|
306
|
+
expect(parsed.data.description).toBe("Test description")
|
|
307
|
+
expect(parsed.body).toContain("Do the thing")
|
|
308
|
+
})
|
|
276
309
|
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { promises as fs } from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import os from "os"
|
|
5
|
+
import { detectInstalledTools, getDetectedTargetNames } from "../src/utils/detect-tools"
|
|
6
|
+
|
|
7
|
+
describe("detectInstalledTools", () => {
|
|
8
|
+
test("detects tools when config directories exist", async () => {
|
|
9
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-tools-"))
|
|
10
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-tools-cwd-"))
|
|
11
|
+
|
|
12
|
+
// Create directories for some tools
|
|
13
|
+
await fs.mkdir(path.join(tempHome, ".codex"), { recursive: true })
|
|
14
|
+
await fs.mkdir(path.join(tempHome, ".codeium", "windsurf"), { recursive: true })
|
|
15
|
+
await fs.mkdir(path.join(tempHome, ".gemini"), { recursive: true })
|
|
16
|
+
await fs.mkdir(path.join(tempHome, ".copilot"), { recursive: true })
|
|
17
|
+
|
|
18
|
+
const results = await detectInstalledTools(tempHome, tempCwd)
|
|
19
|
+
|
|
20
|
+
const codex = results.find((t) => t.name === "codex")
|
|
21
|
+
expect(codex?.detected).toBe(true)
|
|
22
|
+
expect(codex?.reason).toContain(".codex")
|
|
23
|
+
|
|
24
|
+
const windsurf = results.find((t) => t.name === "windsurf")
|
|
25
|
+
expect(windsurf?.detected).toBe(true)
|
|
26
|
+
expect(windsurf?.reason).toContain(".codeium/windsurf")
|
|
27
|
+
|
|
28
|
+
const gemini = results.find((t) => t.name === "gemini")
|
|
29
|
+
expect(gemini?.detected).toBe(true)
|
|
30
|
+
expect(gemini?.reason).toContain(".gemini")
|
|
31
|
+
|
|
32
|
+
const copilot = results.find((t) => t.name === "copilot")
|
|
33
|
+
expect(copilot?.detected).toBe(true)
|
|
34
|
+
expect(copilot?.reason).toContain(".copilot")
|
|
35
|
+
|
|
36
|
+
// Tools without directories should not be detected
|
|
37
|
+
const opencode = results.find((t) => t.name === "opencode")
|
|
38
|
+
expect(opencode?.detected).toBe(false)
|
|
39
|
+
|
|
40
|
+
const droid = results.find((t) => t.name === "droid")
|
|
41
|
+
expect(droid?.detected).toBe(false)
|
|
42
|
+
|
|
43
|
+
const pi = results.find((t) => t.name === "pi")
|
|
44
|
+
expect(pi?.detected).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("returns all tools with detected=false when no directories exist", async () => {
|
|
48
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-empty-"))
|
|
49
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-empty-cwd-"))
|
|
50
|
+
|
|
51
|
+
const results = await detectInstalledTools(tempHome, tempCwd)
|
|
52
|
+
|
|
53
|
+
expect(results.length).toBe(10)
|
|
54
|
+
for (const tool of results) {
|
|
55
|
+
expect(tool.detected).toBe(false)
|
|
56
|
+
expect(tool.reason).toBe("not found")
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("detects home-based tools", async () => {
|
|
61
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-home-"))
|
|
62
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-home-cwd-"))
|
|
63
|
+
|
|
64
|
+
await fs.mkdir(path.join(tempHome, ".config", "opencode"), { recursive: true })
|
|
65
|
+
await fs.mkdir(path.join(tempHome, ".factory"), { recursive: true })
|
|
66
|
+
await fs.mkdir(path.join(tempHome, ".pi"), { recursive: true })
|
|
67
|
+
await fs.mkdir(path.join(tempHome, ".openclaw"), { recursive: true })
|
|
68
|
+
|
|
69
|
+
const results = await detectInstalledTools(tempHome, tempCwd)
|
|
70
|
+
|
|
71
|
+
expect(results.find((t) => t.name === "opencode")?.detected).toBe(true)
|
|
72
|
+
expect(results.find((t) => t.name === "droid")?.detected).toBe(true)
|
|
73
|
+
expect(results.find((t) => t.name === "pi")?.detected).toBe(true)
|
|
74
|
+
expect(results.find((t) => t.name === "openclaw")?.detected).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("detects copilot from project-specific skills without generic .github false positives", async () => {
|
|
78
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-copilot-home-"))
|
|
79
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-copilot-cwd-"))
|
|
80
|
+
|
|
81
|
+
await fs.mkdir(path.join(tempCwd, ".github"), { recursive: true })
|
|
82
|
+
|
|
83
|
+
let results = await detectInstalledTools(tempHome, tempCwd)
|
|
84
|
+
expect(results.find((t) => t.name === "copilot")?.detected).toBe(false)
|
|
85
|
+
|
|
86
|
+
await fs.mkdir(path.join(tempCwd, ".github", "skills"), { recursive: true })
|
|
87
|
+
|
|
88
|
+
results = await detectInstalledTools(tempHome, tempCwd)
|
|
89
|
+
expect(results.find((t) => t.name === "copilot")?.detected).toBe(true)
|
|
90
|
+
expect(results.find((t) => t.name === "copilot")?.reason).toContain(".github/skills")
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("getDetectedTargetNames", () => {
|
|
95
|
+
test("returns only names of detected tools", async () => {
|
|
96
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-names-"))
|
|
97
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-names-cwd-"))
|
|
98
|
+
|
|
99
|
+
await fs.mkdir(path.join(tempHome, ".codex"), { recursive: true })
|
|
100
|
+
await fs.mkdir(path.join(tempHome, ".gemini"), { recursive: true })
|
|
101
|
+
|
|
102
|
+
const names = await getDetectedTargetNames(tempHome, tempCwd)
|
|
103
|
+
|
|
104
|
+
expect(names).toContain("codex")
|
|
105
|
+
expect(names).toContain("gemini")
|
|
106
|
+
expect(names).not.toContain("opencode")
|
|
107
|
+
expect(names).not.toContain("droid")
|
|
108
|
+
expect(names).not.toContain("pi")
|
|
109
|
+
expect(names).not.toContain("cursor")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("returns empty array when nothing detected", async () => {
|
|
113
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-none-"))
|
|
114
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-none-cwd-"))
|
|
115
|
+
|
|
116
|
+
const names = await getDetectedTargetNames(tempHome, tempCwd)
|
|
117
|
+
expect(names).toEqual([])
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { convertClaudeToOpenClaw } from "../src/converters/claude-to-openclaw"
|
|
3
|
+
import { parseFrontmatter } from "../src/utils/frontmatter"
|
|
4
|
+
import type { ClaudePlugin } from "../src/types/claude"
|
|
5
|
+
|
|
6
|
+
const fixturePlugin: ClaudePlugin = {
|
|
7
|
+
root: "/tmp/plugin",
|
|
8
|
+
manifest: { name: "compound-engineering", version: "1.0.0", description: "A plugin" },
|
|
9
|
+
agents: [
|
|
10
|
+
{
|
|
11
|
+
name: "security-reviewer",
|
|
12
|
+
description: "Security-focused agent",
|
|
13
|
+
capabilities: ["Threat modeling", "OWASP"],
|
|
14
|
+
model: "claude-sonnet-4-20250514",
|
|
15
|
+
body: "Focus on vulnerabilities in ~/.claude/settings.",
|
|
16
|
+
sourcePath: "/tmp/plugin/agents/security-reviewer.md",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
commands: [
|
|
20
|
+
{
|
|
21
|
+
name: "workflows:plan",
|
|
22
|
+
description: "Planning command",
|
|
23
|
+
argumentHint: "[FOCUS]",
|
|
24
|
+
model: "inherit",
|
|
25
|
+
allowedTools: ["Read"],
|
|
26
|
+
body: "Plan the work. See ~/.claude/settings for config.",
|
|
27
|
+
sourcePath: "/tmp/plugin/commands/workflows/plan.md",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "disabled-cmd",
|
|
31
|
+
description: "Disabled command",
|
|
32
|
+
model: "inherit",
|
|
33
|
+
allowedTools: [],
|
|
34
|
+
body: "Should be excluded.",
|
|
35
|
+
disableModelInvocation: true,
|
|
36
|
+
sourcePath: "/tmp/plugin/commands/disabled-cmd.md",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
skills: [
|
|
40
|
+
{
|
|
41
|
+
name: "existing-skill",
|
|
42
|
+
description: "Existing skill",
|
|
43
|
+
sourceDir: "/tmp/plugin/skills/existing-skill",
|
|
44
|
+
skillPath: "/tmp/plugin/skills/existing-skill/SKILL.md",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
hooks: undefined,
|
|
48
|
+
mcpServers: {
|
|
49
|
+
local: { command: "npx", args: ["-y", "some-mcp-server"] },
|
|
50
|
+
remote: { url: "https://mcp.example.com/api", headers: { Authorization: "Bearer token" } },
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const defaultOptions = {
|
|
55
|
+
agentMode: "subagent" as const,
|
|
56
|
+
inferTemperature: false,
|
|
57
|
+
permissions: "none" as const,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("convertClaudeToOpenClaw", () => {
|
|
61
|
+
test("converts agents to skill files with SKILL.md content", () => {
|
|
62
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
63
|
+
|
|
64
|
+
const skill = bundle.skills.find((s) => s.name === "security-reviewer")
|
|
65
|
+
expect(skill).toBeDefined()
|
|
66
|
+
expect(skill!.dir).toBe("agent-security-reviewer")
|
|
67
|
+
const parsed = parseFrontmatter(skill!.content)
|
|
68
|
+
expect(parsed.data.name).toBe("security-reviewer")
|
|
69
|
+
expect(parsed.data.description).toBe("Security-focused agent")
|
|
70
|
+
expect(parsed.data.model).toBe("claude-sonnet-4-20250514")
|
|
71
|
+
expect(parsed.body).toContain("Focus on vulnerabilities")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("converts commands to skill files (excluding disableModelInvocation)", () => {
|
|
75
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
76
|
+
|
|
77
|
+
const cmdSkill = bundle.skills.find((s) => s.name === "workflows:plan")
|
|
78
|
+
expect(cmdSkill).toBeDefined()
|
|
79
|
+
expect(cmdSkill!.dir).toBe("cmd-workflows:plan")
|
|
80
|
+
|
|
81
|
+
const disabledSkill = bundle.skills.find((s) => s.name === "disabled-cmd")
|
|
82
|
+
expect(disabledSkill).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test("commands list excludes disableModelInvocation commands", () => {
|
|
86
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
87
|
+
|
|
88
|
+
const cmd = bundle.commands.find((c) => c.name === "workflows-plan")
|
|
89
|
+
expect(cmd).toBeDefined()
|
|
90
|
+
expect(cmd!.description).toBe("Planning command")
|
|
91
|
+
expect(cmd!.acceptsArgs).toBe(true)
|
|
92
|
+
|
|
93
|
+
const disabled = bundle.commands.find((c) => c.name === "disabled-cmd")
|
|
94
|
+
expect(disabled).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("command colons are replaced with dashes in command registrations", () => {
|
|
98
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
99
|
+
|
|
100
|
+
const cmd = bundle.commands.find((c) => c.name === "workflows-plan")
|
|
101
|
+
expect(cmd).toBeDefined()
|
|
102
|
+
expect(cmd!.name).not.toContain(":")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("manifest includes plugin id, display name, and skills list", () => {
|
|
106
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
107
|
+
|
|
108
|
+
expect(bundle.manifest.id).toBe("compound-engineering")
|
|
109
|
+
expect(bundle.manifest.name).toBe("Compound Engineering")
|
|
110
|
+
expect(bundle.manifest.kind).toBe("tool")
|
|
111
|
+
expect(bundle.manifest.skills).toContain("skills/agent-security-reviewer")
|
|
112
|
+
expect(bundle.manifest.skills).toContain("skills/cmd-workflows:plan")
|
|
113
|
+
expect(bundle.manifest.skills).toContain("skills/existing-skill")
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("package.json uses plugin name and version", () => {
|
|
117
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
118
|
+
|
|
119
|
+
expect(bundle.packageJson.name).toBe("openclaw-compound-engineering")
|
|
120
|
+
expect(bundle.packageJson.version).toBe("1.0.0")
|
|
121
|
+
expect(bundle.packageJson.type).toBe("module")
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test("skillDirCopies includes original skill directories", () => {
|
|
125
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
126
|
+
|
|
127
|
+
const copy = bundle.skillDirCopies.find((s) => s.name === "existing-skill")
|
|
128
|
+
expect(copy).toBeDefined()
|
|
129
|
+
expect(copy!.sourceDir).toBe("/tmp/plugin/skills/existing-skill")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test("stdio MCP servers included in openclaw config", () => {
|
|
133
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
134
|
+
|
|
135
|
+
expect(bundle.openclawConfig).toBeDefined()
|
|
136
|
+
const mcp = (bundle.openclawConfig!.mcpServers as Record<string, unknown>)
|
|
137
|
+
expect(mcp.local).toBeDefined()
|
|
138
|
+
expect((mcp.local as any).type).toBe("stdio")
|
|
139
|
+
expect((mcp.local as any).command).toBe("npx")
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("HTTP MCP servers included as http type in openclaw config", () => {
|
|
143
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
144
|
+
|
|
145
|
+
const mcp = (bundle.openclawConfig!.mcpServers as Record<string, unknown>)
|
|
146
|
+
expect(mcp.remote).toBeDefined()
|
|
147
|
+
expect((mcp.remote as any).type).toBe("http")
|
|
148
|
+
expect((mcp.remote as any).url).toBe("https://mcp.example.com/api")
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("paths are rewritten from .claude/ to .openclaw/ in skill content", () => {
|
|
152
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
153
|
+
|
|
154
|
+
const agentSkill = bundle.skills.find((s) => s.name === "security-reviewer")
|
|
155
|
+
expect(agentSkill!.content).toContain("~/.openclaw/settings")
|
|
156
|
+
expect(agentSkill!.content).not.toContain("~/.claude/settings")
|
|
157
|
+
|
|
158
|
+
const cmdSkill = bundle.skills.find((s) => s.name === "workflows:plan")
|
|
159
|
+
expect(cmdSkill!.content).toContain("~/.openclaw/settings")
|
|
160
|
+
expect(cmdSkill!.content).not.toContain("~/.claude/settings")
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test("generateEntryPoint uses JSON.stringify for safe string escaping", () => {
|
|
164
|
+
const plugin: ClaudePlugin = {
|
|
165
|
+
...fixturePlugin,
|
|
166
|
+
commands: [
|
|
167
|
+
{
|
|
168
|
+
name: "tricky-cmd",
|
|
169
|
+
description: 'Has "quotes" and \\backslashes\\ and\nnewlines',
|
|
170
|
+
model: "inherit",
|
|
171
|
+
allowedTools: [],
|
|
172
|
+
body: "body",
|
|
173
|
+
sourcePath: "/tmp/cmd.md",
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
}
|
|
177
|
+
const bundle = convertClaudeToOpenClaw(plugin, defaultOptions)
|
|
178
|
+
|
|
179
|
+
// Entry point must be valid JS/TS — JSON.stringify handles all special chars
|
|
180
|
+
expect(bundle.entryPoint).toContain('"tricky-cmd"')
|
|
181
|
+
expect(bundle.entryPoint).toContain('\\"quotes\\"')
|
|
182
|
+
expect(bundle.entryPoint).toContain("\\\\backslashes\\\\")
|
|
183
|
+
expect(bundle.entryPoint).toContain("\\n")
|
|
184
|
+
// No raw unescaped newline inside a string literal
|
|
185
|
+
const lines = bundle.entryPoint.split("\n")
|
|
186
|
+
const nameLine = lines.find((l) => l.includes("tricky-cmd") && l.includes("name:"))
|
|
187
|
+
expect(nameLine).toBeDefined()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test("generateEntryPoint emits typed skills record", () => {
|
|
191
|
+
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
|
192
|
+
expect(bundle.entryPoint).toContain("const skills: Record<string, string> = {}")
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test("plugin without MCP servers has no openclawConfig", () => {
|
|
196
|
+
const plugin: ClaudePlugin = { ...fixturePlugin, mcpServers: undefined }
|
|
197
|
+
const bundle = convertClaudeToOpenClaw(plugin, defaultOptions)
|
|
198
|
+
expect(bundle.openclawConfig).toBeUndefined()
|
|
199
|
+
})
|
|
200
|
+
})
|