@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 +1 -1
- package/dist/{index-95qwq9ph.js → index-yxbcy3s7.js} +36 -24
- package/dist/index.js +80 -24
- package/dist/lib/skill-loader.d.ts +6 -0
- package/dist/lib/skill-tool.d.ts +6 -0
- package/dist/lib/skills.d.ts +20 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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:
|
|
507
|
-
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
Skills provide specialized knowledge and step-by-step guidance.
|
|
282
|
-
Use this when a task matches an available skill's description
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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(
|
|
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
|
|
305
|
-
|
|
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
|
|
313
|
-
|
|
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;
|
package/dist/lib/skill-tool.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/skills.d.ts
CHANGED
|
@@ -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;
|