@fro.bot/systematic 2.12.2 → 2.13.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/README.md +6 -6
- package/dist/cli.js +1 -1
- package/dist/{index-b4ht76qd.js → index-4qhjf4tb.js} +7 -14
- package/dist/index.js +407 -100
- package/dist/lib/agent-overlays.d.ts +0 -23
- package/dist/lib/config-handler.d.ts +3 -2
- package/dist/lib/config-schema.d.ts +0 -1
- package/dist/lib/model-availability.d.ts +77 -0
- package/dist/lib/source-model-defaults.d.ts +117 -0
- package/dist/schemas/systematic-config.schema.json +5 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -338,7 +338,7 @@ Systematic separates config-source precedence from overlay precedence. Config fi
|
|
|
338
338
|
|
|
339
339
|
Source category model defaults are primary model choices only — they are not fallback chains. Systematic does not support `fallback_models`, inherited retry semantics, runtime fallback behavior, or fallback to the parent model when a source model is unavailable. Explicit and source model IDs are structurally validated and may still fail at OpenCode runtime if the provider or model is unavailable.
|
|
340
340
|
|
|
341
|
-
Source category model defaults are now ordered preference arrays per category rather than single strings. At plugin load, Systematic reads OpenCode's authentication state from `auth.json` and selects the first array entry whose provider is authenticated. For example, the `review` category defaults to `['anthropic/claude-
|
|
341
|
+
Source category model defaults are now ordered preference arrays per category rather than single strings. At plugin load, Systematic reads OpenCode's authentication state from `auth.json` and selects the first array entry whose provider is authenticated. For example, the `review` category defaults to `['anthropic/claude-sonnet-4-6', 'openai/gpt-5.3-codex']` — a user authenticated only to OpenAI receives `openai/gpt-5.5` (first match), while a user authenticated to Anthropic (or both) receives the more preferred `anthropic/claude-sonnet-4-6`. If no array entry's provider is authenticated, the first entry is used as the default. The arrays are an ordered preference list, not a runtime fallback chain — `fallback_models` is still not supported.
|
|
342
342
|
|
|
343
343
|
If you want to restore OpenCode parent-model inheritance for a bundled agent or category (opting out of the source default), set `"model": null` in high-trust user or `$OPENCODE_CONFIG_DIR/systematic.json` config. Project config cannot use `model: null` — project config cannot set, erase, or shadow `model` at any value.
|
|
344
344
|
|
|
@@ -346,11 +346,11 @@ The source defaults are:
|
|
|
346
346
|
|
|
347
347
|
| Category | Default `model` | Rationale |
|
|
348
348
|
|----------|-----------------|-----------|
|
|
349
|
-
| `design` | `
|
|
350
|
-
| `docs` | `
|
|
351
|
-
| `document-review` | `anthropic/claude-opus-4
|
|
352
|
-
| `research` | `openai/gpt-5.
|
|
353
|
-
| `review` | `anthropic/claude-
|
|
349
|
+
| `design` | `github-copilot/gemini-3.1-pro-preview` | High-judgment UX/product/design work benefits from a strong general reasoning model. |
|
|
350
|
+
| `docs` | `github-copilot/gemini-3.1-pro-preview` | Documentation and summarization should start cheaper/faster. |
|
|
351
|
+
| `document-review` | `anthropic/claude-opus-4-7` | Requirements and plan critique benefit from strongest nuanced reasoning. |
|
|
352
|
+
| `research` | `openai/gpt-5.4-mini` | Tool-heavy synthesis and source evaluation benefit from a strong general reasoning model. |
|
|
353
|
+
| `review` | `anthropic/claude-sonnet-4-6` | Code/security/adversarial review benefits from strongest reasoning. |
|
|
354
354
|
| `workflow` | `openai/gpt-5.4-mini` | Orchestration and bounded implementation should default cheaper/faster. |
|
|
355
355
|
|
|
356
356
|
These defaults are owned by Systematic code and emitted for bundled agents in each category when no stronger high-trust exact or category `model` override exists. Uncategorized bundled agents receive no source default and continue inheriting the parent OpenCode model. Native OpenCode agents with the same emitted key are full replacements and receive no Systematic source model default.
|
package/dist/cli.js
CHANGED
|
@@ -12324,7 +12324,7 @@ function finalize(ctx, schema) {
|
|
|
12324
12324
|
result.$schema = "http://json-schema.org/draft-07/schema#";
|
|
12325
12325
|
} else if (ctx.target === "draft-04") {
|
|
12326
12326
|
result.$schema = "http://json-schema.org/draft-04/schema#";
|
|
12327
|
-
} else if (ctx.target === "openapi-3.0") {}
|
|
12327
|
+
} else if (ctx.target === "openapi-3.0") {}
|
|
12328
12328
|
if (ctx.external?.uri) {
|
|
12329
12329
|
const id = ctx.external.registry.get(schema)?.id;
|
|
12330
12330
|
if (!id)
|
|
@@ -12568,7 +12568,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
12568
12568
|
if (val === undefined) {
|
|
12569
12569
|
if (ctx.unrepresentable === "throw") {
|
|
12570
12570
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
12571
|
-
}
|
|
12571
|
+
}
|
|
12572
12572
|
} else if (typeof val === "bigint") {
|
|
12573
12573
|
if (ctx.unrepresentable === "throw") {
|
|
12574
12574
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -15163,7 +15163,7 @@ var modelSchema = exports_external.string().min(1).regex(MODEL_FORMAT_REGEX, MOD
|
|
|
15163
15163
|
description: "Model identifier in provider/model format, or null to inherit parent model",
|
|
15164
15164
|
examples: ["anthropic/claude-sonnet-4", null]
|
|
15165
15165
|
});
|
|
15166
|
-
var variantSchema = exports_external.string().min(1).regex(/^\S+$/, "must be a non-empty string without whitespace").meta({
|
|
15166
|
+
var variantSchema = exports_external.string().min(1).max(128, "variant must be at most 128 characters").regex(/^\S+$/, "must be a non-empty string without whitespace").meta({
|
|
15167
15167
|
description: "Model variant identifier",
|
|
15168
15168
|
examples: ["v2", "extended"]
|
|
15169
15169
|
});
|
|
@@ -15224,7 +15224,7 @@ var AgentOverlaySchema = exports_external.object({
|
|
|
15224
15224
|
description: "Per-agent configuration overlay",
|
|
15225
15225
|
examples: [
|
|
15226
15226
|
{
|
|
15227
|
-
model: "anthropic/claude-opus-4
|
|
15227
|
+
model: "anthropic/claude-opus-4-7",
|
|
15228
15228
|
temperature: 0.1,
|
|
15229
15229
|
mode: "subagent"
|
|
15230
15230
|
}
|
|
@@ -15243,7 +15243,7 @@ var CategoryOverlaySchema = exports_external.object({
|
|
|
15243
15243
|
permission: trustProtected(permissionSchema).optional()
|
|
15244
15244
|
}).strict().meta({
|
|
15245
15245
|
description: "Per-category configuration overlay (same fields as agent minus disable)",
|
|
15246
|
-
examples: [{ model: "anthropic/claude-opus-4
|
|
15246
|
+
examples: [{ model: "anthropic/claude-opus-4-7", temperature: 0.1 }]
|
|
15247
15247
|
});
|
|
15248
15248
|
var BootstrapSchema = exports_external.object({
|
|
15249
15249
|
enabled: exports_external.boolean().default(true).meta({
|
|
@@ -15271,7 +15271,7 @@ var SystematicConfigSchema = exports_external.object({
|
|
|
15271
15271
|
}),
|
|
15272
15272
|
categories: exports_external.record(exports_external.string(), CategoryOverlaySchema).default({}).meta({
|
|
15273
15273
|
description: "Per-category configuration overlays keyed by category name",
|
|
15274
|
-
examples: [{ review: { model: "anthropic/claude-opus-4
|
|
15274
|
+
examples: [{ review: { model: "anthropic/claude-opus-4-7" } }, {}]
|
|
15275
15275
|
}),
|
|
15276
15276
|
disabled_skills: exports_external.array(exports_external.string()).default([]).meta({
|
|
15277
15277
|
description: "Array of skill names to disable globally",
|
|
@@ -15296,13 +15296,6 @@ var SystematicConfigSchema = exports_external.object({
|
|
|
15296
15296
|
description: "Systematic user configuration file (systematic.json / systematic.jsonc)",
|
|
15297
15297
|
examples: [{ disabled_skills: ["ce:plan"], bootstrap: { enabled: false } }]
|
|
15298
15298
|
});
|
|
15299
|
-
var SourceCategoryModelDefaultsSchema = exports_external.record(exports_external.string(), exports_external.array(exports_external.string().min(1).regex(MODEL_FORMAT_REGEX, MODEL_FORMAT_MESSAGE)).min(1)).meta({
|
|
15300
|
-
description: "Validates source category model defaults shape",
|
|
15301
|
-
examples: [{ design: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"] }]
|
|
15302
|
-
});
|
|
15303
|
-
function assertSourceCategoryModelDefaults(defaults) {
|
|
15304
|
-
SourceCategoryModelDefaultsSchema.parse(defaults);
|
|
15305
|
-
}
|
|
15306
15299
|
var SECURITY_OVERLAY_FIELDS = [
|
|
15307
15300
|
"model",
|
|
15308
15301
|
"variant",
|
|
@@ -16101,4 +16094,4 @@ function findSkillsInDir(dir, maxDepth = 3) {
|
|
|
16101
16094
|
return skills;
|
|
16102
16095
|
}
|
|
16103
16096
|
|
|
16104
|
-
export { parseFrontmatter, AgentOverlaySchema, CategoryOverlaySchema,
|
|
16097
|
+
export { parseFrontmatter, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, isRecord2 as isRecord, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir };
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
AgentOverlaySchema,
|
|
4
4
|
CategoryOverlaySchema,
|
|
5
|
-
assertSourceCategoryModelDefaults,
|
|
6
5
|
convertFileWithCache,
|
|
6
|
+
exports_external,
|
|
7
7
|
extractAgentFrontmatter,
|
|
8
8
|
extractCommandFrontmatter,
|
|
9
9
|
findAgentsInDir,
|
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
loadConfig,
|
|
14
14
|
loadConfigWithSources,
|
|
15
15
|
parseFrontmatter
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-4qhjf4tb.js";
|
|
17
17
|
|
|
18
18
|
// src/index.ts
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import { fileURLToPath } from "url";
|
|
19
|
+
import fs5 from "fs";
|
|
20
|
+
import path7 from "path";
|
|
21
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
22
22
|
|
|
23
23
|
// src/lib/bootstrap.ts
|
|
24
24
|
import fs from "fs";
|
|
@@ -107,30 +107,232 @@ ${toolMapping}
|
|
|
107
107
|
|
|
108
108
|
// src/lib/agent-overlays.ts
|
|
109
109
|
import fs2 from "fs";
|
|
110
|
-
import
|
|
111
|
-
|
|
110
|
+
import path3 from "path";
|
|
111
|
+
|
|
112
|
+
// src/lib/source-model-defaults.ts
|
|
113
|
+
import * as path2 from "path";
|
|
114
|
+
import { fileURLToPath } from "url";
|
|
115
|
+
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");
|
|
119
|
+
var ProviderID = exports_external.union([
|
|
120
|
+
exports_external.literal("vercel"),
|
|
121
|
+
exports_external.literal("opencode"),
|
|
122
|
+
exports_external.literal("github-copilot"),
|
|
123
|
+
exports_external.literal("opencode-go"),
|
|
124
|
+
exports_external.literal("openai"),
|
|
125
|
+
exports_external.literal("anthropic"),
|
|
126
|
+
exports_external.literal("google")
|
|
127
|
+
]);
|
|
128
|
+
var variantSchema = exports_external.string().min(1, "variant must be a non-empty string").max(128, "variant must be at most 128 characters").regex(/^\S+$/, "variant must not contain whitespace");
|
|
129
|
+
var tableCellSchema = exports_external.string().min(1, "must be a non-empty string").regex(/^[^|\r\n]+$/, "must not contain pipe (|) or newline characters \u2014 these break Markdown table generation");
|
|
130
|
+
var ModelEntrySchema = exports_external.object({
|
|
131
|
+
model: exports_external.string().min(1, "model must be a non-empty string"),
|
|
132
|
+
variant: variantSchema.optional()
|
|
133
|
+
}).strict();
|
|
134
|
+
var ProviderEntrySchema = exports_external.object({
|
|
135
|
+
provider: ProviderID,
|
|
136
|
+
models: exports_external.array(ModelEntrySchema).min(1, "models must be non-empty \u2014 every provider entry must list at least one model")
|
|
137
|
+
}).strict().refine((entry) => {
|
|
138
|
+
const seen = new Set;
|
|
139
|
+
for (const m of entry.models) {
|
|
140
|
+
const key = `${m.model}::${m.variant ?? ""}`;
|
|
141
|
+
if (seen.has(key))
|
|
142
|
+
return false;
|
|
143
|
+
seen.add(key);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}, {
|
|
147
|
+
message: "duplicate (model, variant) pair within a provider entry \u2014 each model+variant combination must be unique"
|
|
148
|
+
});
|
|
149
|
+
var CategoryDefaultSchema = exports_external.object({
|
|
150
|
+
rationale: tableCellSchema,
|
|
151
|
+
whenToOverride: tableCellSchema.optional(),
|
|
152
|
+
providers: exports_external.array(ProviderEntrySchema).min(1, "providers must be non-empty \u2014 every category must list at least one provider")
|
|
153
|
+
}).strict().refine((cat) => {
|
|
154
|
+
const seen = new Set;
|
|
155
|
+
for (const p of cat.providers) {
|
|
156
|
+
if (seen.has(p.provider))
|
|
157
|
+
return false;
|
|
158
|
+
seen.add(p.provider);
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}, {
|
|
162
|
+
message: "duplicate provider ID within a category \u2014 each provider must appear at most once per category"
|
|
163
|
+
});
|
|
164
|
+
var SourceCategoryDefaultsSchema = exports_external.record(exports_external.string(), CategoryDefaultSchema);
|
|
112
165
|
var SOURCE_CATEGORY_MODEL_DEFAULTS = {
|
|
113
|
-
design:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
design: {
|
|
167
|
+
rationale: "High-judgment UX, product, and design work benefits from a strong general reasoning model with broad creative capability.",
|
|
168
|
+
whenToOverride: "Override to a faster/cheaper model when design tasks are primarily templating or low-stakes layout work.",
|
|
169
|
+
providers: [
|
|
170
|
+
{
|
|
171
|
+
provider: "github-copilot",
|
|
172
|
+
models: [{ model: "gemini-3.1-pro-preview" }]
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
provider: "openai",
|
|
176
|
+
models: [{ model: "gpt-5.5", variant: "high" }]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
provider: "anthropic",
|
|
180
|
+
models: [{ model: "claude-opus-4-7", variant: "max" }]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
provider: "vercel",
|
|
184
|
+
models: [{ model: "v0-1.5-md" }]
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
docs: {
|
|
189
|
+
rationale: "Documentation and summarization tasks should start cheaper and faster; quality is sufficient at mid-tier models.",
|
|
190
|
+
providers: [
|
|
191
|
+
{
|
|
192
|
+
provider: "github-copilot",
|
|
193
|
+
models: [{ model: "gemini-3.1-pro-preview" }]
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
provider: "openai",
|
|
197
|
+
models: [{ model: "gpt-5.4-mini" }]
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
provider: "anthropic",
|
|
201
|
+
models: [{ model: "claude-haiku-4-5" }]
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
provider: "opencode",
|
|
205
|
+
models: [{ model: "claude-haiku-4-5" }]
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
"document-review": {
|
|
210
|
+
rationale: "Requirements and plan critique benefit from the strongest nuanced reasoning to surface non-obvious gaps and contradictions.",
|
|
211
|
+
providers: [
|
|
212
|
+
{
|
|
213
|
+
provider: "anthropic",
|
|
214
|
+
models: [{ model: "claude-opus-4-7", variant: "max" }]
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
provider: "openai",
|
|
218
|
+
models: [{ model: "gpt-5.5", variant: "high" }]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
provider: "github-copilot",
|
|
222
|
+
models: [{ model: "gemini-3.1-pro-preview" }]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
provider: "opencode",
|
|
226
|
+
models: [{ model: "claude-sonnet-4-6" }]
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
research: {
|
|
231
|
+
rationale: "Tool-heavy synthesis and source evaluation benefit from a strong general reasoning model with broad knowledge.",
|
|
232
|
+
providers: [
|
|
233
|
+
{
|
|
234
|
+
provider: "openai",
|
|
235
|
+
models: [{ model: "gpt-5.4-mini" }]
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
provider: "anthropic",
|
|
239
|
+
models: [{ model: "claude-sonnet-4-6" }]
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
provider: "github-copilot",
|
|
243
|
+
models: [{ model: "gemini-3.1-pro-preview" }]
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
provider: "opencode",
|
|
247
|
+
models: [{ model: "claude-sonnet-4-6" }]
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
review: {
|
|
252
|
+
rationale: "Code, security, and adversarial review benefits from the strongest reasoning to catch subtle bugs and security issues.",
|
|
253
|
+
whenToOverride: "Override to a faster model when review tasks are primarily style or formatting checks rather than correctness or security analysis.",
|
|
254
|
+
providers: [
|
|
255
|
+
{
|
|
256
|
+
provider: "anthropic",
|
|
257
|
+
models: [{ model: "claude-sonnet-4-6" }]
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
provider: "openai",
|
|
261
|
+
models: [{ model: "gpt-5.3-codex" }]
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
provider: "github-copilot",
|
|
265
|
+
models: [{ model: "gemini-3.1-pro-preview" }]
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
provider: "opencode",
|
|
269
|
+
models: [{ model: "claude-sonnet-4-6" }]
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
},
|
|
273
|
+
workflow: {
|
|
274
|
+
rationale: "Orchestration and bounded implementation tasks should default cheaper and faster; strong reasoning is rarely needed for routing decisions.",
|
|
275
|
+
providers: [
|
|
276
|
+
{
|
|
277
|
+
provider: "openai",
|
|
278
|
+
models: [{ model: "gpt-5.4-mini" }]
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
provider: "anthropic",
|
|
282
|
+
models: [{ model: "claude-sonnet-4-6" }]
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
provider: "opencode",
|
|
286
|
+
models: [{ model: "claude-haiku-4-5" }]
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
provider: "opencode-go",
|
|
290
|
+
models: [{ model: "claude-haiku-4-5" }]
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
}
|
|
119
294
|
};
|
|
295
|
+
function resolveSourceModel(category, availabilitySet) {
|
|
296
|
+
const categoryDefault = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
|
|
297
|
+
if (!categoryDefault) {
|
|
298
|
+
throw new Error(`resolveSourceModel: unknown category "${category}". Valid categories: ${Object.keys(SOURCE_CATEGORY_MODEL_DEFAULTS).join(", ")}`);
|
|
299
|
+
}
|
|
300
|
+
for (const providerEntry of categoryDefault.providers) {
|
|
301
|
+
for (const modelEntry of providerEntry.models) {
|
|
302
|
+
const key = `${providerEntry.provider}/${modelEntry.model}`;
|
|
303
|
+
if (availabilitySet.has(key)) {
|
|
304
|
+
return {
|
|
305
|
+
provider: providerEntry.provider,
|
|
306
|
+
model: modelEntry.model,
|
|
307
|
+
variant: modelEntry.variant
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const firstProvider = categoryDefault.providers[0];
|
|
313
|
+
const firstModel = firstProvider.models[0];
|
|
314
|
+
return {
|
|
315
|
+
provider: firstProvider.provider,
|
|
316
|
+
model: firstModel.model,
|
|
317
|
+
variant: firstModel.variant
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/lib/agent-overlays.ts
|
|
120
322
|
function buildBundledAgentInventory(agentsDir, disabledAgents) {
|
|
121
323
|
const categories = readCategoryDirs(agentsDir);
|
|
122
324
|
const agentsByQualifiedId = {};
|
|
123
325
|
const stemCategories = new Map;
|
|
124
326
|
const disabledSet = new Set(disabledAgents);
|
|
125
327
|
for (const category of categories) {
|
|
126
|
-
for (const fileName of readMarkdownFiles(
|
|
328
|
+
for (const fileName of readMarkdownFiles(path3.join(agentsDir, category))) {
|
|
127
329
|
const key = fileName.replace(/\.md$/, "");
|
|
128
330
|
const id = `${category}/${key}`;
|
|
129
331
|
agentsByQualifiedId[id] = {
|
|
130
332
|
id,
|
|
131
333
|
key,
|
|
132
334
|
category,
|
|
133
|
-
file:
|
|
335
|
+
file: path3.join(agentsDir, category, fileName),
|
|
134
336
|
disabled: disabledSet.has(key) || disabledSet.has(id)
|
|
135
337
|
};
|
|
136
338
|
const existing = stemCategories.get(key) ?? [];
|
|
@@ -183,62 +385,12 @@ function inferBuiltInTemperature(name, description) {
|
|
|
183
385
|
}
|
|
184
386
|
return 0.3;
|
|
185
387
|
}
|
|
186
|
-
function getAuthenticatedProviders(rootDirOverride) {
|
|
187
|
-
const xdgDataHome = process.env.XDG_DATA_HOME?.trim();
|
|
188
|
-
const rootDir = rootDirOverride || (xdgDataHome && path2.isAbsolute(xdgDataHome) ? xdgDataHome : path2.join(os2.homedir(), ".local/share"));
|
|
189
|
-
const authPath = path2.join(rootDir, "opencode", "auth.json");
|
|
190
|
-
let raw;
|
|
191
|
-
try {
|
|
192
|
-
raw = fs2.readFileSync(authPath, "utf8");
|
|
193
|
-
} catch (err) {
|
|
194
|
-
if (isSystemError(err) && err.code === "ENOENT") {
|
|
195
|
-
return new Set;
|
|
196
|
-
}
|
|
197
|
-
console.warn(`[systematic] auth.json unreadable at ${authPath}; ignoring`);
|
|
198
|
-
return new Set;
|
|
199
|
-
}
|
|
200
|
-
let parsed;
|
|
201
|
-
try {
|
|
202
|
-
parsed = JSON.parse(raw);
|
|
203
|
-
} catch {
|
|
204
|
-
console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
|
|
205
|
-
return new Set;
|
|
206
|
-
}
|
|
207
|
-
if (!isRecord(parsed)) {
|
|
208
|
-
console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
|
|
209
|
-
return new Set;
|
|
210
|
-
}
|
|
211
|
-
return new Set(Object.keys(parsed));
|
|
212
|
-
}
|
|
213
|
-
function getSourceCategoryModel(category, authedProviders) {
|
|
214
|
-
if (!category)
|
|
215
|
-
return;
|
|
216
|
-
const candidates = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
|
|
217
|
-
if (!candidates || candidates.length === 0)
|
|
218
|
-
return;
|
|
219
|
-
if (!authedProviders || authedProviders.size === 0)
|
|
220
|
-
return candidates[0];
|
|
221
|
-
for (const entry of candidates) {
|
|
222
|
-
const slashIndex = entry.indexOf("/");
|
|
223
|
-
if (slashIndex <= 0)
|
|
224
|
-
continue;
|
|
225
|
-
const providerId = entry.slice(0, slashIndex);
|
|
226
|
-
if (authedProviders.has(providerId)) {
|
|
227
|
-
return entry;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return candidates[0];
|
|
231
|
-
}
|
|
232
388
|
function assertSourceCategoryModelCoverage(categories) {
|
|
233
|
-
validateSourceCategoryModelDefaults();
|
|
234
389
|
const missingCategories = categories.filter((category) => !Object.hasOwn(SOURCE_CATEGORY_MODEL_DEFAULTS, category));
|
|
235
390
|
if (missingCategories.length > 0) {
|
|
236
391
|
throw new Error(`Source category model defaults missing intentional coverage for: ${missingCategories.join(", ")}`);
|
|
237
392
|
}
|
|
238
393
|
}
|
|
239
|
-
function validateSourceCategoryModelDefaults(defaults = SOURCE_CATEGORY_MODEL_DEFAULTS) {
|
|
240
|
-
assertSourceCategoryModelDefaults(defaults);
|
|
241
|
-
}
|
|
242
394
|
function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
|
|
243
395
|
const result = [];
|
|
244
396
|
const seenTargets = new Map;
|
|
@@ -339,12 +491,158 @@ function validAgentKeys(inventory) {
|
|
|
339
491
|
function throwConfigError(sourcePath, keyPath, message) {
|
|
340
492
|
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
|
|
341
493
|
}
|
|
342
|
-
|
|
343
|
-
|
|
494
|
+
|
|
495
|
+
// src/lib/model-availability.ts
|
|
496
|
+
import { createHash } from "crypto";
|
|
497
|
+
import fs3 from "fs";
|
|
498
|
+
import os2 from "os";
|
|
499
|
+
import path4 from "path";
|
|
500
|
+
function emptyAvailability() {
|
|
501
|
+
return { status: "unknown", models: new Set };
|
|
502
|
+
}
|
|
503
|
+
var MAX_CACHE_FILE_BYTES = 16 * 1024 * 1024;
|
|
504
|
+
var DEFAULT_API_TIMEOUT_MS = 1500;
|
|
505
|
+
var MODELS_JSON_FILENAME = "models.json";
|
|
506
|
+
function resolveCacheDir() {
|
|
507
|
+
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");
|
|
510
|
+
}
|
|
511
|
+
function fastHash(input) {
|
|
512
|
+
return createHash("sha1").update(input).digest("hex");
|
|
513
|
+
}
|
|
514
|
+
function isProviderRecord(value) {
|
|
515
|
+
if (!isRecord(value))
|
|
516
|
+
return false;
|
|
517
|
+
for (const entry of Object.values(value)) {
|
|
518
|
+
if (!isRecord(entry))
|
|
519
|
+
return false;
|
|
520
|
+
if (!isRecord(entry.models))
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
function readModelsFromCache(filePath) {
|
|
526
|
+
let fd;
|
|
527
|
+
try {
|
|
528
|
+
fd = fs3.openSync(filePath, "r");
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
let raw;
|
|
533
|
+
try {
|
|
534
|
+
let stat;
|
|
535
|
+
try {
|
|
536
|
+
stat = fs3.fstatSync(fd);
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
if (!stat.isFile())
|
|
541
|
+
return null;
|
|
542
|
+
if (stat.size === 0)
|
|
543
|
+
return null;
|
|
544
|
+
if (stat.size > MAX_CACHE_FILE_BYTES) {
|
|
545
|
+
console.warn(`[systematic] models.json at ${filePath} is ${stat.size} bytes (>${MAX_CACHE_FILE_BYTES}); treating as cache miss.`);
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
const buffer = Buffer.alloc(stat.size);
|
|
549
|
+
let bytesRead;
|
|
550
|
+
try {
|
|
551
|
+
bytesRead = fs3.readSync(fd, buffer, 0, stat.size, 0);
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
if (bytesRead !== stat.size)
|
|
556
|
+
return null;
|
|
557
|
+
raw = buffer.toString("utf8");
|
|
558
|
+
} finally {
|
|
559
|
+
try {
|
|
560
|
+
fs3.closeSync(fd);
|
|
561
|
+
} catch {}
|
|
562
|
+
}
|
|
563
|
+
if (raw.trim().length === 0)
|
|
564
|
+
return null;
|
|
565
|
+
let parsed;
|
|
566
|
+
try {
|
|
567
|
+
parsed = JSON.parse(raw);
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
if (!isProviderRecord(parsed)) {
|
|
572
|
+
console.warn(`[systematic] models.json schema mismatch at ${filePath}; treating as cache miss. Upstream shape may have drifted.`);
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const result = new Set;
|
|
576
|
+
for (const [providerId, providerData] of Object.entries(parsed)) {
|
|
577
|
+
for (const modelId of Object.keys(providerData.models)) {
|
|
578
|
+
result.add(`${providerId}/${modelId}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
function readFallbackCache() {
|
|
584
|
+
const cacheDir = resolveCacheDir();
|
|
585
|
+
const openCodeModelsUrl = process.env.OPENCODE_MODELS_URL?.trim();
|
|
586
|
+
if (openCodeModelsUrl) {
|
|
587
|
+
const urlDerivedPath = path4.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
|
|
588
|
+
const urlResult = readModelsFromCache(urlDerivedPath);
|
|
589
|
+
if (urlResult !== null) {
|
|
590
|
+
return { status: "cache", models: urlResult };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const defaultPath = path4.join(cacheDir, MODELS_JSON_FILENAME);
|
|
594
|
+
const defaultResult = readModelsFromCache(defaultPath);
|
|
595
|
+
if (defaultResult !== null) {
|
|
596
|
+
return { status: "cache", models: defaultResult };
|
|
597
|
+
}
|
|
598
|
+
return emptyAvailability();
|
|
599
|
+
}
|
|
600
|
+
function buildSetFromProviders(providers) {
|
|
601
|
+
const result = new Set;
|
|
602
|
+
for (const provider of providers) {
|
|
603
|
+
for (const modelId of Object.keys(provider.models)) {
|
|
604
|
+
result.add(`${provider.id}/${modelId}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
async function getAvailableModels(client, options = {}) {
|
|
610
|
+
const timeoutMs = options.apiTimeoutMs === undefined ? DEFAULT_API_TIMEOUT_MS : options.apiTimeoutMs;
|
|
611
|
+
if (typeof client.config?.providers !== "function") {
|
|
612
|
+
return readFallbackCache();
|
|
613
|
+
}
|
|
614
|
+
const apiCall = client.config.providers();
|
|
615
|
+
let response;
|
|
616
|
+
try {
|
|
617
|
+
if (timeoutMs === null) {
|
|
618
|
+
response = await apiCall;
|
|
619
|
+
} else {
|
|
620
|
+
const TIMEOUT_SENTINEL = Symbol("timeout");
|
|
621
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
622
|
+
const timer = setTimeout(() => resolve2(TIMEOUT_SENTINEL), timeoutMs);
|
|
623
|
+
timer.unref?.();
|
|
624
|
+
});
|
|
625
|
+
const raced = await Promise.race([apiCall, timeoutPromise]);
|
|
626
|
+
if (raced === TIMEOUT_SENTINEL) {
|
|
627
|
+
console.warn(`[systematic] client.config.providers() exceeded ${timeoutMs}ms; falling back to models.json cache.`);
|
|
628
|
+
return readFallbackCache();
|
|
629
|
+
}
|
|
630
|
+
response = raced;
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
return readFallbackCache();
|
|
634
|
+
}
|
|
635
|
+
if (response.error !== undefined || response.data === undefined) {
|
|
636
|
+
return readFallbackCache();
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
status: "api",
|
|
640
|
+
models: buildSetFromProviders(response.data.providers)
|
|
641
|
+
};
|
|
344
642
|
}
|
|
345
643
|
|
|
346
644
|
// src/lib/skill-loader.ts
|
|
347
|
-
import
|
|
645
|
+
import path5 from "path";
|
|
348
646
|
var SKILL_PREFIX = "systematic:";
|
|
349
647
|
var SKILL_DESCRIPTION_PREFIX = "(Systematic - Skill) ";
|
|
350
648
|
function formatSkillCommandName(name) {
|
|
@@ -361,7 +659,7 @@ function formatSkillDescription(description, fallbackName) {
|
|
|
361
659
|
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
362
660
|
}
|
|
363
661
|
function wrapSkillTemplate(skillPath, body) {
|
|
364
|
-
const skillDir =
|
|
662
|
+
const skillDir = path5.dirname(skillPath);
|
|
365
663
|
return `<skill-instruction>
|
|
366
664
|
Base directory for this skill: ${skillDir}/
|
|
367
665
|
File references (@path) in this skill are relative to this directory.
|
|
@@ -511,7 +809,7 @@ function loadSkillAsCommand(loaded) {
|
|
|
511
809
|
config.subtask = loaded.subtask;
|
|
512
810
|
return config;
|
|
513
811
|
}
|
|
514
|
-
function collectAgents(dir, disabledAgents, nativeAgents, overlays,
|
|
812
|
+
function collectAgents(dir, disabledAgents, nativeAgents, overlays, availabilitySet) {
|
|
515
813
|
const agents = {};
|
|
516
814
|
const agentList = findAgentsInDir(dir);
|
|
517
815
|
const disabledSet = new Set(disabledAgents);
|
|
@@ -526,12 +824,12 @@ function collectAgents(dir, disabledAgents, nativeAgents, overlays, authedProvid
|
|
|
526
824
|
continue;
|
|
527
825
|
const config = loadAgentAsConfig(agentInfo);
|
|
528
826
|
if (config) {
|
|
529
|
-
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays,
|
|
827
|
+
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, availabilitySet);
|
|
530
828
|
}
|
|
531
829
|
}
|
|
532
830
|
return agents;
|
|
533
831
|
}
|
|
534
|
-
function applyAgentOverlays(config, agentInfo, overlays,
|
|
832
|
+
function applyAgentOverlays(config, agentInfo, overlays, availabilitySet) {
|
|
535
833
|
const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
|
|
536
834
|
const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
|
|
537
835
|
const exactOverlay = overlays.agentsByTargetId.get(id);
|
|
@@ -542,17 +840,20 @@ function applyAgentOverlays(config, agentInfo, overlays, authedProviders) {
|
|
|
542
840
|
addPermissionRules(permissionRules, config.permission);
|
|
543
841
|
}
|
|
544
842
|
result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
|
|
545
|
-
if (agentInfo.category) {
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
843
|
+
if (agentInfo.category && availabilitySet !== undefined) {
|
|
844
|
+
const resolved = resolveSourceModel(agentInfo.category, availabilitySet);
|
|
845
|
+
result.model = `${resolved.provider}/${resolved.model}`;
|
|
846
|
+
if (resolved.variant !== undefined) {
|
|
847
|
+
result.variant = resolved.variant;
|
|
848
|
+
} else {
|
|
849
|
+
delete result.variant;
|
|
549
850
|
}
|
|
550
851
|
}
|
|
551
852
|
if (categoryOverlay) {
|
|
552
|
-
|
|
853
|
+
applyOverlayObjectWithVariantClearing(result, categoryOverlay.value, permissionRules);
|
|
553
854
|
}
|
|
554
855
|
if (exactOverlay) {
|
|
555
|
-
|
|
856
|
+
applyOverlayObjectWithVariantClearing(result, exactOverlay.value, permissionRules);
|
|
556
857
|
}
|
|
557
858
|
if (hasPermissionOverlay) {
|
|
558
859
|
const permission = permissionFromRules(permissionRules);
|
|
@@ -577,7 +878,12 @@ var OVERLAY_ASSIGN_FIELDS = [
|
|
|
577
878
|
"steps",
|
|
578
879
|
"hidden"
|
|
579
880
|
];
|
|
580
|
-
function
|
|
881
|
+
function applyOverlayObjectWithVariantClearing(target, overlay, permissionRules) {
|
|
882
|
+
const overlayHasModel = Object.hasOwn(overlay, "model");
|
|
883
|
+
const overlayHasVariant = Object.hasOwn(overlay, "variant");
|
|
884
|
+
if (overlayHasModel && !overlayHasVariant) {
|
|
885
|
+
delete target.variant;
|
|
886
|
+
}
|
|
581
887
|
for (const field of OVERLAY_ASSIGN_FIELDS) {
|
|
582
888
|
if (Object.hasOwn(overlay, field)) {
|
|
583
889
|
if (field === "model" && overlay[field] === null) {
|
|
@@ -672,15 +978,14 @@ function collectEnabledSkillNames(dir, disabledSkills) {
|
|
|
672
978
|
return findSkillsInDir(dir).filter((skillInfo) => !disabledSet.has(skillInfo.name)).map((skillInfo) => skillInfo.name);
|
|
673
979
|
}
|
|
674
980
|
function createConfigHandler(deps) {
|
|
675
|
-
const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
|
|
676
|
-
const readAuthProviders = deps.getAuthenticatedProviders ?? getAuthenticatedProviders;
|
|
981
|
+
const { directory, bundledSkillsDir, bundledAgentsDir: bundledAgentsDir2, bundledCommandsDir } = deps;
|
|
677
982
|
return async (config) => {
|
|
678
983
|
const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
|
|
679
984
|
const existingAgents = { ...config.agent ?? {} };
|
|
680
985
|
const existingCommands = { ...config.command ?? {} };
|
|
681
986
|
const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, systematicConfig.disabled_skills);
|
|
682
987
|
const enabledSkillNames = collectEnabledSkillNames(bundledSkillsDir, systematicConfig.disabled_skills);
|
|
683
|
-
const inventory = buildBundledAgentInventory(
|
|
988
|
+
const inventory = buildBundledAgentInventory(bundledAgentsDir2, systematicConfig.disabled_agents);
|
|
684
989
|
assertSourceCategoryModelCoverage(inventory.categories);
|
|
685
990
|
const validatedOverlays = validateAgentOverlays({
|
|
686
991
|
inventory,
|
|
@@ -689,8 +994,9 @@ function createConfigHandler(deps) {
|
|
|
689
994
|
enabledSkills: enabledSkillNames
|
|
690
995
|
});
|
|
691
996
|
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
692
|
-
const
|
|
693
|
-
const
|
|
997
|
+
const availability = deps.client ? await getAvailableModels(deps.client) : undefined;
|
|
998
|
+
const availabilitySet = availability && availability.status !== "unknown" ? availability.models : undefined;
|
|
999
|
+
const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, existingAgents, resolvedOverlays, availabilitySet);
|
|
694
1000
|
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
695
1001
|
config.agent = {
|
|
696
1002
|
...bundledAgents,
|
|
@@ -715,8 +1021,8 @@ function registerSkillsPaths(config, skillsDir) {
|
|
|
715
1021
|
}
|
|
716
1022
|
|
|
717
1023
|
// src/lib/skill-tool.ts
|
|
718
|
-
import
|
|
719
|
-
import
|
|
1024
|
+
import fs4 from "fs";
|
|
1025
|
+
import path6 from "path";
|
|
720
1026
|
import { pathToFileURL } from "url";
|
|
721
1027
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
722
1028
|
function formatSkillsXml(skills) {
|
|
@@ -742,17 +1048,17 @@ function discoverSkillFiles(dir, limit = 10) {
|
|
|
742
1048
|
function handleEntry(entry, currentDir) {
|
|
743
1049
|
if (entry.isDirectory()) {
|
|
744
1050
|
if (!shouldSkipDirectory(entry.name)) {
|
|
745
|
-
recurse(
|
|
1051
|
+
recurse(path6.resolve(currentDir, entry.name));
|
|
746
1052
|
}
|
|
747
1053
|
} else if (shouldIncludeFile(entry.name)) {
|
|
748
|
-
files.push(
|
|
1054
|
+
files.push(path6.resolve(currentDir, entry.name));
|
|
749
1055
|
}
|
|
750
1056
|
}
|
|
751
1057
|
function recurse(currentDir) {
|
|
752
1058
|
if (files.length >= limit)
|
|
753
1059
|
return;
|
|
754
1060
|
try {
|
|
755
|
-
const entries =
|
|
1061
|
+
const entries = fs4.readdirSync(currentDir, { withFileTypes: true });
|
|
756
1062
|
for (const entry of entries) {
|
|
757
1063
|
if (files.length >= limit)
|
|
758
1064
|
break;
|
|
@@ -833,7 +1139,7 @@ function createSkillTool(options) {
|
|
|
833
1139
|
throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
|
|
834
1140
|
}
|
|
835
1141
|
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
836
|
-
const dir =
|
|
1142
|
+
const dir = path6.dirname(matchedSkill.skillFile);
|
|
837
1143
|
const base = pathToFileURL(dir).href;
|
|
838
1144
|
const files = discoverSkillFiles(dir);
|
|
839
1145
|
await context.ask({
|
|
@@ -870,17 +1176,17 @@ function createSkillTool(options) {
|
|
|
870
1176
|
}
|
|
871
1177
|
|
|
872
1178
|
// src/index.ts
|
|
873
|
-
var
|
|
874
|
-
var
|
|
875
|
-
var bundledSkillsDir =
|
|
876
|
-
var
|
|
877
|
-
var bundledCommandsDir =
|
|
878
|
-
var packageJsonPath =
|
|
1179
|
+
var __dirname3 = path7.dirname(fileURLToPath2(import.meta.url));
|
|
1180
|
+
var packageRoot2 = path7.resolve(__dirname3, "..");
|
|
1181
|
+
var bundledSkillsDir = path7.join(packageRoot2, "skills");
|
|
1182
|
+
var bundledAgentsDir2 = path7.join(packageRoot2, "agents");
|
|
1183
|
+
var bundledCommandsDir = path7.join(packageRoot2, "commands");
|
|
1184
|
+
var packageJsonPath = path7.join(packageRoot2, "package.json");
|
|
879
1185
|
var getPackageVersion = () => {
|
|
880
1186
|
try {
|
|
881
|
-
if (!
|
|
1187
|
+
if (!fs5.existsSync(packageJsonPath))
|
|
882
1188
|
return "unknown";
|
|
883
|
-
const content =
|
|
1189
|
+
const content = fs5.readFileSync(packageJsonPath, "utf8");
|
|
884
1190
|
const parsed = JSON.parse(content);
|
|
885
1191
|
return parsed.version ?? "unknown";
|
|
886
1192
|
} catch {
|
|
@@ -894,8 +1200,9 @@ var initializePlugin = async ({ client, directory }) => {
|
|
|
894
1200
|
const configHandler = createConfigHandler({
|
|
895
1201
|
directory,
|
|
896
1202
|
bundledSkillsDir,
|
|
897
|
-
bundledAgentsDir,
|
|
898
|
-
bundledCommandsDir
|
|
1203
|
+
bundledAgentsDir: bundledAgentsDir2,
|
|
1204
|
+
bundledCommandsDir,
|
|
1205
|
+
client
|
|
899
1206
|
});
|
|
900
1207
|
return {
|
|
901
1208
|
config: configHandler,
|
|
@@ -42,27 +42,4 @@ export declare function buildBundledAgentInventory(agentsDir: string, disabledAg
|
|
|
42
42
|
export declare function validateAgentOverlays({ inventory, overlays, nativeAgents, enabledSkills, }: ValidateAgentOverlaysOptions): ValidatedAgentOverlays;
|
|
43
43
|
export declare function resolveAgentOverlaySet(overlays: ValidatedAgentOverlays): ResolvedAgentOverlaySet;
|
|
44
44
|
export declare function inferBuiltInTemperature(name: string, description?: string): number;
|
|
45
|
-
/**
|
|
46
|
-
* Read which providers are authenticated from OpenCode's auth.json.
|
|
47
|
-
*
|
|
48
|
-
* Reads only top-level keys (provider IDs). Nested values are NEVER
|
|
49
|
-
* inspected, logged, persisted, or transmitted. This is a hard contract:
|
|
50
|
-
* the auth file holds API keys and OAuth tokens, and Systematic must
|
|
51
|
-
* never expose them via stderr, telemetry, or any other channel.
|
|
52
|
-
*
|
|
53
|
-
* Intended for one invocation per plugin config(cfg) cycle. Repeated
|
|
54
|
-
* calls trigger repeated file reads and, on malformed input, repeated
|
|
55
|
-
* stderr diagnostics.
|
|
56
|
-
*
|
|
57
|
-
* @param rootDirOverride - Optional path override for tests. When
|
|
58
|
-
* non-empty, the auth file is resolved as
|
|
59
|
-
* `path.join(rootDirOverride, 'opencode', 'auth.json')`. When
|
|
60
|
-
* omitted, resolution follows XDG_DATA_HOME -> ~/.local/share
|
|
61
|
-
* convention.
|
|
62
|
-
* @returns A readonly set of authenticated provider IDs (empty set on
|
|
63
|
-
* any failure).
|
|
64
|
-
*/
|
|
65
|
-
export declare function getAuthenticatedProviders(rootDirOverride?: string): ReadonlySet<string>;
|
|
66
|
-
export declare function getSourceCategoryModel(category: string | undefined, authedProviders?: ReadonlySet<string>): string | undefined;
|
|
67
45
|
export declare function assertSourceCategoryModelCoverage(categories: string[]): void;
|
|
68
|
-
export declare function validateSourceCategoryModelDefaults(defaults?: Record<string, unknown>): void;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Config } from '@opencode-ai/plugin';
|
|
2
|
+
import { type OpencodeClientLike } from './model-availability.js';
|
|
2
3
|
export interface ConfigHandlerDeps {
|
|
3
4
|
directory: string;
|
|
4
5
|
bundledSkillsDir: string;
|
|
5
6
|
bundledAgentsDir: string;
|
|
6
7
|
bundledCommandsDir: string;
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
/** OpenCode client for availability lookup. When omitted, availability falls back to empty set (last-resort resolution). */
|
|
9
|
+
client?: OpencodeClientLike;
|
|
9
10
|
}
|
|
10
11
|
export declare function toTitleCase(name: string): string;
|
|
11
12
|
export declare function formatAgentDescription(name: string, description: string | undefined): string;
|
|
@@ -166,7 +166,6 @@ export type ValidationResult = {
|
|
|
166
166
|
errors: readonly z.ZodIssue[];
|
|
167
167
|
};
|
|
168
168
|
export declare function validateConfig(input: unknown): ValidationResult;
|
|
169
|
-
export declare function assertSourceCategoryModelDefaults(defaults: Record<string, unknown>): void;
|
|
170
169
|
/**
|
|
171
170
|
* Overlay fields that require a project-or-higher trust source.
|
|
172
171
|
*
|
|
@@ -0,0 +1,77 @@
|
|
|
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. `models` may be empty if no providers
|
|
26
|
+
* are authenticated; that is authoritative.
|
|
27
|
+
* - `cache`: The API call failed (error envelope, thrown, or timed out) and
|
|
28
|
+
* the local `models.json` cache was readable. `models` reflects whatever
|
|
29
|
+
* OpenCode last wrote to disk.
|
|
30
|
+
* - `unknown`: Both the API call and the cache fallback failed (cache
|
|
31
|
+
* missing, unreadable, corrupt, or schema-mismatched). Resolution should
|
|
32
|
+
* degrade gracefully — callers should treat `unknown` as a signal to
|
|
33
|
+
* skip source-default model pinning so users do not get agents pinned
|
|
34
|
+
* to inaccessible models. `models` is the empty set.
|
|
35
|
+
*/
|
|
36
|
+
export type DiscoveryStatus = 'api' | 'cache' | 'unknown';
|
|
37
|
+
export interface ModelAvailability {
|
|
38
|
+
status: DiscoveryStatus;
|
|
39
|
+
/**
|
|
40
|
+
* Set of `${providerId}/${modelId}` strings. Typed `ReadonlySet` because
|
|
41
|
+
* callers must not mutate the returned collection — mutation would corrupt
|
|
42
|
+
* future calls in the same process. Each `ModelAvailability` is a fresh
|
|
43
|
+
* instance (see `emptyAvailability()`), so mutation via cast cannot
|
|
44
|
+
* propagate, but the type makes intent explicit.
|
|
45
|
+
*/
|
|
46
|
+
models: ReadonlySet<string>;
|
|
47
|
+
}
|
|
48
|
+
interface AvailabilityOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Maximum time to wait for `client.config.providers()` before falling
|
|
51
|
+
* back to the local cache. Defaults to 1500ms — a startup-budget value
|
|
52
|
+
* that prevents a slow/half-open OpenCode server from holding the plugin
|
|
53
|
+
* indefinitely.
|
|
54
|
+
*
|
|
55
|
+
* Set to `null` to disable the timeout entirely (not recommended).
|
|
56
|
+
*/
|
|
57
|
+
apiTimeoutMs?: number | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Discover the set of `provider/model` keys the OpenCode server considers
|
|
61
|
+
* connected (or, on API failure, the set last written to the on-disk
|
|
62
|
+
* `models.json` cache).
|
|
63
|
+
*
|
|
64
|
+
* The returned `status` lets callers distinguish three discovery outcomes:
|
|
65
|
+
* - `api`: live answer; safe to pin source-default models against it
|
|
66
|
+
* - `cache`: degraded but informed; the cached `provider/model` keys are
|
|
67
|
+
* plausibly still authoritative
|
|
68
|
+
* - `unknown`: both the API and the cache failed; callers should fall back
|
|
69
|
+
* to OpenCode's parent-model inheritance rather than pinning a source
|
|
70
|
+
* default the user may not have access to
|
|
71
|
+
*
|
|
72
|
+
* The API call is bounded by `apiTimeoutMs` (default 1500ms). On timeout,
|
|
73
|
+
* thrown error, error-envelope response, or undefined data, the cache
|
|
74
|
+
* fallback runs. The function never rejects.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getAvailableModels(client: OpencodeClientLike, options?: AvailabilityOptions): Promise<ModelAvailability>;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-grouped source category model defaults for Systematic bundled agents.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the canonical shape, Zod schema, and constant for the
|
|
5
|
+
* per-category model resolution chain. The resolution algorithm walks
|
|
6
|
+
* the provider list in order and picks the first available provider/model pair.
|
|
7
|
+
*
|
|
8
|
+
* Provider catalog is constrained to the 7 IDs with empirical OMO usage-frequency
|
|
9
|
+
* justification: vercel=80, opencode=55, github-copilot=39, opencode-go=26,
|
|
10
|
+
* openai=20, anthropic=18, google=10.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
/**
|
|
14
|
+
* Zod literal union of the 7 supported provider IDs.
|
|
15
|
+
* Ordered by OMO empirical usage frequency (highest first).
|
|
16
|
+
*/
|
|
17
|
+
export declare const ProviderID: z.ZodUnion<readonly [z.ZodLiteral<"vercel">, z.ZodLiteral<"opencode">, z.ZodLiteral<"github-copilot">, z.ZodLiteral<"opencode-go">, z.ZodLiteral<"openai">, z.ZodLiteral<"anthropic">, z.ZodLiteral<"google">]>;
|
|
18
|
+
export type ProviderID = z.infer<typeof ProviderID>;
|
|
19
|
+
/**
|
|
20
|
+
* Zod schema for the full source category model defaults map.
|
|
21
|
+
*
|
|
22
|
+
* Schema is **pure** — it validates only structural correctness:
|
|
23
|
+
* - Shape correctness (CategoryDefaultSchema per value)
|
|
24
|
+
* - Provider lists non-empty (enforced by CategoryDefaultSchema)
|
|
25
|
+
* - Model lists non-empty (enforced by ProviderEntrySchema)
|
|
26
|
+
* - (model, variant) pairs unique within a provider entry
|
|
27
|
+
* - Provider IDs unique within a category
|
|
28
|
+
* - variant is non-empty, whitespace-free, max 128 chars
|
|
29
|
+
* - `rationale` and `whenToOverride` reject pipe (`|`) and newline characters
|
|
30
|
+
* so the generator produces well-formed Markdown tables
|
|
31
|
+
*
|
|
32
|
+
* Filesystem coverage (every key resolves to a real `agents/<category>/`
|
|
33
|
+
* directory) is NOT checked here. Use `assertCategoryCoverageOnDisk` to
|
|
34
|
+
* enforce that invariant from tests where it matters; the production
|
|
35
|
+
* runtime path uses `assertSourceCategoryModelCoverage` from
|
|
36
|
+
* `agent-overlays.ts` against an in-memory inventory rather than reading
|
|
37
|
+
* disk again.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SourceCategoryDefaultsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
40
|
+
rationale: z.ZodString;
|
|
41
|
+
whenToOverride: z.ZodOptional<z.ZodString>;
|
|
42
|
+
providers: z.ZodArray<z.ZodObject<{
|
|
43
|
+
provider: z.ZodUnion<readonly [z.ZodLiteral<"vercel">, z.ZodLiteral<"opencode">, z.ZodLiteral<"github-copilot">, z.ZodLiteral<"opencode-go">, z.ZodLiteral<"openai">, z.ZodLiteral<"anthropic">, z.ZodLiteral<"google">]>;
|
|
44
|
+
models: z.ZodArray<z.ZodObject<{
|
|
45
|
+
model: z.ZodString;
|
|
46
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$strict>>;
|
|
48
|
+
}, z.core.$strict>>;
|
|
49
|
+
}, z.core.$strict>>;
|
|
50
|
+
/**
|
|
51
|
+
* Verify that every category key in `categories` maps to a real
|
|
52
|
+
* `agents/<category>/` directory on disk under `agentsDir` (defaulting to
|
|
53
|
+
* the package's bundled-agents directory). Throws with a useful message if
|
|
54
|
+
* any keys are unrecognized.
|
|
55
|
+
*
|
|
56
|
+
* Use from tests to lock the SOURCE_CATEGORY_MODEL_DEFAULTS ↔ agents/
|
|
57
|
+
* directory layout contract. The production runtime path validates the
|
|
58
|
+
* inverse direction (every bundled-agent category has a source default)
|
|
59
|
+
* via `assertSourceCategoryModelCoverage` in `agent-overlays.ts`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function assertCategoryCoverageOnDisk(categories: readonly string[], agentsDir?: string): void;
|
|
62
|
+
export interface ModelEntry {
|
|
63
|
+
model: string;
|
|
64
|
+
variant?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface ProviderEntry {
|
|
67
|
+
provider: ProviderID;
|
|
68
|
+
models: ModelEntry[];
|
|
69
|
+
}
|
|
70
|
+
export interface CategoryDefault {
|
|
71
|
+
rationale: string;
|
|
72
|
+
whenToOverride?: string;
|
|
73
|
+
providers: ProviderEntry[];
|
|
74
|
+
}
|
|
75
|
+
export type SourceCategoryDefaults = Record<string, CategoryDefault>;
|
|
76
|
+
/**
|
|
77
|
+
* Provider-grouped source model defaults for the 6 Systematic agent categories.
|
|
78
|
+
*
|
|
79
|
+
* Provider chains are ordered by OMO category-fit reasoning. The resolver
|
|
80
|
+
* walks providers in order and picks the first available provider/model pair.
|
|
81
|
+
* If no provider is available, the first entry of the first provider is used as
|
|
82
|
+
* the last-resort fallback.
|
|
83
|
+
*
|
|
84
|
+
* Model choices translate the existing flat-string-array constant in agent-overlays.ts
|
|
85
|
+
* to the new provider-grouped shape, with variant annotations where applicable.
|
|
86
|
+
*/
|
|
87
|
+
export declare const SOURCE_CATEGORY_MODEL_DEFAULTS: SourceCategoryDefaults;
|
|
88
|
+
/**
|
|
89
|
+
* Format the SOURCE_CATEGORY_MODEL_DEFAULTS as a GitHub-flavored markdown table
|
|
90
|
+
* for injection into documentation.
|
|
91
|
+
*
|
|
92
|
+
* Columns: Category | Chain | Rationale | When to Override
|
|
93
|
+
*
|
|
94
|
+
* Chain format: comma-separated `provider/model[+variant]` for the first 2–3
|
|
95
|
+
* provider entries (first model per provider). Appends `, …` when there are
|
|
96
|
+
* more than 3 provider entries.
|
|
97
|
+
*
|
|
98
|
+
* Returns a string ending with `\n` for clean concatenation.
|
|
99
|
+
*/
|
|
100
|
+
export declare function formatForDocs(): string;
|
|
101
|
+
/**
|
|
102
|
+
* Walk the provider-grouped shape for a category and return the first available
|
|
103
|
+
* provider/model pair from the availability set.
|
|
104
|
+
*
|
|
105
|
+
* Algorithm:
|
|
106
|
+
* 1. Look up the category. Unknown category is a programmer error — throw.
|
|
107
|
+
* 2. Walk providers in declared order. For each provider, walk its models in
|
|
108
|
+
* declared order and test `${provider}/${model}` membership in availabilitySet.
|
|
109
|
+
* 3. On first hit, return { provider, model, variant? }.
|
|
110
|
+
* 4. Last-resort fallback (no available model anywhere): return the first model
|
|
111
|
+
* entry of the first provider entry, including its variant if present.
|
|
112
|
+
*/
|
|
113
|
+
export declare function resolveSourceModel(category: string, availabilitySet: ReadonlySet<string>): {
|
|
114
|
+
provider: ProviderID;
|
|
115
|
+
model: string;
|
|
116
|
+
variant?: string;
|
|
117
|
+
};
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"variant": {
|
|
47
47
|
"type": "string",
|
|
48
48
|
"minLength": 1,
|
|
49
|
+
"maxLength": 128,
|
|
49
50
|
"pattern": "^\\S+$",
|
|
50
51
|
"description": "Model variant identifier",
|
|
51
52
|
"examples": ["v2", "extended"],
|
|
@@ -161,7 +162,7 @@
|
|
|
161
162
|
"description": "Per-agent configuration overlay",
|
|
162
163
|
"examples": [
|
|
163
164
|
{
|
|
164
|
-
"model": "anthropic/claude-opus-4
|
|
165
|
+
"model": "anthropic/claude-opus-4-7",
|
|
165
166
|
"temperature": 0.1,
|
|
166
167
|
"mode": "subagent"
|
|
167
168
|
}
|
|
@@ -174,7 +175,7 @@
|
|
|
174
175
|
"examples": [
|
|
175
176
|
{
|
|
176
177
|
"review": {
|
|
177
|
-
"model": "anthropic/claude-opus-4
|
|
178
|
+
"model": "anthropic/claude-opus-4-7"
|
|
178
179
|
}
|
|
179
180
|
},
|
|
180
181
|
{}
|
|
@@ -204,6 +205,7 @@
|
|
|
204
205
|
"variant": {
|
|
205
206
|
"type": "string",
|
|
206
207
|
"minLength": 1,
|
|
208
|
+
"maxLength": 128,
|
|
207
209
|
"pattern": "^\\S+$",
|
|
208
210
|
"description": "Model variant identifier",
|
|
209
211
|
"examples": ["v2", "extended"],
|
|
@@ -314,7 +316,7 @@
|
|
|
314
316
|
"description": "Per-category configuration overlay (same fields as agent minus disable)",
|
|
315
317
|
"examples": [
|
|
316
318
|
{
|
|
317
|
-
"model": "anthropic/claude-opus-4
|
|
319
|
+
"model": "anthropic/claude-opus-4-7",
|
|
318
320
|
"temperature": 0.1
|
|
319
321
|
}
|
|
320
322
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://fro.bot/systematic",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"@types/bun": "latest",
|
|
70
70
|
"@types/js-yaml": "4.0.9",
|
|
71
71
|
"@types/node": "24.12.3",
|
|
72
|
-
"ajv": "
|
|
73
|
-
"ajv-formats": "
|
|
72
|
+
"ajv": "8.20.0",
|
|
73
|
+
"ajv-formats": "3.0.1",
|
|
74
74
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
75
75
|
"markdownlint-cli": "0.48.0",
|
|
76
76
|
"rimraf": "6.1.3",
|