Skip to content

Commit 2ff7334

Browse files
authored
feat(plugin): streamline converter and enhance config handling (#29)
* feat(plugin): streamline converter and enhance config handling Replace file-based converter with in-memory content transformation, add caching, and create skill-tool module with hooking support. Add comprehensive test coverage for converter and skill-tool. * fix(converter): eliminate the race condition between stats and reads
1 parent bb262c9 commit 2ff7334

9 files changed

Lines changed: 478 additions & 269 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ tests/tmp/
2626

2727
# Worktrees
2828
.worktrees/
29+
30+
# Sisyphus (OMO)
31+
.sisyphus/

README.md

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ Quick shortcuts to invoke workflows:
5252
- `/deepen-plan` - Add detail to existing plans
5353
- `/lfg` - Let's go - start working immediately
5454

55-
### Review Agents
55+
### Agents
5656

57-
Specialized code review agents organized by category:
57+
Specialized agents organized by category:
5858

5959
**Review:**
6060

@@ -98,42 +98,6 @@ Create `~/.config/opencode/systematic.json` or `.opencode/systematic.json` to di
9898
}
9999
```
100100

101-
## Converting CEP Content
102-
103-
The CLI includes a converter for adapting Claude Code agents, skills, and commands from Compound Engineering Plugin (CEP) to OpenCode.
104-
105-
### Convert a Skill
106-
107-
Skills are directories containing `SKILL.md` and supporting files:
108-
109-
```bash
110-
npx @fro.bot/systematic convert skill /path/to/cep/skills/my-skill -o ./skills/my-skill
111-
```
112-
113-
### Convert an Agent
114-
115-
Agents are markdown files that get OpenCode-compatible YAML frontmatter:
116-
117-
```bash
118-
npx @fro.bot/systematic convert agent /path/to/cep/agents/review/my-agent.md -o ./agents/review/my-agent.md
119-
```
120-
121-
### Convert a Command
122-
123-
Commands are markdown templates:
124-
125-
```bash
126-
npx @fro.bot/systematic convert command /path/to/cep/commands/my-command.md -o ./commands/my-command.md
127-
```
128-
129-
### Dry Run
130-
131-
Preview conversion without writing files:
132-
133-
```bash
134-
npx @fro.bot/systematic convert skill /path/to/skill --dry-run
135-
```
136-
137101
## Development
138102

139103
```bash

src/cli.ts

Lines changed: 54 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
#!/usr/bin/env node
22
import fs from 'node:fs'
33
import path from 'node:path'
4-
import * as converter from './lib/converter.js'
4+
import {
5+
type AgentMode,
6+
type ContentType,
7+
convertContent,
8+
} from './lib/converter.js'
59
import * as skillsCore from './lib/skills-core.js'
610

