@fro.bot/systematic 1.0.3 → 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 +5 -41
- package/dist/cli.js +54 -240
- package/dist/{index-v8dhd5s2.js → index-33zyxync.js} +139 -10
- package/dist/index.js +203 -81
- package/package.json +2 -2
- package/skills/using-systematic/SKILL.md +5 -5
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
|
|
|
@@ -82,9 +82,9 @@ The plugin provides these tools to OpenCode:
|
|
|
82
82
|
|
|
83
83
|
| Tool | Description |
|
|
84
84
|
|------|-------------|
|
|
85
|
-
| `
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
| `systematic_skill` | Load Systematic bundled skills |
|
|
86
|
+
|
|
87
|
+
The bootstrap skill instructs OpenCode to use the native `skill` tool to load non-Systematic skills.
|
|
88
88
|
|
|
89
89
|
## Configuration
|
|
90
90
|
|
|
@@ -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,21 +1,19 @@
|
|
|
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
13
|
import fs3 from "node:fs";
|
|
12
14
|
import os2 from "node:os";
|
|
13
|
-
import
|
|
15
|
+
import path3 from "node:path";
|
|
14
16
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { tool } from "@opencode-ai/plugin/tool";
|
|
16
|
-
|
|
17
|
-
// src/lib/config-handler.ts
|
|
18
|
-
import fs2 from "node:fs";
|
|
19
17
|
|
|
20
18
|
// src/lib/config.ts
|
|
21
19
|
import fs from "node:fs";
|
|
@@ -70,11 +68,14 @@ function loadConfig(projectDir) {
|
|
|
70
68
|
// src/lib/config-handler.ts
|
|
71
69
|
function loadAgentAsConfig(agentInfo) {
|
|
72
70
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
71
|
+
const converted = convertFileWithCache(agentInfo.file, "agent", {
|
|
72
|
+
source: "bundled",
|
|
73
|
+
agentMode: "subagent"
|
|
74
|
+
});
|
|
75
|
+
const { description, prompt } = extractAgentFrontmatter(converted);
|
|
75
76
|
return {
|
|
76
|
-
description: description || `${
|
|
77
|
-
prompt: prompt || stripFrontmatter(
|
|
77
|
+
description: description || `${agentInfo.name} agent`,
|
|
78
|
+
prompt: prompt || stripFrontmatter(converted)
|
|
78
79
|
};
|
|
79
80
|
} catch {
|
|
80
81
|
return null;
|
|
@@ -82,11 +83,11 @@ function loadAgentAsConfig(agentInfo) {
|
|
|
82
83
|
}
|
|
83
84
|
function loadCommandAsConfig(commandInfo) {
|
|
84
85
|
try {
|
|
85
|
-
const
|
|
86
|
-
const { name, description } = extractCommandFrontmatter(
|
|
86
|
+
const converted = convertFileWithCache(commandInfo.file, "command", { source: "bundled" });
|
|
87
|
+
const { name, description } = extractCommandFrontmatter(converted);
|
|
87
88
|
const cleanName = commandInfo.name.replace(/^\//, "");
|
|
88
89
|
return {
|
|
89
|
-
template: stripFrontmatter(
|
|
90
|
+
template: stripFrontmatter(converted),
|
|
90
91
|
description: description || `${name || cleanName} command`
|
|
91
92
|
};
|
|
92
93
|
} catch {
|
|
@@ -95,9 +96,9 @@ function loadCommandAsConfig(commandInfo) {
|
|
|
95
96
|
}
|
|
96
97
|
function loadSkillAsCommand(skillInfo) {
|
|
97
98
|
try {
|
|
98
|
-
const
|
|
99
|
+
const converted = convertFileWithCache(skillInfo.skillFile, "skill", { source: "bundled" });
|
|
99
100
|
return {
|
|
100
|
-
template: stripFrontmatter(
|
|
101
|
+
template: stripFrontmatter(converted),
|
|
101
102
|
description: skillInfo.description || `${skillInfo.name} skill`
|
|
102
103
|
};
|
|
103
104
|
} catch {
|
|
@@ -165,32 +166,168 @@ function createConfigHandler(deps) {
|
|
|
165
166
|
};
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
// src/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
var
|
|
173
|
-
var
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
169
|
+
// src/lib/skill-tool.ts
|
|
170
|
+
import fs2 from "node:fs";
|
|
171
|
+
import path2 from "node:path";
|
|
172
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
173
|
+
var HOOK_KEY = "systematic_skill_tool_hooked";
|
|
174
|
+
var SYSTEMATIC_MARKER = "__systematic_skill_tool__";
|
|
175
|
+
var globalStore = globalThis;
|
|
176
|
+
function getHookState() {
|
|
177
|
+
let state = globalStore[HOOK_KEY];
|
|
178
|
+
if (state == null) {
|
|
179
|
+
state = {
|
|
180
|
+
hookedTool: null,
|
|
181
|
+
hookedDescription: null,
|
|
182
|
+
initialized: false
|
|
183
|
+
};
|
|
184
|
+
globalStore[HOOK_KEY] = state;
|
|
185
|
+
}
|
|
186
|
+
return state;
|
|
187
|
+
}
|
|
188
|
+
function getHookedTool() {
|
|
189
|
+
return getHookState().hookedTool;
|
|
190
|
+
}
|
|
191
|
+
function formatSkillsXml(skills) {
|
|
192
|
+
if (skills.length === 0)
|
|
193
|
+
return "";
|
|
194
|
+
const skillsXml = skills.map((skill) => {
|
|
195
|
+
const lines = [
|
|
196
|
+
" <skill>",
|
|
197
|
+
` <name>systematic:${skill.name}</name>`,
|
|
198
|
+
` <description>${skill.description}</description>`
|
|
199
|
+
];
|
|
200
|
+
lines.push(" </skill>");
|
|
201
|
+
return lines.join(`
|
|
202
|
+
`);
|
|
203
|
+
}).join(`
|
|
204
|
+
`);
|
|
205
|
+
return `<available_skills>
|
|
206
|
+
${skillsXml}
|
|
207
|
+
</available_skills>`;
|
|
208
|
+
}
|
|
209
|
+
function mergeDescriptions(baseDescription, hookedDescription, systematicSkillsXml) {
|
|
210
|
+
if (hookedDescription == null || hookedDescription.trim() === "") {
|
|
211
|
+
return `${baseDescription}
|
|
212
|
+
|
|
213
|
+
${systematicSkillsXml}`;
|
|
214
|
+
}
|
|
215
|
+
const availableSkillsMatch = hookedDescription.match(/<available_skills>([\s\S]*?)<\/available_skills>/);
|
|
216
|
+
if (availableSkillsMatch) {
|
|
217
|
+
const existingSkillsContent = availableSkillsMatch[1];
|
|
218
|
+
const systematicSkillsContent = systematicSkillsXml.replace("<available_skills>", "").replace("</available_skills>", "").trim();
|
|
219
|
+
const mergedContent = `<available_skills>
|
|
220
|
+
${systematicSkillsContent}
|
|
221
|
+
${existingSkillsContent}</available_skills>`;
|
|
222
|
+
return hookedDescription.replace(/<available_skills>[\s\S]*?<\/available_skills>/, mergedContent);
|
|
181
223
|
}
|
|
182
|
-
return
|
|
224
|
+
return `${hookedDescription}
|
|
225
|
+
|
|
226
|
+
${systematicSkillsXml}`;
|
|
227
|
+
}
|
|
228
|
+
function wrapSkillContent(skillPath, content) {
|
|
229
|
+
const skillDir = path2.dirname(skillPath);
|
|
230
|
+
const converted = convertContent(content, "skill", { source: "bundled" });
|
|
231
|
+
const body = stripFrontmatter(converted);
|
|
232
|
+
return `<skill-instruction>
|
|
233
|
+
Base directory for this skill: ${skillDir}/
|
|
234
|
+
File references (@path) in this skill are relative to this directory.
|
|
235
|
+
|
|
236
|
+
${body.trim()}
|
|
237
|
+
</skill-instruction>`;
|
|
238
|
+
}
|
|
239
|
+
function createSkillTool(options) {
|
|
240
|
+
const { bundledSkillsDir, disabledSkills } = options;
|
|
241
|
+
const getSystematicSkills = () => {
|
|
242
|
+
return findSkillsInDir(bundledSkillsDir, "bundled", 3).filter((s) => !disabledSkills.includes(s.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
243
|
+
};
|
|
244
|
+
const buildDescription = () => {
|
|
245
|
+
const skills = getSystematicSkills();
|
|
246
|
+
const systematicXml = formatSkillsXml(skills);
|
|
247
|
+
const baseDescription = `Load a skill to get detailed instructions for a specific task.
|
|
248
|
+
|
|
249
|
+
Skills provide specialized knowledge and step-by-step guidance.
|
|
250
|
+
Use this when a task matches an available skill's description.`;
|
|
251
|
+
const hookState = getHookState();
|
|
252
|
+
return mergeDescriptions(baseDescription, hookState.hookedDescription, systematicXml);
|
|
253
|
+
};
|
|
254
|
+
let cachedDescription = null;
|
|
255
|
+
const toolDef = tool({
|
|
256
|
+
get description() {
|
|
257
|
+
if (cachedDescription == null) {
|
|
258
|
+
cachedDescription = buildDescription();
|
|
259
|
+
}
|
|
260
|
+
return cachedDescription;
|
|
261
|
+
},
|
|
262
|
+
args: {
|
|
263
|
+
name: tool.schema.string().describe("The skill identifier from available_skills (e.g., 'systematic:brainstorming')")
|
|
264
|
+
},
|
|
265
|
+
async execute(args) {
|
|
266
|
+
const requestedName = args.name;
|
|
267
|
+
const normalizedName = requestedName.startsWith("systematic:") ? requestedName.slice("systematic:".length) : requestedName;
|
|
268
|
+
const skills = getSystematicSkills();
|
|
269
|
+
const matchedSkill = skills.find((s) => s.name === normalizedName);
|
|
270
|
+
if (matchedSkill) {
|
|
271
|
+
try {
|
|
272
|
+
const content = fs2.readFileSync(matchedSkill.skillFile, "utf8");
|
|
273
|
+
const wrapped = wrapSkillContent(matchedSkill.skillFile, content);
|
|
274
|
+
return `## Skill: systematic:${matchedSkill.name}
|
|
275
|
+
|
|
276
|
+
**Base directory**: ${matchedSkill.path}
|
|
277
|
+
|
|
278
|
+
${wrapped}`;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
281
|
+
throw new Error(`Failed to load skill "${requestedName}": ${errorMessage}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const hookedTool = getHookedTool();
|
|
285
|
+
if (hookedTool != null && typeof hookedTool.execute === "function") {
|
|
286
|
+
try {
|
|
287
|
+
return await hookedTool.execute(args);
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
const availableSystematic = skills.map((s) => `systematic:${s.name}`);
|
|
291
|
+
throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
Object.defineProperty(toolDef, SYSTEMATIC_MARKER, {
|
|
295
|
+
value: true,
|
|
296
|
+
enumerable: false,
|
|
297
|
+
writable: false
|
|
298
|
+
});
|
|
299
|
+
return toolDef;
|
|
183
300
|
}
|
|
301
|
+
|
|
302
|
+
// src/index.ts
|
|
303
|
+
var __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
|
|
304
|
+
var packageRoot = path3.resolve(__dirname2, "..");
|
|
305
|
+
var bundledSkillsDir = path3.join(packageRoot, "skills");
|
|
306
|
+
var bundledAgentsDir = path3.join(packageRoot, "agents");
|
|
307
|
+
var bundledCommandsDir = path3.join(packageRoot, "commands");
|
|
308
|
+
var packageJsonPath = path3.join(packageRoot, "package.json");
|
|
309
|
+
var hasLoggedInit = false;
|
|
310
|
+
var getPackageVersion = () => {
|
|
311
|
+
try {
|
|
312
|
+
if (!fs3.existsSync(packageJsonPath))
|
|
313
|
+
return "unknown";
|
|
314
|
+
const content = fs3.readFileSync(packageJsonPath, "utf8");
|
|
315
|
+
const parsed = JSON.parse(content);
|
|
316
|
+
return parsed.version ?? "unknown";
|
|
317
|
+
} catch {
|
|
318
|
+
return "unknown";
|
|
319
|
+
}
|
|
320
|
+
};
|
|
184
321
|
var getBootstrapContent = (config) => {
|
|
185
322
|
if (!config.bootstrap.enabled)
|
|
186
323
|
return null;
|
|
187
324
|
if (config.bootstrap.file) {
|
|
188
|
-
const customPath = config.bootstrap.file.startsWith("~/") ?
|
|
325
|
+
const customPath = config.bootstrap.file.startsWith("~/") ? path3.join(os2.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
|
|
189
326
|
if (fs3.existsSync(customPath)) {
|
|
190
327
|
return fs3.readFileSync(customPath, "utf8");
|
|
191
328
|
}
|
|
192
329
|
}
|
|
193
|
-
const usingSystematicPath =
|
|
330
|
+
const usingSystematicPath = path3.join(bundledSkillsDir, "using-systematic/SKILL.md");
|
|
194
331
|
if (!fs3.existsSync(usingSystematicPath))
|
|
195
332
|
return null;
|
|
196
333
|
const fullContent = fs3.readFileSync(usingSystematicPath, "utf8");
|
|
@@ -200,19 +337,23 @@ When skills reference tools you don't have, substitute OpenCode equivalents:
|
|
|
200
337
|
- \`TodoWrite\` → \`update_plan\`
|
|
201
338
|
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
|
|
202
339
|
- \`Skill\` tool → OpenCode's native \`skill\` tool
|
|
340
|
+
- \`SystematicSkill\` tool → \`systematic_skill\` (Systematic plugin skills)
|
|
203
341
|
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
|
|
204
342
|
|
|
205
343
|
**Skills naming:**
|
|
206
344
|
- Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
|
|
207
345
|
- Skills can also be invoked without prefix if unambiguous
|
|
208
346
|
|
|
347
|
+
**Skills usage:**
|
|
348
|
+
- Use \`systematic_skill\` to load Systematic bundled skills
|
|
349
|
+
- Use the native \`skill\` tool for non-Systematic skills
|
|
350
|
+
|
|
209
351
|
**Skills location:**
|
|
210
|
-
Bundled skills are in \`${bundledSkillsDir}
|
|
211
|
-
Use \`systematic_find_skills\` to list all available skills.`;
|
|
352
|
+
Bundled skills are in \`${bundledSkillsDir}/\``;
|
|
212
353
|
return `<SYSTEMATIC_WORKFLOWS>
|
|
213
354
|
You have access to structured engineering workflows via the systematic plugin.
|
|
214
355
|
|
|
215
|
-
**IMPORTANT: The using-systematic skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the
|
|
356
|
+
**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.**
|
|
216
357
|
|
|
217
358
|
${content}
|
|
218
359
|
|
|
@@ -230,56 +371,37 @@ var SystematicPlugin = async ({ client, directory }) => {
|
|
|
230
371
|
return {
|
|
231
372
|
config: configHandler,
|
|
232
373
|
tool: {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
execute: async () => {
|
|
237
|
-
const bundledSkills = findSkillsInDir(bundledSkillsDir, "bundled", 3);
|
|
238
|
-
const skills = bundledSkills.filter((s) => !config.disabled_skills.includes(s.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
239
|
-
if (skills.length === 0) {
|
|
240
|
-
return "No skills available. Skills are bundled with the systematic plugin.";
|
|
241
|
-
}
|
|
242
|
-
let output = `Available skills:
|
|
243
|
-
|
|
244
|
-
`;
|
|
245
|
-
for (const skill of skills) {
|
|
246
|
-
output += `systematic:${skill.name}
|
|
247
|
-
`;
|
|
248
|
-
if (skill.description) {
|
|
249
|
-
output += ` ${skill.description}
|
|
250
|
-
`;
|
|
251
|
-
}
|
|
252
|
-
output += ` Directory: ${skill.path}
|
|
253
|
-
|
|
254
|
-
`;
|
|
255
|
-
}
|
|
256
|
-
return output.trim();
|
|
257
|
-
}
|
|
258
|
-
}),
|
|
259
|
-
systematic_find_agents: tool({
|
|
260
|
-
description: "List all available review agents.",
|
|
261
|
-
args: {},
|
|
262
|
-
execute: async () => {
|
|
263
|
-
const bundledAgents = findAgentsInDir(bundledAgentsDir, "bundled");
|
|
264
|
-
const agents = bundledAgents.filter((a) => !config.disabled_agents.includes(a.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
265
|
-
return formatItemList(agents, "No agents available.", `Available agents:
|
|
266
|
-
|
|
267
|
-
`);
|
|
268
|
-
}
|
|
269
|
-
}),
|
|
270
|
-
systematic_find_commands: tool({
|
|
271
|
-
description: "List all available commands.",
|
|
272
|
-
args: {},
|
|
273
|
-
execute: async () => {
|
|
274
|
-
const bundledCommands = findCommandsInDir(bundledCommandsDir, "bundled");
|
|
275
|
-
const commands = bundledCommands.filter((c) => !config.disabled_commands.includes(c.name.replace(/^\//, ""))).sort((a, b) => a.name.localeCompare(b.name));
|
|
276
|
-
return formatItemList(commands, "No commands available.", `Available commands:
|
|
277
|
-
|
|
278
|
-
`);
|
|
279
|
-
}
|
|
374
|
+
systematic_skill: createSkillTool({
|
|
375
|
+
bundledSkillsDir,
|
|
376
|
+
disabledSkills: config.disabled_skills
|
|
280
377
|
})
|
|
281
378
|
},
|
|
282
379
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
380
|
+
if (!hasLoggedInit) {
|
|
381
|
+
hasLoggedInit = true;
|
|
382
|
+
const packageVersion = getPackageVersion();
|
|
383
|
+
try {
|
|
384
|
+
await client.app.log({
|
|
385
|
+
body: {
|
|
386
|
+
service: "systematic",
|
|
387
|
+
level: "info",
|
|
388
|
+
message: "Systematic plugin initialized",
|
|
389
|
+
extra: {
|
|
390
|
+
version: packageVersion,
|
|
391
|
+
bootstrapEnabled: config.bootstrap.enabled,
|
|
392
|
+
disabledSkillsCount: config.disabled_skills.length,
|
|
393
|
+
disabledAgentsCount: config.disabled_agents.length,
|
|
394
|
+
disabledCommandsCount: config.disabled_commands.length
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
} catch {}
|
|
399
|
+
}
|
|
400
|
+
const existingSystem = output.system.join(`
|
|
401
|
+
`).toLowerCase();
|
|
402
|
+
if (existingSystem.includes("title generator") || existingSystem.includes("generate a title")) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
283
405
|
const content = getBootstrapContent(config);
|
|
284
406
|
if (content) {
|
|
285
407
|
if (!output.system) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@opencode-ai/plugin": "^1.1.30"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@biomejs/biome": "^
|
|
53
|
+
"@biomejs/biome": "^2.0.0",
|
|
54
54
|
"@opencode-ai/plugin": "^1.1.30",
|
|
55
55
|
"@types/bun": "latest",
|
|
56
56
|
"@types/node": "^22.0.0",
|
|
@@ -13,7 +13,7 @@ This is not negotiable. This is not optional. You cannot rationalize your way ou
|
|
|
13
13
|
|
|
14
14
|
## How to Access Skills
|
|
15
15
|
|
|
16
|
-
Use the `skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly.
|
|
16
|
+
Use the `systematic_skill` tool for Systematic bundled skills. Use the native `skill` tool for non-Systematic skills. When you invoke a skill, its content is loaded and presented to you—follow it directly.
|
|
17
17
|
|
|
18
18
|
# Using Skills
|
|
19
19
|
|
|
@@ -25,7 +25,7 @@ Use the `skill` tool. When you invoke a skill, its content is loaded and present
|
|
|
25
25
|
digraph skill_flow {
|
|
26
26
|
"User message received" [shape=doublecircle];
|
|
27
27
|
"Might any skill apply?" [shape=diamond];
|
|
28
|
-
"Invoke `
|
|
28
|
+
"Invoke `systematic_skill` tool" [shape=box];
|
|
29
29
|
"Announce: 'Using [skill] to [purpose]'" [shape=box];
|
|
30
30
|
"Has checklist?" [shape=diamond];
|
|
31
31
|
"Create todo per item" [shape=box];
|
|
@@ -33,9 +33,9 @@ digraph skill_flow {
|
|
|
33
33
|
"Respond (including clarifications)" [shape=doublecircle];
|
|
34
34
|
|
|
35
35
|
"User message received" -> "Might any skill apply?";
|
|
36
|
-
"Might any skill apply?" -> "Invoke `
|
|
36
|
+
"Might any skill apply?" -> "Invoke `systematic_skill` tool" [label="yes, even 1%"];
|
|
37
37
|
"Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
|
|
38
|
-
"Invoke `
|
|
38
|
+
"Invoke `systematic_skill` tool" -> "Announce: 'Using [skill] to [purpose]'";
|
|
39
39
|
"Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
|
|
40
40
|
"Has checklist?" -> "Create todo per item" [label="yes"];
|
|
41
41
|
"Has checklist?" -> "Follow skill exactly" [label="no"];
|
|
@@ -91,4 +91,4 @@ Skills are resolved in priority order:
|
|
|
91
91
|
2. **User skills**: `~/.config/opencode/skills/`
|
|
92
92
|
3. **Bundled skills**: Provided by systematic plugin
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Systematic bundled skills are listed in the `systematic_skill` tool description. Use the native `skill` tool for skills outside the Systematic plugin.
|