@fro.bot/systematic 1.1.0 → 1.2.1
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/README.md +2 -38
- package/dist/cli.js +57 -242
- package/dist/{index-v8dhd5s2.js → index-hkk4125w.js} +141 -11
- package/dist/index.js +41 -35
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -52,9 +52,9 @@ Quick shortcuts to invoke workflows:
|
|
|
52
52
|
- `/deepen-plan` - Add detail to existing plans
|
|
53
53
|
- `/lfg` - Let's go - start working immediately
|
|
54
54
|
|
|
55
|
-
###
|
|
55
|
+
### Agents
|
|
56
56
|
|
|
57
|
-
Specialized
|
|
57
|
+
Specialized agents organized by category:
|
|
58
58
|
|
|
59
59
|
**Review:**
|
|
60
60
|
|
|
@@ -98,42 +98,6 @@ Create `~/.config/opencode/systematic.json` or `.opencode/systematic.json` to di
|
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
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
|
-
|
|
137
101
|
## Development
|
|
138
102
|
|
|
139
103
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -1,191 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
2
3
|
import {
|
|
4
|
+
convertContent,
|
|
3
5
|
findAgentsInDir,
|
|
4
6
|
findCommandsInDir,
|
|
5
7
|
findSkillsInDir
|
|
6
|
-
} from "./index-
|
|
7
|
-
|
|
8
|
-
// src/cli.ts
|
|
9
|
-
import fs2 from "node:fs";
|
|
10
|
-
import path2 from "node:path";
|
|
11
|
-
|
|
12
|
-
// src/lib/converter.ts
|
|
13
|
-
import fs from "node:fs";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
function parseFrontmatter(raw) {
|
|
16
|
-
const lines = raw.split(/\r?\n/);
|
|
17
|
-
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
18
|
-
return { data: {}, body: raw };
|
|
19
|
-
}
|
|
20
|
-
let endIndex = -1;
|
|
21
|
-
for (let i = 1;i < lines.length; i++) {
|
|
22
|
-
if (lines[i].trim() === "---") {
|
|
23
|
-
endIndex = i;
|
|
24
|
-
break;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
if (endIndex === -1) {
|
|
28
|
-
return { data: {}, body: raw };
|
|
29
|
-
}
|
|
30
|
-
const yamlLines = lines.slice(1, endIndex);
|
|
31
|
-
const body = lines.slice(endIndex + 1).join(`
|
|
32
|
-
`);
|
|
33
|
-
const data = {};
|
|
34
|
-
for (const line of yamlLines) {
|
|
35
|
-
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
36
|
-
if (match) {
|
|
37
|
-
const [, key, value] = match;
|
|
38
|
-
if (value === "true")
|
|
39
|
-
data[key] = true;
|
|
40
|
-
else if (value === "false")
|
|
41
|
-
data[key] = false;
|
|
42
|
-
else if (/^\d+(\.\d+)?$/.test(value))
|
|
43
|
-
data[key] = parseFloat(value);
|
|
44
|
-
else
|
|
45
|
-
data[key] = value;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { data, body };
|
|
49
|
-
}
|
|
50
|
-
function formatFrontmatter(data, body) {
|
|
51
|
-
const lines = [];
|
|
52
|
-
if (data.description)
|
|
53
|
-
lines.push(`description: ${data.description}`);
|
|
54
|
-
if (data.mode)
|
|
55
|
-
lines.push(`mode: ${data.mode}`);
|
|
56
|
-
if (data.model)
|
|
57
|
-
lines.push(`model: ${data.model}`);
|
|
58
|
-
if (data.temperature !== undefined)
|
|
59
|
-
lines.push(`temperature: ${data.temperature}`);
|
|
60
|
-
if (lines.length === 0)
|
|
61
|
-
return body;
|
|
62
|
-
return ["---", ...lines, "---", "", body].join(`
|
|
63
|
-
`);
|
|
64
|
-
}
|
|
65
|
-
function inferTemperature(name, description) {
|
|
66
|
-
const sample = `${name} ${description || ""}`.toLowerCase();
|
|
67
|
-
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
|
68
|
-
return 0.1;
|
|
69
|
-
}
|
|
70
|
-
if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
|
|
71
|
-
return 0.2;
|
|
72
|
-
}
|
|
73
|
-
if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
|
|
74
|
-
return 0.3;
|
|
75
|
-
}
|
|
76
|
-
if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
|
|
77
|
-
return 0.6;
|
|
78
|
-
}
|
|
79
|
-
return 0.3;
|
|
80
|
-
}
|
|
81
|
-
function normalizeModel(model) {
|
|
82
|
-
if (model.includes("/"))
|
|
83
|
-
return model;
|
|
84
|
-
if (/^claude-/.test(model))
|
|
85
|
-
return `anthropic/${model}`;
|
|
86
|
-
if (/^(gpt-|o1-|o3-)/.test(model))
|
|
87
|
-
return `openai/${model}`;
|
|
88
|
-
if (/^gemini-/.test(model))
|
|
89
|
-
return `google/${model}`;
|
|
90
|
-
return `anthropic/${model}`;
|
|
91
|
-
}
|
|
92
|
-
function convertAgent(sourcePath, options = {}) {
|
|
93
|
-
const content = fs.readFileSync(sourcePath, "utf-8");
|
|
94
|
-
const name = path.basename(sourcePath, ".md");
|
|
95
|
-
const { data, body } = parseFrontmatter(content);
|
|
96
|
-
const existingDescription = data.description;
|
|
97
|
-
const existingModel = data.model;
|
|
98
|
-
let description = existingDescription;
|
|
99
|
-
if (!description) {
|
|
100
|
-
const firstLine = body.split(`
|
|
101
|
-
`).find((l) => l.trim() && !l.startsWith("#"));
|
|
102
|
-
description = firstLine?.slice(0, 100) || `${name} agent`;
|
|
103
|
-
}
|
|
104
|
-
const frontmatter = {
|
|
105
|
-
description,
|
|
106
|
-
mode: "subagent",
|
|
107
|
-
temperature: inferTemperature(name, description)
|
|
108
|
-
};
|
|
109
|
-
if (existingModel && existingModel !== "inherit") {
|
|
110
|
-
frontmatter.model = normalizeModel(existingModel);
|
|
111
|
-
}
|
|
112
|
-
const converted = formatFrontmatter(frontmatter, body.trim());
|
|
113
|
-
const outputPath = options.output || sourcePath;
|
|
114
|
-
if (!options.dryRun) {
|
|
115
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
116
|
-
fs.writeFileSync(outputPath, converted);
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
type: "agent",
|
|
120
|
-
sourcePath,
|
|
121
|
-
outputPath,
|
|
122
|
-
converted: true,
|
|
123
|
-
files: [path.basename(outputPath)]
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
function convertCommand(sourcePath, options = {}) {
|
|
127
|
-
const content = fs.readFileSync(sourcePath, "utf-8");
|
|
128
|
-
const outputPath = options.output || sourcePath;
|
|
129
|
-
if (!options.dryRun) {
|
|
130
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
131
|
-
fs.writeFileSync(outputPath, content);
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
type: "command",
|
|
135
|
-
sourcePath,
|
|
136
|
-
outputPath,
|
|
137
|
-
converted: true,
|
|
138
|
-
files: [path.basename(outputPath)]
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
function convertSkill(sourcePath, options = {}) {
|
|
142
|
-
const stats = fs.statSync(sourcePath);
|
|
143
|
-
if (!stats.isDirectory()) {
|
|
144
|
-
throw new Error(`Skill source must be a directory: ${sourcePath}`);
|
|
145
|
-
}
|
|
146
|
-
const skillName = path.basename(sourcePath);
|
|
147
|
-
const outputPath = options.output || sourcePath;
|
|
148
|
-
const files = [];
|
|
149
|
-
function copyDir(src, dest) {
|
|
150
|
-
if (!options.dryRun) {
|
|
151
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
152
|
-
}
|
|
153
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
154
|
-
const srcPath = path.join(src, entry.name);
|
|
155
|
-
const destPath = path.join(dest, entry.name);
|
|
156
|
-
if (entry.isDirectory()) {
|
|
157
|
-
copyDir(srcPath, destPath);
|
|
158
|
-
} else {
|
|
159
|
-
files.push(path.relative(outputPath, destPath));
|
|
160
|
-
if (!options.dryRun) {
|
|
161
|
-
fs.copyFileSync(srcPath, destPath);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
copyDir(sourcePath, outputPath);
|
|
167
|
-
return {
|
|
168
|
-
type: "skill",
|
|
169
|
-
sourcePath,
|
|
170
|
-
outputPath,
|
|
171
|
-
converted: true,
|
|
172
|
-
files
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function convert(type, sourcePath, options = {}) {
|
|
176
|
-
switch (type) {
|
|
177
|
-
case "agent":
|
|
178
|
-
return convertAgent(sourcePath, options);
|
|
179
|
-
case "command":
|
|
180
|
-
return convertCommand(sourcePath, options);
|
|
181
|
-
case "skill":
|
|
182
|
-
return convertSkill(sourcePath, options);
|
|
183
|
-
default:
|
|
184
|
-
throw new Error(`Unknown type: ${type}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
8
|
+
} from "./index-hkk4125w.js";
|
|
187
9
|
|
|
188
10
|
// src/cli.ts
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
189
13
|
var VERSION = "0.1.0";
|
|
190
14
|
var HELP = `
|
|
191
15
|
systematic - OpenCode plugin for systematic engineering workflows
|
|
@@ -195,32 +19,32 @@ Usage:
|
|
|
195
19
|
|
|
196
20
|
Commands:
|
|
197
21
|
list [type] List available skills, agents, or commands
|
|
198
|
-
convert <type> <
|
|
199
|
-
Convert
|
|
200
|
-
Types: skill, agent, command
|
|
22
|
+
convert <type> <file> [--mode=primary|subagent]
|
|
23
|
+
Convert and inspect a file (outputs to stdout)
|
|
201
24
|
config [subcommand] Configuration management
|
|
202
25
|
show Show configuration
|
|
203
26
|
path Print config file locations
|
|
204
27
|
|
|
205
28
|
Options:
|
|
206
|
-
--output, -o Output path for convert command
|
|
207
|
-
--dry-run Preview conversion without writing files
|
|
208
29
|
-h, --help Show this help message
|
|
209
30
|
-v, --version Show version
|
|
210
31
|
|
|
211
32
|
Examples:
|
|
212
33
|
systematic list skills
|
|
213
|
-
systematic
|
|
214
|
-
systematic convert agent /
|
|
34
|
+
systematic list agents
|
|
35
|
+
systematic convert agent ./agents/my-agent.md
|
|
36
|
+
systematic convert agent ./agents/my-agent.md --mode=primary
|
|
37
|
+
systematic convert skill ./skills/my-skill/SKILL.md
|
|
38
|
+
systematic config show
|
|
215
39
|
`;
|
|
216
40
|
function getUserConfigDir() {
|
|
217
|
-
return
|
|
41
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || ".", ".config/opencode");
|
|
218
42
|
}
|
|
219
43
|
function getProjectConfigDir() {
|
|
220
|
-
return
|
|
44
|
+
return path.join(process.cwd(), ".opencode");
|
|
221
45
|
}
|
|
222
46
|
function listItems(type) {
|
|
223
|
-
const packageRoot =
|
|
47
|
+
const packageRoot = path.resolve(import.meta.dirname, "..");
|
|
224
48
|
const bundledDir = packageRoot;
|
|
225
49
|
let finder;
|
|
226
50
|
let subdir;
|
|
@@ -241,7 +65,7 @@ function listItems(type) {
|
|
|
241
65
|
console.error(`Unknown type: ${type}. Use: skills, agents, commands`);
|
|
242
66
|
process.exit(1);
|
|
243
67
|
}
|
|
244
|
-
const items = finder(
|
|
68
|
+
const items = finder(path.join(bundledDir, subdir), "bundled");
|
|
245
69
|
if (items.length === 0) {
|
|
246
70
|
console.log(`No ${type} found.`);
|
|
247
71
|
return;
|
|
@@ -252,71 +76,57 @@ function listItems(type) {
|
|
|
252
76
|
console.log(` ${item.name} (${item.sourceType})`);
|
|
253
77
|
}
|
|
254
78
|
}
|
|
79
|
+
function runConvert(type, filePath, modeArg) {
|
|
80
|
+
const validTypes = ["skill", "agent", "command"];
|
|
81
|
+
if (!validTypes.includes(type)) {
|
|
82
|
+
console.error(`Invalid type: ${type}. Must be one of: ${validTypes.join(", ")}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const resolvedPath = path.resolve(filePath);
|
|
86
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
87
|
+
console.error(`File not found: ${resolvedPath}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
let agentMode = "subagent";
|
|
91
|
+
if (modeArg) {
|
|
92
|
+
const modeMatch = modeArg.match(/^--mode=(primary|subagent)$/);
|
|
93
|
+
if (modeMatch) {
|
|
94
|
+
agentMode = modeMatch[1];
|
|
95
|
+
} else {
|
|
96
|
+
console.error("Invalid --mode flag. Use: --mode=primary or --mode=subagent");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const content = fs.readFileSync(resolvedPath, "utf8");
|
|
101
|
+
const converted = convertContent(content, type, { agentMode });
|
|
102
|
+
console.log(converted);
|
|
103
|
+
}
|
|
255
104
|
function configShow() {
|
|
256
105
|
const userDir = getUserConfigDir();
|
|
257
106
|
const projectDir = getProjectConfigDir();
|
|
258
107
|
console.log(`Configuration locations:
|
|
259
108
|
`);
|
|
260
|
-
console.log(` User config: ${
|
|
261
|
-
console.log(` Project config: ${
|
|
262
|
-
const projectConfig =
|
|
263
|
-
if (
|
|
109
|
+
console.log(` User config: ${path.join(userDir, "systematic.json")}`);
|
|
110
|
+
console.log(` Project config: ${path.join(projectDir, "systematic.json")}`);
|
|
111
|
+
const projectConfig = path.join(projectDir, "systematic.json");
|
|
112
|
+
if (fs.existsSync(projectConfig)) {
|
|
264
113
|
console.log(`
|
|
265
114
|
Project configuration:`);
|
|
266
|
-
console.log(
|
|
115
|
+
console.log(fs.readFileSync(projectConfig, "utf-8"));
|
|
267
116
|
}
|
|
268
|
-
const userConfig =
|
|
269
|
-
if (
|
|
117
|
+
const userConfig = path.join(userDir, "systematic.json");
|
|
118
|
+
if (fs.existsSync(userConfig)) {
|
|
270
119
|
console.log(`
|
|
271
120
|
User configuration:`);
|
|
272
|
-
console.log(
|
|
121
|
+
console.log(fs.readFileSync(userConfig, "utf-8"));
|
|
273
122
|
}
|
|
274
123
|
}
|
|
275
124
|
function configPath() {
|
|
276
125
|
const userDir = getUserConfigDir();
|
|
277
126
|
const projectDir = getProjectConfigDir();
|
|
278
127
|
console.log("Config file paths:");
|
|
279
|
-
console.log(` User: ${
|
|
280
|
-
console.log(` Project: ${
|
|
281
|
-
}
|
|
282
|
-
function runConvert(args) {
|
|
283
|
-
const typeArg = args[1];
|
|
284
|
-
const sourceArg = args[2];
|
|
285
|
-
if (!typeArg || !sourceArg) {
|
|
286
|
-
console.error("Usage: systematic convert <type> <source> [--output <path>] [--dry-run]");
|
|
287
|
-
console.error("Types: skill, agent, command");
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
const validTypes = ["skill", "agent", "command"];
|
|
291
|
-
if (!validTypes.includes(typeArg)) {
|
|
292
|
-
console.error(`Invalid type: ${typeArg}. Must be one of: ${validTypes.join(", ")}`);
|
|
293
|
-
process.exit(1);
|
|
294
|
-
}
|
|
295
|
-
const sourcePath = path2.resolve(sourceArg);
|
|
296
|
-
if (!fs2.existsSync(sourcePath)) {
|
|
297
|
-
console.error(`Source not found: ${sourcePath}`);
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
const outputIndex = args.findIndex((a) => a === "--output" || a === "-o");
|
|
301
|
-
const outputPath = outputIndex !== -1 ? path2.resolve(args[outputIndex + 1]) : undefined;
|
|
302
|
-
const dryRun = args.includes("--dry-run");
|
|
303
|
-
try {
|
|
304
|
-
const result = convert(typeArg, sourcePath, { output: outputPath, dryRun });
|
|
305
|
-
if (dryRun) {
|
|
306
|
-
console.log(`[DRY RUN] Would convert ${result.type}:`);
|
|
307
|
-
} else {
|
|
308
|
-
console.log(`Converted ${result.type}:`);
|
|
309
|
-
}
|
|
310
|
-
console.log(` Source: ${result.sourcePath}`);
|
|
311
|
-
console.log(` Output: ${result.outputPath}`);
|
|
312
|
-
console.log(" Files:");
|
|
313
|
-
for (const file of result.files) {
|
|
314
|
-
console.log(` - ${file}`);
|
|
315
|
-
}
|
|
316
|
-
} catch (err) {
|
|
317
|
-
console.error(`Conversion failed: ${err.message}`);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
128
|
+
console.log(` User: ${path.join(userDir, "systematic.json")}`);
|
|
129
|
+
console.log(` Project: ${path.join(projectDir, "systematic.json")}`);
|
|
320
130
|
}
|
|
321
131
|
var args = process.argv.slice(2);
|
|
322
132
|
var command = args[0];
|
|
@@ -325,7 +135,12 @@ switch (command) {
|
|
|
325
135
|
listItems(args[1] || "skills");
|
|
326
136
|
break;
|
|
327
137
|
case "convert":
|
|
328
|
-
|
|
138
|
+
if (!args[1] || !args[2]) {
|
|
139
|
+
console.error("Usage: systematic convert <type> <file> [--mode=primary|subagent]");
|
|
140
|
+
console.error(" type: skill, agent, or command");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
runConvert(args[1], args[2], args[3]);
|
|
329
144
|
break;
|
|
330
145
|
case "config":
|
|
331
146
|
switch (args[1]) {
|
|
@@ -1,9 +1,139 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/lib/converter.ts
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
var cache = new Map;
|
|
5
|
+
function parseFrontmatter(content) {
|
|
6
|
+
const lines = content.split(/\r?\n/);
|
|
7
|
+
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
8
|
+
return { data: {}, body: content, raw: "" };
|
|
9
|
+
}
|
|
10
|
+
let endIndex = -1;
|
|
11
|
+
for (let i = 1;i < lines.length; i++) {
|
|
12
|
+
if (lines[i].trim() === "---") {
|
|
13
|
+
endIndex = i;
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (endIndex === -1) {
|
|
18
|
+
return { data: {}, body: content, raw: "" };
|
|
19
|
+
}
|
|
20
|
+
const yamlLines = lines.slice(1, endIndex);
|
|
21
|
+
const body = lines.slice(endIndex + 1).join(`
|
|
22
|
+
`);
|
|
23
|
+
const raw = lines.slice(0, endIndex + 1).join(`
|
|
24
|
+
`);
|
|
25
|
+
const data = {};
|
|
26
|
+
for (const line of yamlLines) {
|
|
27
|
+
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
28
|
+
if (match) {
|
|
29
|
+
const [, key, value] = match;
|
|
30
|
+
if (value === "true")
|
|
31
|
+
data[key] = true;
|
|
32
|
+
else if (value === "false")
|
|
33
|
+
data[key] = false;
|
|
34
|
+
else if (/^\d+(\.\d+)?$/.test(value))
|
|
35
|
+
data[key] = parseFloat(value);
|
|
36
|
+
else
|
|
37
|
+
data[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { data, body, raw };
|
|
41
|
+
}
|
|
42
|
+
function formatFrontmatter(data) {
|
|
43
|
+
const lines = ["---"];
|
|
44
|
+
for (const [key, value] of Object.entries(data)) {
|
|
45
|
+
lines.push(`${key}: ${value}`);
|
|
46
|
+
}
|
|
47
|
+
lines.push("---");
|
|
48
|
+
return lines.join(`
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
function inferTemperature(name, description) {
|
|
52
|
+
const sample = `${name} ${description ?? ""}`.toLowerCase();
|
|
53
|
+
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
|
54
|
+
return 0.1;
|
|
55
|
+
}
|
|
56
|
+
if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
|
|
57
|
+
return 0.2;
|
|
58
|
+
}
|
|
59
|
+
if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
|
|
60
|
+
return 0.3;
|
|
61
|
+
}
|
|
62
|
+
if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
|
|
63
|
+
return 0.6;
|
|
64
|
+
}
|
|
65
|
+
return 0.3;
|
|
66
|
+
}
|
|
67
|
+
function normalizeModel(model) {
|
|
68
|
+
if (model.includes("/"))
|
|
69
|
+
return model;
|
|
70
|
+
if (model === "inherit")
|
|
71
|
+
return model;
|
|
72
|
+
if (/^claude-/.test(model))
|
|
73
|
+
return `anthropic/${model}`;
|
|
74
|
+
if (/^(gpt-|o1-|o3-)/.test(model))
|
|
75
|
+
return `openai/${model}`;
|
|
76
|
+
if (/^gemini-/.test(model))
|
|
77
|
+
return `google/${model}`;
|
|
78
|
+
return `anthropic/${model}`;
|
|
79
|
+
}
|
|
80
|
+
function transformAgentFrontmatter(data, agentMode) {
|
|
81
|
+
const name = typeof data.name === "string" ? data.name : "";
|
|
82
|
+
const description = typeof data.description === "string" ? data.description : "";
|
|
83
|
+
const newData = {
|
|
84
|
+
description: description || `${name} agent`,
|
|
85
|
+
mode: agentMode
|
|
86
|
+
};
|
|
87
|
+
if (typeof data.model === "string" && data.model !== "inherit") {
|
|
88
|
+
newData.model = normalizeModel(data.model);
|
|
89
|
+
}
|
|
90
|
+
if (typeof data.temperature === "number") {
|
|
91
|
+
newData.temperature = data.temperature;
|
|
92
|
+
} else {
|
|
93
|
+
newData.temperature = inferTemperature(name, description);
|
|
94
|
+
}
|
|
95
|
+
return newData;
|
|
96
|
+
}
|
|
97
|
+
function convertContent(content, type, options = {}) {
|
|
98
|
+
if (content === "")
|
|
99
|
+
return "";
|
|
100
|
+
const { data, body, raw } = parseFrontmatter(content);
|
|
101
|
+
const hasFrontmatter = raw !== "";
|
|
102
|
+
if (!hasFrontmatter) {
|
|
103
|
+
return content;
|
|
104
|
+
}
|
|
105
|
+
if (type === "agent") {
|
|
106
|
+
const agentMode = options.agentMode ?? "subagent";
|
|
107
|
+
const transformedData = transformAgentFrontmatter(data, agentMode);
|
|
108
|
+
return `${formatFrontmatter(transformedData)}
|
|
109
|
+
${body}`;
|
|
110
|
+
}
|
|
111
|
+
return content;
|
|
112
|
+
}
|
|
113
|
+
function convertFileWithCache(filePath, type, options = {}) {
|
|
114
|
+
const fd = fs.openSync(filePath, "r");
|
|
115
|
+
try {
|
|
116
|
+
const stats = fs.fstatSync(fd);
|
|
117
|
+
const cacheKey = `${filePath}:${type}:${options.source ?? "bundled"}:${options.agentMode ?? "subagent"}`;
|
|
118
|
+
const cached = cache.get(cacheKey);
|
|
119
|
+
if (cached != null && cached.mtimeMs === stats.mtimeMs) {
|
|
120
|
+
return cached.converted;
|
|
121
|
+
}
|
|
122
|
+
const content = fs.readFileSync(fd, "utf8");
|
|
123
|
+
const converted = convertContent(content, type, options);
|
|
124
|
+
cache.set(cacheKey, { mtimeMs: stats.mtimeMs, converted });
|
|
125
|
+
return converted;
|
|
126
|
+
} finally {
|
|
127
|
+
fs.closeSync(fd);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
1
131
|
// src/lib/skills-core.ts
|
|
2
|
-
import
|
|
3
|
-
import path from "
|
|
132
|
+
import fs2 from "fs";
|
|
133
|
+
import path from "path";
|
|
4
134
|
function extractFrontmatter(filePath) {
|
|
5
135
|
try {
|
|
6
|
-
const content =
|
|
136
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
7
137
|
const lines = content.split(`
|
|
8
138
|
`);
|
|
9
139
|
let inFrontmatter = false;
|
|
@@ -56,17 +186,17 @@ function stripFrontmatter(content) {
|
|
|
56
186
|
}
|
|
57
187
|
function findSkillsInDir(dir, sourceType, maxDepth = 3) {
|
|
58
188
|
const skills = [];
|
|
59
|
-
if (!
|
|
189
|
+
if (!fs2.existsSync(dir))
|
|
60
190
|
return skills;
|
|
61
191
|
function recurse(currentDir, depth) {
|
|
62
192
|
if (depth > maxDepth)
|
|
63
193
|
return;
|
|
64
|
-
const entries =
|
|
194
|
+
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
65
195
|
for (const entry of entries) {
|
|
66
196
|
const fullPath = path.join(currentDir, entry.name);
|
|
67
197
|
if (entry.isDirectory()) {
|
|
68
198
|
const skillFile = path.join(fullPath, "SKILL.md");
|
|
69
|
-
if (
|
|
199
|
+
if (fs2.existsSync(skillFile)) {
|
|
70
200
|
const { name, description } = extractFrontmatter(skillFile);
|
|
71
201
|
skills.push({
|
|
72
202
|
path: fullPath,
|
|
@@ -85,12 +215,12 @@ function findSkillsInDir(dir, sourceType, maxDepth = 3) {
|
|
|
85
215
|
}
|
|
86
216
|
function findAgentsInDir(dir, sourceType, maxDepth = 2) {
|
|
87
217
|
const agents = [];
|
|
88
|
-
if (!
|
|
218
|
+
if (!fs2.existsSync(dir))
|
|
89
219
|
return agents;
|
|
90
220
|
function recurse(currentDir, depth, category) {
|
|
91
221
|
if (depth > maxDepth)
|
|
92
222
|
return;
|
|
93
|
-
const entries =
|
|
223
|
+
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
94
224
|
for (const entry of entries) {
|
|
95
225
|
const fullPath = path.join(currentDir, entry.name);
|
|
96
226
|
if (entry.isDirectory()) {
|
|
@@ -110,12 +240,12 @@ function findAgentsInDir(dir, sourceType, maxDepth = 2) {
|
|
|
110
240
|
}
|
|
111
241
|
function findCommandsInDir(dir, sourceType, maxDepth = 2) {
|
|
112
242
|
const commands = [];
|
|
113
|
-
if (!
|
|
243
|
+
if (!fs2.existsSync(dir))
|
|
114
244
|
return commands;
|
|
115
245
|
function recurse(currentDir, depth, category) {
|
|
116
246
|
if (depth > maxDepth)
|
|
117
247
|
return;
|
|
118
|
-
const entries =
|
|
248
|
+
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
119
249
|
for (const entry of entries) {
|
|
120
250
|
const fullPath = path.join(currentDir, entry.name);
|
|
121
251
|
if (entry.isDirectory()) {
|
|
@@ -191,4 +321,4 @@ function extractCommandFrontmatter(content) {
|
|
|
191
321
|
return { name, description, argumentHint };
|
|
192
322
|
}
|
|
193
323
|
|
|
194
|
-
export { stripFrontmatter, findSkillsInDir, findAgentsInDir, findCommandsInDir, extractAgentFrontmatter, extractCommandFrontmatter };
|
|
324
|
+
export { convertContent, convertFileWithCache, stripFrontmatter, findSkillsInDir, findAgentsInDir, findCommandsInDir, extractAgentFrontmatter, extractCommandFrontmatter };
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
+
// @bun
|
|
1
2
|
import {
|
|
3
|
+
convertContent,
|
|
4
|
+
convertFileWithCache,
|
|
2
5
|
extractAgentFrontmatter,
|
|
3
6
|
extractCommandFrontmatter,
|
|
4
7
|
findAgentsInDir,
|
|
5
8
|
findCommandsInDir,
|
|
6
9
|
findSkillsInDir,
|
|
7
10
|
stripFrontmatter
|
|
8
|
-
} from "./index-
|
|
11
|
+
} from "./index-hkk4125w.js";
|
|
9
12
|
|
|
10
13
|
// src/index.ts
|
|
11
|
-
import
|
|
12
|
-
import os2 from "
|
|
13
|
-
import path3 from "
|
|
14
|
-
import { fileURLToPath } from "
|
|
14
|
+
import fs3 from "fs";
|
|
15
|
+
import os2 from "os";
|
|
16
|
+
import path3 from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
15
18
|
|
|
16
19
|
// src/lib/config.ts
|
|
17
|
-
import fs from "
|
|
18
|
-
import path from "
|
|
19
|
-
import os from "
|
|
20
|
+
import fs from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import os from "os";
|
|
20
23
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
21
24
|
var DEFAULT_CONFIG = {
|
|
22
25
|
disabled_skills: [],
|
|
@@ -64,14 +67,16 @@ function loadConfig(projectDir) {
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
// src/lib/config-handler.ts
|
|
67
|
-
import fs2 from "node:fs";
|
|
68
70
|
function loadAgentAsConfig(agentInfo) {
|
|
69
71
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
+
const converted = convertFileWithCache(agentInfo.file, "agent", {
|
|
73
|
+
source: "bundled",
|
|
74
|
+
agentMode: "subagent"
|
|
75
|
+
});
|
|
76
|
+
const { description, prompt } = extractAgentFrontmatter(converted);
|
|
72
77
|
return {
|
|
73
|
-
description: description || `${
|
|
74
|
-
prompt: prompt || stripFrontmatter(
|
|
78
|
+
description: description || `${agentInfo.name} agent`,
|
|
79
|
+
prompt: prompt || stripFrontmatter(converted)
|
|
75
80
|
};
|
|
76
81
|
} catch {
|
|
77
82
|
return null;
|
|
@@ -79,11 +84,11 @@ function loadAgentAsConfig(agentInfo) {
|
|
|
79
84
|
}
|
|
80
85
|
function loadCommandAsConfig(commandInfo) {
|
|
81
86
|
try {
|
|
82
|
-
const
|
|
83
|
-
const { name, description } = extractCommandFrontmatter(
|
|
87
|
+
const converted = convertFileWithCache(commandInfo.file, "command", { source: "bundled" });
|
|
88
|
+
const { name, description } = extractCommandFrontmatter(converted);
|
|
84
89
|
const cleanName = commandInfo.name.replace(/^\//, "");
|
|
85
90
|
return {
|
|
86
|
-
template: stripFrontmatter(
|
|
91
|
+
template: stripFrontmatter(converted),
|
|
87
92
|
description: description || `${name || cleanName} command`
|
|
88
93
|
};
|
|
89
94
|
} catch {
|
|
@@ -92,9 +97,9 @@ function loadCommandAsConfig(commandInfo) {
|
|
|
92
97
|
}
|
|
93
98
|
function loadSkillAsCommand(skillInfo) {
|
|
94
99
|
try {
|
|
95
|
-
const
|
|
100
|
+
const converted = convertFileWithCache(skillInfo.skillFile, "skill", { source: "bundled" });
|
|
96
101
|
return {
|
|
97
|
-
template: stripFrontmatter(
|
|
102
|
+
template: stripFrontmatter(converted),
|
|
98
103
|
description: skillInfo.description || `${skillInfo.name} skill`
|
|
99
104
|
};
|
|
100
105
|
} catch {
|
|
@@ -163,8 +168,8 @@ function createConfigHandler(deps) {
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
// src/lib/skill-tool.ts
|
|
166
|
-
import
|
|
167
|
-
import path2 from "
|
|
171
|
+
import fs2 from "fs";
|
|
172
|
+
import path2 from "path";
|
|
168
173
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
169
174
|
var HOOK_KEY = "systematic_skill_tool_hooked";
|
|
170
175
|
var SYSTEMATIC_MARKER = "__systematic_skill_tool__";
|
|
@@ -223,13 +228,14 @@ ${systematicSkillsXml}`;
|
|
|
223
228
|
}
|
|
224
229
|
function wrapSkillContent(skillPath, content) {
|
|
225
230
|
const skillDir = path2.dirname(skillPath);
|
|
226
|
-
const
|
|
227
|
-
|
|
231
|
+
const converted = convertContent(content, "skill", { source: "bundled" });
|
|
232
|
+
const body = stripFrontmatter(converted);
|
|
233
|
+
return `<skill-instruction>
|
|
228
234
|
Base directory for this skill: ${skillDir}/
|
|
229
235
|
File references (@path) in this skill are relative to this directory.
|
|
230
236
|
|
|
231
237
|
${body.trim()}
|
|
232
|
-
</
|
|
238
|
+
</skill-instruction>`;
|
|
233
239
|
}
|
|
234
240
|
function createSkillTool(options) {
|
|
235
241
|
const { bundledSkillsDir, disabledSkills } = options;
|
|
@@ -264,7 +270,7 @@ Use this when a task matches an available skill's description.`;
|
|
|
264
270
|
const matchedSkill = skills.find((s) => s.name === normalizedName);
|
|
265
271
|
if (matchedSkill) {
|
|
266
272
|
try {
|
|
267
|
-
const content =
|
|
273
|
+
const content = fs2.readFileSync(matchedSkill.skillFile, "utf8");
|
|
268
274
|
const wrapped = wrapSkillContent(matchedSkill.skillFile, content);
|
|
269
275
|
return `## Skill: systematic:${matchedSkill.name}
|
|
270
276
|
|
|
@@ -304,9 +310,9 @@ var packageJsonPath = path3.join(packageRoot, "package.json");
|
|
|
304
310
|
var hasLoggedInit = false;
|
|
305
311
|
var getPackageVersion = () => {
|
|
306
312
|
try {
|
|
307
|
-
if (!
|
|
313
|
+
if (!fs3.existsSync(packageJsonPath))
|
|
308
314
|
return "unknown";
|
|
309
|
-
const content =
|
|
315
|
+
const content = fs3.readFileSync(packageJsonPath, "utf8");
|
|
310
316
|
const parsed = JSON.parse(content);
|
|
311
317
|
return parsed.version ?? "unknown";
|
|
312
318
|
} catch {
|
|
@@ -318,22 +324,22 @@ var getBootstrapContent = (config) => {
|
|
|
318
324
|
return null;
|
|
319
325
|
if (config.bootstrap.file) {
|
|
320
326
|
const customPath = config.bootstrap.file.startsWith("~/") ? path3.join(os2.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
|
|
321
|
-
if (
|
|
322
|
-
return
|
|
327
|
+
if (fs3.existsSync(customPath)) {
|
|
328
|
+
return fs3.readFileSync(customPath, "utf8");
|
|
323
329
|
}
|
|
324
330
|
}
|
|
325
331
|
const usingSystematicPath = path3.join(bundledSkillsDir, "using-systematic/SKILL.md");
|
|
326
|
-
if (!
|
|
332
|
+
if (!fs3.existsSync(usingSystematicPath))
|
|
327
333
|
return null;
|
|
328
|
-
const fullContent =
|
|
334
|
+
const fullContent = fs3.readFileSync(usingSystematicPath, "utf8");
|
|
329
335
|
const content = stripFrontmatter(fullContent);
|
|
330
336
|
const toolMapping = `**Tool Mapping for OpenCode:**
|
|
331
337
|
When skills reference tools you don't have, substitute OpenCode equivalents:
|
|
332
|
-
- \`TodoWrite\`
|
|
333
|
-
- \`Task\` tool with subagents
|
|
334
|
-
- \`Skill\` tool
|
|
335
|
-
- \`SystematicSkill\` tool
|
|
336
|
-
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\`
|
|
338
|
+
- \`TodoWrite\` \u2192 \`update_plan\`
|
|
339
|
+
- \`Task\` tool with subagents \u2192 Use OpenCode's subagent system (@mention)
|
|
340
|
+
- \`Skill\` tool \u2192 OpenCode's native \`skill\` tool
|
|
341
|
+
- \`SystematicSkill\` tool \u2192 \`systematic_skill\` (Systematic plugin skills)
|
|
342
|
+
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` \u2192 Your native tools
|
|
337
343
|
|
|
338
344
|
**Skills naming:**
|
|
339
345
|
- Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"commands"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"
|
|
23
|
+
"clean": "rimraf dist",
|
|
24
|
+
"build": "bun run clean && bun build src/index.ts src/cli.ts --outdir dist --target bun --splitting --packages external",
|
|
24
25
|
"dev": "bun --watch src/index.ts",
|
|
25
26
|
"test": "bun test tests/unit",
|
|
26
27
|
"test:integration": "bun test tests/integration",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"@types/node": "^22.0.0",
|
|
57
58
|
"conventional-changelog-conventionalcommits": "^9.0.0",
|
|
58
59
|
"markdownlint-cli": "^0.47.0",
|
|
60
|
+
"rimraf": "^6.1.2",
|
|
59
61
|
"semantic-release": "^25.0.0",
|
|
60
62
|
"semantic-release-export-data": "^1.2.0",
|
|
61
63
|
"typescript": "^5.7.0"
|