@every-env/compound-plugin 0.1.1 → 0.3.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/commands/triage-prs.md +193 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/.github/workflows/ci.yml +25 -0
- package/README.md +22 -1
- package/docs/plans/2026-02-08-feat-pr-triage-and-merge-plan.md +128 -0
- package/docs/plans/2026-02-08-refactor-reduce-plugin-context-token-usage-plan.md +212 -0
- package/package.json +1 -1
- package/plans/grow-your-own-garden-plugin-architecture.md +1 -1
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +58 -0
- package/plugins/compound-engineering/CLAUDE.md +3 -4
- package/plugins/compound-engineering/README.md +19 -7
- package/plugins/compound-engineering/agents/design/design-implementation-reviewer.md +16 -1
- package/plugins/compound-engineering/agents/design/design-iterator.md +28 -1
- package/plugins/compound-engineering/agents/design/figma-design-sync.md +19 -1
- package/plugins/compound-engineering/agents/docs/ankane-readme-writer.md +16 -1
- package/plugins/compound-engineering/agents/research/best-practices-researcher.md +16 -1
- package/plugins/compound-engineering/agents/research/framework-docs-researcher.md +16 -1
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +18 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +24 -3
- package/plugins/compound-engineering/agents/research/repo-research-analyst.md +22 -1
- package/plugins/compound-engineering/agents/review/agent-native-reviewer.md +16 -1
- package/plugins/compound-engineering/agents/review/architecture-strategist.md +16 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +17 -1
- package/plugins/compound-engineering/agents/review/data-integrity-guardian.md +16 -1
- package/plugins/compound-engineering/agents/review/data-migration-expert.md +16 -1
- package/plugins/compound-engineering/agents/review/deployment-verification-agent.md +16 -1
- package/plugins/compound-engineering/agents/review/dhh-rails-reviewer.md +22 -1
- package/plugins/compound-engineering/agents/review/julik-frontend-races-reviewer.md +20 -21
- package/plugins/compound-engineering/agents/review/kieran-python-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/kieran-rails-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/kieran-typescript-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/pattern-recognition-specialist.md +16 -1
- package/plugins/compound-engineering/agents/review/performance-oracle.md +28 -1
- package/plugins/compound-engineering/agents/review/schema-drift-detector.md +154 -0
- package/plugins/compound-engineering/agents/review/security-sentinel.md +22 -1
- package/plugins/compound-engineering/agents/workflow/bug-reproduction-validator.md +16 -1
- package/plugins/compound-engineering/agents/workflow/every-style-editor.md +1 -1
- package/plugins/compound-engineering/agents/workflow/pr-comment-resolver.md +16 -1
- package/plugins/compound-engineering/agents/workflow/spec-flow-analyzer.md +22 -1
- package/plugins/compound-engineering/commands/agent-native-audit.md +1 -0
- package/plugins/compound-engineering/commands/changelog.md +1 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -0
- package/plugins/compound-engineering/commands/deepen-plan.md +2 -2
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -0
- package/plugins/compound-engineering/commands/generate_command.md +1 -0
- package/plugins/compound-engineering/commands/heal-skill.md +1 -0
- package/plugins/compound-engineering/commands/lfg.md +1 -0
- package/plugins/compound-engineering/commands/release-docs.md +1 -0
- package/plugins/compound-engineering/commands/report-bug.md +1 -0
- package/plugins/compound-engineering/commands/reproduce-bug.md +1 -0
- package/plugins/compound-engineering/commands/resolve_parallel.md +1 -0
- package/plugins/compound-engineering/commands/resolve_todo_parallel.md +2 -0
- package/plugins/compound-engineering/commands/slfg.md +32 -0
- package/plugins/compound-engineering/commands/technical_review.md +8 -0
- package/plugins/compound-engineering/commands/{xcode-test.md → test-xcode.md} +2 -1
- package/plugins/compound-engineering/commands/triage.md +1 -0
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +11 -2
- package/plugins/compound-engineering/commands/workflows/compound.md +64 -27
- package/plugins/compound-engineering/commands/workflows/plan.md +9 -9
- package/plugins/compound-engineering/commands/workflows/review.md +12 -0
- package/plugins/compound-engineering/commands/workflows/work.md +71 -1
- package/plugins/compound-engineering/skills/compound-docs/SKILL.md +9 -8
- package/plugins/compound-engineering/skills/compound-docs/assets/critical-pattern-template.md +1 -1
- package/plugins/compound-engineering/skills/compound-docs/assets/resolution-template.md +3 -3
- package/plugins/compound-engineering/skills/compound-docs/references/yaml-schema.md +1 -1
- package/plugins/compound-engineering/skills/create-agent-skills/SKILL.md +168 -192
- package/plugins/compound-engineering/skills/create-agent-skills/references/official-spec.md +74 -125
- package/plugins/compound-engineering/skills/create-agent-skills/references/skill-structure.md +109 -329
- package/plugins/compound-engineering/skills/document-review/SKILL.md +87 -0
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -0
- package/plugins/compound-engineering/skills/git-worktree/scripts/worktree-manager.sh +2 -10
- package/plugins/compound-engineering/skills/orchestrating-swarms/SKILL.md +1718 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +89 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/scripts/get-pr-comments +68 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/scripts/resolve-pr-thread +23 -0
- package/plugins/compound-engineering/skills/skill-creator/SKILL.md +1 -0
- package/src/commands/sync.ts +84 -0
- package/src/converters/claude-to-codex.ts +61 -3
- package/src/converters/claude-to-opencode.ts +8 -5
- package/src/index.ts +2 -0
- package/src/parsers/claude-home.ts +65 -0
- package/src/parsers/claude.ts +4 -0
- package/src/sync/codex.ts +92 -0
- package/src/sync/opencode.ts +75 -0
- package/src/targets/codex.ts +7 -2
- package/src/targets/opencode.ts +6 -1
- package/src/types/claude.ts +3 -1
- package/src/utils/files.ts +13 -0
- package/src/utils/symlink.ts +43 -0
- package/tests/claude-parser.test.ts +24 -2
- package/tests/codex-converter.test.ts +121 -0
- package/tests/codex-writer.test.ts +32 -0
- package/tests/converter.test.ts +15 -0
- package/tests/fixtures/sample-plugin/commands/disabled-command.md +7 -0
- package/tests/fixtures/sample-plugin/skills/disabled-skill/SKILL.md +7 -0
- package/tests/opencode-writer.test.ts +32 -0
- package/plugins/compound-engineering/commands/plan_review.md +0 -7
- package/plugins/compound-engineering/commands/resolve_pr_parallel.md +0 -49
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resolve_pr_parallel
|
|
3
|
+
description: Resolve all PR comments using parallel processing. Use when addressing PR review feedback, resolving review threads, or batch-fixing PR comments.
|
|
4
|
+
argument-hint: "[optional: PR number or current PR]"
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
allowed-tools: Bash(gh *), Bash(git *), Read
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Resolve PR Comments in Parallel
|
|
10
|
+
|
|
11
|
+
Resolve all unresolved PR review comments by spawning parallel agents for each thread.
|
|
12
|
+
|
|
13
|
+
## Context Detection
|
|
14
|
+
|
|
15
|
+
Claude Code automatically detects git context:
|
|
16
|
+
- Current branch and associated PR
|
|
17
|
+
- All PR comments and review threads
|
|
18
|
+
- Works with any PR by specifying the number
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
### 1. Analyze
|
|
23
|
+
|
|
24
|
+
Fetch unresolved review threads using the GraphQL script:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bash ${CLAUDE_PLUGIN_ROOT}/skills/resolve-pr-parallel/scripts/get-pr-comments PR_NUMBER
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This returns only **unresolved, non-outdated** threads with file paths, line numbers, and comment bodies.
|
|
31
|
+
|
|
32
|
+
If the script fails, fall back to:
|
|
33
|
+
```bash
|
|
34
|
+
gh pr view PR_NUMBER --json reviews,comments
|
|
35
|
+
gh api repos/{owner}/{repo}/pulls/PR_NUMBER/comments
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Plan
|
|
39
|
+
|
|
40
|
+
Create a TodoWrite list of all unresolved items grouped by type:
|
|
41
|
+
- Code changes requested
|
|
42
|
+
- Questions to answer
|
|
43
|
+
- Style/convention fixes
|
|
44
|
+
- Test additions needed
|
|
45
|
+
|
|
46
|
+
### 3. Implement (PARALLEL)
|
|
47
|
+
|
|
48
|
+
Spawn a `pr-comment-resolver` agent for each unresolved item in parallel.
|
|
49
|
+
|
|
50
|
+
If there are 3 comments, spawn 3 agents:
|
|
51
|
+
|
|
52
|
+
1. Task pr-comment-resolver(comment1)
|
|
53
|
+
2. Task pr-comment-resolver(comment2)
|
|
54
|
+
3. Task pr-comment-resolver(comment3)
|
|
55
|
+
|
|
56
|
+
Always run all in parallel subagents/Tasks for each Todo item.
|
|
57
|
+
|
|
58
|
+
### 4. Commit & Resolve
|
|
59
|
+
|
|
60
|
+
- Commit changes with a clear message referencing the PR feedback
|
|
61
|
+
- Resolve each thread programmatically:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bash ${CLAUDE_PLUGIN_ROOT}/skills/resolve-pr-parallel/scripts/resolve-pr-thread THREAD_ID
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Push to remote
|
|
68
|
+
|
|
69
|
+
### 5. Verify
|
|
70
|
+
|
|
71
|
+
Re-fetch comments to confirm all threads are resolved:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
bash ${CLAUDE_PLUGIN_ROOT}/skills/resolve-pr-parallel/scripts/get-pr-comments PR_NUMBER
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Should return an empty array `[]`. If threads remain, repeat from step 1.
|
|
78
|
+
|
|
79
|
+
## Scripts
|
|
80
|
+
|
|
81
|
+
- [scripts/get-pr-comments](scripts/get-pr-comments) - GraphQL query for unresolved review threads
|
|
82
|
+
- [scripts/resolve-pr-thread](scripts/resolve-pr-thread) - GraphQL mutation to resolve a thread by ID
|
|
83
|
+
|
|
84
|
+
## Success Criteria
|
|
85
|
+
|
|
86
|
+
- All unresolved review threads addressed
|
|
87
|
+
- Changes committed and pushed
|
|
88
|
+
- Threads resolved via GraphQL (marked as resolved on GitHub)
|
|
89
|
+
- Empty result from get-pr-comments on verify
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
if [ $# -lt 1 ]; then
|
|
6
|
+
echo "Usage: get-pr-comments PR_NUMBER [OWNER/REPO]"
|
|
7
|
+
echo "Example: get-pr-comments 123"
|
|
8
|
+
echo "Example: get-pr-comments 123 EveryInc/cora"
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
PR_NUMBER=$1
|
|
13
|
+
|
|
14
|
+
if [ -n "$2" ]; then
|
|
15
|
+
OWNER=$(echo "$2" | cut -d/ -f1)
|
|
16
|
+
REPO=$(echo "$2" | cut -d/ -f2)
|
|
17
|
+
else
|
|
18
|
+
OWNER=$(gh repo view --json owner -q .owner.login 2>/dev/null)
|
|
19
|
+
REPO=$(gh repo view --json name -q .name 2>/dev/null)
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
|
|
23
|
+
echo "Error: Could not detect repository. Pass OWNER/REPO as second argument."
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
gh api graphql -f owner="$OWNER" -f repo="$REPO" -F pr="$PR_NUMBER" -f query='
|
|
28
|
+
query FetchUnresolvedComments($owner: String!, $repo: String!, $pr: Int!) {
|
|
29
|
+
repository(owner: $owner, name: $repo) {
|
|
30
|
+
pullRequest(number: $pr) {
|
|
31
|
+
title
|
|
32
|
+
url
|
|
33
|
+
reviewThreads(first: 100) {
|
|
34
|
+
totalCount
|
|
35
|
+
edges {
|
|
36
|
+
node {
|
|
37
|
+
id
|
|
38
|
+
isResolved
|
|
39
|
+
isOutdated
|
|
40
|
+
isCollapsed
|
|
41
|
+
path
|
|
42
|
+
line
|
|
43
|
+
startLine
|
|
44
|
+
diffSide
|
|
45
|
+
comments(first: 100) {
|
|
46
|
+
totalCount
|
|
47
|
+
nodes {
|
|
48
|
+
id
|
|
49
|
+
author {
|
|
50
|
+
login
|
|
51
|
+
}
|
|
52
|
+
body
|
|
53
|
+
createdAt
|
|
54
|
+
updatedAt
|
|
55
|
+
url
|
|
56
|
+
outdated
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
pageInfo {
|
|
62
|
+
hasNextPage
|
|
63
|
+
endCursor
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}' | jq '.data.repository.pullRequest.reviewThreads.edges | map(select(.node.isResolved == false and .node.isOutdated == false))'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
if [ $# -eq 0 ]; then
|
|
6
|
+
echo "Usage: resolve-pr-thread THREAD_ID"
|
|
7
|
+
echo "Example: resolve-pr-thread PRRT_kwDOABC123"
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
THREAD_ID=$1
|
|
12
|
+
|
|
13
|
+
gh api graphql -f threadId="$THREAD_ID" -f query='
|
|
14
|
+
mutation ResolveReviewThread($threadId: ID!) {
|
|
15
|
+
resolveReviewThread(input: {threadId: $threadId}) {
|
|
16
|
+
thread {
|
|
17
|
+
id
|
|
18
|
+
isResolved
|
|
19
|
+
path
|
|
20
|
+
line
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: skill-creator
|
|
3
3
|
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
|
|
4
4
|
license: Complete terms in LICENSE.txt
|
|
5
|
+
disable-model-invocation: true
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
# Skill Creator
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineCommand } from "citty"
|
|
2
|
+
import os from "os"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { loadClaudeHome } from "../parsers/claude-home"
|
|
5
|
+
import { syncToOpenCode } from "../sync/opencode"
|
|
6
|
+
import { syncToCodex } from "../sync/codex"
|
|
7
|
+
|
|
8
|
+
function isValidTarget(value: string): value is "opencode" | "codex" {
|
|
9
|
+
return value === "opencode" || value === "codex"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Check if any MCP servers have env vars that might contain secrets */
|
|
13
|
+
function hasPotentialSecrets(mcpServers: Record<string, unknown>): boolean {
|
|
14
|
+
const sensitivePatterns = /key|token|secret|password|credential|api_key/i
|
|
15
|
+
for (const server of Object.values(mcpServers)) {
|
|
16
|
+
const env = (server as { env?: Record<string, string> }).env
|
|
17
|
+
if (env) {
|
|
18
|
+
for (const key of Object.keys(env)) {
|
|
19
|
+
if (sensitivePatterns.test(key)) return true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default defineCommand({
|
|
27
|
+
meta: {
|
|
28
|
+
name: "sync",
|
|
29
|
+
description: "Sync Claude Code config (~/.claude/) to OpenCode or Codex",
|
|
30
|
+
},
|
|
31
|
+
args: {
|
|
32
|
+
target: {
|
|
33
|
+
type: "string",
|
|
34
|
+
required: true,
|
|
35
|
+
description: "Target: opencode | codex",
|
|
36
|
+
},
|
|
37
|
+
claudeHome: {
|
|
38
|
+
type: "string",
|
|
39
|
+
alias: "claude-home",
|
|
40
|
+
description: "Path to Claude home (default: ~/.claude)",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
async run({ args }) {
|
|
44
|
+
if (!isValidTarget(args.target)) {
|
|
45
|
+
throw new Error(`Unknown target: ${args.target}. Use 'opencode' or 'codex'.`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const claudeHome = expandHome(args.claudeHome ?? path.join(os.homedir(), ".claude"))
|
|
49
|
+
const config = await loadClaudeHome(claudeHome)
|
|
50
|
+
|
|
51
|
+
// Warn about potential secrets in MCP env vars
|
|
52
|
+
if (hasPotentialSecrets(config.mcpServers)) {
|
|
53
|
+
console.warn(
|
|
54
|
+
"⚠️ Warning: MCP servers contain env vars that may include secrets (API keys, tokens).\n" +
|
|
55
|
+
" These will be copied to the target config. Review before sharing the config file.",
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(
|
|
60
|
+
`Syncing ${config.skills.length} skills, ${Object.keys(config.mcpServers).length} MCP servers...`,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const outputRoot =
|
|
64
|
+
args.target === "opencode"
|
|
65
|
+
? path.join(os.homedir(), ".config", "opencode")
|
|
66
|
+
: path.join(os.homedir(), ".codex")
|
|
67
|
+
|
|
68
|
+
if (args.target === "opencode") {
|
|
69
|
+
await syncToOpenCode(config, outputRoot)
|
|
70
|
+
} else {
|
|
71
|
+
await syncToCodex(config, outputRoot)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`✓ Synced to ${args.target}: ${outputRoot}`)
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
function expandHome(value: string): string {
|
|
79
|
+
if (value === "~") return os.homedir()
|
|
80
|
+
if (value.startsWith(`~${path.sep}`)) {
|
|
81
|
+
return path.join(os.homedir(), value.slice(2))
|
|
82
|
+
}
|
|
83
|
+
return value
|
|
84
|
+
}
|
|
@@ -19,7 +19,8 @@ export function convertClaudeToCodex(
|
|
|
19
19
|
|
|
20
20
|
const usedSkillNames = new Set<string>(skillDirs.map((skill) => normalizeName(skill.name)))
|
|
21
21
|
const commandSkills: CodexGeneratedSkill[] = []
|
|
22
|
-
const
|
|
22
|
+
const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
|
|
23
|
+
const prompts = invocableCommands.map((command) => {
|
|
23
24
|
const promptName = uniqueName(normalizeName(command.name), promptNames)
|
|
24
25
|
const commandSkill = convertCommandSkill(command, usedSkillNames)
|
|
25
26
|
commandSkills.push(commandSkill)
|
|
@@ -73,19 +74,76 @@ function convertCommandSkill(command: ClaudeCommand, usedNames: Set<string>): Co
|
|
|
73
74
|
if (command.allowedTools && command.allowedTools.length > 0) {
|
|
74
75
|
sections.push(`## Allowed tools\n${command.allowedTools.map((tool) => `- ${tool}`).join("\n")}`)
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
// Transform Task agent calls to Codex skill references
|
|
78
|
+
const transformedBody = transformTaskCalls(command.body.trim())
|
|
79
|
+
sections.push(transformedBody)
|
|
77
80
|
const body = sections.filter(Boolean).join("\n\n").trim()
|
|
78
81
|
const content = formatFrontmatter(frontmatter, body.length > 0 ? body : command.body)
|
|
79
82
|
return { name, content }
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Transform Claude Code content to Codex-compatible content.
|
|
87
|
+
*
|
|
88
|
+
* Handles multiple syntax differences:
|
|
89
|
+
* 1. Task agent calls: Task agent-name(args) → Use the $agent-name skill to: args
|
|
90
|
+
* 2. Slash commands: /command-name → /prompts:command-name
|
|
91
|
+
* 3. Agent references: @agent-name → $agent-name skill
|
|
92
|
+
*
|
|
93
|
+
* This bridges the gap since Claude Code and Codex have different syntax
|
|
94
|
+
* for invoking commands, agents, and skills.
|
|
95
|
+
*/
|
|
96
|
+
function transformContentForCodex(body: string): string {
|
|
97
|
+
let result = body
|
|
98
|
+
|
|
99
|
+
// 1. Transform Task agent calls
|
|
100
|
+
// Match: Task repo-research-analyst(feature_description)
|
|
101
|
+
// Match: - Task learnings-researcher(args)
|
|
102
|
+
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
|
|
103
|
+
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
|
104
|
+
const skillName = normalizeName(agentName)
|
|
105
|
+
const trimmedArgs = args.trim()
|
|
106
|
+
return `${prefix}Use the $${skillName} skill to: ${trimmedArgs}`
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// 2. Transform slash command references
|
|
110
|
+
// Match: /command-name or /workflows:command but NOT /path/to/file or URLs
|
|
111
|
+
// Look for slash commands in contexts like "Run /command", "use /command", etc.
|
|
112
|
+
// Avoid matching file paths (contain multiple slashes) or URLs (contain ://)
|
|
113
|
+
const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
|
|
114
|
+
result = result.replace(slashCommandPattern, (match, commandName: string) => {
|
|
115
|
+
// Skip if it looks like a file path (contains /)
|
|
116
|
+
if (commandName.includes('/')) return match
|
|
117
|
+
// Skip common non-command patterns
|
|
118
|
+
if (['dev', 'tmp', 'etc', 'usr', 'var', 'bin', 'home'].includes(commandName)) return match
|
|
119
|
+
// Transform to Codex prompt syntax
|
|
120
|
+
const normalizedName = normalizeName(commandName)
|
|
121
|
+
return `/prompts:${normalizedName}`
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// 3. Transform @agent-name references
|
|
125
|
+
// Match: @agent-name in text (not emails)
|
|
126
|
+
const agentRefPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
|
|
127
|
+
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
|
128
|
+
const skillName = normalizeName(agentName)
|
|
129
|
+
return `$${skillName} skill`
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Alias for backward compatibility
|
|
136
|
+
const transformTaskCalls = transformContentForCodex
|
|
137
|
+
|
|
82
138
|
function renderPrompt(command: ClaudeCommand, skillName: string): string {
|
|
83
139
|
const frontmatter: Record<string, unknown> = {
|
|
84
140
|
description: command.description,
|
|
85
141
|
"argument-hint": command.argumentHint,
|
|
86
142
|
}
|
|
87
143
|
const instructions = `Use the $${skillName} skill for this command and follow its instructions.`
|
|
88
|
-
|
|
144
|
+
// Transform Task calls in prompt body too (not just skill body)
|
|
145
|
+
const transformedBody = transformTaskCalls(command.body)
|
|
146
|
+
const body = [instructions, "", transformedBody].join("\n").trim()
|
|
89
147
|
return formatFrontmatter(frontmatter, body)
|
|
90
148
|
}
|
|
91
149
|
|
|
@@ -114,6 +114,7 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) {
|
|
|
114
114
|
function convertCommands(commands: ClaudeCommand[]): Record<string, OpenCodeCommandConfig> {
|
|
115
115
|
const result: Record<string, OpenCodeCommandConfig> = {}
|
|
116
116
|
for (const command of commands) {
|
|
117
|
+
if (command.disableModelInvocation) continue
|
|
117
118
|
const entry: OpenCodeCommandConfig = {
|
|
118
119
|
description: command.description,
|
|
119
120
|
template: command.body,
|
|
@@ -209,9 +210,11 @@ function renderHookStatements(
|
|
|
209
210
|
): string[] {
|
|
210
211
|
if (!matcher.hooks || matcher.hooks.length === 0) return []
|
|
211
212
|
const tools = matcher.matcher
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
? matcher.matcher
|
|
214
|
+
.split("|")
|
|
215
|
+
.map((tool) => tool.trim().toLowerCase())
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
: []
|
|
215
218
|
|
|
216
219
|
const useMatcher = useToolMatcher && tools.length > 0 && !tools.includes("*")
|
|
217
220
|
const condition = useMatcher
|
|
@@ -232,10 +235,10 @@ function renderHookStatements(
|
|
|
232
235
|
continue
|
|
233
236
|
}
|
|
234
237
|
if (hook.type === "prompt") {
|
|
235
|
-
statements.push(`// Prompt hook for ${matcher.matcher}: ${hook.prompt.replace(/\n/g, " ")}`)
|
|
238
|
+
statements.push(`// Prompt hook for ${matcher.matcher ?? "*"}: ${hook.prompt.replace(/\n/g, " ")}`)
|
|
236
239
|
continue
|
|
237
240
|
}
|
|
238
|
-
statements.push(`// Agent hook for ${matcher.matcher}: ${hook.agent}`)
|
|
241
|
+
statements.push(`// Agent hook for ${matcher.matcher ?? "*"}: ${hook.agent}`)
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
return statements
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { defineCommand, runMain } from "citty"
|
|
|
3
3
|
import convert from "./commands/convert"
|
|
4
4
|
import install from "./commands/install"
|
|
5
5
|
import listCommand from "./commands/list"
|
|
6
|
+
import sync from "./commands/sync"
|
|
6
7
|
|
|
7
8
|
const main = defineCommand({
|
|
8
9
|
meta: {
|
|
@@ -14,6 +15,7 @@ const main = defineCommand({
|
|
|
14
15
|
convert: () => convert,
|
|
15
16
|
install: () => install,
|
|
16
17
|
list: () => listCommand,
|
|
18
|
+
sync: () => sync,
|
|
17
19
|
},
|
|
18
20
|
})
|
|
19
21
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import os from "os"
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
import type { ClaudeSkill, ClaudeMcpServer } from "../types/claude"
|
|
5
|
+
|
|
6
|
+
export interface ClaudeHomeConfig {
|
|
7
|
+
skills: ClaudeSkill[]
|
|
8
|
+
mcpServers: Record<string, ClaudeMcpServer>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function loadClaudeHome(claudeHome?: string): Promise<ClaudeHomeConfig> {
|
|
12
|
+
const home = claudeHome ?? path.join(os.homedir(), ".claude")
|
|
13
|
+
|
|
14
|
+
const [skills, mcpServers] = await Promise.all([
|
|
15
|
+
loadPersonalSkills(path.join(home, "skills")),
|
|
16
|
+
loadSettingsMcp(path.join(home, "settings.json")),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
return { skills, mcpServers }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function loadPersonalSkills(skillsDir: string): Promise<ClaudeSkill[]> {
|
|
23
|
+
try {
|
|
24
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true })
|
|
25
|
+
const skills: ClaudeSkill[] = []
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
// Check if directory or symlink (symlinks are common for skills)
|
|
29
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
30
|
+
|
|
31
|
+
const entryPath = path.join(skillsDir, entry.name)
|
|
32
|
+
const skillPath = path.join(entryPath, "SKILL.md")
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await fs.access(skillPath)
|
|
36
|
+
// Resolve symlink to get the actual source directory
|
|
37
|
+
const sourceDir = entry.isSymbolicLink()
|
|
38
|
+
? await fs.realpath(entryPath)
|
|
39
|
+
: entryPath
|
|
40
|
+
skills.push({
|
|
41
|
+
name: entry.name,
|
|
42
|
+
sourceDir,
|
|
43
|
+
skillPath,
|
|
44
|
+
})
|
|
45
|
+
} catch {
|
|
46
|
+
// No SKILL.md, skip
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return skills
|
|
50
|
+
} catch {
|
|
51
|
+
return [] // Directory doesn't exist
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function loadSettingsMcp(
|
|
56
|
+
settingsPath: string,
|
|
57
|
+
): Promise<Record<string, ClaudeMcpServer>> {
|
|
58
|
+
try {
|
|
59
|
+
const content = await fs.readFile(settingsPath, "utf-8")
|
|
60
|
+
const settings = JSON.parse(content) as { mcpServers?: Record<string, ClaudeMcpServer> }
|
|
61
|
+
return settings.mcpServers ?? {}
|
|
62
|
+
} catch {
|
|
63
|
+
return {} // File doesn't exist or invalid JSON
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/parsers/claude.ts
CHANGED
|
@@ -83,12 +83,14 @@ async function loadCommands(commandsDirs: string[]): Promise<ClaudeCommand[]> {
|
|
|
83
83
|
const { data, body } = parseFrontmatter(raw)
|
|
84
84
|
const name = (data.name as string) ?? path.basename(file, ".md")
|
|
85
85
|
const allowedTools = parseAllowedTools(data["allowed-tools"])
|
|
86
|
+
const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined
|
|
86
87
|
commands.push({
|
|
87
88
|
name,
|
|
88
89
|
description: data.description as string | undefined,
|
|
89
90
|
argumentHint: data["argument-hint"] as string | undefined,
|
|
90
91
|
model: data.model as string | undefined,
|
|
91
92
|
allowedTools,
|
|
93
|
+
disableModelInvocation,
|
|
92
94
|
body: body.trim(),
|
|
93
95
|
sourcePath: file,
|
|
94
96
|
})
|
|
@@ -104,9 +106,11 @@ async function loadSkills(skillsDirs: string[]): Promise<ClaudeSkill[]> {
|
|
|
104
106
|
const raw = await readText(file)
|
|
105
107
|
const { data } = parseFrontmatter(raw)
|
|
106
108
|
const name = (data.name as string) ?? path.basename(path.dirname(file))
|
|
109
|
+
const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined
|
|
107
110
|
skills.push({
|
|
108
111
|
name,
|
|
109
112
|
description: data.description as string | undefined,
|
|
113
|
+
disableModelInvocation,
|
|
110
114
|
sourceDir: path.dirname(file),
|
|
111
115
|
skillPath: file,
|
|
112
116
|
})
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
export async function syncToCodex(
|
|
8
|
+
config: ClaudeHomeConfig,
|
|
9
|
+
outputRoot: string,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
// Ensure output directories exist
|
|
12
|
+
const skillsDir = path.join(outputRoot, "skills")
|
|
13
|
+
await fs.mkdir(skillsDir, { recursive: true })
|
|
14
|
+
|
|
15
|
+
// Symlink skills (with validation)
|
|
16
|
+
for (const skill of config.skills) {
|
|
17
|
+
if (!isValidSkillName(skill.name)) {
|
|
18
|
+
console.warn(`Skipping skill with invalid name: ${skill.name}`)
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
const target = path.join(skillsDir, skill.name)
|
|
22
|
+
await forceSymlink(skill.sourceDir, target)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Write MCP servers to config.toml (TOML format)
|
|
26
|
+
if (Object.keys(config.mcpServers).length > 0) {
|
|
27
|
+
const configPath = path.join(outputRoot, "config.toml")
|
|
28
|
+
const mcpToml = convertMcpForCodex(config.mcpServers)
|
|
29
|
+
|
|
30
|
+
// Read existing config and merge idempotently
|
|
31
|
+
let existingContent = ""
|
|
32
|
+
try {
|
|
33
|
+
existingContent = await fs.readFile(configPath, "utf-8")
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
36
|
+
throw err
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Remove any existing Claude Code MCP section to make idempotent
|
|
41
|
+
const marker = "# MCP servers synced from Claude Code"
|
|
42
|
+
const markerIndex = existingContent.indexOf(marker)
|
|
43
|
+
if (markerIndex !== -1) {
|
|
44
|
+
existingContent = existingContent.slice(0, markerIndex).trimEnd()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const newContent = existingContent
|
|
48
|
+
? existingContent + "\n\n" + marker + "\n" + mcpToml
|
|
49
|
+
: "# Codex config - synced from Claude Code\n\n" + mcpToml
|
|
50
|
+
|
|
51
|
+
await fs.writeFile(configPath, newContent, { mode: 0o600 })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Escape a string for TOML double-quoted strings */
|
|
56
|
+
function escapeTomlString(str: string): string {
|
|
57
|
+
return str
|
|
58
|
+
.replace(/\\/g, "\\\\")
|
|
59
|
+
.replace(/"/g, '\\"')
|
|
60
|
+
.replace(/\n/g, "\\n")
|
|
61
|
+
.replace(/\r/g, "\\r")
|
|
62
|
+
.replace(/\t/g, "\\t")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function convertMcpForCodex(servers: Record<string, ClaudeMcpServer>): string {
|
|
66
|
+
const sections: string[] = []
|
|
67
|
+
|
|
68
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
69
|
+
if (!server.command) continue
|
|
70
|
+
|
|
71
|
+
const lines: string[] = []
|
|
72
|
+
lines.push(`[mcp_servers.${name}]`)
|
|
73
|
+
lines.push(`command = "${escapeTomlString(server.command)}"`)
|
|
74
|
+
|
|
75
|
+
if (server.args && server.args.length > 0) {
|
|
76
|
+
const argsStr = server.args.map((arg) => `"${escapeTomlString(arg)}"`).join(", ")
|
|
77
|
+
lines.push(`args = [${argsStr}]`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
81
|
+
lines.push("")
|
|
82
|
+
lines.push(`[mcp_servers.${name}.env]`)
|
|
83
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
84
|
+
lines.push(`${key} = "${escapeTomlString(value)}"`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
sections.push(lines.join("\n"))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return sections.join("\n\n") + "\n"
|
|
92
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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 type { OpenCodeMcpServer } from "../types/opencode"
|
|
6
|
+
import { forceSymlink, isValidSkillName } from "../utils/symlink"
|
|
7
|
+
|
|
8
|
+
export async function syncToOpenCode(
|
|
9
|
+
config: ClaudeHomeConfig,
|
|
10
|
+
outputRoot: string,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
// Ensure output directories exist
|
|
13
|
+
const skillsDir = path.join(outputRoot, "skills")
|
|
14
|
+
await fs.mkdir(skillsDir, { recursive: true })
|
|
15
|
+
|
|
16
|
+
// Symlink skills (with validation)
|
|
17
|
+
for (const skill of config.skills) {
|
|
18
|
+
if (!isValidSkillName(skill.name)) {
|
|
19
|
+
console.warn(`Skipping skill with invalid name: ${skill.name}`)
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
const target = path.join(skillsDir, skill.name)
|
|
23
|
+
await forceSymlink(skill.sourceDir, target)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Merge MCP servers into opencode.json
|
|
27
|
+
if (Object.keys(config.mcpServers).length > 0) {
|
|
28
|
+
const configPath = path.join(outputRoot, "opencode.json")
|
|
29
|
+
const existing = await readJsonSafe(configPath)
|
|
30
|
+
const mcpConfig = convertMcpForOpenCode(config.mcpServers)
|
|
31
|
+
existing.mcp = { ...(existing.mcp ?? {}), ...mcpConfig }
|
|
32
|
+
await fs.writeFile(configPath, JSON.stringify(existing, null, 2), { mode: 0o600 })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readJsonSafe(filePath: string): Promise<Record<string, unknown>> {
|
|
37
|
+
try {
|
|
38
|
+
const content = await fs.readFile(filePath, "utf-8")
|
|
39
|
+
return JSON.parse(content) as Record<string, unknown>
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
42
|
+
return {}
|
|
43
|
+
}
|
|
44
|
+
throw err
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function convertMcpForOpenCode(
|
|
49
|
+
servers: Record<string, ClaudeMcpServer>,
|
|
50
|
+
): Record<string, OpenCodeMcpServer> {
|
|
51
|
+
const result: Record<string, OpenCodeMcpServer> = {}
|
|
52
|
+
|
|
53
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
54
|
+
if (server.command) {
|
|
55
|
+
result[name] = {
|
|
56
|
+
type: "local",
|
|
57
|
+
command: [server.command, ...(server.args ?? [])],
|
|
58
|
+
environment: server.env,
|
|
59
|
+
enabled: true,
|
|
60
|
+
}
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (server.url) {
|
|
65
|
+
result[name] = {
|
|
66
|
+
type: "remote",
|
|
67
|
+
url: server.url,
|
|
68
|
+
headers: server.headers,
|
|
69
|
+
enabled: true,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
}
|