@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/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  loadConfig,
14
14
  loadConfigWithSources,
15
15
  parseFrontmatter
16
- } from "./index-3xnyeknn.js";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
- const entry = output.system[i];
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 > 0) {
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("~/") ? path.join(os.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
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 = path.join(bundledSkillsDir, "using-systematic/SKILL.md");
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 path3 from "path";
248
+ import path4 from "path";
111
249
 
112
250
  // src/lib/source-model-defaults.ts
113
- import * as path2 from "path";
251
+ import * as path3 from "path";
114
252
  import { fileURLToPath } from "url";
115
253
  var __filename2 = fileURLToPath(import.meta.url);
116
- var __dirname2 = path2.dirname(__filename2);
117
- var packageRoot = path2.resolve(__dirname2, "..", "..");
118
- var bundledAgentsDir = path2.join(packageRoot, "agents");
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(path3.join(agentsDir, category))) {
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: path3.join(agentsDir, category, fileName),
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 path4 from "path";
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 && path4.isAbsolute(xdgCacheHome) ? xdgCacheHome : path4.join(os2.homedir(), ".cache");
509
- return path4.join(cacheBase, "opencode");
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 = path4.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
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 = path4.join(cacheDir, MODELS_JSON_FILENAME);
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 skills = getDiscoverableSkills();
1084
- if (skills.length === 0) {
1085
- return "Load a skill to get detailed instructions for a specific task. No skills are currently available.";
1086
- }
1087
- const skillInfos = skills.map((s) => ({
1088
- name: s.name,
1089
- description: s.description,
1090
- path: s.path,
1091
- skillFile: s.skillFile
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 skills = getDiscoverableSkills();
1112
- const examples = skills.slice(0, 3).map((s) => `'${s.prefixedName}'`).join(", ");
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 = getDiscoverableSkills().map((s) => s.prefixedName);
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 = pathToFileURL(dir).href;
1194
+ const base = pathToFileURL2(dir).href;
1145
1195
  const files = discoverSkillFiles(dir);
1146
1196
  await context.ask({
1147
1197
  permission: "skill",
@@ -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, replacing any
5
- * existing `<SYSTEMATIC_WORKFLOWS>` block. Multi-load idempotency is via
6
- * the marker — most-recently-registered plugin wins under FIFO hook order.
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.13.3",
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.46",
68
- "@opencode-ai/sdk": "1.14.46",
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",