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