Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ tests/tmp/

# Worktrees
.worktrees/

# Sisyphus (OMO)
.sisyphus/
40 changes: 2 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ Quick shortcuts to invoke workflows:
- `/deepen-plan` - Add detail to existing plans
- `/lfg` - Let's go - start working immediately

### Review Agents
### Agents

Specialized code review agents organized by category:
Specialized agents organized by category:

**Review:**

Expand Down Expand Up @@ -98,42 +98,6 @@ Create `~/.config/opencode/systematic.json` or `.opencode/systematic.json` to di
}
```

## Converting CEP Content

The CLI includes a converter for adapting Claude Code agents, skills, and commands from Compound Engineering Plugin (CEP) to OpenCode.

### Convert a Skill

Skills are directories containing `SKILL.md` and supporting files:

```bash
npx @fro.bot/systematic convert skill /path/to/cep/skills/my-skill -o ./skills/my-skill
```

### Convert an Agent

Agents are markdown files that get OpenCode-compatible YAML frontmatter:

```bash
npx @fro.bot/systematic convert agent /path/to/cep/agents/review/my-agent.md -o ./agents/review/my-agent.md
```

### Convert a Command

Commands are markdown templates:

```bash
npx @fro.bot/systematic convert command /path/to/cep/commands/my-command.md -o ./commands/my-command.md
```

### Dry Run

Preview conversion without writing files:

```bash
npx @fro.bot/systematic convert skill /path/to/skill --dry-run
```

## Development

```bash
Expand Down
118 changes: 54 additions & 64 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#!/usr/bin/env node
import fs from 'node:fs'
import path from 'node:path'
import * as converter from './lib/converter.js'
import {
type AgentMode,
type ContentType,
convertContent,
} from './lib/converter.js'
import * as skillsCore from './lib/skills-core.js'

const VERSION = '0.1.0'
Expand All @@ -14,23 +18,23 @@ Usage:

Commands:
list [type] List available skills, agents, or commands
convert <type> <source> [--output <path>] [--dry-run]
Convert Claude Code content to OpenCode format
Types: skill, agent, command
convert <type> <file> [--mode=primary|subagent]
Convert and inspect a file (outputs to stdout)
config [subcommand] Configuration management
show Show configuration
path Print config file locations

Options:
--output, -o Output path for convert command
--dry-run Preview conversion without writing files
-h, --help Show this help message
-v, --version Show version

