@every-env/compound-plugin 0.1.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 +37 -0
- package/.github/workflows/deploy-docs.yml +39 -0
- package/AGENTS.md +48 -0
- package/CLAUDE.md +380 -0
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/bun.lock +30 -0
- package/docs/css/docs.css +675 -0
- package/docs/css/style.css +2886 -0
- package/docs/index.html +1046 -0
- package/docs/js/main.js +225 -0
- package/docs/pages/agents.html +649 -0
- package/docs/pages/changelog.html +495 -0
- package/docs/pages/commands.html +523 -0
- package/docs/pages/getting-started.html +582 -0
- package/docs/pages/mcp-servers.html +409 -0
- package/docs/pages/skills.html +611 -0
- package/docs/solutions/plugin-versioning-requirements.md +77 -0
- package/docs/specs/claude-code.md +67 -0
- package/docs/specs/codex.md +59 -0
- package/docs/specs/opencode.md +57 -0
- package/package.json +26 -0
- package/plans/grow-your-own-garden-plugin-architecture.md +102 -0
- package/plans/landing-page-launchkit-refresh.md +279 -0
- package/plugins/coding-tutor/.claude-plugin/plugin.json +9 -0
- package/plugins/coding-tutor/README.md +37 -0
- package/plugins/coding-tutor/commands/quiz-me.md +1 -0
- package/plugins/coding-tutor/commands/sync-tutorials.md +25 -0
- package/plugins/coding-tutor/commands/teach-me.md +1 -0
- package/plugins/coding-tutor/skills/coding-tutor/SKILL.md +214 -0
- package/plugins/coding-tutor/skills/coding-tutor/scripts/create_tutorial.py +207 -0
- package/plugins/coding-tutor/skills/coding-tutor/scripts/index_tutorials.py +193 -0
- package/plugins/coding-tutor/skills/coding-tutor/scripts/quiz_priority.py +190 -0
- package/plugins/coding-tutor/skills/coding-tutor/scripts/setup_tutorials.py +118 -0
- package/plugins/compound-engineering/.claude-plugin/plugin.json +33 -0
- package/plugins/compound-engineering/CHANGELOG.md +393 -0
- package/plugins/compound-engineering/CLAUDE.md +90 -0
- package/plugins/compound-engineering/LICENSE +21 -0
- package/plugins/compound-engineering/README.md +219 -0
- package/plugins/compound-engineering/agents/design/design-implementation-reviewer.md +94 -0
- package/plugins/compound-engineering/agents/design/design-iterator.md +197 -0
- package/plugins/compound-engineering/agents/design/figma-design-sync.md +172 -0
- package/plugins/compound-engineering/agents/docs/ankane-readme-writer.md +50 -0
- package/plugins/compound-engineering/agents/research/best-practices-researcher.md +100 -0
- package/plugins/compound-engineering/agents/research/framework-docs-researcher.md +83 -0
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +42 -0
- package/plugins/compound-engineering/agents/research/repo-research-analyst.md +113 -0
- package/plugins/compound-engineering/agents/review/agent-native-reviewer.md +246 -0
- package/plugins/compound-engineering/agents/review/architecture-strategist.md +52 -0
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +85 -0
- package/plugins/compound-engineering/agents/review/data-integrity-guardian.md +70 -0
- package/plugins/compound-engineering/agents/review/data-migration-expert.md +97 -0
- package/plugins/compound-engineering/agents/review/deployment-verification-agent.md +159 -0
- package/plugins/compound-engineering/agents/review/dhh-rails-reviewer.md +45 -0
- package/plugins/compound-engineering/agents/review/julik-frontend-races-reviewer.md +222 -0
- package/plugins/compound-engineering/agents/review/kieran-python-reviewer.md +104 -0
- package/plugins/compound-engineering/agents/review/kieran-rails-reviewer.md +86 -0
- package/plugins/compound-engineering/agents/review/kieran-typescript-reviewer.md +95 -0
- package/plugins/compound-engineering/agents/review/pattern-recognition-specialist.md +57 -0
- package/plugins/compound-engineering/agents/review/performance-oracle.md +110 -0
- package/plugins/compound-engineering/agents/review/security-sentinel.md +93 -0
- package/plugins/compound-engineering/agents/workflow/bug-reproduction-validator.md +67 -0
- package/plugins/compound-engineering/agents/workflow/every-style-editor.md +64 -0
- package/plugins/compound-engineering/agents/workflow/lint.md +16 -0
- package/plugins/compound-engineering/agents/workflow/pr-comment-resolver.md +69 -0
- package/plugins/compound-engineering/agents/workflow/spec-flow-analyzer.md +113 -0
- package/plugins/compound-engineering/commands/agent-native-audit.md +277 -0
- package/plugins/compound-engineering/commands/changelog.md +137 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +8 -0
- package/plugins/compound-engineering/commands/deepen-plan.md +546 -0
- package/plugins/compound-engineering/commands/deploy-docs.md +112 -0
- package/plugins/compound-engineering/commands/feature-video.md +342 -0
- package/plugins/compound-engineering/commands/generate_command.md +162 -0
- package/plugins/compound-engineering/commands/heal-skill.md +142 -0
- package/plugins/compound-engineering/commands/lfg.md +19 -0
- package/plugins/compound-engineering/commands/plan_review.md +7 -0
- package/plugins/compound-engineering/commands/release-docs.md +211 -0
- package/plugins/compound-engineering/commands/report-bug.md +150 -0
- package/plugins/compound-engineering/commands/reproduce-bug.md +99 -0
- package/plugins/compound-engineering/commands/resolve_parallel.md +34 -0
- package/plugins/compound-engineering/commands/resolve_pr_parallel.md +49 -0
- package/plugins/compound-engineering/commands/resolve_todo_parallel.md +35 -0
- package/plugins/compound-engineering/commands/test-browser.md +339 -0
- package/plugins/compound-engineering/commands/triage.md +310 -0
- package/plugins/compound-engineering/commands/workflows/compound.md +202 -0
- package/plugins/compound-engineering/commands/workflows/plan.md +466 -0
- package/plugins/compound-engineering/commands/workflows/review.md +514 -0
- package/plugins/compound-engineering/commands/workflows/work.md +363 -0
- package/plugins/compound-engineering/commands/xcode-test.md +331 -0
- package/plugins/compound-engineering/skills/agent-browser/SKILL.md +223 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/SKILL.md +435 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/action-parity-discipline.md +409 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/agent-execution-patterns.md +467 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/agent-native-testing.md +582 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/architecture-patterns.md +478 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/dynamic-context-injection.md +338 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/files-universal-interface.md +301 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +359 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/mcp-tool-design.md +506 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/mobile-patterns.md +871 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/product-implications.md +443 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +317 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/self-modification.md +269 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/shared-workspace-architecture.md +680 -0
- package/plugins/compound-engineering/skills/agent-native-architecture/references/system-prompt-design.md +250 -0
- package/plugins/compound-engineering/skills/andrew-kane-gem-writer/SKILL.md +184 -0
- package/plugins/compound-engineering/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
- package/plugins/compound-engineering/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
- package/plugins/compound-engineering/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
- package/plugins/compound-engineering/skills/andrew-kane-gem-writer/references/resources.md +119 -0
- package/plugins/compound-engineering/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
- package/plugins/compound-engineering/skills/compound-docs/SKILL.md +510 -0
- package/plugins/compound-engineering/skills/compound-docs/assets/critical-pattern-template.md +34 -0
- package/plugins/compound-engineering/skills/compound-docs/assets/resolution-template.md +93 -0
- package/plugins/compound-engineering/skills/compound-docs/references/yaml-schema.md +65 -0
- package/plugins/compound-engineering/skills/compound-docs/schema.yaml +176 -0
- package/plugins/compound-engineering/skills/create-agent-skills/SKILL.md +299 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/api-security.md +226 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/be-clear-and-direct.md +531 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/best-practices.md +404 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/common-patterns.md +595 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/core-principles.md +437 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/executable-code.md +175 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/iteration-and-testing.md +474 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/official-spec.md +185 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/recommended-structure.md +168 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/skill-structure.md +372 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/using-scripts.md +113 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/using-templates.md +112 -0
- package/plugins/compound-engineering/skills/create-agent-skills/references/workflows-and-validation.md +510 -0
- package/plugins/compound-engineering/skills/create-agent-skills/templates/router-skill.md +73 -0
- package/plugins/compound-engineering/skills/create-agent-skills/templates/simple-skill.md +33 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-reference.md +96 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-script.md +93 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-template.md +74 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-workflow.md +120 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/audit-skill.md +138 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-domain-expertise-skill.md +605 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md +191 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/get-guidance.md +121 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/upgrade-to-router.md +161 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/verify-skill.md +204 -0
- package/plugins/compound-engineering/skills/dhh-rails-style/SKILL.md +185 -0
- package/plugins/compound-engineering/skills/dhh-rails-style/references/architecture.md +653 -0
- package/plugins/compound-engineering/skills/dhh-rails-style/references/controllers.md +303 -0
- package/plugins/compound-engineering/skills/dhh-rails-style/references/frontend.md +510 -0
- package/plugins/compound-engineering/skills/dhh-rails-style/references/gems.md +266 -0
- package/plugins/compound-engineering/skills/dhh-rails-style/references/models.md +359 -0
- package/plugins/compound-engineering/skills/dhh-rails-style/references/testing.md +338 -0
- package/plugins/compound-engineering/skills/dspy-ruby/SKILL.md +594 -0
- package/plugins/compound-engineering/skills/dspy-ruby/assets/config-template.rb +359 -0
- package/plugins/compound-engineering/skills/dspy-ruby/assets/module-template.rb +326 -0
- package/plugins/compound-engineering/skills/dspy-ruby/assets/signature-template.rb +143 -0
- package/plugins/compound-engineering/skills/dspy-ruby/references/core-concepts.md +265 -0
- package/plugins/compound-engineering/skills/dspy-ruby/references/optimization.md +623 -0
- package/plugins/compound-engineering/skills/dspy-ruby/references/providers.md +338 -0
- package/plugins/compound-engineering/skills/every-style-editor/SKILL.md +134 -0
- package/plugins/compound-engineering/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +251 -0
- package/plugins/compound-engineering/skills/file-todos/assets/todo-template.md +155 -0
- package/plugins/compound-engineering/skills/frontend-design/SKILL.md +42 -0
- package/plugins/compound-engineering/skills/gemini-imagegen/SKILL.md +237 -0
- package/plugins/compound-engineering/skills/gemini-imagegen/requirements.txt +2 -0
- package/plugins/compound-engineering/skills/gemini-imagegen/scripts/compose_images.py +157 -0
- package/plugins/compound-engineering/skills/gemini-imagegen/scripts/edit_image.py +144 -0
- package/plugins/compound-engineering/skills/gemini-imagegen/scripts/gemini_images.py +263 -0
- package/plugins/compound-engineering/skills/gemini-imagegen/scripts/generate_image.py +133 -0
- package/plugins/compound-engineering/skills/gemini-imagegen/scripts/multi_turn_chat.py +216 -0
- package/plugins/compound-engineering/skills/git-worktree/SKILL.md +302 -0
- package/plugins/compound-engineering/skills/git-worktree/scripts/worktree-manager.sh +345 -0
- package/plugins/compound-engineering/skills/rclone/SKILL.md +150 -0
- package/plugins/compound-engineering/skills/rclone/scripts/check_setup.sh +60 -0
- package/plugins/compound-engineering/skills/skill-creator/SKILL.md +209 -0
- package/plugins/compound-engineering/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/compound-engineering/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/compound-engineering/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/src/commands/convert.ts +156 -0
- package/src/commands/install.ts +221 -0
- package/src/commands/list.ts +37 -0
- package/src/converters/claude-to-codex.ts +124 -0
- package/src/converters/claude-to-opencode.ts +392 -0
- package/src/index.ts +20 -0
- package/src/parsers/claude.ts +248 -0
- package/src/targets/codex.ts +91 -0
- package/src/targets/index.ts +29 -0
- package/src/targets/opencode.ts +48 -0
- package/src/types/claude.ts +88 -0
- package/src/types/codex.ts +23 -0
- package/src/types/opencode.ts +54 -0
- package/src/utils/codex-agents.ts +64 -0
- package/src/utils/files.ts +64 -0
- package/src/utils/frontmatter.ts +65 -0
- package/tests/claude-parser.test.ts +89 -0
- package/tests/cli.test.ts +289 -0
- package/tests/codex-agents.test.ts +62 -0
- package/tests/codex-converter.test.ts +121 -0
- package/tests/codex-writer.test.ts +76 -0
- package/tests/converter.test.ts +171 -0
- package/tests/fixtures/custom-paths/.claude-plugin/plugin.json +8 -0
- package/tests/fixtures/custom-paths/agents/default-agent.md +5 -0
- package/tests/fixtures/custom-paths/commands/default-command.md +5 -0
- package/tests/fixtures/custom-paths/custom-agents/custom-agent.md +5 -0
- package/tests/fixtures/custom-paths/custom-commands/custom-command.md +5 -0
- package/tests/fixtures/custom-paths/custom-hooks/hooks.json +7 -0
- package/tests/fixtures/custom-paths/custom-skills/custom-skill/SKILL.md +5 -0
- package/tests/fixtures/custom-paths/hooks/hooks.json +7 -0
- package/tests/fixtures/custom-paths/skills/default-skill/SKILL.md +5 -0
- package/tests/fixtures/invalid-command-path/.claude-plugin/plugin.json +5 -0
- package/tests/fixtures/invalid-hooks-path/.claude-plugin/plugin.json +5 -0
- package/tests/fixtures/invalid-mcp-path/.claude-plugin/plugin.json +5 -0
- package/tests/fixtures/mcp-file/.claude-plugin/plugin.json +5 -0
- package/tests/fixtures/mcp-file/.mcp.json +6 -0
- package/tests/fixtures/sample-plugin/.claude-plugin/plugin.json +30 -0
- package/tests/fixtures/sample-plugin/agents/agent-one.md +10 -0
- package/tests/fixtures/sample-plugin/agents/security-reviewer.md +7 -0
- package/tests/fixtures/sample-plugin/commands/command-one.md +7 -0
- package/tests/fixtures/sample-plugin/commands/model-command.md +8 -0
- package/tests/fixtures/sample-plugin/commands/nested/command-two.md +9 -0
- package/tests/fixtures/sample-plugin/commands/pattern-command.md +7 -0
- package/tests/fixtures/sample-plugin/commands/skill-command.md +7 -0
- package/tests/fixtures/sample-plugin/commands/todo-command.md +7 -0
- package/tests/fixtures/sample-plugin/hooks/hooks.json +156 -0
- package/tests/fixtures/sample-plugin/skills/skill-one/SKILL.md +6 -0
- package/tests/frontmatter.test.ts +20 -0
- package/tests/opencode-writer.test.ts +62 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { formatFrontmatter } from "../utils/frontmatter"
|
|
2
|
+
import type {
|
|
3
|
+
ClaudeAgent,
|
|
4
|
+
ClaudeCommand,
|
|
5
|
+
ClaudeHooks,
|
|
6
|
+
ClaudePlugin,
|
|
7
|
+
ClaudeMcpServer,
|
|
8
|
+
} from "../types/claude"
|
|
9
|
+
import type {
|
|
10
|
+
OpenCodeBundle,
|
|
11
|
+
OpenCodeCommandConfig,
|
|
12
|
+
OpenCodeConfig,
|
|
13
|
+
OpenCodeMcpServer,
|
|
14
|
+
} from "../types/opencode"
|
|
15
|
+
|
|
16
|
+
export type PermissionMode = "none" | "broad" | "from-commands"
|
|
17
|
+
|
|
18
|
+
export type ClaudeToOpenCodeOptions = {
|
|
19
|
+
agentMode: "primary" | "subagent"
|
|
20
|
+
inferTemperature: boolean
|
|
21
|
+
permissions: PermissionMode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TOOL_MAP: Record<string, string> = {
|
|
25
|
+
bash: "bash",
|
|
26
|
+
read: "read",
|
|
27
|
+
write: "write",
|
|
28
|
+
edit: "edit",
|
|
29
|
+
grep: "grep",
|
|
30
|
+
glob: "glob",
|
|
31
|
+
list: "list",
|
|
32
|
+
webfetch: "webfetch",
|
|
33
|
+
skill: "skill",
|
|
34
|
+
patch: "patch",
|
|
35
|
+
task: "task",
|
|
36
|
+
question: "question",
|
|
37
|
+
todowrite: "todowrite",
|
|
38
|
+
todoread: "todoread",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type HookEventMapping = {
|
|
42
|
+
events: string[]
|
|
43
|
+
type: "tool" | "session" | "permission" | "message"
|
|
44
|
+
requireError?: boolean
|
|
45
|
+
note?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const HOOK_EVENT_MAP: Record<string, HookEventMapping> = {
|
|
49
|
+
PreToolUse: { events: ["tool.execute.before"], type: "tool" },
|
|
50
|
+
PostToolUse: { events: ["tool.execute.after"], type: "tool" },
|
|
51
|
+
PostToolUseFailure: { events: ["tool.execute.after"], type: "tool", requireError: true, note: "Claude PostToolUseFailure" },
|
|
52
|
+
SessionStart: { events: ["session.created"], type: "session" },
|
|
53
|
+
SessionEnd: { events: ["session.deleted"], type: "session" },
|
|
54
|
+
Stop: { events: ["session.idle"], type: "session" },
|
|
55
|
+
PreCompact: { events: ["experimental.session.compacting"], type: "session" },
|
|
56
|
+
PermissionRequest: { events: ["permission.requested", "permission.replied"], type: "permission", note: "Claude PermissionRequest" },
|
|
57
|
+
UserPromptSubmit: { events: ["message.created", "message.updated"], type: "message", note: "Claude UserPromptSubmit" },
|
|
58
|
+
Notification: { events: ["message.updated"], type: "message", note: "Claude Notification" },
|
|
59
|
+
Setup: { events: ["session.created"], type: "session", note: "Claude Setup" },
|
|
60
|
+
SubagentStart: { events: ["message.updated"], type: "message", note: "Claude SubagentStart" },
|
|
61
|
+
SubagentStop: { events: ["message.updated"], type: "message", note: "Claude SubagentStop" },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function convertClaudeToOpenCode(
|
|
65
|
+
plugin: ClaudePlugin,
|
|
66
|
+
options: ClaudeToOpenCodeOptions,
|
|
67
|
+
): OpenCodeBundle {
|
|
68
|
+
const agentFiles = plugin.agents.map((agent) => convertAgent(agent, options))
|
|
69
|
+
const commandMap = convertCommands(plugin.commands)
|
|
70
|
+
const mcp = plugin.mcpServers ? convertMcp(plugin.mcpServers) : undefined
|
|
71
|
+
const plugins = plugin.hooks ? [convertHooks(plugin.hooks)] : []
|
|
72
|
+
|
|
73
|
+
const config: OpenCodeConfig = {
|
|
74
|
+
$schema: "https://opencode.ai/config.json",
|
|
75
|
+
command: Object.keys(commandMap).length > 0 ? commandMap : undefined,
|
|
76
|
+
mcp: mcp && Object.keys(mcp).length > 0 ? mcp : undefined,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
applyPermissions(config, plugin.commands, options.permissions)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
config,
|
|
83
|
+
agents: agentFiles,
|
|
84
|
+
plugins,
|
|
85
|
+
skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) {
|
|
90
|
+
const frontmatter: Record<string, unknown> = {
|
|
91
|
+
description: agent.description,
|
|
92
|
+
mode: options.agentMode,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (agent.model && agent.model !== "inherit") {
|
|
96
|
+
frontmatter.model = normalizeModel(agent.model)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.inferTemperature) {
|
|
100
|
+
const temperature = inferTemperature(agent)
|
|
101
|
+
if (temperature !== undefined) {
|
|
102
|
+
frontmatter.temperature = temperature
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const content = formatFrontmatter(frontmatter, agent.body)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name: agent.name,
|
|
110
|
+
content,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function convertCommands(commands: ClaudeCommand[]): Record<string, OpenCodeCommandConfig> {
|
|
115
|
+
const result: Record<string, OpenCodeCommandConfig> = {}
|
|
116
|
+
for (const command of commands) {
|
|
117
|
+
const entry: OpenCodeCommandConfig = {
|
|
118
|
+
description: command.description,
|
|
119
|
+
template: command.body,
|
|
120
|
+
}
|
|
121
|
+
if (command.model && command.model !== "inherit") {
|
|
122
|
+
entry.model = normalizeModel(command.model)
|
|
123
|
+
}
|
|
124
|
+
result[command.name] = entry
|
|
125
|
+
}
|
|
126
|
+
return result
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function convertMcp(servers: Record<string, ClaudeMcpServer>): Record<string, OpenCodeMcpServer> {
|
|
130
|
+
const result: Record<string, OpenCodeMcpServer> = {}
|
|
131
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
132
|
+
if (server.command) {
|
|
133
|
+
result[name] = {
|
|
134
|
+
type: "local",
|
|
135
|
+
command: [server.command, ...(server.args ?? [])],
|
|
136
|
+
environment: server.env,
|
|
137
|
+
enabled: true,
|
|
138
|
+
}
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (server.url) {
|
|
143
|
+
result[name] = {
|
|
144
|
+
type: "remote",
|
|
145
|
+
url: server.url,
|
|
146
|
+
headers: server.headers,
|
|
147
|
+
enabled: true,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return result
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function convertHooks(hooks: ClaudeHooks) {
|
|
155
|
+
const handlerBlocks: string[] = []
|
|
156
|
+
const hookMap = hooks.hooks
|
|
157
|
+
const unmappedEvents: string[] = []
|
|
158
|
+
|
|
159
|
+
for (const [eventName, matchers] of Object.entries(hookMap)) {
|
|
160
|
+
const mapping = HOOK_EVENT_MAP[eventName]
|
|
161
|
+
if (!mapping) {
|
|
162
|
+
unmappedEvents.push(eventName)
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
if (matchers.length === 0) continue
|
|
166
|
+
for (const event of mapping.events) {
|
|
167
|
+
handlerBlocks.push(
|
|
168
|
+
renderHookHandlers(event, matchers, {
|
|
169
|
+
useToolMatcher: mapping.type === "tool" || mapping.type === "permission",
|
|
170
|
+
requireError: mapping.requireError ?? false,
|
|
171
|
+
note: mapping.note,
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const unmappedComment = unmappedEvents.length > 0
|
|
178
|
+
? `// Unmapped Claude hook events: ${unmappedEvents.join(", ")}\n`
|
|
179
|
+
: ""
|
|
180
|
+
|
|
181
|
+
const content = `${unmappedComment}import type { Plugin } from "@opencode-ai/plugin"\n\nexport const ConvertedHooks: Plugin = async ({ $ }) => {\n return {\n${handlerBlocks.join(",\n")}\n }\n}\n\nexport default ConvertedHooks\n`
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
name: "converted-hooks.ts",
|
|
185
|
+
content,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderHookHandlers(
|
|
190
|
+
event: string,
|
|
191
|
+
matchers: ClaudeHooks["hooks"][string],
|
|
192
|
+
options: { useToolMatcher: boolean; requireError: boolean; note?: string },
|
|
193
|
+
) {
|
|
194
|
+
const statements: string[] = []
|
|
195
|
+
for (const matcher of matchers) {
|
|
196
|
+
statements.push(...renderHookStatements(matcher, options.useToolMatcher))
|
|
197
|
+
}
|
|
198
|
+
const rendered = statements.map((line) => ` ${line}`).join("\n")
|
|
199
|
+
const wrapped = options.requireError
|
|
200
|
+
? ` if (input?.error) {\n${statements.map((line) => ` ${line}`).join("\n")}\n }`
|
|
201
|
+
: rendered
|
|
202
|
+
const note = options.note ? ` // ${options.note}\n` : ""
|
|
203
|
+
return ` "${event}": async (input) => {\n${note}${wrapped}\n }`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function renderHookStatements(
|
|
207
|
+
matcher: ClaudeHooks["hooks"][string][number],
|
|
208
|
+
useToolMatcher: boolean,
|
|
209
|
+
): string[] {
|
|
210
|
+
if (!matcher.hooks || matcher.hooks.length === 0) return []
|
|
211
|
+
const tools = matcher.matcher
|
|
212
|
+
.split("|")
|
|
213
|
+
.map((tool) => tool.trim().toLowerCase())
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
|
|
216
|
+
const useMatcher = useToolMatcher && tools.length > 0 && !tools.includes("*")
|
|
217
|
+
const condition = useMatcher
|
|
218
|
+
? tools.map((tool) => `input.tool === "${tool}"`).join(" || ")
|
|
219
|
+
: null
|
|
220
|
+
const statements: string[] = []
|
|
221
|
+
|
|
222
|
+
for (const hook of matcher.hooks) {
|
|
223
|
+
if (hook.type === "command") {
|
|
224
|
+
if (condition) {
|
|
225
|
+
statements.push(`if (${condition}) { await $\`${hook.command}\` }`)
|
|
226
|
+
} else {
|
|
227
|
+
statements.push(`await $\`${hook.command}\``)
|
|
228
|
+
}
|
|
229
|
+
if (hook.timeout) {
|
|
230
|
+
statements.push(`// timeout: ${hook.timeout}s (not enforced)`)
|
|
231
|
+
}
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (hook.type === "prompt") {
|
|
235
|
+
statements.push(`// Prompt hook for ${matcher.matcher}: ${hook.prompt.replace(/\n/g, " ")}`)
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
statements.push(`// Agent hook for ${matcher.matcher}: ${hook.agent}`)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return statements
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeModel(model: string): string {
|
|
245
|
+
if (model.includes("/")) return model
|
|
246
|
+
if (/^claude-/.test(model)) return `anthropic/${model}`
|
|
247
|
+
if (/^(gpt-|o1-|o3-)/.test(model)) return `openai/${model}`
|
|
248
|
+
if (/^gemini-/.test(model)) return `google/${model}`
|
|
249
|
+
return `anthropic/${model}`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function inferTemperature(agent: ClaudeAgent): number | undefined {
|
|
253
|
+
const sample = `${agent.name} ${agent.description ?? ""}`.toLowerCase()
|
|
254
|
+
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
|
255
|
+
return 0.1
|
|
256
|
+
}
|
|
257
|
+
if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
|
|
258
|
+
return 0.2
|
|
259
|
+
}
|
|
260
|
+
if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
|
|
261
|
+
return 0.3
|
|
262
|
+
}
|
|
263
|
+
if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
|
|
264
|
+
return 0.6
|
|
265
|
+
}
|
|
266
|
+
return 0.3
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function applyPermissions(
|
|
270
|
+
config: OpenCodeConfig,
|
|
271
|
+
commands: ClaudeCommand[],
|
|
272
|
+
mode: PermissionMode,
|
|
273
|
+
) {
|
|
274
|
+
if (mode === "none") return
|
|
275
|
+
|
|
276
|
+
const sourceTools = [
|
|
277
|
+
"read",
|
|
278
|
+
"write",
|
|
279
|
+
"edit",
|
|
280
|
+
"bash",
|
|
281
|
+
"grep",
|
|
282
|
+
"glob",
|
|
283
|
+
"list",
|
|
284
|
+
"webfetch",
|
|
285
|
+
"skill",
|
|
286
|
+
"patch",
|
|
287
|
+
"task",
|
|
288
|
+
"question",
|
|
289
|
+
"todowrite",
|
|
290
|
+
"todoread",
|
|
291
|
+
]
|
|
292
|
+
let enabled = new Set<string>()
|
|
293
|
+
const patterns: Record<string, Set<string>> = {}
|
|
294
|
+
|
|
295
|
+
if (mode === "broad") {
|
|
296
|
+
enabled = new Set(sourceTools)
|
|
297
|
+
} else {
|
|
298
|
+
for (const command of commands) {
|
|
299
|
+
if (!command.allowedTools) continue
|
|
300
|
+
for (const tool of command.allowedTools) {
|
|
301
|
+
const parsed = parseToolSpec(tool)
|
|
302
|
+
if (!parsed.tool) continue
|
|
303
|
+
enabled.add(parsed.tool)
|
|
304
|
+
if (parsed.pattern) {
|
|
305
|
+
const normalizedPattern = normalizePattern(parsed.tool, parsed.pattern)
|
|
306
|
+
if (!patterns[parsed.tool]) patterns[parsed.tool] = new Set()
|
|
307
|
+
patterns[parsed.tool].add(normalizedPattern)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const permission: Record<string, "allow" | "deny"> = {}
|
|
314
|
+
const tools: Record<string, boolean> = {}
|
|
315
|
+
|
|
316
|
+
for (const tool of sourceTools) {
|
|
317
|
+
tools[tool] = mode === "broad" ? true : enabled.has(tool)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (mode === "broad") {
|
|
321
|
+
for (const tool of sourceTools) {
|
|
322
|
+
permission[tool] = "allow"
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
for (const tool of sourceTools) {
|
|
326
|
+
const toolPatterns = patterns[tool]
|
|
327
|
+
if (toolPatterns && toolPatterns.size > 0) {
|
|
328
|
+
const patternPermission: Record<string, "allow" | "deny"> = { "*": "deny" }
|
|
329
|
+
for (const pattern of toolPatterns) {
|
|
330
|
+
patternPermission[pattern] = "allow"
|
|
331
|
+
}
|
|
332
|
+
;(permission as Record<string, typeof patternPermission>)[tool] = patternPermission
|
|
333
|
+
} else {
|
|
334
|
+
permission[tool] = enabled.has(tool) ? "allow" : "deny"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (mode !== "broad") {
|
|
340
|
+
for (const [tool, toolPatterns] of Object.entries(patterns)) {
|
|
341
|
+
if (!toolPatterns || toolPatterns.size === 0) continue
|
|
342
|
+
const patternPermission: Record<string, "allow" | "deny"> = { "*": "deny" }
|
|
343
|
+
for (const pattern of toolPatterns) {
|
|
344
|
+
patternPermission[pattern] = "allow"
|
|
345
|
+
}
|
|
346
|
+
;(permission as Record<string, typeof patternPermission>)[tool] = patternPermission
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (enabled.has("write") || enabled.has("edit")) {
|
|
351
|
+
if (typeof permission.edit === "string") permission.edit = "allow"
|
|
352
|
+
if (typeof permission.write === "string") permission.write = "allow"
|
|
353
|
+
}
|
|
354
|
+
if (patterns.write || patterns.edit) {
|
|
355
|
+
const combined = new Set<string>()
|
|
356
|
+
for (const pattern of patterns.write ?? []) combined.add(pattern)
|
|
357
|
+
for (const pattern of patterns.edit ?? []) combined.add(pattern)
|
|
358
|
+
const combinedPermission: Record<string, "allow" | "deny"> = { "*": "deny" }
|
|
359
|
+
for (const pattern of combined) {
|
|
360
|
+
combinedPermission[pattern] = "allow"
|
|
361
|
+
}
|
|
362
|
+
;(permission as Record<string, typeof combinedPermission>).edit = combinedPermission
|
|
363
|
+
;(permission as Record<string, typeof combinedPermission>).write = combinedPermission
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
config.permission = permission
|
|
367
|
+
config.tools = tools
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function normalizeTool(raw: string): string | null {
|
|
371
|
+
return parseToolSpec(raw).tool
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseToolSpec(raw: string): { tool: string | null; pattern?: string } {
|
|
375
|
+
const trimmed = raw.trim()
|
|
376
|
+
if (!trimmed) return { tool: null }
|
|
377
|
+
const [namePart, patternPart] = trimmed.split("(", 2)
|
|
378
|
+
const name = namePart.trim().toLowerCase()
|
|
379
|
+
const tool = TOOL_MAP[name] ?? null
|
|
380
|
+
if (!patternPart) return { tool }
|
|
381
|
+
const normalizedPattern = patternPart.endsWith(")")
|
|
382
|
+
? patternPart.slice(0, -1).trim()
|
|
383
|
+
: patternPart.trim()
|
|
384
|
+
return { tool, pattern: normalizedPattern }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function normalizePattern(tool: string, pattern: string): string {
|
|
388
|
+
if (tool === "bash") {
|
|
389
|
+
return pattern.replace(/:/g, " ").trim()
|
|
390
|
+
}
|
|
391
|
+
return pattern
|
|
392
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { defineCommand, runMain } from "citty"
|
|
3
|
+
import convert from "./commands/convert"
|
|
4
|
+
import install from "./commands/install"
|
|
5
|
+
import listCommand from "./commands/list"
|
|
6
|
+
|
|
7
|
+
const main = defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "compound-plugin",
|
|
10
|
+
version: "0.1.0",
|
|
11
|
+
description: "Convert Claude Code plugins into other agent formats",
|
|
12
|
+
},
|
|
13
|
+
subCommands: {
|
|
14
|
+
convert: () => convert,
|
|
15
|
+
install: () => install,
|
|
16
|
+
list: () => listCommand,
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
runMain(main)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { parseFrontmatter } from "../utils/frontmatter"
|
|
3
|
+
import { readJson, readText, pathExists, walkFiles } from "../utils/files"
|
|
4
|
+
import type {
|
|
5
|
+
ClaudeAgent,
|
|
6
|
+
ClaudeCommand,
|
|
7
|
+
ClaudeHooks,
|
|
8
|
+
ClaudeManifest,
|
|
9
|
+
ClaudeMcpServer,
|
|
10
|
+
ClaudePlugin,
|
|
11
|
+
ClaudeSkill,
|
|
12
|
+
} from "../types/claude"
|
|
13
|
+
|
|
14
|
+
const PLUGIN_MANIFEST = path.join(".claude-plugin", "plugin.json")
|
|
15
|
+
|
|
16
|
+
export async function loadClaudePlugin(inputPath: string): Promise<ClaudePlugin> {
|
|
17
|
+
const root = await resolveClaudeRoot(inputPath)
|
|
18
|
+
const manifestPath = path.join(root, PLUGIN_MANIFEST)
|
|
19
|
+
const manifest = await readJson<ClaudeManifest>(manifestPath)
|
|
20
|
+
|
|
21
|
+
const agents = await loadAgents(resolveComponentDirs(root, "agents", manifest.agents))
|
|
22
|
+
const commands = await loadCommands(resolveComponentDirs(root, "commands", manifest.commands))
|
|
23
|
+
const skills = await loadSkills(resolveComponentDirs(root, "skills", manifest.skills))
|
|
24
|
+
const hooks = await loadHooks(root, manifest.hooks)
|
|
25
|
+
|
|
26
|
+
const mcpServers = await loadMcpServers(root, manifest)
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
root,
|
|
30
|
+
manifest,
|
|
31
|
+
agents,
|
|
32
|
+
commands,
|
|
33
|
+
skills,
|
|
34
|
+
hooks,
|
|
35
|
+
mcpServers,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function resolveClaudeRoot(inputPath: string): Promise<string> {
|
|
40
|
+
const absolute = path.resolve(inputPath)
|
|
41
|
+
const manifestAtPath = path.join(absolute, PLUGIN_MANIFEST)
|
|
42
|
+
if (await pathExists(manifestAtPath)) {
|
|
43
|
+
return absolute
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (absolute.endsWith(PLUGIN_MANIFEST)) {
|
|
47
|
+
return path.dirname(path.dirname(absolute))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (absolute.endsWith("plugin.json")) {
|
|
51
|
+
return path.dirname(path.dirname(absolute))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error(`Could not find ${PLUGIN_MANIFEST} under ${inputPath}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function loadAgents(agentsDirs: string[]): Promise<ClaudeAgent[]> {
|
|
58
|
+
const files = await collectMarkdownFiles(agentsDirs)
|
|
59
|
+
|
|
60
|
+
const agents: ClaudeAgent[] = []
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const raw = await readText(file)
|
|
63
|
+
const { data, body } = parseFrontmatter(raw)
|
|
64
|
+
const name = (data.name as string) ?? path.basename(file, ".md")
|
|
65
|
+
agents.push({
|
|
66
|
+
name,
|
|
67
|
+
description: data.description as string | undefined,
|
|
68
|
+
capabilities: data.capabilities as string[] | undefined,
|
|
69
|
+
model: data.model as string | undefined,
|
|
70
|
+
body: body.trim(),
|
|
71
|
+
sourcePath: file,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
return agents
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function loadCommands(commandsDirs: string[]): Promise<ClaudeCommand[]> {
|
|
78
|
+
const files = await collectMarkdownFiles(commandsDirs)
|
|
79
|
+
|
|
80
|
+
const commands: ClaudeCommand[] = []
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const raw = await readText(file)
|
|
83
|
+
const { data, body } = parseFrontmatter(raw)
|
|
84
|
+
const name = (data.name as string) ?? path.basename(file, ".md")
|
|
85
|
+
const allowedTools = parseAllowedTools(data["allowed-tools"])
|
|
86
|
+
commands.push({
|
|
87
|
+
name,
|
|
88
|
+
description: data.description as string | undefined,
|
|
89
|
+
argumentHint: data["argument-hint"] as string | undefined,
|
|
90
|
+
model: data.model as string | undefined,
|
|
91
|
+
allowedTools,
|
|
92
|
+
body: body.trim(),
|
|
93
|
+
sourcePath: file,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
return commands
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function loadSkills(skillsDirs: string[]): Promise<ClaudeSkill[]> {
|
|
100
|
+
const entries = await collectFiles(skillsDirs)
|
|
101
|
+
const skillFiles = entries.filter((file) => path.basename(file) === "SKILL.md")
|
|
102
|
+
const skills: ClaudeSkill[] = []
|
|
103
|
+
for (const file of skillFiles) {
|
|
104
|
+
const raw = await readText(file)
|
|
105
|
+
const { data } = parseFrontmatter(raw)
|
|
106
|
+
const name = (data.name as string) ?? path.basename(path.dirname(file))
|
|
107
|
+
skills.push({
|
|
108
|
+
name,
|
|
109
|
+
description: data.description as string | undefined,
|
|
110
|
+
sourceDir: path.dirname(file),
|
|
111
|
+
skillPath: file,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
return skills
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function loadHooks(root: string, hooksField?: ClaudeManifest["hooks"]): Promise<ClaudeHooks | undefined> {
|
|
118
|
+
const hookConfigs: ClaudeHooks[] = []
|
|
119
|
+
|
|
120
|
+
const defaultPath = path.join(root, "hooks", "hooks.json")
|
|
121
|
+
if (await pathExists(defaultPath)) {
|
|
122
|
+
hookConfigs.push(await readJson<ClaudeHooks>(defaultPath))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hooksField) {
|
|
126
|
+
if (typeof hooksField === "string" || Array.isArray(hooksField)) {
|
|
127
|
+
const hookPaths = toPathList(hooksField)
|
|
128
|
+
for (const hookPath of hookPaths) {
|
|
129
|
+
const resolved = resolveWithinRoot(root, hookPath, "hooks path")
|
|
130
|
+
if (await pathExists(resolved)) {
|
|
131
|
+
hookConfigs.push(await readJson<ClaudeHooks>(resolved))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
hookConfigs.push(hooksField)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (hookConfigs.length === 0) return undefined
|
|
140
|
+
return mergeHooks(hookConfigs)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function loadMcpServers(
|
|
144
|
+
root: string,
|
|
145
|
+
manifest: ClaudeManifest,
|
|
146
|
+
): Promise<Record<string, ClaudeMcpServer> | undefined> {
|
|
147
|
+
const field = manifest.mcpServers
|
|
148
|
+
if (field) {
|
|
149
|
+
if (typeof field === "string" || Array.isArray(field)) {
|
|
150
|
+
return mergeMcpConfigs(await loadMcpPaths(root, field))
|
|
151
|
+
}
|
|
152
|
+
return field as Record<string, ClaudeMcpServer>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const mcpPath = path.join(root, ".mcp.json")
|
|
156
|
+
if (await pathExists(mcpPath)) {
|
|
157
|
+
return readJson<Record<string, ClaudeMcpServer>>(mcpPath)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseAllowedTools(value: unknown): string[] | undefined {
|
|
164
|
+
if (!value) return undefined
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
return value.map((item) => String(item))
|
|
167
|
+
}
|
|
168
|
+
if (typeof value === "string") {
|
|
169
|
+
return value
|
|
170
|
+
.split(/,/)
|
|
171
|
+
.map((item) => item.trim())
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
}
|
|
174
|
+
return undefined
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveComponentDirs(
|
|
178
|
+
root: string,
|
|
179
|
+
defaultDir: string,
|
|
180
|
+
custom?: string | string[],
|
|
181
|
+
): string[] {
|
|
182
|
+
const dirs = [path.join(root, defaultDir)]
|
|
183
|
+
for (const entry of toPathList(custom)) {
|
|
184
|
+
dirs.push(resolveWithinRoot(root, entry, `${defaultDir} path`))
|
|
185
|
+
}
|
|
186
|
+
return dirs
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toPathList(value?: string | string[]): string[] {
|
|
190
|
+
if (!value) return []
|
|
191
|
+
if (Array.isArray(value)) return value
|
|
192
|
+
return [value]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function collectMarkdownFiles(dirs: string[]): Promise<string[]> {
|
|
196
|
+
const entries = await collectFiles(dirs)
|
|
197
|
+
return entries.filter((file) => file.endsWith(".md"))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function collectFiles(dirs: string[]): Promise<string[]> {
|
|
201
|
+
const files: string[] = []
|
|
202
|
+
for (const dir of dirs) {
|
|
203
|
+
if (!(await pathExists(dir))) continue
|
|
204
|
+
const entries = await walkFiles(dir)
|
|
205
|
+
files.push(...entries)
|
|
206
|
+
}
|
|
207
|
+
return files
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function mergeHooks(hooksList: ClaudeHooks[]): ClaudeHooks {
|
|
211
|
+
const merged: ClaudeHooks = { hooks: {} }
|
|
212
|
+
for (const hooks of hooksList) {
|
|
213
|
+
for (const [event, matchers] of Object.entries(hooks.hooks)) {
|
|
214
|
+
if (!merged.hooks[event]) {
|
|
215
|
+
merged.hooks[event] = []
|
|
216
|
+
}
|
|
217
|
+
merged.hooks[event].push(...matchers)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return merged
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function loadMcpPaths(
|
|
224
|
+
root: string,
|
|
225
|
+
value: string | string[],
|
|
226
|
+
): Promise<Record<string, ClaudeMcpServer>[]> {
|
|
227
|
+
const configs: Record<string, ClaudeMcpServer>[] = []
|
|
228
|
+
for (const entry of toPathList(value)) {
|
|
229
|
+
const resolved = resolveWithinRoot(root, entry, "mcpServers path")
|
|
230
|
+
if (await pathExists(resolved)) {
|
|
231
|
+
configs.push(await readJson<Record<string, ClaudeMcpServer>>(resolved))
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return configs
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function mergeMcpConfigs(configs: Record<string, ClaudeMcpServer>[]): Record<string, ClaudeMcpServer> {
|
|
238
|
+
return configs.reduce((acc, config) => ({ ...acc, ...config }), {})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveWithinRoot(root: string, entry: string, label: string): string {
|
|
242
|
+
const resolvedRoot = path.resolve(root)
|
|
243
|
+
const resolvedPath = path.resolve(root, entry)
|
|
244
|
+
if (resolvedPath === resolvedRoot || resolvedPath.startsWith(resolvedRoot + path.sep)) {
|
|
245
|
+
return resolvedPath
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Invalid ${label}: ${entry}. Paths must stay within the plugin root.`)
|
|
248
|
+
}
|