@fro.bot/systematic 1.2.0 → 1.2.2

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/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ // @bun
2
3
  import {
3
4
  convertContent,
4
5
  findAgentsInDir,
5
6
  findCommandsInDir,
6
- findSkillsInDir
7
- } from "./index-33zyxync.js";
7
+ findSkillsInDir,
8
+ getConfigPaths
9
+ } from "./index-hvkf19rd.js";
8
10
 
9
11
  // src/cli.ts
10
- import fs from "node:fs";
11
- import path from "node:path";
12
+ import fs from "fs";
13
+ import path from "path";
12
14
  var VERSION = "0.1.0";
13
15
  var HELP = `
14
16
  systematic - OpenCode plugin for systematic engineering workflows
@@ -36,12 +38,6 @@ Examples:
36
38
  systematic convert skill ./skills/my-skill/SKILL.md
37
39
  systematic config show
38
40
  `;
39
- function getUserConfigDir() {
40
- return path.join(process.env.HOME || process.env.USERPROFILE || ".", ".config/opencode");
41
- }
42
- function getProjectConfigDir() {
43
- return path.join(process.cwd(), ".opencode");
44
- }
45
41
  function listItems(type) {
46
42
  const packageRoot = path.resolve(import.meta.dirname, "..");
47
43
  const bundledDir = packageRoot;
@@ -64,7 +60,7 @@ function listItems(type) {
64
60
  console.error(`Unknown type: ${type}. Use: skills, agents, commands`);
65
61
  process.exit(1);
66
62
  }
67
- const items = finder(path.join(bundledDir, subdir), "bundled");
63
+ const items = finder(path.join(bundledDir, subdir));
68
64
  if (items.length === 0) {
69
65
  console.log(`No ${type} found.`);
70
66
  return;
@@ -72,7 +68,7 @@ function listItems(type) {
72
68
  console.log(`Available ${type}:
73
69
  `);
74
70
  for (const item of items.sort((a, b) => a.name.localeCompare(b.name))) {
75
- console.log(` ${item.name} (${item.sourceType})`);
71
+ console.log(` ${item.name}`);
76
72
  }
77
73
  }
78
74
  function runConvert(type, filePath, modeArg) {
@@ -101,31 +97,27 @@ function runConvert(type, filePath, modeArg) {
101
97
  console.log(converted);
102
98
  }
103
99
  function configShow() {
104
- const userDir = getUserConfigDir();
105
- const projectDir = getProjectConfigDir();
100
+ const paths = getConfigPaths(process.cwd());
106
101
  console.log(`Configuration locations:
107
102
  `);
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)) {
103
+ console.log(` User config: ${paths.userConfig}`);
104
+ console.log(` Project config: ${paths.projectConfig}`);
105
+ if (fs.existsSync(paths.projectConfig)) {
112
106
  console.log(`
113
107
  Project configuration:`);
114
- console.log(fs.readFileSync(projectConfig, "utf-8"));
108
+ console.log(fs.readFileSync(paths.projectConfig, "utf-8"));
115
109
  }
116
- const userConfig = path.join(userDir, "systematic.json");
117
- if (fs.existsSync(userConfig)) {
110
+ if (fs.existsSync(paths.userConfig)) {
118
111
  console.log(`
119
112
  User configuration:`);
120
- console.log(fs.readFileSync(userConfig, "utf-8"));
113
+ console.log(fs.readFileSync(paths.userConfig, "utf-8"));
121
114
  }
122
115
  }
123
116
  function configPath() {
124
- const userDir = getUserConfigDir();
125
- const projectDir = getProjectConfigDir();
117
+ const paths = getConfigPaths(process.cwd());
126
118
  console.log("Config file paths:");
127
- console.log(` User: ${path.join(userDir, "systematic.json")}`);
128
- console.log(` Project: ${path.join(projectDir, "systematic.json")}`);
119
+ console.log(` User: ${paths.userConfig}`);
120
+ console.log(` Project: ${paths.projectConfig}`);
129
121
  }
130
122
  var args = process.argv.slice(2);
131
123
  var command = args[0];
@@ -1,6 +1,67 @@
1
+ // @bun
2
+ // src/lib/config.ts
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import os from "os";
6
+ import { parse as parseJsonc } from "jsonc-parser";
7
+ var DEFAULT_CONFIG = {
8
+ disabled_skills: [],
9
+ disabled_agents: [],
10
+ disabled_commands: [],
11
+ bootstrap: {
12
+ enabled: true
13
+ }
14
+ };
15
+ function loadJsoncFile(filePath) {
16
+ try {
17
+ if (!fs.existsSync(filePath))
18
+ return null;
19
+ const content = fs.readFileSync(filePath, "utf-8");
20
+ return parseJsonc(content);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function mergeArraysUnique(arr1, arr2) {
26
+ const set = new Set;
27
+ if (arr1)
28
+ arr1.forEach((item) => set.add(item));
29
+ if (arr2)
30
+ arr2.forEach((item) => set.add(item));
31
+ return Array.from(set);
32
+ }
33
+ function loadConfig(projectDir) {
34
+ const homeDir = os.homedir();
35
+ const userConfigPath = path.join(homeDir, ".config/opencode/systematic.json");
36
+ const projectConfigPath = path.join(projectDir, ".opencode/systematic.json");
37
+ const userConfig = loadJsoncFile(userConfigPath);
38
+ const projectConfig = loadJsoncFile(projectConfigPath);
39
+ const result = {
40
+ disabled_skills: mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_skills, userConfig?.disabled_skills), projectConfig?.disabled_skills),
41
+ disabled_agents: mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_agents, userConfig?.disabled_agents), projectConfig?.disabled_agents),
42
+ disabled_commands: mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_commands, userConfig?.disabled_commands), projectConfig?.disabled_commands),
43
+ bootstrap: {
44
+ ...DEFAULT_CONFIG.bootstrap,
45
+ ...userConfig?.bootstrap,
46
+ ...projectConfig?.bootstrap
47
+ }
48
+ };
49
+ return result;
50
+ }
51
+ function getConfigPaths(projectDir) {
52
+ const homeDir = os.homedir();
53
+ return {
54
+ userConfig: path.join(homeDir, ".config/opencode/systematic.json"),
55
+ projectConfig: path.join(projectDir, ".opencode/systematic.json"),
56
+ userDir: path.join(homeDir, ".config/opencode/systematic"),
57
+ projectDir: path.join(projectDir, ".opencode/systematic")
58
+ };
59
+ }
60
+
1
61
  // src/lib/converter.ts
