@fro.bot/systematic 2.13.3 → 2.14.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-3xnyeknn.js → index-7100fhg3.js} +15547 -15543
- package/dist/index.js +186 -136
- package/dist/lib/bootstrap.d.ts +8 -3
- package/dist/lib/skill-catalog.d.ts +33 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
loadConfig,
|
|
14
14
|
loadConfigWithSources,
|
|
15
15
|
parseFrontmatter
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-7100fhg3.js";
|
|
17
17
|
|
|
18
18
|
// src/index.ts
|
|
19
19
|
import fs5 from "fs";
|
|
@@ -23,7 +23,115 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
23
23
|
// src/lib/bootstrap.ts
|
|
24
24
|
import fs from "fs";
|
|
25
25
|
import os from "os";
|
|
26
|
+
import path2 from "path";
|
|
27
|
+
|
|
28
|
+
// src/lib/skill-catalog.ts
|
|
29
|
+
import { pathToFileURL } from "url";
|
|
30
|
+
|
|
31
|
+
// src/lib/skill-loader.ts
|
|
26
32
|
import path from "path";
|
|
33
|
+
var SKILL_PREFIX = "systematic:";
|
|
34
|
+
var SKILL_DESCRIPTION_PREFIX = "(Systematic - Skill) ";
|
|
35
|
+
function formatSkillCommandName(name) {
|
|
36
|
+
if (name.includes(":")) {
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
return `${SKILL_PREFIX}${name}`;
|
|
40
|
+
}
|
|
41
|
+
function formatSkillDescription(description, fallbackName) {
|
|
42
|
+
const desc = description || `${fallbackName} skill`;
|
|
43
|
+
if (desc.startsWith(SKILL_DESCRIPTION_PREFIX)) {
|
|
44
|
+
return desc;
|
|
45
|
+
}
|
|
46
|
+
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
47
|
+
}
|
|
48
|
+
function wrapSkillTemplate(skillPath, body) {
|
|
49
|
+
const skillDir = path.dirname(skillPath);
|
|
50
|
+
return `<skill-instruction>
|
|
51
|
+
Base directory for this skill: ${skillDir}/
|
|
52
|
+
File references (@path) in this skill are relative to this directory.
|
|
53
|
+
|
|
54
|
+
${body.trim()}
|
|
55
|
+
</skill-instruction>
|
|
56
|
+
|
|
57
|
+
<user-request>
|
|
58
|
+
$ARGUMENTS
|
|
59
|
+
</user-request>`;
|
|
60
|
+
}
|
|
61
|
+
function extractSkillBody(wrappedTemplate) {
|
|
62
|
+
const match = wrappedTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/);
|
|
63
|
+
return match ? match[1].trim() : wrappedTemplate;
|
|
64
|
+
}
|
|
65
|
+
function loadSkill(skillInfo) {
|
|
66
|
+
try {
|
|
67
|
+
const converted = convertFileWithCache(skillInfo.skillFile, "skill", {
|
|
68
|
+
source: "bundled"
|
|
69
|
+
});
|
|
70
|
+
const { body } = parseFrontmatter(converted);
|
|
71
|
+
const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
|
|
72
|
+
return {
|
|
73
|
+
name: skillInfo.name,
|
|
74
|
+
prefixedName: formatSkillCommandName(skillInfo.name),
|
|
75
|
+
description: formatSkillDescription(skillInfo.description, skillInfo.name),
|
|
76
|
+
path: skillInfo.path,
|
|
77
|
+
skillFile: skillInfo.skillFile,
|
|
78
|
+
wrappedTemplate,
|
|
79
|
+
disableModelInvocation: skillInfo.disableModelInvocation,
|
|
80
|
+
userInvocable: skillInfo.userInvocable,
|
|
81
|
+
subtask: skillInfo.subtask,
|
|
82
|
+
agent: skillInfo.agent,
|
|
83
|
+
model: skillInfo.model,
|
|
84
|
+
argumentHint: skillInfo.argumentHint
|
|
85
|
+
};
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/lib/skill-catalog.ts
|
|
92
|
+
function escapeXml(text) {
|
|
93
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
94
|
+
}
|
|
95
|
+
function buildCatalogEntries(options) {
|
|
96
|
+
const { bundledSkillsDir, disabledSkills } = options;
|
|
97
|
+
return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).filter((s) => s.disableModelInvocation !== true).sort((a, b) => a.name.localeCompare(b.name)).map((s) => ({
|
|
98
|
+
name: s.name,
|
|
99
|
+
prefixedName: formatSkillCommandName(s.name),
|
|
100
|
+
description: s.description,
|
|
101
|
+
path: s.path,
|
|
102
|
+
skillFile: s.skillFile
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
function renderCatalogVerbose(options) {
|
|
106
|
+
const entries = buildCatalogEntries(options);
|
|
107
|
+
if (entries.length === 0)
|
|
108
|
+
return "";
|
|
109
|
+
const skillLines = entries.flatMap((entry) => [
|
|
110
|
+
" <skill>",
|
|
111
|
+
` <name>${escapeXml(entry.prefixedName)}</name>`,
|
|
112
|
+
` <description>${escapeXml(entry.description)}</description>`,
|
|
113
|
+
` <location>${pathToFileURL(entry.path).href}</location>`,
|
|
114
|
+
" </skill>"
|
|
115
|
+
]);
|
|
116
|
+
return ["<available_skills>", ...skillLines, "</available_skills>"].join(`
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
function renderCatalogCompact(options) {
|
|
120
|
+
const entries = buildCatalogEntries(options);
|
|
121
|
+
const heading = "## Available Systematic Skills";
|
|
122
|
+
if (entries.length === 0) {
|
|
123
|
+
return `${heading}
|
|
124
|
+
|
|
125
|
+
No Systematic skills are currently available.`;
|
|
126
|
+
}
|
|
127
|
+
const bullets = entries.map((entry) => `- ${entry.prefixedName}: ${entry.description}`).join(`
|
|
128
|
+
`);
|
|
129
|
+
return `${heading}
|
|
130
|
+
|
|
131
|
+
${bullets}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/lib/bootstrap.ts
|
|
27
135
|
var INTERNAL_AGENT_SIGNATURES = [
|
|
28
136
|
"You are a title generator",
|
|
29
137
|
"You are a helpful AI assistant tasked with summarizing conversations",
|
|
@@ -31,31 +139,54 @@ var INTERNAL_AGENT_SIGNATURES = [
|
|
|
31
139
|
];
|
|
32
140
|
var BOOTSTRAP_MARKER_OPEN = "<SYSTEMATIC_WORKFLOWS>";
|
|
33
141
|
var BOOTSTRAP_MARKER_CLOSE = "</SYSTEMATIC_WORKFLOWS>";
|
|
34
|
-
var findBootstrapMarkerBlock = (entry) => {
|
|
35
|
-
const start = entry.indexOf(BOOTSTRAP_MARKER_OPEN);
|
|
142
|
+
var findBootstrapMarkerBlock = (entry, fromIndex = 0) => {
|
|
143
|
+
const start = entry.indexOf(BOOTSTRAP_MARKER_OPEN, fromIndex);
|
|
36
144
|
if (start === -1)
|
|
37
145
|
return null;
|
|
38
146
|
const closeStart = entry.indexOf(BOOTSTRAP_MARKER_CLOSE, start + BOOTSTRAP_MARKER_OPEN.length);
|
|
39
147
|
if (closeStart === -1)
|
|
40
148
|
return null;
|
|
41
|
-
return { start, end: closeStart + BOOTSTRAP_MARKER_CLOSE.length };
|
|
149
|
+
return { start, closeStart, end: closeStart + BOOTSTRAP_MARKER_CLOSE.length };
|
|
150
|
+
};
|
|
151
|
+
var removeCompleteBootstrapBlocks = (entry) => {
|
|
152
|
+
const segments = [];
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
let block = findBootstrapMarkerBlock(entry, cursor);
|
|
155
|
+
let hadNestedBlock = false;
|
|
156
|
+
while (block !== null) {
|
|
157
|
+
const nestedStart = entry.indexOf(BOOTSTRAP_MARKER_OPEN, block.start + BOOTSTRAP_MARKER_OPEN.length);
|
|
158
|
+
if (nestedStart !== -1 && nestedStart < block.closeStart) {
|
|
159
|
+
hadNestedBlock = true;
|
|
160
|
+
segments.push(entry.slice(cursor, nestedStart));
|
|
161
|
+
cursor = nestedStart;
|
|
162
|
+
block = findBootstrapMarkerBlock(entry, cursor);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
segments.push(entry.slice(cursor, block.start));
|
|
166
|
+
cursor = block.end;
|
|
167
|
+
block = findBootstrapMarkerBlock(entry, cursor);
|
|
168
|
+
}
|
|
169
|
+
if (cursor === 0)
|
|
170
|
+
return entry;
|
|
171
|
+
segments.push(entry.slice(cursor));
|
|
172
|
+
const result = segments.join("");
|
|
173
|
+
if (hadNestedBlock) {
|
|
174
|
+
return removeCompleteBootstrapBlocks(result);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
42
177
|
};
|
|
43
178
|
var applyBootstrapContent = (output, content) => {
|
|
44
179
|
for (let i = 0;i < output.system.length; i++) {
|
|
45
|
-
|
|
46
|
-
const block = findBootstrapMarkerBlock(entry);
|
|
47
|
-
if (block !== null) {
|
|
48
|
-
output.system[i] = entry.slice(0, block.start) + content + entry.slice(block.end);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
180
|
+
output.system[i] = removeCompleteBootstrapBlocks(output.system[i]);
|
|
51
181
|
}
|
|
52
|
-
if (output.system.length
|
|
53
|
-
output.system[output.system.length - 1] += `
|
|
54
|
-
|
|
55
|
-
${content}`;
|
|
56
|
-
} else {
|
|
182
|
+
if (output.system.length === 0) {
|
|
57
183
|
output.system.push(content);
|
|
184
|
+
return;
|
|
58
185
|
}
|
|
186
|
+
const first = output.system[0];
|
|
187
|
+
output.system[0] = first.length > 0 ? `${first}
|
|
188
|
+
|
|
189
|
+
${content}` : content;
|
|
59
190
|
};
|
|
60
191
|
function getToolMappingTemplate() {
|
|
61
192
|
return `**Tool Mapping for OpenCode:**
|
|
@@ -82,18 +213,25 @@ function getBootstrapContent(config, deps) {
|
|
|
82
213
|
if (!config.bootstrap.enabled)
|
|
83
214
|
return null;
|
|
84
215
|
if (config.bootstrap.file) {
|
|
85
|
-
const customPath = config.bootstrap.file.startsWith("~/") ?
|
|
216
|
+
const customPath = config.bootstrap.file.startsWith("~/") ? path2.join(os.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
|
|
86
217
|
if (fs.existsSync(customPath)) {
|
|
87
218
|
return fs.readFileSync(customPath, "utf8");
|
|
88
219
|
}
|
|
89
220
|
}
|
|
90
|
-
const usingSystematicPath =
|
|
221
|
+
const usingSystematicPath = path2.join(bundledSkillsDir, "using-systematic/SKILL.md");
|
|
91
222
|
if (!fs.existsSync(usingSystematicPath))
|
|
92
223
|
return null;
|
|
93
224
|
const fullContent = fs.readFileSync(usingSystematicPath, "utf8");
|
|
94
225
|
const { body } = parseFrontmatter(fullContent);
|
|
95
226
|
const content = body.trim();
|
|
96
227
|
const toolMapping = getToolMappingTemplate();
|
|
228
|
+
const catalog = renderCatalogVerbose({
|
|
229
|
+
bundledSkillsDir,
|
|
230
|
+
disabledSkills: config.disabled_skills
|
|
231
|
+
});
|
|
232
|
+
const catalogSection = catalog.length > 0 ? `
|
|
233
|
+
|
|
234
|
+
${catalog}` : "";
|
|
97
235
|
return `<SYSTEMATIC_WORKFLOWS>
|
|
98
236
|
You have access to structured engineering workflows via the systematic plugin.
|
|
99
237
|
|
|
@@ -101,21 +239,21 @@ You have access to structured engineering workflows via the systematic plugin.
|
|
|
101
239
|
|
|
102
240
|
${content}
|
|
103
241
|
|
|
104
|
-
${toolMapping}
|
|
242
|
+
${toolMapping}${catalogSection}
|
|
105
243
|
</SYSTEMATIC_WORKFLOWS>`;
|
|
106
244
|
}
|
|
107
245
|
|
|
108
246
|
// src/lib/agent-overlays.ts
|
|
109
247
|
import fs2 from "fs";
|
|
110
|
-
import
|
|
248
|
+
import path4 from "path";
|
|
111
249
|
|
|
112
250
|
// src/lib/source-model-defaults.ts
|
|
113
|
-
import * as
|
|
251
|
+
import * as path3 from "path";
|
|
114
252
|
import { fileURLToPath } from "url";
|
|
115
253
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
116
|
-
var __dirname2 =
|
|
117
|
-
var packageRoot =
|
|
118
|
-
var bundledAgentsDir =
|
|
254
|
+
var __dirname2 = path3.dirname(__filename2);
|
|
255
|
+
var packageRoot = path3.resolve(__dirname2, "..", "..");
|
|
256
|
+
var bundledAgentsDir = path3.join(packageRoot, "agents");
|
|
119
257
|
var ProviderID = exports_external.union([
|
|
120
258
|
exports_external.literal("vercel"),
|
|
121
259
|
exports_external.literal("opencode"),
|
|
@@ -325,14 +463,14 @@ function buildBundledAgentInventory(agentsDir, disabledAgents) {
|
|
|
325
463
|
const stemCategories = new Map;
|
|
326
464
|
const disabledSet = new Set(disabledAgents);
|
|
327
465
|
for (const category of categories) {
|
|
328
|
-
for (const fileName of readMarkdownFiles(
|
|
466
|
+
for (const fileName of readMarkdownFiles(path4.join(agentsDir, category))) {
|
|
329
467
|
const key = fileName.replace(/\.md$/, "");
|
|
330
468
|
const id = `${category}/${key}`;
|
|
331
469
|
agentsByQualifiedId[id] = {
|
|
332
470
|
id,
|
|
333
471
|
key,
|
|
334
472
|
category,
|
|
335
|
-
file:
|
|
473
|
+
file: path4.join(agentsDir, category, fileName),
|
|
336
474
|
disabled: disabledSet.has(key) || disabledSet.has(id)
|
|
337
475
|
};
|
|
338
476
|
const existing = stemCategories.get(key) ?? [];
|
|
@@ -496,7 +634,7 @@ function throwConfigError(sourcePath, keyPath, message) {
|
|
|
496
634
|
import { createHash } from "crypto";
|
|
497
635
|
import fs3 from "fs";
|
|
498
636
|
import os2 from "os";
|
|
499
|
-
import
|
|
637
|
+
import path5 from "path";
|
|
500
638
|
function emptyAvailability() {
|
|
501
639
|
return { status: "unknown", models: new Set };
|
|
502
640
|
}
|
|
@@ -505,8 +643,8 @@ var DEFAULT_API_TIMEOUT_MS = 1500;
|
|
|
505
643
|
var MODELS_JSON_FILENAME = "models.json";
|
|
506
644
|
function resolveCacheDir() {
|
|
507
645
|
const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
|
|
508
|
-
const cacheBase = xdgCacheHome &&
|
|
509
|
-
return
|
|
646
|
+
const cacheBase = xdgCacheHome && path5.isAbsolute(xdgCacheHome) ? xdgCacheHome : path5.join(os2.homedir(), ".cache");
|
|
647
|
+
return path5.join(cacheBase, "opencode");
|
|
510
648
|
}
|
|
511
649
|
function fastHash(input) {
|
|
512
650
|
return createHash("sha1").update(input).digest("hex");
|
|
@@ -584,14 +722,14 @@ function readFallbackCache() {
|
|
|
584
722
|
const cacheDir = resolveCacheDir();
|
|
585
723
|
const openCodeModelsUrl = process.env.OPENCODE_MODELS_URL?.trim();
|
|
586
724
|
if (openCodeModelsUrl) {
|
|
587
|
-
const urlDerivedPath =
|
|
725
|
+
const urlDerivedPath = path5.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
|
|
588
726
|
const urlResult = readModelsFromCache(urlDerivedPath);
|
|
589
727
|
if (urlResult !== null) {
|
|
590
728
|
return { status: "cache", models: urlResult };
|
|
591
729
|
}
|
|
592
730
|
return emptyAvailability();
|
|
593
731
|
}
|
|
594
|
-
const defaultPath =
|
|
732
|
+
const defaultPath = path5.join(cacheDir, MODELS_JSON_FILENAME);
|
|
595
733
|
const defaultResult = readModelsFromCache(defaultPath);
|
|
596
734
|
if (defaultResult !== null) {
|
|
597
735
|
return { status: "cache", models: defaultResult };
|
|
@@ -642,66 +780,6 @@ async function getAvailableModels(client, options = {}) {
|
|
|
642
780
|
};
|
|
643
781
|
}
|
|
644
782
|
|
|
645
|
-
// src/lib/skill-loader.ts
|
|
646
|
-
import path5 from "path";
|
|
647
|
-
var SKILL_PREFIX = "systematic:";
|
|
648
|
-
var SKILL_DESCRIPTION_PREFIX = "(Systematic - Skill) ";
|
|
649
|
-
function formatSkillCommandName(name) {
|
|
650
|
-
if (name.includes(":")) {
|
|
651
|
-
return name;
|
|
652
|
-
}
|
|
653
|
-
return `${SKILL_PREFIX}${name}`;
|
|
654
|
-
}
|
|
655
|
-
function formatSkillDescription(description, fallbackName) {
|
|
656
|
-
const desc = description || `${fallbackName} skill`;
|
|
657
|
-
if (desc.startsWith(SKILL_DESCRIPTION_PREFIX)) {
|
|
658
|
-
return desc;
|
|
659
|
-
}
|
|
660
|
-
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
661
|
-
}
|
|
662
|
-
function wrapSkillTemplate(skillPath, body) {
|
|
663
|
-
const skillDir = path5.dirname(skillPath);
|
|
664
|
-
return `<skill-instruction>
|
|
665
|
-
Base directory for this skill: ${skillDir}/
|
|
666
|
-
File references (@path) in this skill are relative to this directory.
|
|
667
|
-
|
|
668
|
-
${body.trim()}
|
|
669
|
-
</skill-instruction>
|
|
670
|
-
|
|
671
|
-
<user-request>
|
|
672
|
-
$ARGUMENTS
|
|
673
|
-
</user-request>`;
|
|
674
|
-
}
|
|
675
|
-
function extractSkillBody(wrappedTemplate) {
|
|
676
|
-
const match = wrappedTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/);
|
|
677
|
-
return match ? match[1].trim() : wrappedTemplate;
|
|
678
|
-
}
|
|
679
|
-
function loadSkill(skillInfo) {
|
|
680
|
-
try {
|
|
681
|
-
const converted = convertFileWithCache(skillInfo.skillFile, "skill", {
|
|
682
|
-
source: "bundled"
|
|
683
|
-
});
|
|
684
|
-
const { body } = parseFrontmatter(converted);
|
|
685
|
-
const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
|
|
686
|
-
return {
|
|
687
|
-
name: skillInfo.name,
|
|
688
|
-
prefixedName: formatSkillCommandName(skillInfo.name),
|
|
689
|
-
description: formatSkillDescription(skillInfo.description, skillInfo.name),
|
|
690
|
-
path: skillInfo.path,
|
|
691
|
-
skillFile: skillInfo.skillFile,
|
|
692
|
-
wrappedTemplate,
|
|
693
|
-
disableModelInvocation: skillInfo.disableModelInvocation,
|
|
694
|
-
userInvocable: skillInfo.userInvocable,
|
|
695
|
-
subtask: skillInfo.subtask,
|
|
696
|
-
agent: skillInfo.agent,
|
|
697
|
-
model: skillInfo.model,
|
|
698
|
-
argumentHint: skillInfo.argumentHint
|
|
699
|
-
};
|
|
700
|
-
} catch {
|
|
701
|
-
return null;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
783
|
// src/lib/config-handler.ts
|
|
706
784
|
function toTitleCase(name) {
|
|
707
785
|
return name.split("-").map((segment) => segment.length > 0 ? segment.charAt(0).toUpperCase() + segment.slice(1) : segment).join("-");
|
|
@@ -1024,20 +1102,8 @@ function registerSkillsPaths(config, skillsDir) {
|
|
|
1024
1102
|
// src/lib/skill-tool.ts
|
|
1025
1103
|
import fs4 from "fs";
|
|
1026
1104
|
import path6 from "path";
|
|
1027
|
-
import { pathToFileURL } from "url";
|
|
1105
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1028
1106
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
1029
|
-
function formatSkillsXml(skills) {
|
|
1030
|
-
if (skills.length === 0)
|
|
1031
|
-
return "";
|
|
1032
|
-
const skillLines = skills.flatMap((skill) => [
|
|
1033
|
-
" <skill>",
|
|
1034
|
-
` <name>${formatSkillCommandName(skill.name)}</name>`,
|
|
1035
|
-
` <description>${skill.description}</description>`,
|
|
1036
|
-
` <location>${pathToFileURL(skill.path).href}</location>`,
|
|
1037
|
-
" </skill>"
|
|
1038
|
-
]);
|
|
1039
|
-
return ["<available_skills>", ...skillLines, "</available_skills>"].join(" ");
|
|
1040
|
-
}
|
|
1041
1107
|
function discoverSkillFiles(dir, limit = 10) {
|
|
1042
1108
|
const files = [];
|
|
1043
1109
|
function shouldSkipDirectory(name) {
|
|
@@ -1076,40 +1142,21 @@ function createSkillTool(options) {
|
|
|
1076
1142
|
const getAllSkills = () => {
|
|
1077
1143
|
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));
|
|
1078
1144
|
};
|
|
1079
|
-
const getDiscoverableSkills = () => {
|
|
1080
|
-
return getAllSkills().filter((s) => s.disableModelInvocation !== true);
|
|
1081
|
-
};
|
|
1082
1145
|
const buildDescription = () => {
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
const systematicXml = formatSkillsXml(skillInfos);
|
|
1094
|
-
return [
|
|
1095
|
-
"Load a specialized skill that provides domain-specific instructions and workflows.",
|
|
1096
|
-
"",
|
|
1097
|
-
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
|
|
1098
|
-
"",
|
|
1099
|
-
"The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
|
|
1100
|
-
"",
|
|
1101
|
-
'Tool output includes a `<skill_content name="...">` block with the loaded content.',
|
|
1102
|
-
"",
|
|
1103
|
-
"The following skills provide specialized sets of instructions for particular tasks.",
|
|
1104
|
-
"Invoke this tool to load a skill when a task matches one of the available skills listed below:",
|
|
1105
|
-
"",
|
|
1106
|
-
systematicXml
|
|
1107
|
-
].join(`
|
|
1108
|
-
`);
|
|
1146
|
+
const catalog = renderCatalogCompact({ bundledSkillsDir, disabledSkills });
|
|
1147
|
+
return `Load a specialized skill that provides domain-specific instructions and workflows.
|
|
1148
|
+
|
|
1149
|
+
When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.
|
|
1150
|
+
|
|
1151
|
+
The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.
|
|
1152
|
+
|
|
1153
|
+
Tool output includes a \`<skill_content name="...">\` block with the loaded content.
|
|
1154
|
+
|
|
1155
|
+
${catalog}`;
|
|
1109
1156
|
};
|
|
1110
1157
|
const buildParameterHint = () => {
|
|
1111
|
-
const
|
|
1112
|
-
const examples =
|
|
1158
|
+
const entries = buildCatalogEntries({ bundledSkillsDir, disabledSkills });
|
|
1159
|
+
const examples = entries.slice(0, 3).map((s) => `'${s.prefixedName}'`).join(", ");
|
|
1113
1160
|
const hint = examples.length > 0 ? ` (e.g., ${examples}, ...)` : "";
|
|
1114
1161
|
return `The name of the skill from available_skills${hint}`;
|
|
1115
1162
|
};
|
|
@@ -1136,12 +1183,15 @@ function createSkillTool(options) {
|
|
|
1136
1183
|
const skills = getAllSkills();
|
|
1137
1184
|
const matchedSkill = skills.find((s) => s.name === normalizedName);
|
|
1138
1185
|
if (!matchedSkill) {
|
|
1139
|
-
const availableSystematic =
|
|
1186
|
+
const availableSystematic = buildCatalogEntries({
|
|
1187
|
+
bundledSkillsDir,
|
|
1188
|
+
disabledSkills
|
|
1189
|
+
}).map((s) => s.prefixedName);
|
|
1140
1190
|
throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
|
|
1141
1191
|
}
|
|
1142
1192
|
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
1143
1193
|
const dir = path6.dirname(matchedSkill.skillFile);
|
|
1144
|
-
const base =
|
|
1194
|
+
const base = pathToFileURL2(dir).href;
|
|
1145
1195
|
const files = discoverSkillFiles(dir);
|
|
1146
1196
|
await context.ask({
|
|
1147
1197
|
permission: "skill",
|
package/dist/lib/bootstrap.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { SystematicConfig } from './config.js';
|
|
2
2
|
export declare const INTERNAL_AGENT_SIGNATURES: string[];
|
|
3
3
|
/**
|
|
4
|
-
* Inject bootstrap content into the system prompt array,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Inject bootstrap content into the system prompt array, placing exactly one
|
|
5
|
+
* canonical block at the end of `output.system[0]`.
|
|
6
|
+
*
|
|
7
|
+
* All complete `<SYSTEMATIC_WORKFLOWS>…</SYSTEMATIC_WORKFLOWS>` blocks are
|
|
8
|
+
* removed from every system entry first, then the current content is appended
|
|
9
|
+
* once to `output.system[0]`. Partial/malformed marker fragments are left
|
|
10
|
+
* untouched. This makes duplicate registration and prior last-entry placement
|
|
11
|
+
* converge to the new canonical location; later invocations win.
|
|
7
12
|
*
|
|
8
13
|
* Exported for test access — must NOT be re-exported from the plugin entry
|
|
9
14
|
* point (src/index.ts) because OpenCode's plugin loader expects a single
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape XML special characters (&, <, >) in text content.
|
|
3
|
+
* Quotes are not escaped because catalog names and descriptions are rendered
|
|
4
|
+
* as element text, not attribute values.
|
|
5
|
+
*/
|
|
6
|
+
export declare function escapeXml(text: string): string;
|
|
7
|
+
export interface CatalogEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
prefixedName: string;
|
|
10
|
+
description: string;
|
|
11
|
+
path: string;
|
|
12
|
+
skillFile: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CatalogOptions {
|
|
15
|
+
bundledSkillsDir: string;
|
|
16
|
+
disabledSkills: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Discovers and filters bundled Systematic skills into catalog entries.
|
|
20
|
+
* Excludes disabled skills and skills with disableModelInvocation === true.
|
|
21
|
+
* Returns entries sorted by name.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildCatalogEntries(options: CatalogOptions): CatalogEntry[];
|
|
24
|
+
/**
|
|
25
|
+
* Renders discoverable skills as native-style verbose XML for bootstrap content.
|
|
26
|
+
* Returns empty string when no skills are available.
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderCatalogVerbose(options: CatalogOptions): string;
|
|
29
|
+
/**
|
|
30
|
+
* Renders discoverable skills as a compact markdown list for tool descriptions.
|
|
31
|
+
* Always includes the heading; renders an explicit no-skills message when empty.
|
|
32
|
+
*/
|
|
33
|
+
export declare function renderCatalogCompact(options: CatalogOptions): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://fro.bot/systematic",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@biomejs/biome": "2.4.15",
|
|
67
|
-
"@opencode-ai/plugin": "1.14.
|
|
68
|
-
"@opencode-ai/sdk": "1.14.
|
|
67
|
+
"@opencode-ai/plugin": "1.14.47",
|
|
68
|
+
"@opencode-ai/sdk": "1.14.47",
|
|
69
69
|
"@types/bun": "latest",
|
|
70
70
|
"@types/js-yaml": "4.0.9",
|
|
71
71
|
"@types/node": "24.12.3",
|