Examples:
systematic list skills
systematic convert skill /path/to/cep/skills/agent-browser -o ./skills/agent-browser
systematic convert agent /path/to/agent.md --dry-run
systematic list agents
systematic convert agent ./agents/my-agent.md
systematic convert agent ./agents/my-agent.md --mode=primary
systematic convert skill ./skills/my-skill/SKILL.md
systematic config show
`

function getUserConfigDir(): string {
Expand Down Expand Up @@ -85,6 +89,40 @@ function listItems(type: string): void {
}
}

function runConvert(type: string, filePath: string, modeArg?: string): void {
const validTypes = ['skill', 'agent', 'command']
if (!validTypes.includes(type)) {
console.error(
`Invalid type: ${type}. Must be one of: ${validTypes.join(', ')}`,
)
process.exit(1)
}

const resolvedPath = path.resolve(filePath)
if (!fs.existsSync(resolvedPath)) {
console.error(`File not found: ${resolvedPath}`)
process.exit(1)
}

let agentMode: AgentMode = 'subagent'
if (modeArg) {
const modeMatch = modeArg.match(/^--mode=(primary|subagent)$/)
if (modeMatch) {
agentMode = modeMatch[1] as AgentMode
} else {
console.error(
'Invalid --mode flag. Use: --mode=primary or --mode=subagent',
)
process.exit(1)
}
}

const content = fs.readFileSync(resolvedPath, 'utf8')
const converted = convertContent(content, type as ContentType, { agentMode })

console.log(converted)
}

function configShow(): void {
const userDir = getUserConfigDir()
const projectDir = getProjectConfigDir()
Expand Down Expand Up @@ -115,61 +153,6 @@ function configPath(): void {
console.log(` Project: ${path.join(projectDir, 'systematic.json')}`)
}

function runConvert(args: string[]): void {
const typeArg = args[1]
const sourceArg = args[2]

if (!typeArg || !sourceArg) {
console.error(
'Usage: systematic convert <type> <source> [--output <path>] [--dry-run]',
)
console.error('Types: skill, agent, command')
process.exit(1)
}

const validTypes = ['skill', 'agent', 'command']
if (!validTypes.includes(typeArg)) {
console.error(
`Invalid type: ${typeArg}. Must be one of: ${validTypes.join(', ')}`,
)
process.exit(1)
}

const sourcePath = path.resolve(sourceArg)
if (!fs.existsSync(sourcePath)) {
console.error(`Source not found: ${sourcePath}`)
process.exit(1)
}

const outputIndex = args.findIndex((a) => a === '--output' || a === '-o')
const outputPath =
outputIndex !== -1 ? path.resolve(args[outputIndex + 1]) : undefined
const dryRun = args.includes('--dry-run')

try {
const result = converter.convert(
typeArg as converter.ConvertType,
sourcePath,
{ output: outputPath, dryRun },
)

if (dryRun) {
console.log(`[DRY RUN] Would convert ${result.type}:`)
} else {
console.log(`Converted ${result.type}:`)
}
console.log(` Source: ${result.sourcePath}`)
console.log(` Output: ${result.outputPath}`)
console.log(' Files:')
for (const file of result.files) {
console.log(` - ${file}`)
}
} catch (err) {
console.error(`Conversion failed: ${(err as Error).message}`)
process.exit(1)
}
}

const args = process.argv.slice(2)
const command = args[0]

Expand All @@ -178,7 +161,14 @@ switch (command) {
listItems(args[1] || 'skills')
break
case 'convert':
runConvert(args)
if (!args[1] || !args[2]) {
console.error(
'Usage: systematic convert <type> <file> [--mode=primary|subagent]',
)
console.error(' type: skill, agent, or command')
process.exit(1)
}
runConvert(args[1], args[2], args[3])
break
case 'config':
switch (args[1]) {
Expand Down
23 changes: 13 additions & 10 deletions src/lib/config-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'node:fs'
import type { AgentConfig, Config } from '@opencode-ai/sdk'
import { loadConfig } from './config.js'
import { convertFileWithCache } from './converter.js'
import * as skillsCore from './skills-core.js'

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

return {
description: description || `${name || agentInfo.name} agent`,
prompt: prompt || skillsCore.stripFrontmatter(content),
description: description || `${agentInfo.name} agent`,
prompt: prompt || skillsCore.stripFrontmatter(converted),
}
} catch {
return null
Expand All @@ -32,13 +35,13 @@ function loadCommandAsConfig(
commandInfo: { name: string; file: string; sourceType: string; category?: string }
): CommandConfig | null {
try {
const content = fs.readFileSync(commandInfo.file, 'utf8')
const { name, description } = skillsCore.extractCommandFrontmatter(content)
const converted = convertFileWithCache(commandInfo.file, 'command', { source: 'bundled' })
const { name, description } = skillsCore.extractCommandFrontmatter(converted)

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

return {
template: skillsCore.stripFrontmatter(content),
template: skillsCore.stripFrontmatter(converted),
description: description || `${name || cleanName} command`,
}
} catch {
Expand All @@ -50,10 +53,10 @@ function loadSkillAsCommand(
skillInfo: skillsCore.SkillInfo
): CommandConfig | null {
try {
const content = fs.readFileSync(skillInfo.skillFile, 'utf8')
const converted = convertFileWithCache(skillInfo.skillFile, 'skill', { source: 'bundled' })

return {
template: skillsCore.stripFrontmatter(content),
template: skillsCore.stripFrontmatter(converted),
description: skillInfo.description || `${skillInfo.name} skill`,
}
} catch {
Expand Down
Loading