2
- import fs from "node:fs";
3
- var cache = new Map;
62
+ import fs2 from "fs";
63
+
64
+ // src/lib/frontmatter.ts
4
65
  function parseFrontmatter(content) {
5
66
  const lines = content.split(/\r?\n/);
6
67
  if (lines.length === 0 || lines[0].trim() !== "---") {
@@ -47,6 +108,31 @@ function formatFrontmatter(data) {
47
108
  return lines.join(`
48
109
  `);
49
110
  }
111
+ function stripFrontmatter(content) {
112
+ const lines = content.split(`
113
+ `);
114
+ let inFrontmatter = false;
115
+ let frontmatterEnded = false;
116
+ const contentLines = [];
117
+ for (const line of lines) {
118
+ if (line.trim() === "---") {
119
+ if (inFrontmatter) {
120
+ frontmatterEnded = true;
121
+ continue;
122
+ }
123
+ inFrontmatter = true;
124
+ continue;
125
+ }
126
+ if (frontmatterEnded || !inFrontmatter) {
127
+ contentLines.push(line);
128
+ }
129
+ }
130
+ return contentLines.join(`
131
+ `).trim();
132
+ }
133
+
134
+ // src/lib/converter.ts
135
+ var cache = new Map;
50
136
  function inferTemperature(name, description) {
51
137
  const sample = `${name} ${description ?? ""}`.toLowerCase();
52
138
  if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
@@ -110,159 +196,67 @@ ${body}`;
110
196
  return content;
111
197
  }
112
198
  function convertFileWithCache(filePath, type, options = {}) {
113
- const fd = fs.openSync(filePath, "r");
199
+ const fd = fs2.openSync(filePath, "r");
114
200
  try {
115
- const stats = fs.fstatSync(fd);
201
+ const stats = fs2.fstatSync(fd);
116
202
  const cacheKey = `${filePath}:${type}:${options.source ?? "bundled"}:${options.agentMode ?? "subagent"}`;
117
203
  const cached = cache.get(cacheKey);
118
204
  if (cached != null && cached.mtimeMs === stats.mtimeMs) {
119
205
  return cached.converted;
120
206
  }
121
- const content = fs.readFileSync(fd, "utf8");
207
+ const content = fs2.readFileSync(fd, "utf8");
122
208
  const converted = convertContent(content, type, options);
123
209
  cache.set(cacheKey, { mtimeMs: stats.mtimeMs, converted });
124
210
  return converted;
125
211
  } finally {
126
- fs.closeSync(fd);
212
+ fs2.closeSync(fd);
127
213
  }
128
214
  }
129
215
 
130
- // src/lib/skills-core.ts
131
- import fs2 from "node:fs";
132
- import path from "node:path";
133
- function extractFrontmatter(filePath) {
134
- try {
135
- const content = fs2.readFileSync(filePath, "utf8");
136
- const lines = content.split(`
137
- `);
138
- let inFrontmatter = false;
139
- let name = "";
140
- let description = "";
141
- for (const line of lines) {
142
- if (line.trim() === "---") {
143
- if (inFrontmatter)
144
- break;
145
- inFrontmatter = true;
146
- continue;
147
- }
148
- if (inFrontmatter) {
149
- const match = line.match(/^(\w+):\s*(.*)$/);
150
- if (match) {
151
- const [, key, value] = match;
152
- if (key === "name")
153
- name = value.trim();
154
- if (key === "description")
155
- description = value.trim();
156
- }
157
- }
158
- }
159
- return { name, description };
160
- } catch {
161
- return { name: "", description: "" };
162
- }
163
- }
164
- function stripFrontmatter(content) {
165
- const lines = content.split(`
166
- `);
167
- let inFrontmatter = false;
168
- let frontmatterEnded = false;
169
- const contentLines = [];
170
- for (const line of lines) {
171
- if (line.trim() === "---") {
172
- if (inFrontmatter) {
173
- frontmatterEnded = true;
174
- continue;
175
- }
176
- inFrontmatter = true;
177
- continue;
178
- }
179
- if (frontmatterEnded || !inFrontmatter) {
180
- contentLines.push(line);
181
- }
182
- }
183
- return contentLines.join(`
184
- `).trim();
185
- }
186
- function findSkillsInDir(dir, sourceType, maxDepth = 3) {
187
- const skills = [];
188
- if (!fs2.existsSync(dir))
189
- return skills;
190
- function recurse(currentDir, depth) {
191
- if (depth > maxDepth)
192
- return;
193
- const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
194
- for (const entry of entries) {
195
- const fullPath = path.join(currentDir, entry.name);
196
- if (entry.isDirectory()) {
197
- const skillFile = path.join(fullPath, "SKILL.md");
198
- if (fs2.existsSync(skillFile)) {
199
- const { name, description } = extractFrontmatter(skillFile);
200
- skills.push({
201
- path: fullPath,
202
- skillFile,
203
- name: name || entry.name,
204
- description: description || "",
205
- sourceType
206
- });
207
- }
208
- recurse(fullPath, depth + 1);
209
- }
210
- }
211
- }
212
- recurse(dir, 0);
213
- return skills;
214
- }
215
- function findAgentsInDir(dir, sourceType, maxDepth = 2) {
216
- const agents = [];
217
- if (!fs2.existsSync(dir))
218
- return agents;
216
+ // src/lib/walk-dir.ts
217
+ import fs3 from "fs";
218
+ import path2 from "path";
219
+ function walkDir(rootDir, options = {}) {
220
+ const { maxDepth = 3, filter } = options;
221
+ const results = [];
222
+ if (!fs3.existsSync(rootDir))
223
+ return results;
219
224
  function recurse(currentDir, depth, category) {
220
225
  if (depth > maxDepth)
221
226
  return;
222
- const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
227
+ const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
223
228
  for (const entry of entries) {
224
- const fullPath = path.join(currentDir, entry.name);
225
- if (entry.isDirectory()) {
226
- recurse(fullPath, depth + 1, entry.name);
227
- } else if (entry.name.endsWith(".md")) {
228
- agents.push({
229
- name: entry.name.replace(/\.md$/, ""),
230
- file: fullPath,
231
- sourceType,
232
- category
233
- });
229
+ const fullPath = path2.join(currentDir, entry.name);
230
+ const walkEntry = {
231
+ path: fullPath,
232
+ name: entry.name,
233
+ isDirectory: entry.isDirectory(),
234
+ depth,
235
+ category
236
+ };
237
+ if (!filter || filter(walkEntry)) {
238
+ results.push(walkEntry);
234
239
  }
235
- }
236
- }
237
- recurse(dir, 0);
238
- return agents;
239
- }
240
- function findCommandsInDir(dir, sourceType, maxDepth = 2) {
241
- const commands = [];
242
- if (!fs2.existsSync(dir))
243
- return commands;
244
- function recurse(currentDir, depth, category) {
245
- if (depth > maxDepth)
246
- return;
247
- const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
248
- for (const entry of entries) {
249
- const fullPath = path.join(currentDir, entry.name);
250
240
  if (entry.isDirectory()) {
251
241
  recurse(fullPath, depth + 1, entry.name);
252
- } else if (entry.name.endsWith(".md")) {
253
- const baseName = entry.name.replace(/\.md$/, "");
254
- const commandName = category ? `/${category}:${baseName}` : `/${baseName}`;
255
- commands.push({
256
- name: commandName,
257
- file: fullPath,
258
- sourceType,
259
- category
260
- });
261
242
  }
262
243
  }
263
244
  }
264
- recurse(dir, 0);
265
- return commands;
245
+ recurse(rootDir, 0);
246
+ return results;
247
+ }
248
+
249
+ // src/lib/agents.ts
250
+ function findAgentsInDir(dir, maxDepth = 2) {
251
+ const entries = walkDir(dir, {
252
+ maxDepth,
253
+ filter: (e) => !e.isDirectory && e.name.endsWith(".md")
254
+ });
255
+ return entries.map((entry) => ({
256
+ name: entry.name.replace(/\.md$/, ""),
257
+ file: entry.path,
258
+ category: entry.category
259
+ }));
266
260
  }
267
261
  function extractAgentFrontmatter(content) {
268
262
  const lines = content.split(`
@@ -290,6 +284,23 @@ function extractAgentFrontmatter(content) {
290
284
  }
291
285
  return { name, description, prompt: stripFrontmatter(content) };
292
286
  }
287
+
288
+ // src/lib/commands.ts
289
+ function findCommandsInDir(dir, maxDepth = 2) {
290
+ const entries = walkDir(dir, {
291
+ maxDepth,
292
+ filter: (e) => !e.isDirectory && e.name.endsWith(".md")
293
+ });
294
+ return entries.map((entry) => {
295
+ const baseName = entry.name.replace(/\.md$/, "");
296
+ const commandName = entry.category ? `/${entry.category}:${baseName}` : `/${baseName}`;
297
+ return {
298
+ name: commandName,
299
+ file: entry.path,
300
+ category: entry.category
301
+ };
302
+ });
303
+ }
293
304
  function extractCommandFrontmatter(content) {
294
305
  const lines = content.split(`
295
306
  `);
@@ -320,4 +331,77 @@ function extractCommandFrontmatter(content) {
320
331
  return { name, description, argumentHint };
321
332
  }
322
333
 
323
- export { convertContent, convertFileWithCache, stripFrontmatter, findSkillsInDir, findAgentsInDir, findCommandsInDir, extractAgentFrontmatter, extractCommandFrontmatter };
334
+ // src/lib/skills.ts
335
+ import fs4 from "fs";
336
+ import path3 from "path";
337
+ function extractFrontmatter(filePath) {
338
+ try {
339
+ const content = fs4.readFileSync(filePath, "utf8");
340
+ const lines = content.split(`
341
+ `);
342
+ let inFrontmatter = false;
343
+ let name = "";
344
+ let description = "";
345
+ for (const line of lines) {
346
+ if (line.trim() === "---") {
347
+ if (inFrontmatter)
348
+ break;
349
+ inFrontmatter = true;
350
+ continue;
351
+ }
352
+ if (inFrontmatter) {
353
+ const match = line.match(/^(\w+):\s*(.*)$/);
354
+ if (match) {
355
+ const [, key, value] = match;
356
+ if (key === "name")
357
+ name = value.trim();
358
+ if (key === "description")
359
+ description = value.trim();
360
+ }
361
+ }
362
+ }
363
+ return { name, description };
364
+ } catch {
365
+ return { name: "", description: "" };
366
+ }
367
+ }
368
+ function findSkillsInDir(dir, maxDepth = 3) {
369
+ const skills = [];
370
+ const entries = walkDir(dir, {
371
+ maxDepth,
372
+ filter: (e) => e.isDirectory
373
+ });
374
+ for (const entry of entries) {
375
+ const skillFile = path3.join(entry.path, "SKILL.md");
376
+ if (fs4.existsSync(skillFile)) {
377
+ const { name, description } = extractFrontmatter(skillFile);
378
+ skills.push({
379
+ path: entry.path,
380
+ skillFile,
381
+ name: name || entry.name,
382
+ description: description || ""
383
+ });
384
+ }
385
+ }
386
+ return skills;
387
+ }
388
+ function formatSkillsXml(skills) {
389
+ if (skills.length === 0)
390
+ return "";
391
+ const skillsXml = skills.map((skill) => {
392
+ const lines = [
393
+ " <skill>",
394
+ ` <name>systematic:${skill.name}</name>`,
395
+ ` <description>${skill.description}</description>`
396
+ ];
397
+ lines.push(" </skill>");
398
+ return lines.join(`
399
+ `);
400
+ }).join(`
401
+ `);
402
+ return `<available_skills>
403
+ ${skillsXml}
404
+ </available_skills>`;
405
+ }
406
+
407
+ export { stripFrontmatter, loadConfig, getConfigPaths, convertContent, convertFileWithCache, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, findSkillsInDir, formatSkillsXml };
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+ export declare const SystematicPlugin: Plugin;
3
+ export default SystematicPlugin;
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ // @bun
1
2
  import {
2
3
  convertContent,
3
4
  convertFileWithCache,
@@ -6,63 +7,65 @@ import {
6
7
  findAgentsInDir,
7
8
  findCommandsInDir,
8
9
  findSkillsInDir,
10
+ formatSkillsXml,
11
+ loadConfig,
9
12
  stripFrontmatter
10
- } from "./index-33zyxync.js";
13
+ } from "./index-hvkf19rd.js";
11
14
 
12
15
  // src/index.ts
13
- import fs3 from "node:fs";
14
- import os2 from "node:os";
15
- import path3 from "node:path";
16
- import { fileURLToPath } from "node:url";
16
+ import fs3 from "fs";
17
+ import path3 from "path";
18
+ import { fileURLToPath } from "url";
17
19
 
18
- // src/lib/config.ts
19
- import fs from "node:fs";
20
- import path from "node:path";
21
- import os from "node:os";
22
- import { parse as parseJsonc } from "jsonc-parser";
23
- var DEFAULT_CONFIG = {
24
- disabled_skills: [],
25
- disabled_agents: [],
26
- disabled_commands: [],
27
- bootstrap: {
28
- enabled: true
29
- }
30
- };
31
- function loadJsoncFile(filePath) {
32
- try {
33
- if (!fs.existsSync(filePath))
34
- return null;
35
- const content = fs.readFileSync(filePath, "utf-8");
36
- return parseJsonc(content);
37
- } catch {
38
- return null;
39
- }
40
- }
41
- function mergeArraysUnique(arr1, arr2) {
42
- const set = new Set;
43
- if (arr1)
44
- arr1.forEach((item) => set.add(item));
45
- if (arr2)
46
- arr2.forEach((item) => set.add(item));
47
- return Array.from(set);
20
+ // src/lib/bootstrap.ts
21
+ import fs from "fs";
22
+ import os from "os";
23
+ import path from "path";
24
+ function getToolMappingTemplate(bundledSkillsDir) {
25
+ return `**Tool Mapping for OpenCode:**
26
+ When skills reference tools you don't have, substitute OpenCode equivalents:
27
+ - \`TodoWrite\` \u2192 \`update_plan\`
28
+ - \`Task\` tool with subagents \u2192 Use OpenCode's subagent system (@mention)
29
+ - \`Skill\` tool \u2192 OpenCode's native \`skill\` tool
30
+ - \`SystematicSkill\` tool \u2192 \`systematic_skill\` (Systematic plugin skills)
31
+ - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` \u2192 Your native tools
32
+
33
+ **Skills naming:**
34
+ - Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
35
+ - Skills can also be invoked without prefix if unambiguous
36
+
37
+ **Skills usage:**
38
+ - Use \`systematic_skill\` to load Systematic bundled skills
39
+ - Use the native \`skill\` tool for non-Systematic skills
40
+
41
+ **Skills location:**
42
+ Bundled skills are in \`${bundledSkillsDir}/\``;
48
43
  }
49
- function loadConfig(projectDir) {
50
- const homeDir = os.homedir();
51
- const userConfigPath = path.join(homeDir, ".config/opencode/systematic.json");
52
- const projectConfigPath = path.join(projectDir, ".opencode/systematic.json");
53
- const userConfig = loadJsoncFile(userConfigPath);
54
- const projectConfig = loadJsoncFile(projectConfigPath);
55
- const result = {
56
- disabled_skills: mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_skills, userConfig?.disabled_skills), projectConfig?.disabled_skills),
57
- disabled_agents: mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_agents, userConfig?.disabled_agents), projectConfig?.disabled_agents),
58
- disabled_commands: mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_commands, userConfig?.disabled_commands), projectConfig?.disabled_commands),
59
- bootstrap: {
60
- ...DEFAULT_CONFIG.bootstrap,
61
- ...userConfig?.bootstrap,
62
- ...projectConfig?.bootstrap
44
+ function getBootstrapContent(config, deps) {
45
+ const { bundledSkillsDir } = deps;
46
+ if (!config.bootstrap.enabled)
47
+ return null;
48
+ if (config.bootstrap.file) {
49
+ const customPath = config.bootstrap.file.startsWith("~/") ? path.join(os.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
50
+ if (fs.existsSync(customPath)) {
51
+ return fs.readFileSync(customPath, "utf8");
63
52
  }
64
- };
65
- return result;
53
+ }
54
+ const usingSystematicPath = path.join(bundledSkillsDir, "using-systematic/SKILL.md");
55
+ if (!fs.existsSync(usingSystematicPath))
56
+ return null;
57
+ const fullContent = fs.readFileSync(usingSystematicPath, "utf8");
58
+ const content = stripFrontmatter(fullContent);
59
+ const toolMapping = getToolMappingTemplate(bundledSkillsDir);
60
+ return `<SYSTEMATIC_WORKFLOWS>
61
+ You have access to structured engineering workflows via the systematic plugin.
62
+
63
+ **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.**
64
+
65
+ ${content}
66
+
67
+ ${toolMapping}
68
+ </SYSTEMATIC_WORKFLOWS>`;
66
69
  }
67
70
 
68
71
  // src/lib/config-handler.ts
@@ -105,9 +108,9 @@ function loadSkillAsCommand(skillInfo) {
105
108
  return null;
106
109
  }
107
110
  }
