@fro.bot/systematic 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/{index-hvkf19rd.js → index-ymsavt2y.js} +210 -213
- package/dist/index.js +91 -52
- package/dist/lib/converter.d.ts +2 -0
- package/dist/lib/frontmatter.d.ts +15 -6
- package/dist/lib/skill-loader.d.ts +14 -0
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/lib/config.ts
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
import path from "path";
|
|
5
4
|
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
6
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
7
7
|
var DEFAULT_CONFIG = {
|
|
8
8
|
disabled_skills: [],
|
|
@@ -25,9 +25,11 @@ function loadJsoncFile(filePath) {
|
|
|
25
25
|
function mergeArraysUnique(arr1, arr2) {
|
|
26
26
|
const set = new Set;
|
|
27
27
|
if (arr1)
|
|
28
|
-
|
|
28
|
+
for (const item of arr1)
|
|
29
|
+
set.add(item);
|
|
29
30
|
if (arr2)
|
|
30
|
-
|
|
31
|
+
for (const item of arr2)
|
|
32
|
+
set.add(item);
|
|
31
33
|
return Array.from(set);
|
|
32
34
|
}
|
|
33
35
|
function loadConfig(projectDir) {
|
|
@@ -58,81 +60,168 @@ function getConfigPaths(projectDir) {
|
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
// src/lib/converter.ts
|
|
62
|
-
import fs2 from "fs";
|
|
63
|
-
|
|
64
63
|
// src/lib/frontmatter.ts
|
|
64
|
+
import yaml from "js-yaml";
|
|
65
65
|
function parseFrontmatter(content) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (endIndex === -1) {
|
|
78
|
-
return { data: {}, body: content, raw: "" };
|
|
66
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n?---\r?\n([\s\S]*)$/;
|
|
67
|
+
const match = content.match(frontmatterRegex);
|
|
68
|
+
if (!match) {
|
|
69
|
+
return {
|
|
70
|
+
data: {},
|
|
71
|
+
body: content,
|
|
72
|
+
hadFrontmatter: false,
|
|
73
|
+
parseError: false
|
|
74
|
+
};
|
|
79
75
|
}
|
|
80
|
-
const
|
|
81
|
-
const body =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (match) {
|
|
89
|
-
const [, key, value] = match;
|
|
90
|
-
if (value === "true")
|
|
91
|
-
data[key] = true;
|
|
92
|
-
else if (value === "false")
|
|
93
|
-
data[key] = false;
|
|
94
|
-
else if (/^\d+(\.\d+)?$/.test(value))
|
|
95
|
-
data[key] = parseFloat(value);
|
|
96
|
-
else
|
|
97
|
-
data[key] = value;
|
|
98
|
-
}
|
|
76
|
+
const yamlContent = match[1];
|
|
77
|
+
const body = match[2];
|
|
78
|
+
try {
|
|
79
|
+
const parsed = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA });
|
|
80
|
+
const data = parsed ?? {};
|
|
81
|
+
return { data, body, hadFrontmatter: true, parseError: false };
|
|
82
|
+
} catch {
|
|
83
|
+
return { data: {}, body, hadFrontmatter: true, parseError: true };
|
|
99
84
|
}
|
|
100
|
-
return { data, body, raw };
|
|
101
85
|
}
|
|
102
86
|
function formatFrontmatter(data) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
87
|
+
if (Object.keys(data).length === 0) {
|
|
88
|
+
return ["---", "---"].join(`
|
|
89
|
+
`);
|
|
106
90
|
}
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
const yamlContent = yaml.dump(data, {
|
|
92
|
+
schema: yaml.JSON_SCHEMA,
|
|
93
|
+
lineWidth: -1,
|
|
94
|
+
noRefs: true
|
|
95
|
+
}).trimEnd();
|
|
96
|
+
return ["---", yamlContent, "---"].join(`
|
|
109
97
|
`);
|
|
110
98
|
}
|
|
111
99
|
function stripFrontmatter(content) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
100
|
+
const { body, hadFrontmatter } = parseFrontmatter(content);
|
|
101
|
+
return hadFrontmatter ? body.trim() : content.trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/lib/walk-dir.ts
|
|
105
|
+
import fs2 from "fs";
|
|
106
|
+
import path2 from "path";
|
|
107
|
+
function walkDir(rootDir, options = {}) {
|
|
108
|
+
const { maxDepth = 3, filter } = options;
|
|
109
|
+
const results = [];
|
|
110
|
+
if (!fs2.existsSync(rootDir))
|
|
111
|
+
return results;
|
|
112
|
+
function recurse(currentDir, depth, category) {
|
|
113
|
+
if (depth > maxDepth)
|
|
114
|
+
return;
|
|
115
|
+
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const fullPath = path2.join(currentDir, entry.name);
|
|
118
|
+
const walkEntry = {
|
|
119
|
+
path: fullPath,
|
|
120
|
+
name: entry.name,
|
|
121
|
+
isDirectory: entry.isDirectory(),
|
|
122
|
+
depth,
|
|
123
|
+
category
|
|
124
|
+
};
|
|
125
|
+
if (!filter || filter(walkEntry)) {
|
|
126
|
+
results.push(walkEntry);
|
|
127
|
+
}
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
recurse(fullPath, depth + 1, entry.name);
|
|
122
130
|
}
|
|
123
|
-
inFrontmatter = true;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (frontmatterEnded || !inFrontmatter) {
|
|
127
|
-
contentLines.push(line);
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
recurse(rootDir, 0);
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/lib/agents.ts
|
|
138
|
+
function findAgentsInDir(dir, maxDepth = 2) {
|
|
139
|
+
const entries = walkDir(dir, {
|
|
140
|
+
maxDepth,
|
|
141
|
+
filter: (e) => !e.isDirectory && e.name.endsWith(".md")
|
|
142
|
+
});
|
|
143
|
+
return entries.map((entry) => ({
|
|
144
|
+
name: entry.name.replace(/\.md$/, ""),
|
|
145
|
+
file: entry.path,
|
|
146
|
+
category: entry.category
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
function extractAgentFrontmatter(content) {
|
|
150
|
+
const { data, parseError } = parseFrontmatter(content);
|
|
151
|
+
return {
|
|
152
|
+
name: !parseError && typeof data.name === "string" ? data.name : "",
|
|
153
|
+
description: !parseError && typeof data.description === "string" ? data.description : "",
|
|
154
|
+
prompt: stripFrontmatter(content)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/lib/commands.ts
|
|
159
|
+
function findCommandsInDir(dir, maxDepth = 2) {
|
|
160
|
+
const entries = walkDir(dir, {
|
|
161
|
+
maxDepth,
|
|
162
|
+
filter: (e) => !e.isDirectory && e.name.endsWith(".md")
|
|
163
|
+
});
|
|
164
|
+
return entries.map((entry) => {
|
|
165
|
+
const baseName = entry.name.replace(/\.md$/, "");
|
|
166
|
+
const commandName = entry.category ? `/${entry.category}:${baseName}` : `/${baseName}`;
|
|
167
|
+
return {
|
|
168
|
+
name: commandName,
|
|
169
|
+
file: entry.path,
|
|
170
|
+
category: entry.category
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function extractCommandFrontmatter(content) {
|
|
175
|
+
const { data, parseError } = parseFrontmatter(content);
|
|
176
|
+
const argumentHintRaw = !parseError && typeof data["argument-hint"] === "string" ? data["argument-hint"] : "";
|
|
177
|
+
return {
|
|
178
|
+
name: !parseError && typeof data.name === "string" ? data.name : "",
|
|
179
|
+
description: !parseError && typeof data.description === "string" ? data.description : "",
|
|
180
|
+
argumentHint: argumentHintRaw.replace(/^["']|["']$/g, "")
|
|
181
|
+
};
|
|
132
182
|
}
|
|
133
183
|
|
|
134
184
|
// src/lib/converter.ts
|
|
185
|
+
import fs3 from "fs";
|
|
135
186
|
var cache = new Map;
|
|
187
|
+
var TOOL_MAPPINGS = [
|
|
188
|
+
[/\bTask\s+tool\b/gi, "delegate_task tool"],
|
|
189
|
+
[/\bTask\s+([\w-]+)\s*\(/g, "delegate_task $1("],
|
|
190
|
+
[/\bTask\s*\(/g, "delegate_task("],
|
|
191
|
+
[/\bTask\b(?=\s+to\s+\w)/g, "delegate_task"],
|
|
192
|
+
[/\bTodoWrite\b/g, "todowrite"],
|
|
193
|
+
[/\bAskUserQuestion\b/g, "question"],
|
|
194
|
+
[/\bWebSearch\b/g, "google_search"],
|
|
195
|
+
[/\bRead\b(?=\s+tool|\s+to\s+|\()/g, "read"],
|
|
196
|
+
[/\bWrite\b(?=\s+tool|\s+to\s+|\()/g, "write"],
|
|
197
|
+
[/\bEdit\b(?=\s+tool|\s+to\s+|\()/g, "edit"],
|
|
198
|
+
[/\bBash\b(?=\s+tool|\s+to\s+|\()/g, "bash"],
|
|
199
|
+
[/\bGrep\b(?=\s+tool|\s+to\s+|\()/g, "grep"],
|
|
200
|
+
[/\bGlob\b(?=\s+tool|\s+to\s+|\()/g, "glob"],
|
|
201
|
+
[/\bWebFetch\b/g, "webfetch"],
|
|
202
|
+
[/\bSkill\b(?=\s+tool)/g, "skill"]
|
|
203
|
+
];
|
|
204
|
+
var PATH_REPLACEMENTS = [
|
|
205
|
+
[/\.claude\/skills\//g, ".opencode/skills/"],
|
|
206
|
+
[/\.claude\/commands\//g, ".opencode/commands/"],
|
|
207
|
+
[/\.claude\/agents\//g, ".opencode/agents/"],
|
|
208
|
+
[/~\/\.claude\//g, "~/.config/opencode/"],
|
|
209
|
+
[/CLAUDE\.md/g, "AGENTS.md"],
|
|
210
|
+
[/\/compound-engineering:/g, "/systematic:"],
|
|
211
|
+
[/compound-engineering:/g, "systematic:"]
|
|
212
|
+
];
|
|
213
|
+
var CC_ONLY_SKILL_FIELDS = [
|
|
214
|
+
"model",
|
|
215
|
+
"allowed-tools",
|
|
216
|
+
"allowedTools",
|
|
217
|
+
"disable-model-invocation",
|
|
218
|
+
"disableModelInvocation",
|
|
219
|
+
"user-invocable",
|
|
220
|
+
"userInvocable",
|
|
221
|
+
"context",
|
|
222
|
+
"agent"
|
|
223
|
+
];
|
|
224
|
+
var CC_ONLY_COMMAND_FIELDS = ["argument-hint", "argumentHint"];
|
|
136
225
|
function inferTemperature(name, description) {
|
|
137
226
|
const sample = `${name} ${description ?? ""}`.toLowerCase();
|
|
138
227
|
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
|
@@ -149,6 +238,37 @@ function inferTemperature(name, description) {
|
|
|
149
238
|
}
|
|
150
239
|
return 0.3;
|
|
151
240
|
}
|
|
241
|
+
function transformBody(body) {
|
|
242
|
+
let result = body;
|
|
243
|
+
for (const [pattern, replacement] of TOOL_MAPPINGS) {
|
|
244
|
+
result = result.replace(pattern, replacement);
|
|
245
|
+
}
|
|
246
|
+
for (const [pattern, replacement] of PATH_REPLACEMENTS) {
|
|
247
|
+
result = result.replace(pattern, replacement);
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
function removeFields(data, fieldsToRemove) {
|
|
252
|
+
const result = {};
|
|
253
|
+
for (const [key, value] of Object.entries(data)) {
|
|
254
|
+
if (!fieldsToRemove.includes(key)) {
|
|
255
|
+
result[key] = value;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
function transformSkillFrontmatter(data) {
|
|
261
|
+
return removeFields(data, CC_ONLY_SKILL_FIELDS);
|
|
262
|
+
}
|
|
263
|
+
function transformCommandFrontmatter(data) {
|
|
264
|
+
const cleaned = removeFields(data, CC_ONLY_COMMAND_FIELDS);
|
|
265
|
+
if (typeof cleaned.model === "string" && cleaned.model !== "inherit") {
|
|
266
|
+
cleaned.model = normalizeModel(cleaned.model);
|
|
267
|
+
} else if (cleaned.model === "inherit") {
|
|
268
|
+
delete cleaned.model;
|
|
269
|
+
}
|
|
270
|
+
return cleaned;
|
|
271
|
+
}
|
|
152
272
|
function normalizeModel(model) {
|
|
153
273
|
if (model.includes("/"))
|
|
154
274
|
return model;
|
|
@@ -182,153 +302,46 @@ function transformAgentFrontmatter(data, agentMode) {
|
|
|
182
302
|
function convertContent(content, type, options = {}) {
|
|
183
303
|
if (content === "")
|
|
184
304
|
return "";
|
|
185
|
-
const { data, body,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return content;
|
|
305
|
+
const { data, body, hadFrontmatter } = parseFrontmatter(content);
|
|
306
|
+
if (!hadFrontmatter) {
|
|
307
|
+
return options.skipBodyTransform ? content : transformBody(content);
|
|
189
308
|
}
|
|
309
|
+
const shouldTransformBody = !options.skipBodyTransform;
|
|
310
|
+
const transformedBody = shouldTransformBody ? transformBody(body) : body;
|
|
190
311
|
if (type === "agent") {
|
|
191
312
|
const agentMode = options.agentMode ?? "subagent";
|
|
192
313
|
const transformedData = transformAgentFrontmatter(data, agentMode);
|
|
193
314
|
return `${formatFrontmatter(transformedData)}
|
|
194
|
-
${
|
|
315
|
+
${transformedBody}`;
|
|
316
|
+
}
|
|
317
|
+
if (type === "skill") {
|
|
318
|
+
const transformedData = transformSkillFrontmatter(data);
|
|
319
|
+
return `${formatFrontmatter(transformedData)}
|
|
320
|
+
${transformedBody}`;
|
|
321
|
+
}
|
|
322
|
+
if (type === "command") {
|
|
323
|
+
const transformedData = transformCommandFrontmatter(data);
|
|
324
|
+
return `${formatFrontmatter(transformedData)}
|
|
325
|
+
${transformedBody}`;
|
|
195
326
|
}
|
|
196
327
|
return content;
|
|
197
328
|
}
|
|
198
329
|
function convertFileWithCache(filePath, type, options = {}) {
|
|
199
|
-
const fd =
|
|
330
|
+
const fd = fs3.openSync(filePath, "r");
|
|
200
331
|
try {
|
|
201
|
-
const stats =
|
|
202
|
-
const cacheKey = `${filePath}:${type}:${options.source ?? "bundled"}:${options.agentMode ?? "subagent"}`;
|
|
332
|
+
const stats = fs3.fstatSync(fd);
|
|
333
|
+
const cacheKey = `${filePath}:${type}:${options.source ?? "bundled"}:${options.agentMode ?? "subagent"}:${options.skipBodyTransform ?? false}`;
|
|
203
334
|
const cached = cache.get(cacheKey);
|
|
204
335
|
if (cached != null && cached.mtimeMs === stats.mtimeMs) {
|
|
205
336
|
return cached.converted;
|
|
206
337
|
}
|
|
207
|
-
const content =
|
|
338
|
+
const content = fs3.readFileSync(fd, "utf8");
|
|
208
339
|
const converted = convertContent(content, type, options);
|
|
209
340
|
cache.set(cacheKey, { mtimeMs: stats.mtimeMs, converted });
|
|
210
341
|
return converted;
|
|
211
342
|
} finally {
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
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;
|
|
224
|
-
function recurse(currentDir, depth, category) {
|
|
225
|
-
if (depth > maxDepth)
|
|
226
|
-
return;
|
|
227
|
-
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
228
|
-
for (const entry of entries) {
|
|
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);
|
|
239
|
-
}
|
|
240
|
-
if (entry.isDirectory()) {
|
|
241
|
-
recurse(fullPath, depth + 1, entry.name);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
343
|
+
fs3.closeSync(fd);
|
|
244
344
|
}
|
|
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
|
-
}));
|
|
260
|
-
}
|
|
261
|
-
function extractAgentFrontmatter(content) {
|
|
262
|
-
const lines = content.split(`
|
|
263
|
-
`);
|
|
264
|
-
let inFrontmatter = false;
|
|
265
|
-
let name = "";
|
|
266
|
-
let description = "";
|
|
267
|
-
for (const line of lines) {
|
|
268
|
-
if (line.trim() === "---") {
|
|
269
|
-
if (inFrontmatter)
|
|
270
|
-
break;
|
|
271
|
-
inFrontmatter = true;
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (inFrontmatter) {
|
|
275
|
-
const match = line.match(/^(\w+(?:-\w+)*):\s*(.*)$/);
|
|
276
|
-
if (match) {
|
|
277
|
-
const [, key, value] = match;
|
|
278
|
-
if (key === "name")
|
|
279
|
-
name = value.trim();
|
|
280
|
-
if (key === "description")
|
|
281
|
-
description = value.trim();
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return { name, description, prompt: stripFrontmatter(content) };
|
|
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
|
-
}
|
|
304
|
-
function extractCommandFrontmatter(content) {
|
|
305
|
-
const lines = content.split(`
|
|
306
|
-
`);
|
|
307
|
-
let inFrontmatter = false;
|
|
308
|
-
let name = "";
|
|
309
|
-
let description = "";
|
|
310
|
-
let argumentHint = "";
|
|
311
|
-
for (const line of lines) {
|
|
312
|
-
if (line.trim() === "---") {
|
|
313
|
-
if (inFrontmatter)
|
|
314
|
-
break;
|
|
315
|
-
inFrontmatter = true;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (inFrontmatter) {
|
|
319
|
-
const match = line.match(/^(\w+(?:-\w+)*):\s*(.*)$/);
|
|
320
|
-
if (match) {
|
|
321
|
-
const [, key, value] = match;
|
|
322
|
-
if (key === "name")
|
|
323
|
-
name = value.trim();
|
|
324
|
-
if (key === "description")
|
|
325
|
-
description = value.trim();
|
|
326
|
-
if (key === "argument-hint")
|
|
327
|
-
argumentHint = value.trim().replace(/^["']|["']$/g, "");
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return { name, description, argumentHint };
|
|
332
345
|
}
|
|
333
346
|
|
|
334
347
|
// src/lib/skills.ts
|
|
@@ -337,30 +350,14 @@ import path3 from "path";
|
|
|
337
350
|
function extractFrontmatter(filePath) {
|
|
338
351
|
try {
|
|
339
352
|
const content = fs4.readFileSync(filePath, "utf8");
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
}
|
|
353
|
+
const { data, parseError } = parseFrontmatter(content);
|
|
354
|
+
if (parseError) {
|
|
355
|
+
return { name: "", description: "" };
|
|
362
356
|
}
|
|
363
|
-
return {
|
|
357
|
+
return {
|
|
358
|
+
name: typeof data.name === "string" ? data.name : "",
|
|
359
|
+
description: typeof data.description === "string" ? data.description : ""
|
|
360
|
+
};
|
|
364
361
|
} catch {
|
|
365
362
|
return { name: "", description: "" };
|
|
366
363
|
}
|
|
@@ -404,4 +401,4 @@ ${skillsXml}
|
|
|
404
401
|
</available_skills>`;
|
|
405
402
|
}
|
|
406
403
|
|
|
407
|
-
export { stripFrontmatter, loadConfig, getConfigPaths,
|
|
404
|
+
export { stripFrontmatter, loadConfig, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir, formatSkillsXml };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
convertContent,
|
|
4
3
|
convertFileWithCache,
|
|
5
4
|
extractAgentFrontmatter,
|
|
6
5
|
extractCommandFrontmatter,
|
|
@@ -10,11 +9,11 @@ import {
|
|
|
10
9
|
formatSkillsXml,
|
|
11
10
|
loadConfig,
|
|
12
11
|
stripFrontmatter
|
|
13
|
-
} from "./index-
|
|
12
|
+
} from "./index-ymsavt2y.js";
|
|
14
13
|
|
|
15
14
|
// src/index.ts
|
|
16
|
-
import
|
|
17
|
-
import
|
|
15
|
+
import fs2 from "fs";
|
|
16
|
+
import path4 from "path";
|
|
18
17
|
import { fileURLToPath } from "url";
|
|
19
18
|
|
|
20
19
|
// src/lib/bootstrap.ts
|
|
@@ -68,6 +67,60 @@ ${toolMapping}
|
|
|
68
67
|
</SYSTEMATIC_WORKFLOWS>`;
|
|
69
68
|
}
|
|
70
69
|
|
|
70
|
+
// src/lib/skill-loader.ts
|
|
71
|
+
import path2 from "path";
|
|
72
|
+
var SKILL_PREFIX = "systematic:";
|
|
73
|
+
var SKILL_DESCRIPTION_PREFIX = "(systematic - Skill) ";
|
|
74
|
+
function formatSkillCommandName(name) {
|
|
75
|
+
if (name.startsWith(SKILL_PREFIX)) {
|
|
76
|
+
return name;
|
|
77
|
+
}
|
|
78
|
+
return `${SKILL_PREFIX}${name}`;
|
|
79
|
+
}
|
|
80
|
+
function formatSkillDescription(description, fallbackName) {
|
|
81
|
+
const desc = description || `${fallbackName} skill`;
|
|
82
|
+
if (desc.startsWith(SKILL_DESCRIPTION_PREFIX)) {
|
|
83
|
+
return desc;
|
|
84
|
+
}
|
|
85
|
+
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
86
|
+
}
|
|
87
|
+
function wrapSkillTemplate(skillPath, body) {
|
|
88
|
+
const skillDir = path2.dirname(skillPath);
|
|
89
|
+
return `<skill-instruction>
|
|
90
|
+
Base directory for this skill: ${skillDir}/
|
|
91
|
+
File references (@path) in this skill are relative to this directory.
|
|
92
|
+
|
|
93
|
+
${body.trim()}
|
|
94
|
+
</skill-instruction>
|
|
95
|
+
|
|
96
|
+
<user-request>
|
|
97
|
+
$ARGUMENTS
|
|
98
|
+
</user-request>`;
|
|
99
|
+
}
|
|
100
|
+
function extractSkillBody(wrappedTemplate) {
|
|
101
|
+
const match = wrappedTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/);
|
|
102
|
+
return match ? match[1].trim() : wrappedTemplate;
|
|
103
|
+
}
|
|
104
|
+
function loadSkill(skillInfo) {
|
|
105
|
+
try {
|
|
106
|
+
const converted = convertFileWithCache(skillInfo.skillFile, "skill", {
|
|
107
|
+
source: "bundled"
|
|
108
|
+
});
|
|
109
|
+
const body = stripFrontmatter(converted);
|
|
110
|
+
const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
|
|
111
|
+
return {
|
|
112
|
+
name: skillInfo.name,
|
|
113
|
+
prefixedName: formatSkillCommandName(skillInfo.name),
|
|
114
|
+
description: formatSkillDescription(skillInfo.description, skillInfo.name),
|
|
115
|
+
path: skillInfo.path,
|
|
116
|
+
skillFile: skillInfo.skillFile,
|
|
117
|
+
wrappedTemplate
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
// src/lib/config-handler.ts
|
|
72
125
|
function loadAgentAsConfig(agentInfo) {
|
|
73
126
|
try {
|
|
@@ -86,7 +139,9 @@ function loadAgentAsConfig(agentInfo) {
|
|
|
86
139
|
}
|
|
87
140
|
function loadCommandAsConfig(commandInfo) {
|
|
88
141
|
try {
|
|
89
|
-
const converted = convertFileWithCache(commandInfo.file, "command", {
|
|
142
|
+
const converted = convertFileWithCache(commandInfo.file, "command", {
|
|
143
|
+
source: "bundled"
|
|
144
|
+
});
|
|
90
145
|
const { name, description } = extractCommandFrontmatter(converted);
|
|
91
146
|
const cleanName = commandInfo.name.replace(/^\//, "");
|
|
92
147
|
return {
|
|
@@ -97,16 +152,11 @@ function loadCommandAsConfig(commandInfo) {
|
|
|
97
152
|
return null;
|
|
98
153
|
}
|
|
99
154
|
}
|
|
100
|
-
function loadSkillAsCommand(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
description: skillInfo.description || `${skillInfo.name} skill`
|
|
106
|
-
};
|
|
107
|
-
} catch {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
155
|
+
function loadSkillAsCommand(loaded) {
|
|
156
|
+
return {
|
|
157
|
+
template: loaded.wrappedTemplate,
|
|
158
|
+
description: loaded.description
|
|
159
|
+
};
|
|
110
160
|
}
|
|
111
161
|
function collectAgents(dir, disabledAgents) {
|
|
112
162
|
const agents = {};
|
|
@@ -141,9 +191,9 @@ function collectSkillsAsCommands(dir, disabledSkills) {
|
|
|
141
191
|
for (const skillInfo of skillList) {
|
|
142
192
|
if (disabledSkills.includes(skillInfo.name))
|
|
143
193
|
continue;
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
commands[
|
|
194
|
+
const loaded = loadSkill(skillInfo);
|
|
195
|
+
if (loaded) {
|
|
196
|
+
commands[loaded.prefixedName] = loadSkillAsCommand(loaded);
|
|
147
197
|
}
|
|
148
198
|
}
|
|
149
199
|
return commands;
|
|
@@ -170,28 +220,22 @@ function createConfigHandler(deps) {
|
|
|
170
220
|
}
|
|
171
221
|
|
|
172
222
|
// src/lib/skill-tool.ts
|
|
173
|
-
import
|
|
174
|
-
import path2 from "path";
|
|
223
|
+
import path3 from "path";
|
|
175
224
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
176
|
-
function wrapSkillContent(skillPath, content) {
|
|
177
|
-
const skillDir = path2.dirname(skillPath);
|
|
178
|
-
const converted = convertContent(content, "skill", { source: "bundled" });
|
|
179
|
-
const body = stripFrontmatter(converted);
|
|
180
|
-
return `<skill-instruction>
|
|
181
|
-
Base directory for this skill: ${skillDir}/
|
|
182
|
-
File references (@path) in this skill are relative to this directory.
|
|
183
|
-
|
|
184
|
-
${body.trim()}
|
|
185
|
-
</skill-instruction>`;
|
|
186
|
-
}
|
|
187
225
|
function createSkillTool(options) {
|
|
188
226
|
const { bundledSkillsDir, disabledSkills } = options;
|
|
189
227
|
const getSystematicSkills = () => {
|
|
190
|
-
return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
228
|
+
return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).map((skillInfo) => loadSkill(skillInfo)).filter((s) => s !== null).sort((a, b) => a.name.localeCompare(b.name));
|
|
191
229
|
};
|
|
192
230
|
const buildDescription = () => {
|
|
193
231
|
const skills = getSystematicSkills();
|
|
194
|
-
const
|
|
232
|
+
const skillInfos = skills.map((s) => ({
|
|
233
|
+
name: s.name,
|
|
234
|
+
description: s.description,
|
|
235
|
+
path: s.path,
|
|
236
|
+
skillFile: s.skillFile
|
|
237
|
+
}));
|
|
238
|
+
const systematicXml = formatSkillsXml(skillInfos);
|
|
195
239
|
const baseDescription = `Load a skill to get detailed instructions for a specific task.
|
|
196
240
|
|
|
197
241
|
Skills provide specialized knowledge and step-by-step guidance.
|
|
@@ -217,38 +261,33 @@ ${systematicXml}`;
|
|
|
217
261
|
const skills = getSystematicSkills();
|
|
218
262
|
const matchedSkill = skills.find((s) => s.name === normalizedName);
|
|
219
263
|
if (matchedSkill) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return `## Skill: systematic:${matchedSkill.name}
|
|
264
|
+
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
265
|
+
const dir = path3.dirname(matchedSkill.skillFile);
|
|
266
|
+
return `## Skill: ${matchedSkill.prefixedName}
|
|
224
267
|
|
|
225
|
-
**Base directory**: ${
|
|
268
|
+
**Base directory**: ${dir}
|
|
226
269
|
|
|
227
|
-
${
|
|
228
|
-
} catch (error) {
|
|
229
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
230
|
-
throw new Error(`Failed to load skill "${requestedName}": ${errorMessage}`);
|
|
231
|
-
}
|
|
270
|
+
${body}`;
|
|
232
271
|
}
|
|
233
|
-
const availableSystematic = skills.map((s) =>
|
|
272
|
+
const availableSystematic = skills.map((s) => s.prefixedName);
|
|
234
273
|
throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
|
|
235
274
|
}
|
|
236
275
|
});
|
|
237
276
|
}
|
|
238
277
|
|
|
239
278
|
// src/index.ts
|
|
240
|
-
var __dirname2 =
|
|
241
|
-
var packageRoot =
|
|
242
|
-
var bundledSkillsDir =
|
|
243
|
-
var bundledAgentsDir =
|
|
244
|
-
var bundledCommandsDir =
|
|
245
|
-
var packageJsonPath =
|
|
279
|
+
var __dirname2 = path4.dirname(fileURLToPath(import.meta.url));
|
|
280
|
+
var packageRoot = path4.resolve(__dirname2, "..");
|
|
281
|
+
var bundledSkillsDir = path4.join(packageRoot, "skills");
|
|
282
|
+
var bundledAgentsDir = path4.join(packageRoot, "agents");
|
|
283
|
+
var bundledCommandsDir = path4.join(packageRoot, "commands");
|
|
284
|
+
var packageJsonPath = path4.join(packageRoot, "package.json");
|
|
246
285
|
var hasLoggedInit = false;
|
|
247
286
|
var getPackageVersion = () => {
|
|
248
287
|
try {
|
|
249
|
-
if (!
|
|
288
|
+
if (!fs2.existsSync(packageJsonPath))
|
|
250
289
|
return "unknown";
|
|
251
|
-
const content =
|
|
290
|
+
const content = fs2.readFileSync(packageJsonPath, "utf8");
|
|
252
291
|
const parsed = JSON.parse(content);
|
|
253
292
|
return parsed.version ?? "unknown";
|
|
254
293
|
} catch {
|
package/dist/lib/converter.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type AgentMode = 'primary' | 'subagent';
|
|
|
4
4
|
export interface ConvertOptions {
|
|
5
5
|
source?: SourceType;
|
|
6
6
|
agentMode?: AgentMode;
|
|
7
|
+
/** Skip body content transformations (tool names, paths, etc.) */
|
|
8
|
+
skipBodyTransform?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare function convertContent(content: string, type: ContentType, options?: ConvertOptions): string;
|
|
9
11
|
export declare function convertFileWithCache(filePath: string, type: ContentType, options?: ConvertOptions): string;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
interface
|
|
2
|
-
data:
|
|
1
|
+
export interface FrontmatterResult<T = Record<string, unknown>> {
|
|
2
|
+
data: T;
|
|
3
3
|
body: string;
|
|
4
|
-
|
|
4
|
+
hadFrontmatter: boolean;
|
|
5
|
+
parseError: boolean;
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Parses YAML frontmatter from Markdown content.
|
|
9
|
+
*
|
|
10
|
+
* Uses js-yaml with JSON_SCHEMA for security (prevents code execution via YAML tags).
|
|
11
|
+
* Supports all standard YAML keys including hyphenated ones (e.g., 'argument-hint').
|
|
12
|
+
*
|
|
13
|
+
* @param content - Markdown content with optional frontmatter
|
|
14
|
+
* @returns Parsed frontmatter data, body content, and parsing status
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseFrontmatter<T = Record<string, unknown>>(content: string): FrontmatterResult<T>;
|
|
17
|
+
export declare function formatFrontmatter(data: Record<string, unknown>): string;
|
|
9
18
|
export declare function stripFrontmatter(content: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SkillInfo } from './skills.js';
|
|
2
|
+
export interface LoadedSkill {
|
|
3
|
+
name: string;
|
|
4
|
+
prefixedName: string;
|
|
5
|
+
description: string;
|
|
6
|
+
path: string;
|
|
7
|
+
skillFile: string;
|
|
8
|
+
wrappedTemplate: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function formatSkillCommandName(name: string): string;
|
|
11
|
+
export declare function formatSkillDescription(description: string, fallbackName: string): string;
|
|
12
|
+
export declare function wrapSkillTemplate(skillPath: string, body: string): string;
|
|
13
|
+
export declare function extractSkillBody(wrappedTemplate: string): string;
|
|
14
|
+
export declare function loadSkill(skillInfo: SkillInfo): LoadedSkill | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@biomejs/biome": "^2.0.0",
|
|
55
55
|
"@opencode-ai/plugin": "^1.1.30",
|
|
56
56
|
"@types/bun": "latest",
|
|
57
|
+
"@types/js-yaml": "^4.0.9",
|
|
57
58
|
"@types/node": "^22.0.0",
|
|
58
59
|
"conventional-changelog-conventionalcommits": "^9.0.0",
|
|
59
60
|
"markdownlint-cli": "^0.47.0",
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"typescript": "^5.7.0"
|
|
64
65
|
},
|
|
65
66
|
"dependencies": {
|
|
67
|
+
"js-yaml": "^4.1.1",
|
|
66
68
|
"jsonc-parser": "^3.3.0"
|
|
67
69
|
},
|
|
68
70
|
"publishConfig": {
|