711
const VERSION = '0.1.0'
@@ -14,23 +18,23 @@ Usage:
1418
1519
Commands:
1620
list [type] List available skills, agents, or commands
17-
convert <type> <source> [--output <path>] [--dry-run]
18-
Convert Claude Code content to OpenCode format
19-
Types: skill, agent, command
21+
convert <type> <file> [--mode=primary|subagent]
22+
Convert and inspect a file (outputs to stdout)
2023
config [subcommand] Configuration management
2124
show Show configuration
2225
path Print config file locations
2326
2427
Options:
25-
--output, -o Output path for convert command
26-
--dry-run Preview conversion without writing files
2728
-h, --help Show this help message
2829
-v, --version Show version
2930
3031
Examples:
3132
systematic list skills
32-
systematic convert skill /path/to/cep/skills/agent-browser -o ./skills/agent-browser
33-
systematic convert agent /path/to/agent.md --dry-run
33+
systematic list agents
34+
systematic convert agent ./agents/my-agent.md
35+
systematic convert agent ./agents/my-agent.md --mode=primary
36+
systematic convert skill ./skills/my-skill/SKILL.md
37+
systematic config show
3438
`
3539

3640
function getUserConfigDir(): string {
@@ -85,6 +89,40 @@ function listItems(type: string): void {
8589
}
8690
}
8791

92+
function runConvert(type: string, filePath: string, modeArg?: string): void {
93+
const validTypes = ['skill', 'agent', 'command']
94+
if (!validTypes.includes(type)) {
95+
console.error(
96+
`Invalid type: ${type}. Must be one of: ${validTypes.join(', ')}`,
97+
)
98+
process.exit(1)
99+
}
100+
101+
const resolvedPath = path.resolve(filePath)
102+
if (!fs.existsSync(resolvedPath)) {
103+
console.error(`File not found: ${resolvedPath}`)
104+
process.exit(1)
105+
}
106+
107+
let agentMode: AgentMode = 'subagent'
108+
if (modeArg) {
109+
const modeMatch = modeArg.match(/^--mode=(primary|subagent)$/)
110+
if (modeMatch) {
111+
agentMode = modeMatch[1] as AgentMode
112+
} else {
113+
console.error(
114+
'Invalid --mode flag. Use: --mode=primary or --mode=subagent',
115+
)
116+
process.exit(1)
117+
}
118+
}
119+
120+
const content = fs.readFileSync(resolvedPath, 'utf8')
121+
const converted = convertContent(content, type as ContentType, { agentMode })
122+
123+
console.log(converted)
124+
}
125+
88126
function configShow(): void {
89127
const userDir = getUserConfigDir()
90128
const projectDir = getProjectConfigDir()
@@ -115,61 +153,6 @@ function configPath(): void {
115153
console.log(` Project: ${path.join(projectDir, 'systematic.json')}`)
116154
}
117155

118-
function runConvert(args: string[]): void {
119-
const typeArg = args[1]
120-
const sourceArg = args[2]
121-
122-
if (!typeArg || !sourceArg) {
123-
console.error(
124-
'Usage: systematic convert <type> <source> [--output <path>] [--dry-run]',
125-
)
126-
console.error('Types: skill, agent, command')
127-
process.exit(1)
128-
}
129-
130-
const validTypes = ['skill', 'agent', 'command']
131-
if (!validTypes.includes(typeArg)) {
132-
console.error(
133-
`Invalid type: ${typeArg}. Must be one of: ${validTypes.join(', ')}`,
134-
)
135-
process.exit(1)
136-
}
137-
138-
const sourcePath = path.resolve(sourceArg)
139-
if (!fs.existsSync(sourcePath)) {
140-
console.error(`Source not found: ${sourcePath}`)
141-
process.exit(1)
142-
}
143-
144-
const outputIndex = args.findIndex((a) => a === '--output' || a === '-o')
145-
const outputPath =
146-
outputIndex !== -1 ? path.resolve(args[outputIndex + 1]) : undefined
147-
const dryRun = args.includes('--dry-run')
148-
149-
try {
150-
const result = converter.convert(
151-
typeArg as converter.ConvertType,
152-
sourcePath,
153-
{ output: outputPath, dryRun },
154-
)
155-
156-
if (dryRun) {
157-
console.log(`[DRY RUN] Would convert ${result.type}:`)
158-
} else {
159-
console.log(`Converted ${result.type}:`)
160-
}
161-
console.log(` Source: ${result.sourcePath}`)
162-
console.log(` Output: ${result.outputPath}`)
163-
console.log(' Files:')
164-
for (const file of result.files) {
165-
console.log(` - ${file}`)
166-
}
167-
} catch (err) {
168-
console.error(`Conversion failed: ${(err as Error).message}`)
169-
process.exit(1)
170-
}
171-
}
172-
173156
const args = process.argv.slice(2)
174157
const command = args[0]
175158

@@ -178,7 +161,14 @@ switch (command) {
178161
listItems(args[1] || 'skills')
179162
break
180163
case 'convert':
181-
runConvert(args)
164+
if (!args[1] || !args[2]) {
165+
console.error(
166+
'Usage: systematic convert <type> <file> [--mode=primary|subagent]',
167+
)
168+
console.error(' type: skill, agent, or command')
169+
process.exit(1)
170+
}
171+
runConvert(args[1], args[2], args[3])
182172
break
183173
case 'config':
184174
switch (args[1]) {

src/lib/config-handler.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import fs from 'node:fs'
21
import type { AgentConfig, Config } from '@opencode-ai/sdk'
32
import { loadConfig } from './config.js'
3+
import { convertFileWithCache } from './converter.js'
44
import * as skillsCore from './skills-core.js'
55

66
export interface ConfigHandlerDeps {
@@ -16,12 +16,15 @@ function loadAgentAsConfig(
1616
agentInfo: { name: string; file: string; sourceType: string; category?: string }
1717
): AgentConfig | null {
1818
try {
19-
const content = fs.readFileSync(agentInfo.file, 'utf8')
20-
const { name, description, prompt } = skillsCore.extractAgentFrontmatter(content)
19+
const converted = convertFileWithCache(agentInfo.file, 'agent', {
20+
source: 'bundled',
21+
agentMode: 'subagent',
22+
})
23+
const { description, prompt } = skillsCore.extractAgentFrontmatter(converted)
2124

2225
return {
23-
description: description || `${name || agentInfo.name} agent`,
24-
prompt: prompt || skillsCore.stripFrontmatter(content),
26+
description: description || `${agentInfo.name} agent`,
27+
prompt: prompt || skillsCore.stripFrontmatter(converted),
2528
}
2629
} catch {
2730
return null
@@ -32,13 +35,13 @@ function loadCommandAsConfig(
3235
commandInfo: { name: string; file: string; sourceType: string; category?: string }
3336
): CommandConfig | null {
3437
try {
35-
const content = fs.readFileSync(commandInfo.file, 'utf8')
36-
const { name, description } = skillsCore.extractCommandFrontmatter(content)
38+
const converted = convertFileWithCache(commandInfo.file, 'command', { source: 'bundled' })
39+
const { name, description } = skillsCore.extractCommandFrontmatter(converted)
3740

3841
const cleanName = commandInfo.name.replace(/^\//, '')
3942

4043
return {
41-
template: skillsCore.stripFrontmatter(content),
44+
template: skillsCore.stripFrontmatter(converted),
4245
description: description || `${name || cleanName} command`,
4346
}
4447
} catch {
@@ -50,10 +53,10 @@ function loadSkillAsCommand(
5053
skillInfo: skillsCore.SkillInfo
5154
): CommandConfig | null {
5255
try {
53-
const content = fs.readFileSync(skillInfo.skillFile, 'utf8')
56+
const converted = convertFileWithCache(skillInfo.skillFile, 'skill', { source: 'bundled' })
5457

5558
return {
56-
template: skillsCore.stripFrontmatter(content),
59+
template: skillsCore.stripFrontmatter(converted),
5760
description: skillInfo.description || `${skillInfo.name} skill`,
5861
}
5962
} catch {

0 commit comments

Comments
 (0)