@fro.bot/systematic 1.6.0 → 1.7.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 CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  findCommandsInDir,
7
7
  findSkillsInDir,
8
8
  getConfigPaths
9
- } from "./index-95qwq9ph.js";
9
+ } from "./index-yxbcy3s7.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import fs from "fs";
@@ -502,9 +502,29 @@ function extractFrontmatter(filePath) {
502
502
  if (parseError) {
503
503
  return { name: "", description: "" };
504
504
  }
505
+ const metadataRaw = data.metadata;
506
+ let metadata;
507
+ if (isRecord(metadataRaw)) {
508
+ const entries = Object.entries(metadataRaw);
509
+ if (entries.every(([, v]) => typeof v === "string")) {
510
+ metadata = Object.fromEntries(entries);
511
+ }
512
+ }
513
+ const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
514
+ const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
505
515
  return {
506
- name: typeof data.name === "string" ? data.name : "",
507
- description: typeof data.description === "string" ? data.description : ""
516
+ name: extractString(data, "name"),
517
+ description: extractString(data, "description"),
518
+ license: extractNonEmptyString(data, "license"),
519
+ compatibility: extractNonEmptyString(data, "compatibility"),
520
+ metadata,
521
+ disableModelInvocation: extractBoolean(data, "disable-model-invocation"),
522
+ userInvocable: extractBoolean(data, "user-invocable"),
523
+ subtask: data.context === "fork" ? true : undefined,
524
+ agent: extractNonEmptyString(data, "agent"),
525
+ model: extractNonEmptyString(data, "model"),
526
+ argumentHint: argumentHint !== "" ? argumentHint : undefined,
527
+ allowedTools: extractNonEmptyString(data, "allowed-tools")
508
528
  };
509
529
  } catch {
510
530
  return { name: "", description: "" };
@@ -519,34 +539,26 @@ function findSkillsInDir(dir, maxDepth = 3) {
519
539
  for (const entry of entries) {
520
540
  const skillFile = path3.join(entry.path, "SKILL.md");
521
541
  if (fs4.existsSync(skillFile)) {
522
- const { name, description } = extractFrontmatter(skillFile);
542
+ const frontmatter = extractFrontmatter(skillFile);
523
543
  skills.push({
524
544
  path: entry.path,
525
545
  skillFile,
526
- name: name || entry.name,
527
- description: description || ""
546
+ name: frontmatter.name || entry.name,
547
+ description: frontmatter.description || "",
548
+ license: frontmatter.license,
549
+ compatibility: frontmatter.compatibility,
550
+ metadata: frontmatter.metadata,
551
+ disableModelInvocation: frontmatter.disableModelInvocation,
552
+ userInvocable: frontmatter.userInvocable,
553
+ subtask: frontmatter.subtask,
554
+ agent: frontmatter.agent,
555
+ model: frontmatter.model,
556
+ argumentHint: frontmatter.argumentHint,
557
+ allowedTools: frontmatter.allowedTools
528
558
  });
529
559
  }
530
560
  }
531
561
  return skills;
532
562
  }
533
- function formatSkillsXml(skills) {
534
- if (skills.length === 0)
535
- return "";
536
- const skillsXml = skills.map((skill) => {
537
- const lines = [
538
- " <skill>",
539
- ` <name>systematic:${skill.name}</name>`,
540
- ` <description>${skill.description}</description>`
541
- ];
542
- lines.push(" </skill>");
543
- return lines.join(`
544
- `);
545
- }).join(`
546
- `);
547
- return `<available_skills>
548
- ${skillsXml}
549
- </available_skills>`;
550
- }
551
563
 
552
- export { parseFrontmatter, loadConfig, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir, formatSkillsXml };
564
+ export { parseFrontmatter, loadConfig, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir };
package/dist/index.js CHANGED
@@ -6,10 +6,9 @@ import {
6
6
  findAgentsInDir,
7
7
  findCommandsInDir,
8
8
  findSkillsInDir,
9
- formatSkillsXml,
10
9
  loadConfig,
11
10
  parseFrontmatter
12
- } from "./index-95qwq9ph.js";
11
+ } from "./index-yxbcy3s7.js";
13
12
 
14
13
  // src/index.ts
15
14
  import fs2 from "fs";
@@ -115,7 +114,13 @@ function loadSkill(skillInfo) {
115
114
  description: formatSkillDescription(skillInfo.description, skillInfo.name),
116
115
  path: skillInfo.path,
117
116
  skillFile: skillInfo.skillFile,
118
- wrappedTemplate
117
+ wrappedTemplate,
118
+ disableModelInvocation: skillInfo.disableModelInvocation,
119
+ userInvocable: skillInfo.userInvocable,
120
+ subtask: skillInfo.subtask,
121
+ agent: skillInfo.agent,
122
+ model: skillInfo.model,
123
+ argumentHint: skillInfo.argumentHint
119
124
  };
