@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.
@@ -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.
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 = path4.join(bundledSkillsDir, "using-systematic/references", `${name}-profile.md`);
3323
+ const profilePath = path2.join(bundledSkillsDir, "using-systematic/references", `${name}-profile.md`);
3507
3324
  try {
3508
- return fs5.readFileSync(profilePath, "utf8");
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("~/") ? path4.join(os.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
3561
- if (fs5.existsSync(customPath)) {
3562
- return fs5.readFileSync(customPath, "utf8");
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 = path4.join(bundledSkillsDir, "using-systematic/SKILL.md");
3566
- if (!fs5.existsSync(usingSystematicPath))
3382
+ const usingSystematicPath = path2.join(bundledSkillsDir, "using-systematic/SKILL.md");
3383
+ if (!fs3.existsSync(usingSystematicPath))
3567
3384
  return null;
3568
- const fullContent = fs5.readFileSync(usingSystematicPath, "utf8");
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}${catalogSection}
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 as pathToFileURL2 } from "node:url";
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 = pathToFileURL2(dir).href;
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.9.0",
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.17",
97
- "@opencode-ai/sdk": "1.18.17",
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 {};