108
- function collectAgents(dir, sourceType, disabledAgents) {
111
+ function collectAgents(dir, disabledAgents) {
109
112
  const agents = {};
110
- const agentList = findAgentsInDir(dir, sourceType);
113
+ const agentList = findAgentsInDir(dir);
111
114
  for (const agentInfo of agentList) {
112
115
  if (disabledAgents.includes(agentInfo.name))
113
116
  continue;
@@ -118,9 +121,9 @@ function collectAgents(dir, sourceType, disabledAgents) {
118
121
  }
119
122
  return agents;
120
123
  }
121
- function collectCommands(dir, sourceType, disabledCommands) {
124
+ function collectCommands(dir, disabledCommands) {
122
125
  const commands = {};
123
- const commandList = findCommandsInDir(dir, sourceType);
126
+ const commandList = findCommandsInDir(dir);
124
127
  for (const commandInfo of commandList) {
125
128
  const cleanName = commandInfo.name.replace(/^\//, "");
126
129
  if (disabledCommands.includes(cleanName))
@@ -132,9 +135,9 @@ function collectCommands(dir, sourceType, disabledCommands) {
132
135
  }
133
136
  return commands;
134
137
  }
135
- function collectSkillsAsCommands(dir, sourceType, disabledSkills) {
138
+ function collectSkillsAsCommands(dir, disabledSkills) {
136
139
  const commands = {};
137
- const skillList = findSkillsInDir(dir, sourceType, 3);
140
+ const skillList = findSkillsInDir(dir);
138
141
  for (const skillInfo of skillList) {
139
142
  if (disabledSkills.includes(skillInfo.name))
140
143
  continue;
@@ -149,9 +152,9 @@ function createConfigHandler(deps) {
149
152
  const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
150
153
  return async (config) => {
151
154
  const systematicConfig = loadConfig(directory);
152
- const bundledAgents = collectAgents(bundledAgentsDir, "bundled", systematicConfig.disabled_agents);
153
- const bundledCommands = collectCommands(bundledCommandsDir, "bundled", systematicConfig.disabled_commands);
154
- const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, "bundled", systematicConfig.disabled_skills);
155
+ const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents);
156
+ const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
157
+ const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, systematicConfig.disabled_skills);
155
158
  const existingAgents = config.agent ?? {};
156
159
  config.agent = {
157
160
  ...bundledAgents,
@@ -167,64 +170,9 @@ function createConfigHandler(deps) {
167
170
  }
168
171
 
169
172
  // src/lib/skill-tool.ts
170
- import fs2 from "node:fs";
171
- import path2 from "node:path";
173
+ import fs2 from "fs";
174
+ import path2 from "path";
172
175
  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);
223
- }
224
- return `${hookedDescription}
225
-
226
- ${systematicSkillsXml}`;
227
- }
228
176
  function wrapSkillContent(skillPath, content) {
229
177
  const skillDir = path2.dirname(skillPath);
230
178
  const converted = convertContent(content, "skill", { source: "bundled" });
@@ -239,7 +187,7 @@ ${body.trim()}
239
187
  function createSkillTool(options) {
240
188
  const { bundledSkillsDir, disabledSkills } = options;
241
189
  const getSystematicSkills = () => {
242
- return findSkillsInDir(bundledSkillsDir, "bundled", 3).filter((s) => !disabledSkills.includes(s.name)).sort((a, b) => a.name.localeCompare(b.name));
190
+ return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).sort((a, b) => a.name.localeCompare(b.name));
243
191
  };
244
192
  const buildDescription = () => {
245
193
  const skills = getSystematicSkills();
@@ -248,11 +196,12 @@ function createSkillTool(options) {
248
196
 
249
197
  Skills provide specialized knowledge and step-by-step guidance.
250
198
  Use this when a task matches an available skill's description.`;
251
- const hookState = getHookState();
252
- return mergeDescriptions(baseDescription, hookState.hookedDescription, systematicXml);
199
+ return `${baseDescription}
200
+
201
+ ${systematicXml}`;
253
202
  };
254
203
  let cachedDescription = null;
255
- const toolDef = tool({
204
+ return tool({
256
205
  get description() {
257
206
  if (cachedDescription == null) {
258
207
  cachedDescription = buildDescription();
@@ -281,22 +230,10 @@ ${wrapped}`;
281
230
  throw new Error(`Failed to load skill "${requestedName}": ${errorMessage}`);
282
231
  }
283
232
  }
284
- const hookedTool = getHookedTool();
285
- if (hookedTool != null && typeof hookedTool.execute === "function") {
286
- try {
287
- return await hookedTool.execute(args);
288
- } catch {}
289
- }
290
233
  const availableSystematic = skills.map((s) => `systematic:${s.name}`);
291
234
  throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
292
235
  }
293
236
  });
294
- Object.defineProperty(toolDef, SYSTEMATIC_MARKER, {
295
- value: true,
296
- enumerable: false,
297
- writable: false
298
- });
299
- return toolDef;
300
237
  }
301
238
 
302
239
  // src/index.ts
@@ -318,48 +255,6 @@ var getPackageVersion = () => {
318
255
  return "unknown";
319
256
  }
320
257
  };
321
- var getBootstrapContent = (config) => {
322
- if (!config.bootstrap.enabled)
323
- return null;
324
- if (config.bootstrap.file) {
325
- const customPath = config.bootstrap.file.startsWith("~/") ? path3.join(os2.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
326
- if (fs3.existsSync(customPath)) {
327
- return fs3.readFileSync(customPath, "utf8");
328
- }
329
- }
330
- const usingSystematicPath = path3.join(bundledSkillsDir, "using-systematic/SKILL.md");
331
- if (!fs3.existsSync(usingSystematicPath))
332
- return null;
333
- const fullContent = fs3.readFileSync(usingSystematicPath, "utf8");
334
- const content = stripFrontmatter(fullContent);
335
- const toolMapping = `**Tool Mapping for OpenCode:**
336
- When skills reference tools you don't have, substitute OpenCode equivalents:
337
- - \`TodoWrite\` → \`update_plan\`
338
- - \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
339
- - \`Skill\` tool → OpenCode's native \`skill\` tool
340
- - \`SystematicSkill\` tool → \`systematic_skill\` (Systematic plugin skills)
341
- - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
342
-
343
- **Skills naming:**
344
- - Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
345
- - Skills can also be invoked without prefix if unambiguous
346
-
347
- **Skills usage:**
348
- - Use \`systematic_skill\` to load Systematic bundled skills
349
- - Use the native \`skill\` tool for non-Systematic skills
350
-
351
- **Skills location:**
352
- Bundled skills are in \`${bundledSkillsDir}/\``;
353
- return `<SYSTEMATIC_WORKFLOWS>
354
- You have access to structured engineering workflows via the systematic plugin.
355
-
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.**
357
-
358
- ${content}
359
-
360
- ${toolMapping}
361
- </SYSTEMATIC_WORKFLOWS>`;
362
- };
363
258
  var SystematicPlugin = async ({ client, directory }) => {
364
259
  const config = loadConfig(directory);
365
260
  const configHandler = createConfigHandler({
@@ -402,7 +297,7 @@ var SystematicPlugin = async ({ client, directory }) => {
402
297
  if (existingSystem.includes("title generator") || existingSystem.includes("generate a title")) {
403
298
  return;
404
299
  }
405
- const content = getBootstrapContent(config);
300
+ const content = getBootstrapContent(config, { bundledSkillsDir });
406
301
  if (content) {
407
302
  if (!output.system) {
408
303
  output.system = [];
@@ -0,0 +1,12 @@
1
+ export interface AgentFrontmatter {
2
+ name: string;
3
+ description: string;
4
+ prompt: string;
5
+ }
6
+ export interface AgentInfo {
7
+ name: string;
8
+ file: string;
9
+ category?: string;
10
+ }
11
+ export declare function findAgentsInDir(dir: string, maxDepth?: number): AgentInfo[];
12
+ export declare function extractAgentFrontmatter(content: string): AgentFrontmatter;
@@ -0,0 +1,5 @@
1
+ import type { SystematicConfig } from './config.js';
2
+ export interface BootstrapDeps {
3
+ bundledSkillsDir: string;
4
+ }
5
+ export declare function getBootstrapContent(config: SystematicConfig, deps: BootstrapDeps): string | null;
@@ -0,0 +1,12 @@
1
+ export interface CommandFrontmatter {
2
+ name: string;
3
+ description: string;
4
+ argumentHint: string;
5
+ }
6
+ export interface CommandInfo {
7
+ name: string;
8
+ file: string;
9
+ category?: string;
10
+ }
11
+ export declare function findCommandsInDir(dir: string, maxDepth?: number): CommandInfo[];
12
+ export declare function extractCommandFrontmatter(content: string): CommandFrontmatter;
@@ -0,0 +1,17 @@
1
+ import type { Config } from '@opencode-ai/sdk';
2
+ export interface ConfigHandlerDeps {
3
+ directory: string;
4
+ bundledSkillsDir: string;
5
+ bundledAgentsDir: string;
6
+ bundledCommandsDir: string;
7
+ }
8
+ /**
9
+ * Create the config hook handler for the Systematic plugin.
10
+ *
11
+ * This follows the pattern used by oh-my-opencode to inject bundled agents,
12
+ * skills (as commands), and commands into OpenCode's configuration.
13
+ *
14
+ * Only bundled content is loaded. User/project overrides are not supported.
15
+ * Existing OpenCode config is preserved and takes precedence.
16
+ */
17
+ export declare function createConfigHandler(deps: ConfigHandlerDeps): (config: Config) => Promise<void>;
@@ -0,0 +1,18 @@
1
+ export interface BootstrapConfig {
2
+ enabled: boolean;
3
+ file?: string;
4
+ }
5
+ export interface SystematicConfig {
6
+ disabled_skills: string[];
7
+ disabled_agents: string[];
8
+ disabled_commands: string[];
9
+ bootstrap: BootstrapConfig;
10
+ }
11
+ export declare const DEFAULT_CONFIG: SystematicConfig;
12
+ export declare function loadConfig(projectDir: string): SystematicConfig;
13
+ export declare function getConfigPaths(projectDir: string): {
14
+ userConfig: string;
15
+ projectConfig: string;
16
+ userDir: string;
17
+ projectDir: string;
18
+ };
@@ -0,0 +1,10 @@
1
+ export type ContentType = 'skill' | 'agent' | 'command';
2
+ export type SourceType = 'bundled' | 'external';
3
+ export type AgentMode = 'primary' | 'subagent';
4
+ export interface ConvertOptions {
5
+ source?: SourceType;
6
+ agentMode?: AgentMode;
7
+ }
8
+ export declare function convertContent(content: string, type: ContentType, options?: ConvertOptions): string;
9
+ export declare function convertFileWithCache(filePath: string, type: ContentType, options?: ConvertOptions): string;
10
+ export declare function clearConverterCache(): void;
@@ -0,0 +1,9 @@
1
+ interface ParsedFrontmatter {
2
+ data: Record<string, string | number | boolean>;
3
+ body: string;
4
+ raw: string;
5
+ }
6
+ export type { ParsedFrontmatter };
7
+ export declare function parseFrontmatter(content: string): ParsedFrontmatter;
8
+ export declare function formatFrontmatter(data: Record<string, string | number | boolean>): string;
9
+ export declare function stripFrontmatter(content: string): string;
@@ -0,0 +1,6 @@
1
+ import type { ToolDefinition } from '@opencode-ai/plugin';
2
+ export interface SkillToolOptions {
3
+ bundledSkillsDir: string;
4
+ disabledSkills: string[];
5
+ }
6
+ export declare function createSkillTool(options: SkillToolOptions): ToolDefinition;
@@ -0,0 +1,13 @@
1
+ export interface SkillFrontmatter {
2
+ name: string;
3
+ description: string;
4
+ }
5
+ export interface SkillInfo {
6
+ path: string;
7
+ skillFile: string;
8
+ name: string;
9
+ description: string;
10
+ }
11
+ export declare function extractFrontmatter(filePath: string): SkillFrontmatter;
12
+ export declare function findSkillsInDir(dir: string, maxDepth?: number): SkillInfo[];
13
+ export declare function formatSkillsXml(skills: SkillInfo[]): string;
@@ -0,0 +1,12 @@
1
+ export interface WalkEntry {
2
+ path: string;
3
+ name: string;
4
+ isDirectory: boolean;
5
+ depth: number;
6
+ category?: string;
7
+ }
8
+ export interface WalkOptions {
9
+ maxDepth?: number;
10
+ filter?: (entry: WalkEntry) => boolean;
11
+ }
12
+ export declare function walkDir(rootDir: string, options?: WalkOptions): WalkEntry[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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 && tsc --emitDeclarationOnly",
24
25
  "dev": "bun --watch src/index.ts",
25
26
  "test": "bun test tests/unit",
26
27
  "test:integration": "bun test tests/integration",
@@ -28,7 +29,7 @@
28
29
  "typecheck": "tsc --noEmit",
29
30
  "lint": "biome check .",
30
31
  "fix": "bun run lint --fix",
31
- "prepublishOnly": "bun run build && bun run test"
32
+ "prepublishOnly": "bun run build"
32
33
  },
33
34
  "keywords": [
34
35
  "opencode",
@@ -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"