120
125
  } catch {
121
126
  return null;
@@ -193,10 +198,17 @@ function loadCommandAsConfig(commandInfo) {
193
198
  }
194
199
  }
195
200
  function loadSkillAsCommand(loaded) {
196
- return {
201
+ const config = {
197
202
  template: loaded.wrappedTemplate,
198
203
  description: loaded.description
199
204
  };
205
+ if (loaded.agent !== undefined)
206
+ config.agent = loaded.agent;
207
+ if (loaded.model !== undefined)
208
+ config.model = loaded.model;
209
+ if (loaded.subtask !== undefined)
210
+ config.subtask = loaded.subtask;
211
+ return config;
200
212
  }
201
213
  function collectAgents(dir, disabledAgents) {
202
214
  const agents = {};
@@ -233,6 +245,8 @@ function collectSkillsAsCommands(dir, disabledSkills) {
233
245
  continue;
234
246
  const loaded = loadSkill(skillInfo);
235
247
  if (loaded) {
248
+ if (loaded.userInvocable === false)
249
+ continue;
236
250
  commands[loaded.prefixedName] = loadSkillAsCommand(loaded);
237
251
  }
238
252
  }
@@ -262,13 +276,27 @@ function createConfigHandler(deps) {
262
276
  // src/lib/skill-tool.ts
263
277
  import path3 from "path";
264
278
  import { tool } from "@opencode-ai/plugin/tool";
279
+ function formatSkillsXml(skills) {
280
+ if (skills.length === 0)
281
+ return "";
282
+ const skillLines = skills.flatMap((skill) => [
283
+ " <skill>",
284
+ ` <name>systematic:${skill.name}</name>`,
285
+ ` <description>${skill.description}</description>`,
286
+ " </skill>"
287
+ ]);
288
+ return ["<available_skills>", ...skillLines, "</available_skills>"].join(" ");
289
+ }
265
290
  function createSkillTool(options) {
266
291
  const { bundledSkillsDir, disabledSkills } = options;
267
292
  const getSystematicSkills = () => {
268
- 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));
293
+ return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).map((skillInfo) => loadSkill(skillInfo)).filter((s) => s !== null).filter((s) => s.disableModelInvocation !== true).sort((a, b) => a.name.localeCompare(b.name));
269
294
  };
270
295
  const buildDescription = () => {
271
296
  const skills = getSystematicSkills();
297
+ if (skills.length === 0) {
298
+ return "Load a skill to get detailed instructions for a specific task. No skills are currently available.";
299
+ }
272
300
  const skillInfos = skills.map((s) => ({
273
301
  name: s.name,
274
302
  description: s.description,
@@ -276,15 +304,22 @@ function createSkillTool(options) {
276
304
  skillFile: s.skillFile
277
305
  }));
278
306
  const systematicXml = formatSkillsXml(skillInfos);
279
- const baseDescription = `Load a skill to get detailed instructions for a specific task.
280
-
281
- Skills provide specialized knowledge and step-by-step guidance.
282
- Use this when a task matches an available skill's description.`;
283
- return `${baseDescription}
284
-
285
- ${systematicXml}`;
307
+ return [
308
+ "Load a skill to get detailed instructions for a specific task.",
309
+ "Skills provide specialized knowledge and step-by-step guidance.",
310
+ "Use this when a task matches an available skill's description.",
311
+ "Only the skills listed here are available:",
312
+ systematicXml
313
+ ].join(" ");
314
+ };
315
+ const buildParameterHint = () => {
316
+ const skills = getSystematicSkills();
317
+ const examples = skills.slice(0, 3).map((s) => `'systematic:${s.name}'`).join(", ");
318
+ const hint = examples.length > 0 ? ` (e.g., ${examples}, ...)` : "";
319
+ return `The skill identifier from available_skills${hint}`;
286
320
  };
287
321
  let cachedDescription = null;
322
+ let cachedParameterHint = null;
288
323
  return tool({
289
324
  get description() {
290
325
  if (cachedDescription == null) {
@@ -293,24 +328,45 @@ ${systematicXml}`;
293
328
  return cachedDescription;
294
329
  },
295
330
  args: {
296
- name: tool.schema.string().describe("The skill identifier from available_skills (e.g., 'systematic:brainstorming')")
331
+ name: tool.schema.string().describe((() => {
332
+ if (cachedParameterHint == null) {
333
+ cachedParameterHint = buildParameterHint();
334
+ }
335
+ return cachedParameterHint;
336
+ })())
297
337
  },
298
- async execute(args) {
338
+ async execute(args, context) {
299
339
  const requestedName = args.name;
300
340
  const normalizedName = requestedName.startsWith("systematic:") ? requestedName.slice("systematic:".length) : requestedName;
301
341
  const skills = getSystematicSkills();
302
342
  const matchedSkill = skills.find((s) => s.name === normalizedName);
303
- if (matchedSkill) {
304
- const body = extractSkillBody(matchedSkill.wrappedTemplate);
305
- const dir = path3.dirname(matchedSkill.skillFile);
306
- return `## Skill: ${matchedSkill.prefixedName}
307
-
308
- **Base directory**: ${dir}
309
-
310
- ${body}`;
343
+ if (!matchedSkill) {
344
+ const availableSystematic = skills.map((s) => s.prefixedName);
345
+ throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
311
346
  }
312
- const availableSystematic = skills.map((s) => s.prefixedName);
313
- throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
347
+ const body = extractSkillBody(matchedSkill.wrappedTemplate);
348
+ const dir = path3.dirname(matchedSkill.skillFile);
349
+ await context.ask({
350
+ permission: "skill",
351
+ patterns: [matchedSkill.prefixedName],
352
+ always: [matchedSkill.prefixedName],
353
+ metadata: {}
354
+ });
355
+ context.metadata({
356
+ title: `Loaded skill: ${matchedSkill.prefixedName}`,
357
+ metadata: {
358
+ name: matchedSkill.prefixedName,
359
+ dir
360
+ }
361
+ });
362
+ return [
363
+ `## Skill: ${matchedSkill.prefixedName}`,
364
+ "",
365
+ `**Base directory**: ${dir}`,
366
+ "",
367
+ body.trim()
368
+ ].join(`
369
+ `);
314
370
  }
315
371
  });
316
372
  }
@@ -6,6 +6,12 @@ export interface LoadedSkill {
6
6
  path: string;
7
7
  skillFile: string;
8
8
  wrappedTemplate: string;
9
+ disableModelInvocation?: boolean;
10
+ userInvocable?: boolean;
11
+ subtask?: boolean;
12
+ agent?: string;
13
+ model?: string;
14
+ argumentHint?: string;
9
15
  }
10
16
  export declare function formatSkillCommandName(name: string): string;
11
17
  export declare function formatSkillDescription(description: string, fallbackName: string): string;
@@ -1,6 +1,12 @@
1
1
  import type { ToolDefinition } from '@opencode-ai/plugin';
2
+ import { type SkillInfo } from './skills.js';
2
3
  export interface SkillToolOptions {
3
4
  bundledSkillsDir: string;
4
5
  disabledSkills: string[];
5
6
  }
7
+ /**
8
+ * Formats skills as XML for tool description.
9
+ * Uses indented format matching OpenCode's native skill tool.
10
+ */
11
+ export declare function formatSkillsXml(skills: SkillInfo[]): string;
6
12
  export declare function createSkillTool(options: SkillToolOptions): ToolDefinition;
@@ -1,13 +1,32 @@
1
1
  export interface SkillFrontmatter {
2
2
  name: string;
3
3
  description: string;
4
+ license?: string;
5
+ compatibility?: string;
6
+ metadata?: Record<string, string>;
7
+ disableModelInvocation?: boolean;
8
+ userInvocable?: boolean;
9
+ subtask?: boolean;
10
+ agent?: string;
11
+ model?: string;
12
+ argumentHint?: string;
13
+ allowedTools?: string;
4
14
  }
5
15
  export interface SkillInfo {
6
16
  path: string;
7
17
  skillFile: string;
8
18
  name: string;
9
19
  description: string;
20
+ license?: string;
21
+ compatibility?: string;
22
+ metadata?: Record<string, string>;
23
+ disableModelInvocation?: boolean;
24
+ userInvocable?: boolean;
25
+ subtask?: boolean;
26
+ agent?: string;
27
+ model?: string;
28
+ argumentHint?: string;
29
+ allowedTools?: string;
10
30
  }
11
31
  export declare function extractFrontmatter(filePath: string): SkillFrontmatter;
12
32
  export declare function findSkillsInDir(dir: string, maxDepth?: number): SkillInfo[];
13
- export declare function formatSkillsXml(skills: SkillInfo[]): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Structured engineering workflows for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",