@fro.bot/systematic 3.9.0 → 3.10.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/HARNESSES.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/{index-hjbt4p5s.js → index-y8nc60tz.js} +297 -299
- package/dist/index.js +205 -610
- package/dist/lib/agent-overlays.d.ts +0 -1
- package/dist/lib/config-handler.d.ts +0 -3
- package/dist/lib/skill-catalog.d.ts +0 -13
- package/dist/lib/skill-tool.d.ts +0 -6
- package/dist/pi.js +173 -202
- package/package.json +3 -3
- package/dist/lib/model-availability.d.ts +0 -83
- package/dist/lib/source-model-defaults.d.ts +0 -117
|
@@ -41,4 +41,3 @@ export interface ResolvedAgentOverlaySet {
|
|
|
41
41
|
export declare function buildBundledAgentInventory(agentsDir: string, disabledAgents: string[]): BundledAgentInventory;
|
|
42
42
|
export declare function validateAgentOverlays({ inventory, overlays, nativeAgents, enabledSkills, }: ValidateAgentOverlaysOptions): ValidatedAgentOverlays;
|
|
43
43
|
export declare function resolveAgentOverlaySet(overlays: ValidatedAgentOverlays): ResolvedAgentOverlaySet;
|
|
44
|
-
export declare function assertSourceCategoryModelCoverage(categories: string[]): void;
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import type { Config } from '@opencode-ai/plugin';
|
|
2
|
-
import { type OpencodeClientLike } from './model-availability.js';
|
|
3
2
|
export interface ConfigHandlerDeps {
|
|
4
3
|
directory: string;
|
|
5
4
|
bundledSkillsDir: string;
|
|
6
5
|
bundledAgentsDir: string;
|
|
7
6
|
bundledCommandsDir: string;
|
|
8
|
-
/** OpenCode client for availability lookup. When omitted, availability falls back to empty set (last-resort resolution). */
|
|
9
|
-
client?: OpencodeClientLike;
|
|
10
7
|
/** Home directory for discovered-skill lookups. Defaults to `os.homedir()`; inject a temp dir in tests. */
|
|
11
8
|
homeDir?: string;
|
|
12
9
|
/** OpenCode global config directory override for discovered-skill lookups. Defaults to `<homeDir>/.config/opencode`. */
|
|
@@ -1,15 +1,7 @@
|
|
|
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
1
|
export interface CatalogEntry {
|
|
8
2
|
name: string;
|
|
9
3
|
prefixedName: string;
|
|
10
4
|
description: string;
|
|
11
|
-
path: string;
|
|
12
|
-
skillFile: string;
|
|
13
5
|
}
|
|
14
6
|
export interface CatalogOptions {
|
|
15
7
|
bundledSkillsDir: string;
|
|
@@ -21,11 +13,6 @@ export interface CatalogOptions {
|
|
|
21
13
|
* Returns entries sorted by name.
|
|
22
14
|
*/
|
|
23
15
|
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
16
|
/**
|
|
30
17
|
* Renders discoverable skills as a compact markdown list for tool descriptions.
|
|
31
18
|
* Always includes the heading; renders an explicit no-skills message when empty.
|
package/dist/lib/skill-tool.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import type { ToolDefinition } from '@opencode-ai/plugin';
|
|
2
|
-
import type { SkillInfo } from './skills.js';
|
|
3
2
|
export { discoverSkillFiles } from './skill-resolver.js';
|
|
4
3
|
export interface SkillToolOptions {
|
|
5
4
|
bundledSkillsDir: string;
|
|
6
5
|
disabledSkills: string[];
|
|
7
6
|
}
|
|
8
|
-
/**
|
|
9
|
-
* Formats skills as XML for tool description.
|
|
10
|
-
* Uses indented format matching OpenCode's native skill tool.
|
|
11
|
-
*/
|
|
12
|
-
export declare function formatSkillsXml(skills: SkillInfo[]): string;
|
|
13
7
|
export declare function createSkillTool(options: SkillToolOptions): ToolDefinition;
|
package/dist/pi.js
CHANGED
|
@@ -3316,196 +3316,13 @@ function resolveToolAllowlist(toolsSource) {
|
|
|
3316
3316
|
}
|
|
3317
3317
|
|
|
3318
3318
|
// src/lib/bootstrap.ts
|
|
3319
|
-
import fs5 from "node:fs";
|
|
3320
|
-
import os from "node:os";
|
|
3321
|
-
import path4 from "node:path";
|
|
3322
|
-
|
|
3323
|
-
// src/lib/skill-catalog.ts
|
|
3324
|
-
import { pathToFileURL } from "node:url";
|
|
3325
|
-
|
|
3326
|
-
// src/lib/skill-loader.ts
|
|
3327
3319
|
import fs3 from "node:fs";
|
|
3320
|
+
import os from "node:os";
|
|
3328
3321
|
import path2 from "node:path";
|
|
3329
|
-
var SKILL_PREFIX = "systematic:";
|
|
3330
|
-
var SKILL_DESCRIPTION_PREFIX = "(Systematic) ";
|
|
3331
|
-
function formatSkillCommandName(name) {
|
|
3332
|
-
if (name.includes(":")) {
|
|
3333
|
-
return name;
|
|
3334
|
-
}
|
|
3335
|
-
return `${SKILL_PREFIX}${name}`;
|
|
3336
|
-
}
|
|
3337
|
-
function formatSkillDescription(description, fallbackName) {
|
|
3338
|
-
const desc = description || `${fallbackName} skill`;
|
|
3339
|
-
if (desc.startsWith(SKILL_DESCRIPTION_PREFIX)) {
|
|
3340
|
-
return desc;
|
|
3341
|
-
}
|
|
3342
|
-
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
3343
|
-
}
|
|
3344
|
-
function wrapSkillTemplate(skillPath, body) {
|
|
3345
|
-
const skillDir = path2.dirname(skillPath);
|
|
3346
|
-
return `<skill-instruction>
|
|
3347
|
-
Base directory for this skill: ${skillDir}/
|
|
3348
|
-
File references (@path) in this skill are relative to this directory.
|
|
3349
|
-
|
|
3350
|
-
${body.trim()}
|
|
3351
|
-
</skill-instruction>
|
|
3352
|
-
|
|
3353
|
-
<user-request>
|
|
3354
|
-
$ARGUMENTS
|
|
3355
|
-
</user-request>`;
|
|
3356
|
-
}
|
|
3357
|
-
function extractSkillBody(wrappedTemplate) {
|
|
3358
|
-
const match = wrappedTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/);
|
|
3359
|
-
return match ? match[1].trim() : wrappedTemplate;
|
|
3360
|
-
}
|
|
3361
|
-
function loadSkill(skillInfo) {
|
|
3362
|
-
try {
|
|
3363
|
-
const content = fs3.readFileSync(skillInfo.skillFile, "utf8");
|
|
3364
|
-
const { body } = parseFrontmatter(content);
|
|
3365
|
-
const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
|
|
3366
|
-
return {
|
|
3367
|
-
name: skillInfo.name,
|
|
3368
|
-
prefixedName: formatSkillCommandName(skillInfo.name),
|
|
3369
|
-
description: formatSkillDescription(skillInfo.description, skillInfo.name),
|
|
3370
|
-
path: skillInfo.path,
|
|
3371
|
-
skillFile: skillInfo.skillFile,
|
|
3372
|
-
wrappedTemplate,
|
|
3373
|
-
disableModelInvocation: skillInfo.disableModelInvocation,
|
|
3374
|
-
userInvocable: skillInfo.userInvocable,
|
|
3375
|
-
subtask: skillInfo.subtask,
|
|
3376
|
-
agent: skillInfo.agent,
|
|
3377
|
-
model: skillInfo.model,
|
|
3378
|
-
argumentHint: skillInfo.argumentHint
|
|
3379
|
-
};
|
|
3380
|
-
} catch {
|
|
3381
|
-
return null;
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
|
|
3385
|
-
// src/lib/skills.ts
|
|
3386
|
-
import fs4 from "node:fs";
|
|
3387
|
-
import path3 from "node:path";
|
|
3388
|
-
function parseMetadata(data) {
|
|
3389
|
-
const metadataRaw = data.metadata;
|
|
3390
|
-
if (!isRecord(metadataRaw)) {
|
|
3391
|
-
return;
|
|
3392
|
-
}
|
|
3393
|
-
const entries = Object.entries(metadataRaw);
|
|
3394
|
-
if (!entries.every(([, v]) => typeof v === "string")) {
|
|
3395
|
-
return;
|
|
3396
|
-
}
|
|
3397
|
-
return Object.fromEntries(entries);
|
|
3398
|
-
}
|
|
3399
|
-
function extractFrontmatterFromContent(content) {
|
|
3400
|
-
const { data, parseError } = parseFrontmatter(content);
|
|
3401
|
-
if (parseError) {
|
|
3402
|
-
return { name: "", description: "" };
|
|
3403
|
-
}
|
|
3404
|
-
const metadata = parseMetadata(data);
|
|
3405
|
-
const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
|
|
3406
|
-
const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
|
|
3407
|
-
return {
|
|
3408
|
-
name: extractString(data, "name"),
|
|
3409
|
-
description: extractString(data, "description"),
|
|
3410
|
-
license: extractNonEmptyString(data, "license"),
|
|
3411
|
-
compatibility: extractNonEmptyString(data, "compatibility"),
|
|
3412
|
-
metadata,
|
|
3413
|
-
disableModelInvocation: extractBoolean(data, "disable-model-invocation"),
|
|
3414
|
-
userInvocable: extractBoolean(data, "user-invocable"),
|
|
3415
|
-
subtask: data.context === "fork" ? true : extractBoolean(data, "subtask") ?? undefined,
|
|
3416
|
-
agent: extractNonEmptyString(data, "agent"),
|
|
3417
|
-
model: extractNonEmptyString(data, "model"),
|
|
3418
|
-
argumentHint: argumentHint !== "" ? argumentHint : undefined,
|
|
3419
|
-
allowedTools: extractNonEmptyString(data, "allowed-tools")
|
|
3420
|
-
};
|
|
3421
|
-
}
|
|
3422
|
-
function extractFrontmatter(filePath) {
|
|
3423
|
-
try {
|
|
3424
|
-
const content = fs4.readFileSync(filePath, "utf8");
|
|
3425
|
-
return extractFrontmatterFromContent(content);
|
|
3426
|
-
} catch {
|
|
3427
|
-
return { name: "", description: "" };
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
function findSkillsInDir(dir, maxDepth = 3) {
|
|
3431
|
-
const skills = [];
|
|
3432
|
-
const entries = walkDir(dir, {
|
|
3433
|
-
maxDepth,
|
|
3434
|
-
filter: (e) => e.isDirectory
|
|
3435
|
-
});
|
|
3436
|
-
for (const entry of entries) {
|
|
3437
|
-
const skillFile = path3.join(entry.path, "SKILL.md");
|
|
3438
|
-
if (fs4.existsSync(skillFile)) {
|
|
3439
|
-
const frontmatter = extractFrontmatter(skillFile);
|
|
3440
|
-
skills.push({
|
|
3441
|
-
path: entry.path,
|
|
3442
|
-
skillFile,
|
|
3443
|
-
name: frontmatter.name || entry.name,
|
|
3444
|
-
description: frontmatter.description || "",
|
|
3445
|
-
license: frontmatter.license,
|
|
3446
|
-
compatibility: frontmatter.compatibility,
|
|
3447
|
-
metadata: frontmatter.metadata,
|
|
3448
|
-
disableModelInvocation: frontmatter.disableModelInvocation,
|
|
3449
|
-
userInvocable: frontmatter.userInvocable,
|
|
3450
|
-
subtask: frontmatter.subtask,
|
|
3451
|
-
agent: frontmatter.agent,
|
|
3452
|
-
model: frontmatter.model,
|
|
3453
|
-
argumentHint: frontmatter.argumentHint,
|
|
3454
|
-
allowedTools: frontmatter.allowedTools
|
|
3455
|
-
});
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
return skills;
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3461
|
-
// src/lib/skill-catalog.ts
|
|
3462
|
-
function escapeXml(text) {
|
|
3463
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3464
|
-
}
|
|
3465
|
-
function buildCatalogEntries(options) {
|
|
3466
|
-
const { bundledSkillsDir, disabledSkills } = options;
|
|
3467
|
-
return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).filter((s) => s.disableModelInvocation !== true).sort((a, b) => a.name.localeCompare(b.name)).map((s) => ({
|
|
3468
|
-
name: s.name,
|
|
3469
|
-
prefixedName: formatSkillCommandName(s.name),
|
|
3470
|
-
description: s.description,
|
|
3471
|
-
path: s.path,
|
|
3472
|
-
skillFile: s.skillFile
|
|
3473
|
-
}));
|
|
3474
|
-
}
|
|
3475
|
-
function renderCatalogVerbose(options) {
|
|
3476
|
-
const entries = buildCatalogEntries(options);
|
|
3477
|
-
if (entries.length === 0)
|
|
3478
|
-
return "";
|
|
3479
|
-
const skillLines = entries.flatMap((entry) => [
|
|
3480
|
-
" <skill>",
|
|
3481
|
-
` <name>${escapeXml(entry.prefixedName)}</name>`,
|
|
3482
|
-
` <description>${escapeXml(entry.description)}</description>`,
|
|
3483
|
-
` <location>${pathToFileURL(entry.path).href}</location>`,
|
|
3484
|
-
" </skill>"
|
|
3485
|
-
]);
|
|
3486
|
-
return ["<available_skills>", ...skillLines, "</available_skills>"].join(`
|
|
3487
|
-
`);
|
|
3488
|
-
}
|
|
3489
|
-
function renderCatalogCompact(options) {
|
|
3490
|
-
const entries = buildCatalogEntries(options);
|
|
3491
|
-
const heading = "## Available Systematic Skills";
|
|
3492
|
-
if (entries.length === 0) {
|
|
3493
|
-
return `${heading}
|
|
3494
|
-
|
|
3495
|
-
No Systematic skills are currently available.`;
|
|
3496
|
-
}
|
|
3497
|
-
const bullets = entries.map((entry) => `- ${entry.prefixedName}: ${entry.description}`).join(`
|
|
3498
|
-
`);
|
|
3499
|
-
return `${heading}
|
|
3500
|
-
|
|
3501
|
-
${bullets}`;
|
|
3502
|
-
}
|
|
3503
|
-
|
|
3504
|
-
// src/lib/bootstrap.ts
|
|
3505
3322
|
function readHarnessProfile(bundledSkillsDir, name) {
|
|
3506
|
-
const profilePath =
|
|
3323
|
+
const profilePath = path2.join(bundledSkillsDir, "using-systematic/references", `${name}-profile.md`);
|
|
3507
3324
|
try {
|
|
3508
|
-
return
|
|
3325
|
+
return fs3.readFileSync(profilePath, "utf8");
|
|
3509
3326
|
} catch (error) {
|
|
3510
3327
|
console.error(`Failed to read harness profile ${profilePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3511
3328
|
return null;
|
|
@@ -3557,25 +3374,18 @@ function getBootstrapContent(config, deps) {
|
|
|
3557
3374
|
if (!config.bootstrap.enabled)
|
|
3558
3375
|
return null;
|
|
3559
3376
|
if (config.bootstrap.file) {
|
|
3560
|
-
const customPath = config.bootstrap.file.startsWith("~/") ?
|
|
3561
|
-
if (
|
|
3562
|
-
return
|
|
3377
|
+
const customPath = config.bootstrap.file.startsWith("~/") ? path2.join(os.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
|
|
3378
|
+
if (fs3.existsSync(customPath)) {
|
|
3379
|
+
return fs3.readFileSync(customPath, "utf8");
|
|
3563
3380
|
}
|
|
3564
3381
|
}
|
|
3565
|
-
const usingSystematicPath =
|
|
3566
|
-
if (!
|
|
3382
|
+
const usingSystematicPath = path2.join(bundledSkillsDir, "using-systematic/SKILL.md");
|
|
3383
|
+
if (!fs3.existsSync(usingSystematicPath))
|
|
3567
3384
|
return null;
|
|
3568
|
-
const fullContent =
|
|
3385
|
+
const fullContent = fs3.readFileSync(usingSystematicPath, "utf8");
|
|
3569
3386
|
const { body } = parseFrontmatter(fullContent);
|
|
3570
3387
|
const content = body.trim();
|
|
3571
3388
|
const skillUsage = usageTemplate ?? getSkillUsageTemplate();
|
|
3572
|
-
const catalog = renderCatalogVerbose({
|
|
3573
|
-
bundledSkillsDir,
|
|
3574
|
-
disabledSkills: config.disabled_skills
|
|
3575
|
-
});
|
|
3576
|
-
const catalogSection = catalog.length > 0 ? `
|
|
3577
|
-
|
|
3578
|
-
${catalog}` : "";
|
|
3579
3389
|
const profileSection = profileBlock === undefined ? "" : `
|
|
3580
3390
|
|
|
3581
3391
|
${profileBlock}`;
|
|
@@ -3586,7 +3396,7 @@ You have access to structured engineering workflows via the Systematic plugin.
|
|
|
3586
3396
|
|
|
3587
3397
|
${content}
|
|
3588
3398
|
|
|
3589
|
-
${skillUsage}${profileSection}
|
|
3399
|
+
${skillUsage}${profileSection}
|
|
3590
3400
|
</SYSTEMATIC_WORKFLOWS>`;
|
|
3591
3401
|
}
|
|
3592
3402
|
|
|
@@ -3920,7 +3730,168 @@ var createRealPiDelegateSession = createDelegateSessionWith(REAL_PI_DELEGATE_SES
|
|
|
3920
3730
|
// src/lib/skill-resolver.ts
|
|
3921
3731
|
import fs6 from "node:fs";
|
|
3922
3732
|
import path5 from "node:path";
|
|
3923
|
-
import { pathToFileURL
|
|
3733
|
+
import { pathToFileURL } from "node:url";
|
|
3734
|
+
|
|
3735
|
+
// src/lib/skill-loader.ts
|
|
3736
|
+
import fs4 from "node:fs";
|
|
3737
|
+
import path3 from "node:path";
|
|
3738
|
+
var SKILL_PREFIX = "systematic:";
|
|
3739
|
+
var SKILL_DESCRIPTION_PREFIX = "(Systematic) ";
|
|
3740
|
+
function formatSkillCommandName(name) {
|
|
3741
|
+
if (name.includes(":")) {
|
|
3742
|
+
return name;
|
|
3743
|
+
}
|
|
3744
|
+
return `${SKILL_PREFIX}${name}`;
|
|
3745
|
+
}
|
|
3746
|
+
function formatSkillDescription(description, fallbackName) {
|
|
3747
|
+
const desc = description || `${fallbackName} skill`;
|
|
3748
|
+
if (desc.startsWith(SKILL_DESCRIPTION_PREFIX)) {
|
|
3749
|
+
return desc;
|
|
3750
|
+
}
|
|
3751
|
+
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
3752
|
+
}
|
|
3753
|
+
function wrapSkillTemplate(skillPath, body) {
|
|
3754
|
+
const skillDir = path3.dirname(skillPath);
|
|
3755
|
+
return `<skill-instruction>
|
|
3756
|
+
Base directory for this skill: ${skillDir}/
|
|
3757
|
+
File references (@path) in this skill are relative to this directory.
|
|
3758
|
+
|
|
3759
|
+
${body.trim()}
|
|
3760
|
+
</skill-instruction>
|
|
3761
|
+
|
|
3762
|
+
<user-request>
|
|
3763
|
+
$ARGUMENTS
|
|
3764
|
+
</user-request>`;
|
|
3765
|
+
}
|
|
3766
|
+
function extractSkillBody(wrappedTemplate) {
|
|
3767
|
+
const match = wrappedTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/);
|
|
3768
|
+
return match ? match[1].trim() : wrappedTemplate;
|
|
3769
|
+
}
|
|
3770
|
+
function loadSkill(skillInfo) {
|
|
3771
|
+
try {
|
|
3772
|
+
const content = fs4.readFileSync(skillInfo.skillFile, "utf8");
|
|
3773
|
+
const { body } = parseFrontmatter(content);
|
|
3774
|
+
const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
|
|
3775
|
+
return {
|
|
3776
|
+
name: skillInfo.name,
|
|
3777
|
+
prefixedName: formatSkillCommandName(skillInfo.name),
|
|
3778
|
+
description: formatSkillDescription(skillInfo.description, skillInfo.name),
|
|
3779
|
+
path: skillInfo.path,
|
|
3780
|
+
skillFile: skillInfo.skillFile,
|
|
3781
|
+
wrappedTemplate,
|
|
3782
|
+
disableModelInvocation: skillInfo.disableModelInvocation,
|
|
3783
|
+
userInvocable: skillInfo.userInvocable,
|
|
3784
|
+
subtask: skillInfo.subtask,
|
|
3785
|
+
agent: skillInfo.agent,
|
|
3786
|
+
model: skillInfo.model,
|
|
3787
|
+
argumentHint: skillInfo.argumentHint
|
|
3788
|
+
};
|
|
3789
|
+
} catch {
|
|
3790
|
+
return null;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
// src/lib/skills.ts
|
|
3795
|
+
import fs5 from "node:fs";
|
|
3796
|
+
import path4 from "node:path";
|
|
3797
|
+
function parseMetadata(data) {
|
|
3798
|
+
const metadataRaw = data.metadata;
|
|
3799
|
+
if (!isRecord(metadataRaw)) {
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
const entries = Object.entries(metadataRaw);
|
|
3803
|
+
if (!entries.every(([, v]) => typeof v === "string")) {
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3806
|
+
return Object.fromEntries(entries);
|
|
3807
|
+
}
|
|
3808
|
+
function extractFrontmatterFromContent(content) {
|
|
3809
|
+
const { data, parseError } = parseFrontmatter(content);
|
|
3810
|
+
if (parseError) {
|
|
3811
|
+
return { name: "", description: "" };
|
|
3812
|
+
}
|
|
3813
|
+
const metadata = parseMetadata(data);
|
|
3814
|
+
const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
|
|
3815
|
+
const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
|
|
3816
|
+
return {
|
|
3817
|
+
name: extractString(data, "name"),
|
|
3818
|
+
description: extractString(data, "description"),
|
|
3819
|
+
license: extractNonEmptyString(data, "license"),
|
|
3820
|
+
compatibility: extractNonEmptyString(data, "compatibility"),
|
|
3821
|
+
metadata,
|
|
3822
|
+
disableModelInvocation: extractBoolean(data, "disable-model-invocation"),
|
|
3823
|
+
userInvocable: extractBoolean(data, "user-invocable"),
|
|
3824
|
+
subtask: data.context === "fork" ? true : extractBoolean(data, "subtask") ?? undefined,
|
|
3825
|
+
agent: extractNonEmptyString(data, "agent"),
|
|
3826
|
+
model: extractNonEmptyString(data, "model"),
|
|
3827
|
+
argumentHint: argumentHint !== "" ? argumentHint : undefined,
|
|
3828
|
+
allowedTools: extractNonEmptyString(data, "allowed-tools")
|
|
3829
|
+
};
|
|
3830
|
+
}
|
|
3831
|
+
function extractFrontmatter(filePath) {
|
|
3832
|
+
try {
|
|
3833
|
+
const content = fs5.readFileSync(filePath, "utf8");
|
|
3834
|
+
return extractFrontmatterFromContent(content);
|
|
3835
|
+
} catch {
|
|
3836
|
+
return { name: "", description: "" };
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
function findSkillsInDir(dir, maxDepth = 3) {
|
|
3840
|
+
const skills = [];
|
|
3841
|
+
const entries = walkDir(dir, {
|
|
3842
|
+
maxDepth,
|
|
3843
|
+
filter: (e) => e.isDirectory
|
|
3844
|
+
});
|
|
3845
|
+
for (const entry of entries) {
|
|
3846
|
+
const skillFile = path4.join(entry.path, "SKILL.md");
|
|
3847
|
+
if (fs5.existsSync(skillFile)) {
|
|
3848
|
+
const frontmatter = extractFrontmatter(skillFile);
|
|
3849
|
+
skills.push({
|
|
3850
|
+
path: entry.path,
|
|
3851
|
+
skillFile,
|
|
3852
|
+
name: frontmatter.name || entry.name,
|
|
3853
|
+
description: frontmatter.description || "",
|
|
3854
|
+
license: frontmatter.license,
|
|
3855
|
+
compatibility: frontmatter.compatibility,
|
|
3856
|
+
metadata: frontmatter.metadata,
|
|
3857
|
+
disableModelInvocation: frontmatter.disableModelInvocation,
|
|
3858
|
+
userInvocable: frontmatter.userInvocable,
|
|
3859
|
+
subtask: frontmatter.subtask,
|
|
3860
|
+
agent: frontmatter.agent,
|
|
3861
|
+
model: frontmatter.model,
|
|
3862
|
+
argumentHint: frontmatter.argumentHint,
|
|
3863
|
+
allowedTools: frontmatter.allowedTools
|
|
3864
|
+
});
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
return skills;
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
// src/lib/skill-catalog.ts
|
|
3871
|
+
function buildCatalogEntries(options) {
|
|
3872
|
+
const { bundledSkillsDir, disabledSkills } = options;
|
|
3873
|
+
return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).filter((s) => s.disableModelInvocation !== true).sort((a, b) => a.name.localeCompare(b.name)).map((s) => ({
|
|
3874
|
+
name: s.name,
|
|
3875
|
+
prefixedName: formatSkillCommandName(s.name),
|
|
3876
|
+
description: s.description
|
|
3877
|
+
}));
|
|
3878
|
+
}
|
|
3879
|
+
function renderCatalogCompact(options) {
|
|
3880
|
+
const entries = buildCatalogEntries(options);
|
|
3881
|
+
const heading = "## Available Systematic Skills";
|
|
3882
|
+
if (entries.length === 0) {
|
|
3883
|
+
return `${heading}
|
|
3884
|
+
|
|
3885
|
+
No Systematic skills are currently available.`;
|
|
3886
|
+
}
|
|
3887
|
+
const bullets = entries.map((entry) => `- ${entry.prefixedName}: ${entry.description}`).join(`
|
|
3888
|
+
`);
|
|
3889
|
+
return `${heading}
|
|
3890
|
+
|
|
3891
|
+
${bullets}`;
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
// src/lib/skill-resolver.ts
|
|
3924
3895
|
function getAllSkills(options) {
|
|
3925
3896
|
const { bundledSkillsDir, disabledSkills } = options;
|
|
3926
3897
|
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));
|
|
@@ -3956,7 +3927,7 @@ function buildSkillToolParameterHint(options) {
|
|
|
3956
3927
|
function buildSkillContentOutput(matchedSkill) {
|
|
3957
3928
|
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
3958
3929
|
const dir = path5.dirname(matchedSkill.skillFile);
|
|
3959
|
-
const base =
|
|
3930
|
+
const base = pathToFileURL(dir).href;
|
|
3960
3931
|
const files = discoverSkillFiles(dir);
|
|
3961
3932
|
const lines = [
|
|
3962
3933
|
`<skill_content name="${matchedSkill.prefixedName}">`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://fro.bot/systematic",
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@biomejs/biome": "2.5.8",
|
|
95
95
|
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
96
|
-
"@opencode-ai/plugin": "1.18.
|
|
97
|
-
"@opencode-ai/sdk": "1.18.
|
|
96
|
+
"@opencode-ai/plugin": "1.18.18",
|
|
97
|
+
"@opencode-ai/sdk": "1.18.18",
|
|
98
98
|
"@semantic-release/exec": "7.1.0",
|
|
99
99
|
"@tintinweb/pi-subagents": "0.14.3",
|
|
100
100
|
"@types/bun": "latest",
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
interface ConnectedProvider {
|
|
2
|
-
id: string;
|
|
3
|
-
models: Record<string, unknown>;
|
|
4
|
-
}
|
|
5
|
-
interface ProvidersResponse {
|
|
6
|
-
providers: ConnectedProvider[];
|
|
7
|
-
default: Record<string, string>;
|
|
8
|
-
}
|
|
9
|
-
interface ClientConfigApi {
|
|
10
|
-
providers: () => Promise<{
|
|
11
|
-
data: ProvidersResponse;
|
|
12
|
-
error: undefined;
|
|
13
|
-
} | {
|
|
14
|
-
data: undefined;
|
|
15
|
-
error: unknown;
|
|
16
|
-
}>;
|
|
17
|
-
}
|
|
18
|
-
export interface OpencodeClientLike {
|
|
19
|
-
config: ClientConfigApi;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Outcome of model availability discovery.
|
|
23
|
-
*
|
|
24
|
-
* - `api`: The OpenCode server's `/config/providers` endpoint responded with
|
|
25
|
-
* a connected-providers payload AND `models` is non-empty. An authoritatively
|
|
26
|
-
* empty response (`data.providers = []`, or providers present with zero
|
|
27
|
-
* models) collapses to `'unknown'` instead — see below — because the
|
|
28
|
-
* operational consequence is identical and downstream consumers should treat
|
|
29
|
-
* both cases the same way.
|
|
30
|
-
* - `cache`: The API call failed (error envelope, thrown, or timed out) and
|
|
31
|
-
* the local `models.json` cache was readable AND non-empty. `models`
|
|
32
|
-
* reflects whatever OpenCode last wrote to disk. A cache that loads
|
|
33
|
-
* successfully but produces a zero-size set collapses to `'unknown'` for
|
|
34
|
-
* the same operational reason an empty API response does.
|
|
35
|
-
* - `unknown`: Either both the API call and the cache fallback failed (cache
|
|
36
|
-
* missing, unreadable, corrupt, or schema-mismatched), OR the API call
|
|
37
|
-
* succeeded with zero usable models. Resolution should degrade gracefully —
|
|
38
|
-
* callers should treat `unknown` as a signal to skip source-default model
|
|
39
|
-
* pinning so users do not get agents pinned to inaccessible models.
|
|
40
|
-
* `models` is the empty set.
|
|
41
|
-
*/
|
|
42
|
-
export type DiscoveryStatus = 'api' | 'cache' | 'unknown';
|
|
43
|
-
export interface ModelAvailability {
|
|
44
|
-
status: DiscoveryStatus;
|
|
45
|
-
/**
|
|
46
|
-
* Set of `${providerId}/${modelId}` strings. Typed `ReadonlySet` because
|
|
47
|
-
* callers must not mutate the returned collection — mutation would corrupt
|
|
48
|
-
* future calls in the same process. Each `ModelAvailability` is a fresh
|
|
49
|
-
* instance (see `emptyAvailability()`), so mutation via cast cannot
|
|
50
|
-
* propagate, but the type makes intent explicit.
|
|
51
|
-
*/
|
|
52
|
-
models: ReadonlySet<string>;
|
|
53
|
-
}
|
|
54
|
-
interface AvailabilityOptions {
|
|
55
|
-
/**
|
|
56
|
-
* Maximum time to wait for `client.config.providers()` before falling
|
|
57
|
-
* back to the local cache. Defaults to 1500ms — a startup-budget value
|
|
58
|
-
* that prevents a slow/half-open OpenCode server from holding the plugin
|
|
59
|
-
* indefinitely.
|
|
60
|
-
*
|
|
61
|
-
* Set to `null` to disable the timeout entirely (not recommended).
|
|
62
|
-
*/
|
|
63
|
-
apiTimeoutMs?: number | null;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Discover the set of `provider/model` keys the OpenCode server considers
|
|
67
|
-
* connected (or, on API failure, the set last written to the on-disk
|
|
68
|
-
* `models.json` cache).
|
|
69
|
-
*
|
|
70
|
-
* The returned `status` lets callers distinguish three discovery outcomes:
|
|
71
|
-
* - `api`: live answer; safe to pin source-default models against it
|
|
72
|
-
* - `cache`: degraded but informed; the cached `provider/model` keys are
|
|
73
|
-
* plausibly still authoritative
|
|
74
|
-
* - `unknown`: both the API and the cache failed; callers should fall back
|
|
75
|
-
* to OpenCode's parent-model inheritance rather than pinning a source
|
|
76
|
-
* default the user may not have access to
|
|
77
|
-
*
|
|
78
|
-
* The API call is bounded by `apiTimeoutMs` (default 1500ms). On timeout,
|
|
79
|
-
* thrown error, error-envelope response, or undefined data, the cache
|
|
80
|
-
* fallback runs. The function never rejects.
|
|
81
|
-
*/
|
|
82
|
-
export declare function getAvailableModels(client: OpencodeClientLike, options?: AvailabilityOptions): Promise<ModelAvailability>;
|
|
83
|
-
export {};
|