@fro.bot/systematic 1.1.0 → 1.2.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/README.md +2 -38
- package/dist/cli.js +54 -240
- package/dist/{index-v8dhd5s2.js → index-33zyxync.js} +139 -10
- package/dist/index.js +28 -23
- package/package.json +1 -1
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,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
convertContent,
|
|
3
4
|
findAgentsInDir,
|
|
4
5
|
findCommandsInDir,
|
|
5
6
|
findSkillsInDir
|
|
6
|
-
} from "./index-
|
|
7
|
+
} from "./index-33zyxync.js";
|
|
7
8
|
|
|
8
9
|
// src/cli.ts
|
|
9
|
-
import fs2 from "node:fs";
|
|
10
|
-
import path2 from "node:path";
|
|
11
|
-
|
|
12
|
-
// src/lib/converter.ts
|
|
13
10
|
import fs from "node:fs";
|
|
14
11
|
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
|
-
}
|
|
187
|
-
|
|
188
|
-
// src/cli.ts
|
|
189
12
|
var VERSION = "0.1.0";
|
|
190
13
|
var HELP = `
|
|
191
14
|
systematic - OpenCode plugin for systematic engineering workflows
|
|
@@ -195,32 +18,32 @@ Usage:
|
|
|
195
18
|
|
|
196
19
|
Commands:
|
|
197
20
|
list [type] List available skills, agents, or commands
|
|
198
|
-
convert <type> <
|
|
199
|
-
Convert
|
|
200
|
-
Types: skill, agent, command
|
|
21
|
+
convert <type> <file> [--mode=primary|subagent]
|
|
22
|
+
Convert and inspect a file (outputs to stdout)
|
|
201
23
|
config [subcommand] Configuration management
|
|
202
24
|
show Show configuration
|
|
203
25
|
path Print config file locations
|
|
204
26
|
|
|
205
27
|
Options:
|
|
206
|
-
--output, -o Output path for convert command
|
|
207
|
-
--dry-run Preview conversion without writing files
|
|
208
28
|
-h, --help Show this help message
|
|
209
29
|
-v, --version Show version
|
|
210
30
|
|
|
211
31
|
Examples:
|
|
212
32
|
systematic list skills
|
|
213
|
-
systematic
|
|
214
|
-
systematic convert agent /
|
|
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
|
|
215
38
|
`;
|
|
216
39
|
function getUserConfigDir() {
|
|
217
|
-
return
|
|
40
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || ".", ".config/opencode");
|
|
218
41
|
}
|
|
219
42
|
function getProjectConfigDir() {
|
|
220
|
-
return
|
|
43
|
+
return path.join(process.cwd(), ".opencode");
|
|
221
44
|
}
|
|
222
45
|
function listItems(type) {
|
|
223
|
-
const packageRoot =
|
|
46
|
+
const packageRoot = path.resolve(import.meta.dirname, "..");
|
|
224
47
|
const bundledDir = packageRoot;
|
|
225
48
|
let finder;
|
|
226
49
|
let subdir;
|
|
@@ -241,7 +64,7 @@ function listItems(type) {
|
|
|
241
64
|
console.error(`Unknown type: ${type}. Use: skills, agents, commands`);
|
|
242
65
|
process.exit(1);
|
|
243
66
|
}
|
|
244
|
-
const items = finder(
|
|
67
|
+
const items = finder(path.join(bundledDir, subdir), "bundled");
|
|
245
68
|
if (items.length === 0) {
|
|
246
69
|
console.log(`No ${type} found.`);
|
|
247
70
|
return;
|
|
@@ -252,71 +75,57 @@ function listItems(type) {
|
|
|
252
75
|
console.log(` ${item.name} (${item.sourceType})`);
|
|
253
76
|
}
|
|
254
77
|
}
|
|
78
|
+
function runConvert(type, filePath, modeArg) {
|
|
79
|
+
const validTypes = ["skill", "agent", "command"];
|
|
80
|
+
if (!validTypes.includes(type)) {
|
|
81
|
+
console.error(`Invalid type: ${type}. Must be one of: ${validTypes.join(", ")}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const resolvedPath = path.resolve(filePath);
|
|
85
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
86
|
+
console.error(`File not found: ${resolvedPath}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
let agentMode = "subagent";
|
|
90
|
+
if (modeArg) {
|
|
91
|
+
const modeMatch = modeArg.match(/^--mode=(primary|subagent)$/);
|
|
92
|
+
if (modeMatch) {
|
|
93
|
+
agentMode = modeMatch[1];
|
|
94
|
+
} else {
|
|
95
|
+
console.error("Invalid --mode flag. Use: --mode=primary or --mode=subagent");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const content = fs.readFileSync(resolvedPath, "utf8");
|
|
100
|
+
const converted = convertContent(content, type, { agentMode });
|
|
101
|
+
console.log(converted);
|
|
102
|
+
}
|
|
255
103
|
function configShow() {
|
|
256
104
|
const userDir = getUserConfigDir();
|
|
257
105
|
const projectDir = getProjectConfigDir();
|
|
258
106
|
console.log(`Configuration locations:
|
|
259
107
|
`);
|
|
260
|
-
console.log(` User config: ${
|
|
261
|
-
console.log(` Project config: ${
|
|
262
|
-
const projectConfig =
|
|
263
|
-
if (
|
|
108
|
+
console.log(` User config: ${path.join(userDir, "systematic.json")}`);
|
|
109
|
+
console.log(` Project config: ${path.join(projectDir, "systematic.json")}`);
|
|
110
|
+
const projectConfig = path.join(projectDir, "systematic.json");
|
|
111
|
+
if (fs.existsSync(projectConfig)) {
|
|
264
112
|
console.log(`
|
|
265
113
|
Project configuration:`);
|
|
266
|
-
console.log(
|
|
114
|
+
console.log(fs.readFileSync(projectConfig, "utf-8"));
|
|
267
115
|
}
|
|
268
|
-
const userConfig =
|
|
269
|
-
if (
|
|
116
|
+
const userConfig = path.join(userDir, "systematic.json");
|
|
117
|
+
if (fs.existsSync(userConfig)) {
|
|
270
118
|
console.log(`
|
|
271
119
|
User configuration:`);
|
|
272
|
-
console.log(
|
|
120
|
+
console.log(fs.readFileSync(userConfig, "utf-8"));
|
|
273
121
|
}
|
|
274
122
|
}
|
|
275
123
|
function configPath() {
|
|
276
124
|
const userDir = getUserConfigDir();
|
|
277
125
|
const projectDir = getProjectConfigDir();
|
|
278
126
|
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
|
-
}
|
|
127
|
+
console.log(` User: ${path.join(userDir, "systematic.json")}`);
|
|
128
|
+
console.log(` Project: ${path.join(projectDir, "systematic.json")}`);
|
|
320
129
|
}
|
|
321
130
|
var args = process.argv.slice(2);
|
|
322
131
|
var command = args[0];
|
|
@@ -325,7 +134,12 @@ switch (command) {
|
|
|
325
134
|
listItems(args[1] || "skills");
|
|
326
135
|
break;
|
|
327
136
|
case "convert":
|
|
328
|
-
|
|
137
|
+
if (!args[1] || !args[2]) {
|
|
138
|
+
console.error("Usage: systematic convert <type> <file> [--mode=primary|subagent]");
|
|
139
|
+
console.error(" type: skill, agent, or command");
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
runConvert(args[1], args[2], args[3]);
|
|
329
143
|
break;
|
|
330
144
|
case "config":
|
|
331
145
|
switch (args[1]) {
|
|
@@ -1,9 +1,138 @@
|
|
|
1
|
-
// src/lib/
|
|
1
|
+
// src/lib/converter.ts
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
var cache = new Map;
|
|
4
|
+
function parseFrontmatter(content) {
|
|
5
|
+
const lines = content.split(/\r?\n/);
|
|
6
|
+
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
7
|
+
return { data: {}, body: content, raw: "" };
|
|
8
|
+
}
|
|
9
|
+
let endIndex = -1;
|
|
10
|
+
for (let i = 1;i < lines.length; i++) {
|
|
11
|
+
if (lines[i].trim() === "---") {
|
|
12
|
+
endIndex = i;
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (endIndex === -1) {
|
|
17
|
+
return { data: {}, body: content, raw: "" };
|
|
18
|
+
}
|
|
19
|
+
const yamlLines = lines.slice(1, endIndex);
|
|
20
|
+
const body = lines.slice(endIndex + 1).join(`
|
|
21
|
+
`);
|
|
22
|
+
const raw = lines.slice(0, endIndex + 1).join(`
|
|
23
|
+
`);
|
|
24
|
+
const data = {};
|
|
25
|
+
for (const line of yamlLines) {
|
|
26
|
+
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
27
|
+
if (match) {
|
|
28
|
+
const [, key, value] = match;
|
|
29
|
+
if (value === "true")
|
|
30
|
+
data[key] = true;
|
|
31
|
+
else if (value === "false")
|
|
32
|
+
data[key] = false;
|
|
33
|
+
else if (/^\d+(\.\d+)?$/.test(value))
|
|
34
|
+
data[key] = parseFloat(value);
|
|
35
|
+
else
|
|
36
|
+
data[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { data, body, raw };
|
|
40
|
+
}
|
|
41
|
+
function formatFrontmatter(data) {
|
|
42
|
+
const lines = ["---"];
|
|
43
|
+
for (const [key, value] of Object.entries(data)) {
|
|
44
|
+
lines.push(`${key}: ${value}`);
|
|
45
|
+
}
|
|
46
|
+
lines.push("---");
|
|
47
|
+
return lines.join(`
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
function inferTemperature(name, description) {
|
|
51
|
+
const sample = `${name} ${description ?? ""}`.toLowerCase();
|
|
52
|
+
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
|
53
|
+
return 0.1;
|
|
54
|
+
}
|
|
55
|
+
if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
|
|
56
|
+
return 0.2;
|
|
57
|
+
}
|
|
58
|
+
if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
|
|
59
|
+
return 0.3;
|
|
60
|
+
}
|
|
61
|
+
if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
|
|
62
|
+
return 0.6;
|
|
63
|
+
}
|
|
64
|
+
return 0.3;
|
|
65
|
+
}
|
|
66
|
+
function normalizeModel(model) {
|
|
67
|
+
if (model.includes("/"))
|
|
68
|
+
return model;
|
|
69
|
+
if (model === "inherit")
|
|
70
|
+
return model;
|
|
71
|
+
if (/^claude-/.test(model))
|
|
72
|
+
return `anthropic/${model}`;
|
|
73
|
+
if (/^(gpt-|o1-|o3-)/.test(model))
|
|
74
|
+
return `openai/${model}`;
|
|
75
|
+
if (/^gemini-/.test(model))
|
|
76
|
+
return `google/${model}`;
|
|
77
|
+
return `anthropic/${model}`;
|
|
78
|
+
}
|
|
79
|
+
function transformAgentFrontmatter(data, agentMode) {
|
|
80
|
+
const name = typeof data.name === "string" ? data.name : "";
|
|
81
|
+
const description = typeof data.description === "string" ? data.description : "";
|
|
82
|
+
const newData = {
|
|
83
|
+
description: description || `${name} agent`,
|
|
84
|
+
mode: agentMode
|
|
85
|
+
};
|
|
86
|
+
if (typeof data.model === "string" && data.model !== "inherit") {
|
|
87
|
+
newData.model = normalizeModel(data.model);
|
|
88
|
+
}
|
|
89
|
+
if (typeof data.temperature === "number") {
|
|
90
|
+
newData.temperature = data.temperature;
|
|
91
|
+
} else {
|
|
92
|
+
newData.temperature = inferTemperature(name, description);
|
|
93
|
+
}
|
|
94
|
+
return newData;
|
|
95
|
+
}
|
|
96
|
+
function convertContent(content, type, options = {}) {
|
|
97
|
+
if (content === "")
|
|
98
|
+
return "";
|
|
99
|
+
const { data, body, raw } = parseFrontmatter(content);
|
|
100
|
+
const hasFrontmatter = raw !== "";
|
|
101
|
+
if (!hasFrontmatter) {
|
|
102
|
+
return content;
|
|
103
|
+
}
|
|
104
|
+
if (type === "agent") {
|
|
105
|
+
const agentMode = options.agentMode ?? "subagent";
|
|
106
|
+
const transformedData = transformAgentFrontmatter(data, agentMode);
|
|
107
|
+
return `${formatFrontmatter(transformedData)}
|
|
108
|
+
${body}`;
|
|
109
|
+
}
|
|
110
|
+
return content;
|
|
111
|
+
}
|
|
112
|
+
function convertFileWithCache(filePath, type, options = {}) {
|
|
113
|
+
const fd = fs.openSync(filePath, "r");
|
|
114
|
+
try {
|
|
115
|
+
const stats = fs.fstatSync(fd);
|
|
116
|
+
const cacheKey = `${filePath}:${type}:${options.source ?? "bundled"}:${options.agentMode ?? "subagent"}`;
|
|
117
|
+
const cached = cache.get(cacheKey);
|
|
118
|
+
if (cached != null && cached.mtimeMs === stats.mtimeMs) {
|
|
119
|
+
return cached.converted;
|
|
120
|
+
}
|
|
121
|
+
const content = fs.readFileSync(fd, "utf8");
|
|
122
|
+
const converted = convertContent(content, type, options);
|
|
123
|
+
cache.set(cacheKey, { mtimeMs: stats.mtimeMs, converted });
|
|
124
|
+
return converted;
|
|
125
|
+
} finally {
|
|
126
|
+
fs.closeSync(fd);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/lib/skills-core.ts
|
|
131
|
+
import fs2 from "node:fs";
|
|
3
132
|
import path from "node:path";
|
|
4
133
|
function extractFrontmatter(filePath) {
|
|
5
134
|
try {
|
|
6
|
-
const content =
|
|
135
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
7
136
|
const lines = content.split(`
|
|
8
137
|
`);
|
|
9
138
|
let inFrontmatter = false;
|
|
@@ -56,17 +185,17 @@ function stripFrontmatter(content) {
|
|
|
56
185
|
}
|
|
57
186
|
function findSkillsInDir(dir, sourceType, maxDepth = 3) {
|
|
58
187
|
const skills = [];
|
|
59
|
-
if (!
|
|
188
|
+
if (!fs2.existsSync(dir))
|
|
60
189
|
return skills;
|
|
61
190
|
function recurse(currentDir, depth) {
|
|
62
191
|
if (depth > maxDepth)
|
|
63
192
|
return;
|
|
64
|
-
const entries =
|
|
193
|
+
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
65
194
|
for (const entry of entries) {
|
|
66
195
|
const fullPath = path.join(currentDir, entry.name);
|
|
67
196
|
if (entry.isDirectory()) {
|
|
68
197
|
const skillFile = path.join(fullPath, "SKILL.md");
|
|
69
|
-
if (
|
|
198
|
+
if (fs2.existsSync(skillFile)) {
|
|
70
199
|
const { name, description } = extractFrontmatter(skillFile);
|
|
71
200
|
skills.push({
|
|
72
201
|
path: fullPath,
|
|
@@ -85,12 +214,12 @@ function findSkillsInDir(dir, sourceType, maxDepth = 3) {
|
|
|
85
214
|
}
|
|
86
215
|
function findAgentsInDir(dir, sourceType, maxDepth = 2) {
|
|
87
216
|
const agents = [];
|
|
88
|
-
if (!
|
|
217
|
+
if (!fs2.existsSync(dir))
|
|
89
218
|
return agents;
|
|
90
219
|
function recurse(currentDir, depth, category) {
|
|
91
220
|
if (depth > maxDepth)
|
|
92
221
|
return;
|
|
93
|
-
const entries =
|
|
222
|
+
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
94
223
|
for (const entry of entries) {
|
|
95
224
|
const fullPath = path.join(currentDir, entry.name);
|
|
96
225
|
if (entry.isDirectory()) {
|
|
@@ -110,12 +239,12 @@ function findAgentsInDir(dir, sourceType, maxDepth = 2) {
|
|
|
110
239
|
}
|
|
111
240
|
function findCommandsInDir(dir, sourceType, maxDepth = 2) {
|
|
112
241
|
const commands = [];
|
|
113
|
-
if (!
|
|
242
|
+
if (!fs2.existsSync(dir))
|
|
114
243
|
return commands;
|
|
115
244
|
function recurse(currentDir, depth, category) {
|
|
116
245
|
if (depth > maxDepth)
|
|
117
246
|
return;
|
|
118
|
-
const entries =
|
|
247
|
+
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
119
248
|
for (const entry of entries) {
|
|
120
249
|
const fullPath = path.join(currentDir, entry.name);
|
|
121
250
|
if (entry.isDirectory()) {
|
|
@@ -191,4 +320,4 @@ function extractCommandFrontmatter(content) {
|
|
|
191
320
|
return { name, description, argumentHint };
|
|
192
321
|
}
|
|
193
322
|
|
|
194
|
-
export { stripFrontmatter, findSkillsInDir, findAgentsInDir, findCommandsInDir, extractAgentFrontmatter, extractCommandFrontmatter };
|
|
323
|
+
export { convertContent, convertFileWithCache, stripFrontmatter, findSkillsInDir, findAgentsInDir, findCommandsInDir, extractAgentFrontmatter, extractCommandFrontmatter };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
convertContent,
|
|
3
|
+
convertFileWithCache,
|
|
2
4
|
extractAgentFrontmatter,
|
|
3
5
|
extractCommandFrontmatter,
|
|
4
6
|
findAgentsInDir,
|
|
5
7
|
findCommandsInDir,
|
|
6
8
|
findSkillsInDir,
|
|
7
9
|
stripFrontmatter
|
|
8
|
-
} from "./index-
|
|
10
|
+
} from "./index-33zyxync.js";
|
|
9
11
|
|
|
10
12
|
// src/index.ts
|
|
11
|
-
import
|
|
13
|
+
import fs3 from "node:fs";
|
|
12
14
|
import os2 from "node:os";
|
|
13
15
|
import path3 from "node:path";
|
|
14
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -64,14 +66,16 @@ function loadConfig(projectDir) {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
// src/lib/config-handler.ts
|
|
67
|
-
import fs2 from "node:fs";
|
|
68
69
|
function loadAgentAsConfig(agentInfo) {
|
|
69
70
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
71
|
+
const converted = convertFileWithCache(agentInfo.file, "agent", {
|
|
72
|
+
source: "bundled",
|
|
73
|
+
agentMode: "subagent"
|
|
74
|
+
});
|
|
75
|
+
const { description, prompt } = extractAgentFrontmatter(converted);
|
|
72
76
|
return {
|
|
73
|
-
description: description || `${
|
|
74
|
-
prompt: prompt || stripFrontmatter(
|
|
77
|
+
description: description || `${agentInfo.name} agent`,
|
|
78
|
+
prompt: prompt || stripFrontmatter(converted)
|
|
75
79
|
};
|
|
76
80
|
} catch {
|
|
77
81
|
return null;
|
|
@@ -79,11 +83,11 @@ function loadAgentAsConfig(agentInfo) {
|
|
|
79
83
|
}
|
|
80
84
|
function loadCommandAsConfig(commandInfo) {
|
|
81
85
|
try {
|
|
82
|
-
const
|
|
83
|
-
const { name, description } = extractCommandFrontmatter(
|
|
86
|
+
const converted = convertFileWithCache(commandInfo.file, "command", { source: "bundled" });
|
|
87
|
+
const { name, description } = extractCommandFrontmatter(converted);
|
|
84
88
|
const cleanName = commandInfo.name.replace(/^\//, "");
|
|
85
89
|
return {
|
|
86
|
-
template: stripFrontmatter(
|
|
90
|
+
template: stripFrontmatter(converted),
|
|
87
91
|
description: description || `${name || cleanName} command`
|
|
88
92
|
};
|
|
89
93
|
} catch {
|
|
@@ -92,9 +96,9 @@ function loadCommandAsConfig(commandInfo) {
|
|
|
92
96
|
}
|
|
93
97
|
function loadSkillAsCommand(skillInfo) {
|
|
94
98
|
try {
|
|
95
|
-
const
|
|
99
|
+
const converted = convertFileWithCache(skillInfo.skillFile, "skill", { source: "bundled" });
|
|
96
100
|
return {
|
|
97
|
-
template: stripFrontmatter(
|
|
101
|
+
template: stripFrontmatter(converted),
|
|
98
102
|
description: skillInfo.description || `${skillInfo.name} skill`
|
|
99
103
|
};
|
|
100
104
|
} catch {
|
|
@@ -163,7 +167,7 @@ function createConfigHandler(deps) {
|
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
// src/lib/skill-tool.ts
|
|
166
|
-
import
|
|
170
|
+
import fs2 from "node:fs";
|
|
167
171
|
import path2 from "node:path";
|
|
168
172
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
169
173
|
var HOOK_KEY = "systematic_skill_tool_hooked";
|
|
@@ -223,13 +227,14 @@ ${systematicSkillsXml}`;
|
|
|
223
227
|
}
|
|
224
228
|
function wrapSkillContent(skillPath, content) {
|
|
225
229
|
const skillDir = path2.dirname(skillPath);
|
|
226
|
-
const
|
|
227
|
-
|
|
230
|
+
const converted = convertContent(content, "skill", { source: "bundled" });
|
|
231
|
+
const body = stripFrontmatter(converted);
|
|
232
|
+
return `<skill-instruction>
|
|
228
233
|
Base directory for this skill: ${skillDir}/
|
|
229
234
|
File references (@path) in this skill are relative to this directory.
|
|
230
235
|
|
|
231
236
|
${body.trim()}
|
|
232
|
-
</
|
|
237
|
+
</skill-instruction>`;
|
|
233
238
|
}
|
|
234
239
|
function createSkillTool(options) {
|
|
235
240
|
const { bundledSkillsDir, disabledSkills } = options;
|
|
@@ -264,7 +269,7 @@ Use this when a task matches an available skill's description.`;
|
|
|
264
269
|
const matchedSkill = skills.find((s) => s.name === normalizedName);
|
|
265
270
|
if (matchedSkill) {
|
|
266
271
|
try {
|
|
267
|
-
const content =
|
|
272
|
+
const content = fs2.readFileSync(matchedSkill.skillFile, "utf8");
|
|
268
273
|
const wrapped = wrapSkillContent(matchedSkill.skillFile, content);
|
|
269
274
|
return `## Skill: systematic:${matchedSkill.name}
|
|
270
275
|
|
|
@@ -304,9 +309,9 @@ var packageJsonPath = path3.join(packageRoot, "package.json");
|
|
|
304
309
|
var hasLoggedInit = false;
|
|
305
310
|
var getPackageVersion = () => {
|
|
306
311
|
try {
|
|
307
|
-
if (!
|
|
312
|
+
if (!fs3.existsSync(packageJsonPath))
|
|
308
313
|
return "unknown";
|
|
309
|
-
const content =
|
|
314
|
+
const content = fs3.readFileSync(packageJsonPath, "utf8");
|
|
310
315
|
const parsed = JSON.parse(content);
|
|
311
316
|
return parsed.version ?? "unknown";
|
|
312
317
|
} catch {
|
|
@@ -318,14 +323,14 @@ var getBootstrapContent = (config) => {
|
|
|
318
323
|
return null;
|
|
319
324
|
if (config.bootstrap.file) {
|
|
320
325
|
const customPath = config.bootstrap.file.startsWith("~/") ? path3.join(os2.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
|
|
321
|
-
if (
|
|
322
|
-
return
|
|
326
|
+
if (fs3.existsSync(customPath)) {
|
|
327
|
+
return fs3.readFileSync(customPath, "utf8");
|
|
323
328
|
}
|
|
324
329
|
}
|
|
325
330
|
const usingSystematicPath = path3.join(bundledSkillsDir, "using-systematic/SKILL.md");
|
|
326
|
-
if (!
|
|
331
|
+
if (!fs3.existsSync(usingSystematicPath))
|
|
327
332
|
return null;
|
|
328
|
-
const fullContent =
|
|
333
|
+
const fullContent = fs3.readFileSync(usingSystematicPath, "utf8");
|
|
329
334
|
const content = stripFrontmatter(fullContent);
|
|
330
335
|
const toolMapping = `**Tool Mapping for OpenCode:**
|
|
331
336
|
When skills reference tools you don't have, substitute OpenCode equivalents:
|