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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
],
"scripts": {
"clean": "rimraf dist",
"build": "bun run clean && bun build src/index.ts src/cli.ts --outdir dist --target bun --splitting --packages external",
"build": "bun run clean && bun build src/index.ts src/cli.ts --outdir dist --target bun --splitting --packages external && tsc --emitDeclarationOnly",
"dev": "bun --watch src/index.ts",
"test": "bun test tests/unit",
"test:integration": "bun test tests/integration",
"test:all": "bun test",
"typecheck": "tsc --noEmit",
"lint": "biome check .",
"fix": "bun run lint --fix",
"prepublishOnly": "bun run build && bun run test"
"prepublishOnly": "bun run build"
},
"keywords": [
"opencode",
Expand Down
55 changes: 20 additions & 35 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#!/usr/bin/env node
import fs from 'node:fs'
import path from 'node:path'
import * as agents from './lib/agents.js'
import * as commands from './lib/commands.js'
import { getConfigPaths } from './lib/config.js'
import {
type AgentMode,
type ContentType,
convertContent,
} from './lib/converter.js'
import * as skillsCore from './lib/skills-core.js'
import * as skills from './lib/skills.js'

const VERSION = '0.1.0'

Expand Down Expand Up @@ -37,46 +40,32 @@ Examples:
systematic config show
`

function getUserConfigDir(): string {
return path.join(
process.env.HOME || process.env.USERPROFILE || '.',
'.config/opencode',
)
}

function getProjectConfigDir(): string {
return path.join(process.cwd(), '.opencode')
}

function listItems(type: string): void {
const packageRoot = path.resolve(import.meta.dirname, '..')
const bundledDir = packageRoot

let finder: (
dir: string,
sourceType: 'bundled',
) => Array<{ name: string; sourceType: string }>
let finder: (dir: string) => Array<{ name: string }>
let subdir: string

switch (type) {
case 'skills':
finder = skillsCore.findSkillsInDir
finder = skills.findSkillsInDir
subdir = 'skills'
break
case 'agents':
finder = skillsCore.findAgentsInDir
finder = agents.findAgentsInDir
subdir = 'agents'
break
case 'commands':
finder = skillsCore.findCommandsInDir
finder = commands.findCommandsInDir
subdir = 'commands'
break
default:
console.error(`Unknown type: ${type}. Use: skills, agents, commands`)
process.exit(1)
}

const items = finder(path.join(bundledDir, subdir), 'bundled')
const items = finder(path.join(bundledDir, subdir))

if (items.length === 0) {
console.log(`No ${type} found.`)
Expand All @@ -85,7 +74,7 @@ function listItems(type: string): void {

console.log(`Available ${type}:\n`)
for (const item of items.sort((a, b) => a.name.localeCompare(b.name))) {
console.log(` ${item.name} (${item.sourceType})`)
console.log(` ${item.name}`)
}
}

Expand Down Expand Up @@ -124,33 +113,29 @@ function runConvert(type: string, filePath: string, modeArg?: string): void {
}

function configShow(): void {
const userDir = getUserConfigDir()
const projectDir = getProjectConfigDir()
const paths = getConfigPaths(process.cwd())

console.log('Configuration locations:\n')
console.log(` User config: ${path.join(userDir, 'systematic.json')}`)
console.log(` Project config: ${path.join(projectDir, 'systematic.json')}`)
console.log(` User config: ${paths.userConfig}`)
console.log(` Project config: ${paths.projectConfig}`)

const projectConfig = path.join(projectDir, 'systematic.json')
if (fs.existsSync(projectConfig)) {
if (fs.existsSync(paths.projectConfig)) {
console.log('\nProject configuration:')
console.log(fs.readFileSync(projectConfig, 'utf-8'))
console.log(fs.readFileSync(paths.projectConfig, 'utf-8'))
}

const userConfig = path.join(userDir, 'systematic.json')
if (fs.existsSync(userConfig)) {
if (fs.existsSync(paths.userConfig)) {
console.log('\nUser configuration:')
console.log(fs.readFileSync(userConfig, 'utf-8'))
console.log(fs.readFileSync(paths.userConfig, 'utf-8'))
}
}

function configPath(): void {
const userDir = getUserConfigDir()
const projectDir = getProjectConfigDir()
const paths = getConfigPaths(process.cwd())

console.log('Config file paths:')
console.log(` User: ${path.join(userDir, 'systematic.json')}`)
console.log(` Project: ${path.join(projectDir, 'systematic.json')}`)
console.log(` User: ${paths.userConfig}`)
console.log(` Project: ${paths.projectConfig}`)
}

const args = process.argv.slice(2)
Expand Down
58 changes: 3 additions & 55 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Plugin } from '@opencode-ai/plugin'
import { loadConfig, type SystematicConfig } from './lib/config.js'
import { getBootstrapContent } from './lib/bootstrap.js'
import { loadConfig } from './lib/config.js'
import { createConfigHandler } from './lib/config-handler.js'
import { createSkillTool } from './lib/skill-tool.js'
import * as skillsCore from './lib/skills-core.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

Expand All @@ -28,57 +27,6 @@ const getPackageVersion = (): string => {
}
}

const getBootstrapContent = (config: SystematicConfig): string | null => {
if (!config.bootstrap.enabled) return null

if (config.bootstrap.file) {
const customPath = config.bootstrap.file.startsWith('~/')
? path.join(os.homedir(), config.bootstrap.file.slice(2))
: config.bootstrap.file
if (fs.existsSync(customPath)) {
return fs.readFileSync(customPath, 'utf8')
}
}

const usingSystematicPath = path.join(
bundledSkillsDir,
'using-systematic/SKILL.md',
)
if (!fs.existsSync(usingSystematicPath)) return null

const fullContent = fs.readFileSync(usingSystematicPath, 'utf8')
const content = skillsCore.stripFrontmatter(fullContent)

const toolMapping = `**Tool Mapping for OpenCode:**
When skills reference tools you don't have, substitute OpenCode equivalents:
- \`TodoWrite\` → \`update_plan\`
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
- \`Skill\` tool → OpenCode's native \`skill\` tool
- \`SystematicSkill\` tool → \`systematic_skill\` (Systematic plugin skills)
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools

**Skills naming:**
- Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
- Skills can also be invoked without prefix if unambiguous

**Skills usage:**
- Use \`systematic_skill\` to load Systematic bundled skills
- Use the native \`skill\` tool for non-Systematic skills

**Skills location:**
Bundled skills are in \`${bundledSkillsDir}/\``

return `<SYSTEMATIC_WORKFLOWS>
You have access to structured engineering workflows via the systematic plugin.

**IMPORTANT: The using-systematic skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the systematic_skill tool to load "using-systematic" again - that would be redundant.**

${content}

${toolMapping}
</SYSTEMATIC_WORKFLOWS>`
}

