@fro.bot/systematic 1.2.1 → 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.d.ts +2 -0
- package/dist/cli.js +15 -24
- package/dist/index-ymsavt2y.js +404 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +149 -216
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/bootstrap.d.ts +5 -0
- package/dist/lib/commands.d.ts +12 -0
- package/dist/lib/config-handler.d.ts +17 -0
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/converter.d.ts +12 -0
- package/dist/lib/frontmatter.d.ts +18 -0
- package/dist/lib/skill-loader.d.ts +14 -0
- package/dist/lib/skill-tool.d.ts +6 -0
- package/dist/lib/skills.d.ts +13 -0
- package/dist/lib/walk-dir.d.ts +12 -0
- package/package.json +5 -3
- package/dist/index-hkk4125w.js +0 -324
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",
|
|
@@ -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
|
|
32
|
+
"prepublishOnly": "bun run build"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"opencode",
|
|
@@ -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": {
|
package/dist/index-hkk4125w.js
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/lib/converter.ts
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
var cache = new Map;
|
|
5
|
-
function parseFrontmatter(content) {
|
|
6
|
-
const lines = content.split(/\r?\n/);
|
|
7
|
-
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
8
|
-
return { data: {}, body: content, raw: "" };
|
|
9
|
-
}
|
|
10
|
-
let endIndex = -1;
|
|
11
|
-
for (let i = 1;i < lines.length; i++) {
|
|
12
|
-
if (lines[i].trim() === "---") {
|
|
13
|
-
endIndex = i;
|
|
14
|
-
break;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
if (endIndex === -1) {
|
|
18
|
-
return { data: {}, body: content, raw: "" };
|
|
19
|
-
}
|
|
20
|
-
const yamlLines = lines.slice(1, endIndex);
|
|
21
|
-
const body = lines.slice(endIndex + 1).join(`
|
|
22
|
-
`);
|
|
23
|
-
const raw = lines.slice(0, endIndex + 1).join(`
|
|
24
|
-
`);
|
|
25
|
-
const data = {};
|
|
26
|
-
for (const line of yamlLines) {
|
|
27
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
28
|
-
if (match) {
|
|
29
|
-
const [, key, value] = match;
|
|
30
|
-
if (value === "true")
|
|
31
|
-
data[key] = true;
|
|
32
|
-
else if (value === "false")
|
|
33
|
-
data[key] = false;
|
|
34
|
-
else if (/^\d+(\.\d+)?$/.test(value))
|
|
35
|
-
data[key] = parseFloat(value);
|
|
36
|
-
else
|
|
37
|
-
data[key] = value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return { data, body, raw };
|
|
41
|
-
}
|
|
42
|
-
function formatFrontmatter(data) {
|
|
43
|
-
const lines = ["---"];
|
|
44
|
-
for (const [key, value] of Object.entries(data)) {
|
|
45
|
-
lines.push(`${key}: ${value}`);
|
|
46
|
-
}
|
|
47
|
-
lines.push("---");
|
|
48
|
-
return lines.join(`
|
|
49
|
-
`);
|
|
50
|
-
}
|
|
51
|
-
function inferTemperature(name, description) {
|
|
52
|
-
const sample = `${name} ${description ?? ""}`.toLowerCase();
|
|
53
|
-
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
|
54
|
-
return 0.1;
|
|
55
|
-
}
|
|
56
|
-
if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
|
|
57
|
-
return 0.2;
|
|
58
|
-
}
|
|
59
|
-
if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
|
|
60
|
-
return 0.3;
|
|
61
|
-
}
|
|
62
|
-
if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
|
|
63
|
-
return 0.6;
|
|
64
|
-
}
|
|
65
|
-
return 0.3;
|
|
66
|
-
}
|
|
67
|
-
function normalizeModel(model) {
|
|
68
|
-
if (model.includes("/"))
|
|
69
|
-
return model;
|
|
70
|
-
if (model === "inherit")
|
|
71
|
-
return model;
|
|
72
|
-
if (/^claude-/.test(model))
|
|
73
|
-
return `anthropic/${model}`;
|
|
74
|
-
if (/^(gpt-|o1-|o3-)/.test(model))
|
|
75
|
-
return `openai/${model}`;
|
|
76
|
-
if (/^gemini-/.test(model))
|
|
77
|
-
return `google/${model}`;
|
|
78
|
-
return `anthropic/${model}`;
|
|
79
|
-
}
|
|
80
|
-
function transformAgentFrontmatter(data, agentMode) {
|
|
81
|
-
const name = typeof data.name === "string" ? data.name : "";
|
|
82
|
-
const description = typeof data.description === "string" ? data.description : "";
|
|
83
|
-
const newData = {
|
|
84
|
-
description: description || `${name} agent`,
|
|
85
|
-
mode: agentMode
|
|
86
|
-
};
|
|
87
|
-
if (typeof data.model === "string" && data.model !== "inherit") {
|
|
88
|
-
newData.model = normalizeModel(data.model);
|
|
89
|
-
}
|
|
90
|
-
if (typeof data.temperature === "number") {
|
|
91
|
-
newData.temperature = data.temperature;
|
|
92
|
-
} else {
|
|
93
|
-
newData.temperature = inferTemperature(name, description);
|
|
94
|
-
}
|
|
95
|
-
return newData;
|
|
96
|
-
}
|
|
97
|
-
function convertContent(content, type, options = {}) {
|
|
98
|
-
if (content === "")
|
|
99
|
-
return "";
|
|
100
|
-
const { data, body, raw } = parseFrontmatter(content);
|
|
101
|
-
const hasFrontmatter = raw !== "";
|
|
102
|
-
if (!hasFrontmatter) {
|
|
103
|
-
return content;
|
|
104
|
-
}
|
|
105
|
-
if (type === "agent") {
|
|
106
|
-
const agentMode = options.agentMode ?? "subagent";
|
|
107
|
-
const transformedData = transformAgentFrontmatter(data, agentMode);
|
|
108
|
-
return `${formatFrontmatter(transformedData)}
|
|
109
|
-
${body}`;
|
|
110
|
-
}
|
|
111
|
-
return content;
|
|
112
|
-
}
|
|
113
|
-
function convertFileWithCache(filePath, type, options = {}) {
|
|
114
|
-
const fd = fs.openSync(filePath, "r");
|
|
115
|
-
try {
|
|
116
|
-
const stats = fs.fstatSync(fd);
|
|
117
|
-
const cacheKey = `${filePath}:${type}:${options.source ?? "bundled"}:${options.agentMode ?? "subagent"}`;
|
|
118
|
-
const cached = cache.get(cacheKey);
|
|
119
|
-
if (cached != null && cached.mtimeMs === stats.mtimeMs) {
|
|
120
|
-
return cached.converted;
|
|
121
|
-
}
|
|
122
|
-
const content = fs.readFileSync(fd, "utf8");
|
|
123
|
-
const converted = convertContent(content, type, options);
|
|
124
|
-
cache.set(cacheKey, { mtimeMs: stats.mtimeMs, converted });
|
|
125
|
-
return converted;
|
|
126
|
-
} finally {
|
|
127
|
-
fs.closeSync(fd);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
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;
|
|
220
|
-
function recurse(currentDir, depth, category) {
|
|
221
|
-
if (depth > maxDepth)
|
|
222
|
-
return;
|
|
223
|
-
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
224
|
-
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
|
-
});
|
|
235
|
-
}
|
|
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
|
-
if (entry.isDirectory()) {
|
|
252
|
-
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
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
recurse(dir, 0);
|
|
266
|
-
return commands;
|
|
267
|
-
}
|
|
268
|
-
function extractAgentFrontmatter(content) {
|
|
269
|
-
const lines = content.split(`
|
|
270
|
-
`);
|
|
271
|
-
let inFrontmatter = false;
|
|
272
|
-
let name = "";
|
|
273
|
-
let description = "";
|
|
274
|
-
for (const line of lines) {
|
|
275
|
-
if (line.trim() === "---") {
|
|
276
|
-
if (inFrontmatter)
|
|
277
|
-
break;
|
|
278
|
-
inFrontmatter = true;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (inFrontmatter) {
|
|
282
|
-
const match = line.match(/^(\w+(?:-\w+)*):\s*(.*)$/);
|
|
283
|
-
if (match) {
|
|
284
|
-
const [, key, value] = match;
|
|
285
|
-
if (key === "name")
|
|
286
|
-
name = value.trim();
|
|
287
|
-
if (key === "description")
|
|
288
|
-
description = value.trim();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return { name, description, prompt: stripFrontmatter(content) };
|
|
293
|
-
}
|
|
294
|
-
function extractCommandFrontmatter(content) {
|
|
295
|
-
const lines = content.split(`
|
|
296
|
-
`);
|
|
297
|
-
let inFrontmatter = false;
|
|
298
|
-
let name = "";
|
|
299
|
-
let description = "";
|
|
300
|
-
let argumentHint = "";
|
|
301
|
-
for (const line of lines) {
|
|
302
|
-
if (line.trim() === "---") {
|
|
303
|
-
if (inFrontmatter)
|
|
304
|
-
break;
|
|
305
|
-
inFrontmatter = true;
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
if (inFrontmatter) {
|
|
309
|
-
const match = line.match(/^(\w+(?:-\w+)*):\s*(.*)$/);
|
|
310
|
-
if (match) {
|
|
311
|
-
const [, key, value] = match;
|
|
312
|
-
if (key === "name")
|
|
313
|
-
name = value.trim();
|
|
314
|
-
if (key === "description")
|
|
315
|
-
description = value.trim();
|
|
316
|
-
if (key === "argument-hint")
|
|
317
|
-
argumentHint = value.trim().replace(/^["']|["']$/g, "");
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return { name, description, argumentHint };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export { convertContent, convertFileWithCache, stripFrontmatter, findSkillsInDir, findAgentsInDir, findCommandsInDir, extractAgentFrontmatter, extractCommandFrontmatter };
|