@fro.bot/systematic 2.12.3 → 2.13.1
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 +10 -10
- package/dist/cli.js +1 -1
- package/dist/{index-gb1v2n3m.js → index-4qhjf4tb.js} +4 -11
- package/dist/index.js +407 -108
- 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 +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -326,7 +326,7 @@ Configuration is loaded from multiple locations and merged (later sources overri
|
|
|
326
326
|
| `bootstrap.enabled` | `boolean` | `true` | Inject the `using-systematic` guide into system prompts |
|
|
327
327
|
| `bootstrap.file` | `string` | — | Custom bootstrap file path (overrides default) |
|
|
328
328
|
|
|
329
|
-
Agent overlays support `model`, `variant`, `temperature`, `top_p`, `permission`, `mode`, `color`, `steps`, `hidden`, exact-agent-only `disable`, and managed `skills`. `color` accepts `#
|
|
329
|
+
Agent overlays support `model`, `variant`, `temperature`, `top_p`, `permission`, `mode`, `color`, `steps`, `hidden`, exact-agent-only `disable`, and managed `skills`. `color` accepts `#RRGGBB` hex colors or OpenCode theme tokens: `primary`, `secondary`, `accent`, `success`, `warning`, `error`, and `info`. `skills` uses bundled skill frontmatter names like `ce:review`; it is a shortcut that writes OpenCode `permission.skill` rules, not a native OpenCode agent field. Because `model` and `variant` control provider routing/cost/privacy and `permission`/`skills` control tool access, those fields are only accepted from user config or `$OPENCODE_CONFIG_DIR/systematic.json`. Project config may tune non-sensitive presentation and runtime fields such as `temperature`, `top_p`, `mode`, `color`, `steps`, `hidden`, or exact-agent `disable`, but it cannot choose model/provider routing, tune `variant`, or loosen permission/capability policy.
|
|
330
330
|
|
|
331
331
|
Systematic separates config-source precedence from overlay precedence. Config files merge in this order: user config, project config, then `$OPENCODE_CONFIG_DIR/systematic.json` if set. Higher-priority `agents.<key>` and `categories.<id>` entries replace lower-priority entries wholesale, while unrelated keys survive. Project overlays are the exception for trust-sensitive fields: same-key project overlays preserve user-level `model`, `variant`, `permission`, and `skills` fields instead of erasing them. After the effective config is built, Systematic applies agent overlay precedence for bundled agents:
|
|
332
332
|
|
|
@@ -338,20 +338,20 @@ 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
|
|
341
|
+
Source category model defaults are ordered provider/model preference chains per category. At plugin load, Systematic asks OpenCode for connected providers through `client.config.providers()` and selects the first provider/model entry OpenCode reports as available. If the live provider query fails, Systematic falls back to OpenCode's `models.json` cache; if both sources fail, it skips source-default emission so bundled agents inherit the parent OpenCode model instead of pinning an unavailable model. The chains are ordered preference lists, not runtime fallback chains — `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
|
|
|
345
345
|
The source defaults are:
|
|
346
346
|
|
|
347
|
-
| Category |
|
|
348
|
-
|
|
349
|
-
| `design` | `github-copilot/gemini-3.1-pro-preview` | High-judgment UX
|
|
350
|
-
| `docs` | `github-copilot/gemini-3.1-pro-preview` | Documentation and summarization should start cheaper
|
|
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
|
|
354
|
-
| `workflow` | `openai/gpt-5.4-mini` | Orchestration and bounded implementation should default cheaper
|
|
347
|
+
| Category | Chain | Rationale | When to Override |
|
|
348
|
+
| --- | --- | --- | --- |
|
|
349
|
+
| `design` | `github-copilot/gemini-3.1-pro-preview`, `openai/gpt-5.5` (`high`), `anthropic/claude-opus-4-7` (`max`), … | High-judgment UX, product, and design work benefits from a strong general reasoning model with broad creative capability. | Override to a faster/cheaper model when design tasks are primarily templating or low-stakes layout work. |
|
|
350
|
+
| `docs` | `github-copilot/gemini-3.1-pro-preview`, `openai/gpt-5.4-mini`, `anthropic/claude-haiku-4-5`, … | Documentation and summarization tasks should start cheaper and faster; quality is sufficient at mid-tier models. | — |
|
|
351
|
+
| `document-review` | `anthropic/claude-opus-4-7` (`max`), `openai/gpt-5.5` (`high`), `github-copilot/gemini-3.1-pro-preview`, … | Requirements and plan critique benefit from the strongest nuanced reasoning to surface non-obvious gaps and contradictions. | — |
|
|
352
|
+
| `research` | `openai/gpt-5.4-mini`, `anthropic/claude-sonnet-4-6`, `github-copilot/gemini-3.1-pro-preview`, … | Tool-heavy synthesis and source evaluation benefit from a strong general reasoning model with broad knowledge. | — |
|
|
353
|
+
| `review` | `anthropic/claude-sonnet-4-6`, `openai/gpt-5.3-codex`, `github-copilot/gemini-3.1-pro-preview`, … | Code, security, and adversarial review benefits from the strongest reasoning to catch subtle bugs and security issues. | Override to a faster model when review tasks are primarily style or formatting checks rather than correctness or security analysis. |
|
|
354
|
+
| `workflow` | `openai/gpt-5.4-mini`, `anthropic/claude-sonnet-4-6`, `opencode/claude-haiku-4-5`, … | Orchestration and bounded implementation tasks should default cheaper and faster; strong reasoning is rarely needed for routing decisions. | — |
|
|
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.
|
|
357
357
|
|
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
|
});
|
|
@@ -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,38 +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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
127
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
|
|
128
322
|
function buildBundledAgentInventory(agentsDir, disabledAgents) {
|
|
129
323
|
const categories = readCategoryDirs(agentsDir);
|
|
130
324
|
const agentsByQualifiedId = {};
|
|
131
325
|
const stemCategories = new Map;
|
|
132
326
|
const disabledSet = new Set(disabledAgents);
|
|
133
327
|
for (const category of categories) {
|
|
134
|
-
for (const fileName of readMarkdownFiles(
|
|
328
|
+
for (const fileName of readMarkdownFiles(path3.join(agentsDir, category))) {
|
|
135
329
|
const key = fileName.replace(/\.md$/, "");
|
|
136
330
|
const id = `${category}/${key}`;
|
|
137
331
|
agentsByQualifiedId[id] = {
|
|
138
332
|
id,
|
|
139
333
|
key,
|
|
140
334
|
category,
|
|
141
|
-
file:
|
|
335
|
+
file: path3.join(agentsDir, category, fileName),
|
|
142
336
|
disabled: disabledSet.has(key) || disabledSet.has(id)
|
|
143
337
|
};
|
|
144
338
|
const existing = stemCategories.get(key) ?? [];
|
|
@@ -191,62 +385,12 @@ function inferBuiltInTemperature(name, description) {
|
|
|
191
385
|
}
|
|
192
386
|
return 0.3;
|
|
193
387
|
}
|
|
194
|
-
function getAuthenticatedProviders(rootDirOverride) {
|
|
195
|
-
const xdgDataHome = process.env.XDG_DATA_HOME?.trim();
|
|
196
|
-
const rootDir = rootDirOverride || (xdgDataHome && path2.isAbsolute(xdgDataHome) ? xdgDataHome : path2.join(os2.homedir(), ".local/share"));
|
|
197
|
-
const authPath = path2.join(rootDir, "opencode", "auth.json");
|
|
198
|
-
let raw;
|
|
199
|
-
try {
|
|
200
|
-
raw = fs2.readFileSync(authPath, "utf8");
|
|
201
|
-
} catch (err) {
|
|
202
|
-
if (isSystemError(err) && err.code === "ENOENT") {
|
|
203
|
-
return new Set;
|
|
204
|
-
}
|
|
205
|
-
console.warn(`[systematic] auth.json unreadable at ${authPath}; ignoring`);
|
|
206
|
-
return new Set;
|
|
207
|
-
}
|
|
208
|
-
let parsed;
|
|
209
|
-
try {
|
|
210
|
-
parsed = JSON.parse(raw);
|
|
211
|
-
} catch {
|
|
212
|
-
console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
|
|
213
|
-
return new Set;
|
|
214
|
-
}
|
|
215
|
-
if (!isRecord(parsed)) {
|
|
216
|
-
console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
|
|
217
|
-
return new Set;
|
|
218
|
-
}
|
|
219
|
-
return new Set(Object.keys(parsed));
|
|
220
|
-
}
|
|
221
|
-
function getSourceCategoryModel(category, authedProviders) {
|
|
222
|
-
if (!category)
|
|
223
|
-
return;
|
|
224
|
-
const candidates = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
|
|
225
|
-
if (!candidates || candidates.length === 0)
|
|
226
|
-
return;
|
|
227
|
-
if (!authedProviders || authedProviders.size === 0)
|
|
228
|
-
return candidates[0];
|
|
229
|
-
for (const entry of candidates) {
|
|
230
|
-
const slashIndex = entry.indexOf("/");
|
|
231
|
-
if (slashIndex <= 0)
|
|
232
|
-
continue;
|
|
233
|
-
const providerId = entry.slice(0, slashIndex);
|
|
234
|
-
if (authedProviders.has(providerId)) {
|
|
235
|
-
return entry;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return candidates[0];
|
|
239
|
-
}
|
|
240
388
|
function assertSourceCategoryModelCoverage(categories) {
|
|
241
|
-
validateSourceCategoryModelDefaults();
|
|
242
389
|
const missingCategories = categories.filter((category) => !Object.hasOwn(SOURCE_CATEGORY_MODEL_DEFAULTS, category));
|
|
243
390
|
if (missingCategories.length > 0) {
|
|
244
391
|
throw new Error(`Source category model defaults missing intentional coverage for: ${missingCategories.join(", ")}`);
|
|
245
392
|
}
|
|
246
393
|
}
|
|
247
|
-
function validateSourceCategoryModelDefaults(defaults = SOURCE_CATEGORY_MODEL_DEFAULTS) {
|
|
248
|
-
assertSourceCategoryModelDefaults(defaults);
|
|
249
|
-
}
|
|
250
394
|
function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
|
|
251
395
|
const result = [];
|
|
252
396
|
const seenTargets = new Map;
|
|
@@ -347,12 +491,158 @@ function validAgentKeys(inventory) {
|
|
|
347
491
|
function throwConfigError(sourcePath, keyPath, message) {
|
|
348
492
|
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
|
|
349
493
|
}
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
};
|
|
352
642
|
}
|
|
353
643
|
|
|
354
644
|
// src/lib/skill-loader.ts
|
|
355
|
-
import
|
|
645
|
+
import path5 from "path";
|
|
356
646
|
var SKILL_PREFIX = "systematic:";
|
|
357
647
|
var SKILL_DESCRIPTION_PREFIX = "(Systematic - Skill) ";
|
|
358
648
|
function formatSkillCommandName(name) {
|
|
@@ -369,7 +659,7 @@ function formatSkillDescription(description, fallbackName) {
|
|
|
369
659
|
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
370
660
|
}
|
|
371
661
|
function wrapSkillTemplate(skillPath, body) {
|
|
372
|
-
const skillDir =
|
|
662
|
+
const skillDir = path5.dirname(skillPath);
|
|
373
663
|
return `<skill-instruction>
|
|
374
664
|
Base directory for this skill: ${skillDir}/
|
|
375
665
|
File references (@path) in this skill are relative to this directory.
|
|
@@ -519,7 +809,7 @@ function loadSkillAsCommand(loaded) {
|
|
|
519
809
|
config.subtask = loaded.subtask;
|
|
520
810
|
return config;
|
|
521
811
|
}
|
|
522
|
-
function collectAgents(dir, disabledAgents, nativeAgents, overlays,
|
|
812
|
+
function collectAgents(dir, disabledAgents, nativeAgents, overlays, availabilitySet) {
|
|
523
813
|
const agents = {};
|
|
524
814
|
const agentList = findAgentsInDir(dir);
|
|
525
815
|
const disabledSet = new Set(disabledAgents);
|
|
@@ -534,12 +824,12 @@ function collectAgents(dir, disabledAgents, nativeAgents, overlays, authedProvid
|
|
|
534
824
|
continue;
|
|
535
825
|
const config = loadAgentAsConfig(agentInfo);
|
|
536
826
|
if (config) {
|
|
537
|
-
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays,
|
|
827
|
+
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, availabilitySet);
|
|
538
828
|
}
|
|
539
829
|
}
|
|
540
830
|
return agents;
|
|
541
831
|
}
|
|
542
|
-
function applyAgentOverlays(config, agentInfo, overlays,
|
|
832
|
+
function applyAgentOverlays(config, agentInfo, overlays, availabilitySet) {
|
|
543
833
|
const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
|
|
544
834
|
const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
|
|
545
835
|
const exactOverlay = overlays.agentsByTargetId.get(id);
|
|
@@ -550,17 +840,20 @@ function applyAgentOverlays(config, agentInfo, overlays, authedProviders) {
|
|
|
550
840
|
addPermissionRules(permissionRules, config.permission);
|
|
551
841
|
}
|
|
552
842
|
result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
|
|
553
|
-
if (agentInfo.category) {
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
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;
|
|
557
850
|
}
|
|
558
851
|
}
|
|
559
852
|
if (categoryOverlay) {
|
|
560
|
-
|
|
853
|
+
applyOverlayObjectWithVariantClearing(result, categoryOverlay.value, permissionRules);
|
|
561
854
|
}
|
|
562
855
|
if (exactOverlay) {
|
|
563
|
-
|
|
856
|
+
applyOverlayObjectWithVariantClearing(result, exactOverlay.value, permissionRules);
|
|
564
857
|
}
|
|
565
858
|
if (hasPermissionOverlay) {
|
|
566
859
|
const permission = permissionFromRules(permissionRules);
|
|
@@ -585,7 +878,12 @@ var OVERLAY_ASSIGN_FIELDS = [
|
|
|
585
878
|
"steps",
|
|
586
879
|
"hidden"
|
|
587
880
|
];
|
|
588
|
-
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
|
+
}
|
|
589
887
|
for (const field of OVERLAY_ASSIGN_FIELDS) {
|
|
590
888
|
if (Object.hasOwn(overlay, field)) {
|
|
591
889
|
if (field === "model" && overlay[field] === null) {
|
|
@@ -680,15 +978,14 @@ function collectEnabledSkillNames(dir, disabledSkills) {
|
|
|
680
978
|
return findSkillsInDir(dir).filter((skillInfo) => !disabledSet.has(skillInfo.name)).map((skillInfo) => skillInfo.name);
|
|
681
979
|
}
|
|
682
980
|
function createConfigHandler(deps) {
|
|
683
|
-
const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
|
|
684
|
-
const readAuthProviders = deps.getAuthenticatedProviders ?? getAuthenticatedProviders;
|
|
981
|
+
const { directory, bundledSkillsDir, bundledAgentsDir: bundledAgentsDir2, bundledCommandsDir } = deps;
|
|
685
982
|
return async (config) => {
|
|
686
983
|
const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
|
|
687
984
|
const existingAgents = { ...config.agent ?? {} };
|
|
688
985
|
const existingCommands = { ...config.command ?? {} };
|
|
689
986
|
const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, systematicConfig.disabled_skills);
|
|
690
987
|
const enabledSkillNames = collectEnabledSkillNames(bundledSkillsDir, systematicConfig.disabled_skills);
|
|
691
|
-
const inventory = buildBundledAgentInventory(
|
|
988
|
+
const inventory = buildBundledAgentInventory(bundledAgentsDir2, systematicConfig.disabled_agents);
|
|
692
989
|
assertSourceCategoryModelCoverage(inventory.categories);
|
|
693
990
|
const validatedOverlays = validateAgentOverlays({
|
|
694
991
|
inventory,
|
|
@@ -697,8 +994,9 @@ function createConfigHandler(deps) {
|
|
|
697
994
|
enabledSkills: enabledSkillNames
|
|
698
995
|
});
|
|
699
996
|
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
700
|
-
const
|
|
701
|
-
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);
|
|
702
1000
|
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
703
1001
|
config.agent = {
|
|
704
1002
|
...bundledAgents,
|
|
@@ -723,8 +1021,8 @@ function registerSkillsPaths(config, skillsDir) {
|
|
|
723
1021
|
}
|
|
724
1022
|
|
|
725
1023
|
// src/lib/skill-tool.ts
|
|
726
|
-
import
|
|
727
|
-
import
|
|
1024
|
+
import fs4 from "fs";
|
|
1025
|
+
import path6 from "path";
|
|
728
1026
|
import { pathToFileURL } from "url";
|
|
729
1027
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
730
1028
|
function formatSkillsXml(skills) {
|
|
@@ -750,17 +1048,17 @@ function discoverSkillFiles(dir, limit = 10) {
|
|
|
750
1048
|
function handleEntry(entry, currentDir) {
|
|
751
1049
|
if (entry.isDirectory()) {
|
|
752
1050
|
if (!shouldSkipDirectory(entry.name)) {
|
|
753
|
-
recurse(
|
|
1051
|
+
recurse(path6.resolve(currentDir, entry.name));
|
|
754
1052
|
}
|
|
755
1053
|
} else if (shouldIncludeFile(entry.name)) {
|
|
756
|
-
files.push(
|
|
1054
|
+
files.push(path6.resolve(currentDir, entry.name));
|
|
757
1055
|
}
|
|
758
1056
|
}
|
|
759
1057
|
function recurse(currentDir) {
|
|
760
1058
|
if (files.length >= limit)
|
|
761
1059
|
return;
|
|
762
1060
|
try {
|
|
763
|
-
const entries =
|
|
1061
|
+
const entries = fs4.readdirSync(currentDir, { withFileTypes: true });
|
|
764
1062
|
for (const entry of entries) {
|
|
765
1063
|
if (files.length >= limit)
|
|
766
1064
|
break;
|
|
@@ -841,7 +1139,7 @@ function createSkillTool(options) {
|
|
|
841
1139
|
throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
|
|
842
1140
|
}
|
|
843
1141
|
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
844
|
-
const dir =
|
|
1142
|
+
const dir = path6.dirname(matchedSkill.skillFile);
|
|
845
1143
|
const base = pathToFileURL(dir).href;
|
|
846
1144
|
const files = discoverSkillFiles(dir);
|
|
847
1145
|
await context.ask({
|
|
@@ -878,17 +1176,17 @@ function createSkillTool(options) {
|
|
|
878
1176
|
}
|
|
879
1177
|
|
|
880
1178
|
// src/index.ts
|
|
881
|
-
var
|
|
882
|
-
var
|
|
883
|
-
var bundledSkillsDir =
|
|
884
|
-
var
|
|
885
|
-
var bundledCommandsDir =
|
|
886
|
-
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");
|
|
887
1185
|
var getPackageVersion = () => {
|
|
888
1186
|
try {
|
|
889
|
-
if (!
|
|
1187
|
+
if (!fs5.existsSync(packageJsonPath))
|
|
890
1188
|
return "unknown";
|
|
891
|
-
const content =
|
|
1189
|
+
const content = fs5.readFileSync(packageJsonPath, "utf8");
|
|
892
1190
|
const parsed = JSON.parse(content);
|
|
893
1191
|
return parsed.version ?? "unknown";
|
|
894
1192
|
} catch {
|
|
@@ -902,8 +1200,9 @@ var initializePlugin = async ({ client, directory }) => {
|
|
|
902
1200
|
const configHandler = createConfigHandler({
|
|
903
1201
|
directory,
|
|
904
1202
|
bundledSkillsDir,
|
|
905
|
-
bundledAgentsDir,
|
|
906
|
-
bundledCommandsDir
|
|
1203
|
+
bundledAgentsDir: bundledAgentsDir2,
|
|
1204
|
+
bundledCommandsDir,
|
|
1205
|
+
client
|
|
907
1206
|
});
|
|
908
1207
|
return {
|
|
909
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"],
|
|
@@ -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"],
|