export const SystematicPlugin: Plugin = async ({ client, directory }) => {
const config = loadConfig(directory)

Expand Down Expand Up @@ -131,7 +79,7 @@ export const SystematicPlugin: Plugin = async ({ client, directory }) => {
) {
return
}
const content = getBootstrapContent(config)
const content = getBootstrapContent(config, { bundledSkillsDir })
if (content) {
if (!output.system) {
output.system = []
Expand Down
54 changes: 54 additions & 0 deletions src/lib/agents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { walkDir } from './walk-dir.js'
import { stripFrontmatter } from './frontmatter.js'

export interface AgentFrontmatter {
name: string
description: string
prompt: string
}

export interface AgentInfo {
name: string
file: string
category?: string
}

export function findAgentsInDir(dir: string, maxDepth = 2): AgentInfo[] {
const entries = walkDir(dir, {
maxDepth,
filter: (e) => !e.isDirectory && e.name.endsWith('.md'),
})

return entries.map((entry) => ({
name: entry.name.replace(/\.md$/, ''),
file: entry.path,
category: entry.category,
}))
}

export function extractAgentFrontmatter(content: string): AgentFrontmatter {
const lines = content.split('\n')

let inFrontmatter = false
let name = ''
let description = ''

for (const line of lines) {
if (line.trim() === '---') {
if (inFrontmatter) break
inFrontmatter = true
continue
}

if (inFrontmatter) {
const match = line.match(/^(\w+(?:-\w+)*):\s*(.*)$/)
if (match) {
const [, key, value] = match
if (key === 'name') name = value.trim()
if (key === 'description') description = value.trim()
}
}
}

return { name, description, prompt: stripFrontmatter(content) }
}
68 changes: 68 additions & 0 deletions src/lib/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import type { SystematicConfig } from './config.js'
import { stripFrontmatter } from './frontmatter.js'

export interface BootstrapDeps {
bundledSkillsDir: string
}

function getToolMappingTemplate(bundledSkillsDir: string): string {
return `**Tool Mapping for OpenCode:**
When skills reference tools you don't have, substitute OpenCode equivalents:
- \`TodoWrite\` → \`update_plan\`
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
- \`Skill\` tool → OpenCode's native \`skill\` tool
- \`SystematicSkill\` tool → \`systematic_skill\` (Systematic plugin skills)
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools

**Skills naming:**
- Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
- Skills can also be invoked without prefix if unambiguous

**Skills usage:**
- Use \`systematic_skill\` to load Systematic bundled skills
- Use the native \`skill\` tool for non-Systematic skills

**Skills location:**
Bundled skills are in \`${bundledSkillsDir}/\``
}

export function getBootstrapContent(
config: SystematicConfig,
deps: BootstrapDeps,
): string | null {
const { bundledSkillsDir } = deps

if (!config.bootstrap.enabled) return null

if (config.bootstrap.file) {
const customPath = config.bootstrap.file.startsWith('~/')
? path.join(os.homedir(), config.bootstrap.file.slice(2))
: config.bootstrap.file
if (fs.existsSync(customPath)) {
return fs.readFileSync(customPath, 'utf8')
}
}

const usingSystematicPath = path.join(
bundledSkillsDir,
'using-systematic/SKILL.md',
)
if (!fs.existsSync(usingSystematicPath)) return null

const fullContent = fs.readFileSync(usingSystematicPath, 'utf8')
const content = stripFrontmatter(fullContent)
const toolMapping = getToolMappingTemplate(bundledSkillsDir)

return `<SYSTEMATIC_WORKFLOWS>
You have access to structured engineering workflows via the systematic plugin.

**IMPORTANT: The using-systematic skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the systematic_skill tool to load "using-systematic" again - that would be redundant.**

${content}

${toolMapping}
</SYSTEMATIC_WORKFLOWS>`
}
Loading