@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 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
- ### Review Agents
55
+ ### Agents
56
56
 
57
- Specialized code review agents organized by category:
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-v8dhd5s2.js";
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> <source> [--output <path>] [--dry-run]
199
- Convert Claude Code content to OpenCode format
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 convert skill /path/to/cep/skills/agent-browser -o ./skills/agent-browser
214
- systematic convert agent /path/to/agent.md --dry-run
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 path2.join(process.env.HOME || process.env.USERPROFILE || ".", ".config/opencode");
41
+ return path.join(process.env.HOME || process.env.USERPROFILE || ".", ".config/opencode");
218
42
  }
219
43
  function getProjectConfigDir() {
220
- return path2.join(process.cwd(), ".opencode");
44
+ return path.join(process.cwd(), ".opencode");
221
45
  }
222
46
  function listItems(type) {
223
- const packageRoot = path2.resolve(import.meta.dirname, "..");
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(path2.join(bundledDir, subdir), "bundled");
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: ${path2.join(userDir, "systematic.json")}`);
261
- console.log(` Project config: ${path2.join(projectDir, "systematic.json")}`);
262
- const projectConfig = path2.join(projectDir, "systematic.json");
263
- if (fs2.existsSync(projectConfig)) {
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(fs2.readFileSync(projectConfig, "utf-8"));
115
+ console.log(fs.readFileSync(projectConfig, "utf-8"));
267
116
  }
268
- const userConfig = path2.join(userDir, "systematic.json");
269
- if (fs2.existsSync(userConfig)) {
117
+ const userConfig = path.join(userDir, "systematic.json");
118
+ if (fs.existsSync(userConfig)) {
270
119
  console.log(`
271
120
  User configuration:`);
272
- console.log(fs2.readFileSync(userConfig, "utf-8"));
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: ${path2.join(userDir, "systematic.json")}`);
280
- console.log(` Project: ${path2.join(projectDir, "systematic.json")}`);
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
- runConvert(args);
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 fs from "node:fs";
3
- import path from "node:path";
132
+ import fs2 from "fs";
133
+ import path from "path";
4
134
  function extractFrontmatter(filePath) {
5
135
  try {
6
- const content = fs.readFileSync(filePath, "utf8");
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 (!fs.existsSync(dir))
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 = fs.readdirSync(currentDir, { withFileTypes: true });
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 (fs.existsSync(skillFile)) {
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 (!fs.existsSync(dir))
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 = fs.readdirSync(currentDir, { withFileTypes: true });
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 (!fs.existsSync(dir))
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 = fs.readdirSync(currentDir, { withFileTypes: true });
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-v8dhd5s2.js";
11
+ } from "./index-hkk4125w.js";
9
12
 
10
13
  // src/index.ts
11
- import fs4 from "node:fs";
12
- import os2 from "node:os";
13
- import path3 from "node:path";
14
- import { fileURLToPath } from "node:url";
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 "node:fs";
18
- import path from "node:path";
19
- import os from "node:os";
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 content = fs2.readFileSync(agentInfo.file, "utf8");
71
- const { name, description, prompt } = extractAgentFrontmatter(content);
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 || `${name || agentInfo.name} agent`,
74
- prompt: prompt || stripFrontmatter(content)
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 content = fs2.readFileSync(commandInfo.file, "utf8");
83
- const { name, description } = extractCommandFrontmatter(content);
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(content),
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 content = fs2.readFileSync(skillInfo.skillFile, "utf8");
100
+ const converted = convertFileWithCache(skillInfo.skillFile, "skill", { source: "bundled" });
96
101
  return {
97
- template: stripFrontmatter(content),
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 fs3 from "node:fs";
167
- import path2 from "node:path";
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 body = stripFrontmatter(content);
227
- return `<skill_instruction>
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
- </skill_instruction>`;
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 = fs3.readFileSync(matchedSkill.skillFile, "utf8");
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 (!fs4.existsSync(packageJsonPath))
313
+ if (!fs3.existsSync(packageJsonPath))
308
314
  return "unknown";
309
- const content = fs4.readFileSync(packageJsonPath, "utf8");
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 (fs4.existsSync(customPath)) {
322
- return fs4.readFileSync(customPath, "utf8");
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 (!fs4.existsSync(usingSystematicPath))
332
+ if (!fs3.existsSync(usingSystematicPath))
327
333
  return null;
328
- const fullContent = fs4.readFileSync(usingSystematicPath, "utf8");
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\` \`update_plan\`
333
- - \`Task\` tool with subagents Use OpenCode's subagent system (@mention)
334
- - \`Skill\` tool OpenCode's native \`skill\` tool
335
- - \`SystematicSkill\` tool \`systematic_skill\` (Systematic plugin skills)
336
- - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` Your native tools
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.0",
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
- "build": "bun build src/index.ts src/cli.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin/tool --external jsonc-parser",
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"