@fro.bot/systematic 2.10.0 → 2.12.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 +2 -0
- package/dist/cli.js +1 -1
- package/dist/index-g602hys2.js +16104 -0
- package/dist/index.js +84 -166
- package/dist/lib/agent-colors.d.ts +16 -0
- package/dist/lib/agent-overlays.d.ts +22 -1
- package/dist/lib/config-handler.d.ts +2 -0
- package/dist/lib/config-schema.d.ts +179 -0
- package/dist/lib/config.d.ts +22 -0
- package/dist/schemas/systematic-config.schema.json +392 -0
- package/package.json +9 -4
- package/dist/index-mfy9dbdx.js +0 -1620
package/dist/index.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
+
AgentOverlaySchema,
|
|
4
|
+
CategoryOverlaySchema,
|
|
5
|
+
assertSourceCategoryModelDefaults,
|
|
3
6
|
convertFileWithCache,
|
|
4
7
|
extractAgentFrontmatter,
|
|
5
8
|
extractCommandFrontmatter,
|
|
6
9
|
findAgentsInDir,
|
|
7
10
|
findCommandsInDir,
|
|
8
11
|
findSkillsInDir,
|
|
9
|
-
isAgentMode,
|
|
10
|
-
isPermissionSetting,
|
|
11
12
|
isRecord,
|
|
12
13
|
loadConfig,
|
|
13
14
|
loadConfigWithSources,
|
|
14
15
|
parseFrontmatter
|
|
15
|
-
} from "./index-
|
|
16
|
+
} from "./index-g602hys2.js";
|
|
16
17
|
|
|
17
18
|
// src/index.ts
|
|
18
19
|
import fs4 from "fs";
|
|
@@ -78,28 +79,16 @@ ${toolMapping}
|
|
|
78
79
|
|
|
79
80
|
// src/lib/agent-overlays.ts
|
|
80
81
|
import fs2 from "fs";
|
|
82
|
+
import os2 from "os";
|
|
81
83
|
import path2 from "path";
|
|
82
84
|
var SOURCE_CATEGORY_MODEL_DEFAULTS = {
|
|
83
|
-
design: "openai/gpt-5.5",
|
|
84
|
-
docs: "openai/gpt-5.4-mini",
|
|
85
|
-
"document-review": "anthropic/claude-opus-4.7",
|
|
86
|
-
research: "openai/gpt-5.5",
|
|
87
|
-
review: "anthropic/claude-opus-4.7",
|
|
88
|
-
workflow: "openai/gpt-5.4-mini"
|
|
85
|
+
design: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"],
|
|
86
|
+
docs: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"],
|
|
87
|
+
"document-review": ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
|
|
88
|
+
research: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"],
|
|
89
|
+
review: ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
|
|
90
|
+
workflow: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"]
|
|
89
91
|
};
|
|
90
|
-
var ALLOWED_OVERLAY_FIELDS = new Set([
|
|
91
|
-
"model",
|
|
92
|
-
"variant",
|
|
93
|
-
"temperature",
|
|
94
|
-
"top_p",
|
|
95
|
-
"permission",
|
|
96
|
-
"mode",
|
|
97
|
-
"color",
|
|
98
|
-
"steps",
|
|
99
|
-
"hidden",
|
|
100
|
-
"disable",
|
|
101
|
-
"skills"
|
|
102
|
-
]);
|
|
103
92
|
function buildBundledAgentInventory(agentsDir, disabledAgents) {
|
|
104
93
|
const categories = readCategoryDirs(agentsDir);
|
|
105
94
|
const agentsByQualifiedId = {};
|
|
@@ -166,10 +155,51 @@ function inferBuiltInTemperature(name, description) {
|
|
|
166
155
|
}
|
|
167
156
|
return 0.3;
|
|
168
157
|
}
|
|
169
|
-
function
|
|
158
|
+
function getAuthenticatedProviders(rootDirOverride) {
|
|
159
|
+
const xdgDataHome = process.env.XDG_DATA_HOME?.trim();
|
|
160
|
+
const rootDir = rootDirOverride || (xdgDataHome && path2.isAbsolute(xdgDataHome) ? xdgDataHome : path2.join(os2.homedir(), ".local/share"));
|
|
161
|
+
const authPath = path2.join(rootDir, "opencode", "auth.json");
|
|
162
|
+
let raw;
|
|
163
|
+
try {
|
|
164
|
+
raw = fs2.readFileSync(authPath, "utf8");
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (isSystemError(err) && err.code === "ENOENT") {
|
|
167
|
+
return new Set;
|
|
168
|
+
}
|
|
169
|
+
console.warn(`[systematic] auth.json unreadable at ${authPath}; ignoring`);
|
|
170
|
+
return new Set;
|
|
171
|
+
}
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(raw);
|
|
175
|
+
} catch {
|
|
176
|
+
console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
|
|
177
|
+
return new Set;
|
|
178
|
+
}
|
|
179
|
+
if (!isRecord(parsed)) {
|
|
180
|
+
console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
|
|
181
|
+
return new Set;
|
|
182
|
+
}
|
|
183
|
+
return new Set(Object.keys(parsed));
|
|
184
|
+
}
|
|
185
|
+
function getSourceCategoryModel(category, authedProviders) {
|
|
170
186
|
if (!category)
|
|
171
187
|
return;
|
|
172
|
-
|
|
188
|
+
const candidates = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
|
|
189
|
+
if (!candidates || candidates.length === 0)
|
|
190
|
+
return;
|
|
191
|
+
if (!authedProviders || authedProviders.size === 0)
|
|
192
|
+
return candidates[0];
|
|
193
|
+
for (const entry of candidates) {
|
|
194
|
+
const slashIndex = entry.indexOf("/");
|
|
195
|
+
if (slashIndex <= 0)
|
|
196
|
+
continue;
|
|
197
|
+
const providerId = entry.slice(0, slashIndex);
|
|
198
|
+
if (authedProviders.has(providerId)) {
|
|
199
|
+
return entry;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return candidates[0];
|
|
173
203
|
}
|
|
174
204
|
function assertSourceCategoryModelCoverage(categories) {
|
|
175
205
|
validateSourceCategoryModelDefaults();
|
|
@@ -179,9 +209,7 @@ function assertSourceCategoryModelCoverage(categories) {
|
|
|
179
209
|
}
|
|
180
210
|
}
|
|
181
211
|
function validateSourceCategoryModelDefaults(defaults = SOURCE_CATEGORY_MODEL_DEFAULTS) {
|
|
182
|
-
|
|
183
|
-
validateModel("source category model defaults", `source category model defaults.${category}`, model);
|
|
184
|
-
}
|
|
212
|
+
assertSourceCategoryModelDefaults(defaults);
|
|
185
213
|
}
|
|
186
214
|
function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
|
|
187
215
|
const result = [];
|
|
@@ -231,150 +259,35 @@ function validateCategoryOverlays(inventory, overlays, enabledSkills) {
|
|
|
231
259
|
}
|
|
232
260
|
return result;
|
|
233
261
|
}
|
|
234
|
-
function validateOverlayFields(overlay, targetType, enabledSkills) {
|
|
235
|
-
if (Object.hasOwn(overlay.value, "skills") && hasPermissionSkill(overlay.value.permission)) {
|
|
236
|
-
throwConfigError(overlay.sourcePath, overlay.keyPath, "cannot set both skills and permission.skill in the same overlay object");
|
|
237
|
-
}
|
|
238
|
-
for (const [field, value] of Object.entries(overlay.value)) {
|
|
239
|
-
const keyPath = `${overlay.keyPath}.${field}`;
|
|
240
|
-
if (!ALLOWED_OVERLAY_FIELDS.has(field)) {
|
|
241
|
-
throwConfigError(overlay.sourcePath, keyPath, `unsupported agent overlay field "${field}"`);
|
|
242
|
-
}
|
|
243
|
-
if (targetType === "category" && field === "disable") {
|
|
244
|
-
throwConfigError(overlay.sourcePath, keyPath, "disable is only valid for exact agent overlays");
|
|
245
|
-
}
|
|
246
|
-
validateOverlayFieldValue(overlay.sourcePath, keyPath, field, value, enabledSkills);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
262
|
function hasPermissionSkill(permission) {
|
|
250
263
|
return isRecord(permission) && Object.hasOwn(permission, "skill");
|
|
251
264
|
}
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (value === null)
|
|
256
|
-
return;
|
|
257
|
-
validateModel(sourcePath, keyPath, value);
|
|
258
|
-
return;
|
|
259
|
-
case "variant":
|
|
260
|
-
validateNonEmptyString(sourcePath, keyPath, value);
|
|
261
|
-
return;
|
|
262
|
-
case "temperature":
|
|
263
|
-
validateTemperature(sourcePath, keyPath, value);
|
|
264
|
-
return;
|
|
265
|
-
case "top_p":
|
|
266
|
-
validateTopP(sourcePath, keyPath, value);
|
|
267
|
-
return;
|
|
268
|
-
case "permission":
|
|
269
|
-
validatePermission(sourcePath, keyPath, value);
|
|
270
|
-
return;
|
|
271
|
-
case "mode":
|
|
272
|
-
validateMode(sourcePath, keyPath, value);
|
|
273
|
-
return;
|
|
274
|
-
case "color":
|
|
275
|
-
validateColor(sourcePath, keyPath, value);
|
|
276
|
-
return;
|
|
277
|
-
case "steps":
|
|
278
|
-
validatePositiveInteger(sourcePath, keyPath, value);
|
|
279
|
-
return;
|
|
280
|
-
case "hidden":
|
|
281
|
-
case "disable":
|
|
282
|
-
validateBoolean(sourcePath, keyPath, value);
|
|
283
|
-
return;
|
|
284
|
-
case "skills":
|
|
285
|
-
validateSkills(sourcePath, keyPath, value, enabledSkills);
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
function validateModel(sourcePath, keyPath, value) {
|
|
290
|
-
if (typeof value !== "string") {
|
|
291
|
-
throwConfigError(sourcePath, keyPath, "must be a provider/model string");
|
|
292
|
-
}
|
|
293
|
-
if (value !== value.trim() || /\s/.test(value)) {
|
|
294
|
-
throwConfigError(sourcePath, keyPath, "must be a provider/model string");
|
|
295
|
-
}
|
|
296
|
-
const slashIndex = value.indexOf("/");
|
|
297
|
-
if (value === "" || slashIndex <= 0 || slashIndex === value.length - 1) {
|
|
298
|
-
throwConfigError(sourcePath, keyPath, "must be a provider/model string");
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function validateNonEmptyString(sourcePath, keyPath, value) {
|
|
302
|
-
if (typeof value !== "string" || value === "" || value !== value.trim()) {
|
|
303
|
-
throwConfigError(sourcePath, keyPath, "must be a non-empty string");
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
function validateTemperature(sourcePath, keyPath, value) {
|
|
307
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
308
|
-
throwConfigError(sourcePath, keyPath, "must be a non-negative finite number");
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
function validateTopP(sourcePath, keyPath, value) {
|
|
312
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
313
|
-
throwConfigError(sourcePath, keyPath, "must be a number from 0 to 1");
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
function validatePositiveInteger(sourcePath, keyPath, value) {
|
|
317
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
318
|
-
throwConfigError(sourcePath, keyPath, "must be a positive integer");
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
function validateBoolean(sourcePath, keyPath, value) {
|
|
322
|
-
if (typeof value !== "boolean") {
|
|
323
|
-
throwConfigError(sourcePath, keyPath, "must be a boolean");
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function validateMode(sourcePath, keyPath, value) {
|
|
327
|
-
if (!isAgentMode(value)) {
|
|
328
|
-
throwConfigError(sourcePath, keyPath, "must be one of: subagent, primary, all");
|
|
265
|
+
function validateOverlayFields(overlay, targetType, enabledSkills) {
|
|
266
|
+
if (Object.hasOwn(overlay.value, "skills") && hasPermissionSkill(overlay.value.permission)) {
|
|
267
|
+
throwConfigError(overlay.sourcePath, overlay.keyPath, "cannot set both skills and permission.skill in the same overlay object");
|
|
329
268
|
}
|
|
269
|
+
const result = parseOverlayShape(overlay, targetType);
|
|
270
|
+
validateOverlaySkills(overlay, result.skills, enabledSkills);
|
|
330
271
|
}
|
|
331
|
-
function
|
|
332
|
-
|
|
333
|
-
|
|
272
|
+
function parseOverlayShape(overlay, targetType) {
|
|
273
|
+
const schema = targetType === "agent" ? AgentOverlaySchema : CategoryOverlaySchema;
|
|
274
|
+
const result = schema.safeParse(overlay.value);
|
|
275
|
+
if (!result.success) {
|
|
276
|
+
throwOverlaySchemaError(overlay, result.error.issues[0]);
|
|
334
277
|
}
|
|
278
|
+
return { skills: result.data.skills };
|
|
335
279
|
}
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return true;
|
|
341
|
-
return /^[a-zA-Z][a-zA-Z0-9-]*$/.test(value);
|
|
280
|
+
function throwOverlaySchemaError(overlay, issue) {
|
|
281
|
+
const zodPath = issue.code === "unrecognized_keys" ? issue.message.match(/"([^"]+)"/)?.[1] ?? issue.path.join(".") : issue.path.join(".");
|
|
282
|
+
const fullPath = zodPath ? `${overlay.keyPath}.${zodPath}` : overlay.keyPath;
|
|
283
|
+
throwConfigError(overlay.sourcePath, fullPath, issue.message);
|
|
342
284
|
}
|
|
343
|
-
function
|
|
344
|
-
if (!
|
|
345
|
-
throwConfigError(sourcePath, keyPath, "must be an array of non-empty strings");
|
|
346
|
-
}
|
|
347
|
-
if (!enabledSkills)
|
|
285
|
+
function validateOverlaySkills(overlay, skills, enabledSkills) {
|
|
286
|
+
if (!enabledSkills || !skills)
|
|
348
287
|
return;
|
|
349
|
-
for (const skill of
|
|
288
|
+
for (const skill of skills) {
|
|
350
289
|
if (!enabledSkills.has(skill)) {
|
|
351
|
-
throwConfigError(sourcePath, keyPath
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
function validatePermission(sourcePath, keyPath, value) {
|
|
356
|
-
if (!isRecord(value)) {
|
|
357
|
-
throwConfigError(sourcePath, keyPath, "must be an object");
|
|
358
|
-
}
|
|
359
|
-
for (const [toolKey, rule] of Object.entries(value)) {
|
|
360
|
-
if (toolKey.trim() === "") {
|
|
361
|
-
throwConfigError(sourcePath, `${keyPath}.${toolKey}`, "must use a non-empty tool key");
|
|
362
|
-
}
|
|
363
|
-
validatePermissionRule(sourcePath, `${keyPath}.${toolKey}`, rule);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function validatePermissionRule(sourcePath, keyPath, value) {
|
|
367
|
-
if (isPermissionSetting(value))
|
|
368
|
-
return;
|
|
369
|
-
if (!isRecord(value)) {
|
|
370
|
-
throwConfigError(sourcePath, keyPath, "must be ask, allow, deny, or an object of pattern rules");
|
|
371
|
-
}
|
|
372
|
-
for (const [pattern, setting] of Object.entries(value)) {
|
|
373
|
-
if (pattern.trim() === "") {
|
|
374
|
-
throwConfigError(sourcePath, `${keyPath}.${pattern}`, "must use a non-empty permission pattern");
|
|
375
|
-
}
|
|
376
|
-
if (!isPermissionSetting(setting)) {
|
|
377
|
-
throwConfigError(sourcePath, `${keyPath}.${pattern}`, "must be ask, allow, or deny");
|
|
290
|
+
throwConfigError(overlay.sourcePath, `${overlay.keyPath}.skills`, `unknown or disabled skill "${skill}"`);
|
|
378
291
|
}
|
|
379
292
|
}
|
|
380
293
|
}
|
|
@@ -398,6 +311,9 @@ function validAgentKeys(inventory) {
|
|
|
398
311
|
function throwConfigError(sourcePath, keyPath, message) {
|
|
399
312
|
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
|
|
400
313
|
}
|
|
314
|
+
function isSystemError(err) {
|
|
315
|
+
return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string";
|
|
316
|
+
}
|
|
401
317
|
|
|
402
318
|
// src/lib/skill-loader.ts
|
|
403
319
|
import path3 from "path";
|
|
@@ -567,7 +483,7 @@ function loadSkillAsCommand(loaded) {
|
|
|
567
483
|
config.subtask = loaded.subtask;
|
|
568
484
|
return config;
|
|
569
485
|
}
|
|
570
|
-
function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
|
|
486
|
+
function collectAgents(dir, disabledAgents, nativeAgents, overlays, authedProviders) {
|
|
571
487
|
const agents = {};
|
|
572
488
|
const agentList = findAgentsInDir(dir);
|
|
573
489
|
const disabledSet = new Set(disabledAgents);
|
|
@@ -582,12 +498,12 @@ function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
|
|
|
582
498
|
continue;
|
|
583
499
|
const config = loadAgentAsConfig(agentInfo);
|
|
584
500
|
if (config) {
|
|
585
|
-
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays);
|
|
501
|
+
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, authedProviders);
|
|
586
502
|
}
|
|
587
503
|
}
|
|
588
504
|
return agents;
|
|
589
505
|
}
|
|
590
|
-
function applyAgentOverlays(config, agentInfo, overlays) {
|
|
506
|
+
function applyAgentOverlays(config, agentInfo, overlays, authedProviders) {
|
|
591
507
|
const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
|
|
592
508
|
const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
|
|
593
509
|
const exactOverlay = overlays.agentsByTargetId.get(id);
|
|
@@ -599,7 +515,7 @@ function applyAgentOverlays(config, agentInfo, overlays) {
|
|
|
599
515
|
}
|
|
600
516
|
result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
|
|
601
517
|
if (agentInfo.category) {
|
|
602
|
-
const sourceModel = getSourceCategoryModel(agentInfo.category);
|
|
518
|
+
const sourceModel = getSourceCategoryModel(agentInfo.category, authedProviders);
|
|
603
519
|
if (sourceModel) {
|
|
604
520
|
result.model = sourceModel;
|
|
605
521
|
}
|
|
@@ -729,6 +645,7 @@ function collectEnabledSkillNames(dir, disabledSkills) {
|
|
|
729
645
|
}
|
|
730
646
|
function createConfigHandler(deps) {
|
|
731
647
|
const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
|
|
648
|
+
const readAuthProviders = deps.getAuthenticatedProviders ?? getAuthenticatedProviders;
|
|
732
649
|
return async (config) => {
|
|
733
650
|
const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
|
|
734
651
|
const existingAgents = { ...config.agent ?? {} };
|
|
@@ -744,7 +661,8 @@ function createConfigHandler(deps) {
|
|
|
744
661
|
enabledSkills: enabledSkillNames
|
|
745
662
|
});
|
|
746
663
|
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
747
|
-
const
|
|
664
|
+
const authedProviders = readAuthProviders();
|
|
665
|
+
const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, existingAgents, resolvedOverlays, authedProviders);
|
|
748
666
|
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
749
667
|
config.agent = {
|
|
750
668
|
...bundledAgents,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical OpenCode-accepted agent color tokens.
|
|
3
|
+
*
|
|
4
|
+
* These values are validated by OpenCode's `/config` HttpApi schema;
|
|
5
|
+
* any other value is rejected and surfaces to the user as
|
|
6
|
+
* `400: (empty response body)` on TUI launch.
|
|
7
|
+
*
|
|
8
|
+
* See anomalyco/opencode commits 2793502db / 96a534d8c for the
|
|
9
|
+
* server-side schema (PR #346 / v2.9.2 history).
|
|
10
|
+
*/
|
|
11
|
+
export declare const OPENCODE_AGENT_COLOR_TOKENS: readonly ["primary", "secondary", "accent", "success", "warning", "error", "info"];
|
|
12
|
+
/**
|
|
13
|
+
* Check whether a value is a valid agent color — either a hex literal
|
|
14
|
+
* (`#RRGGBB`) or a named token from `OPENCODE_AGENT_COLOR_TOKENS`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isValidAgentColor(value: string): boolean;
|
|
@@ -42,6 +42,27 @@ 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
|
-
|
|
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;
|
|
46
67
|
export declare function assertSourceCategoryModelCoverage(categories: string[]): void;
|
|
47
68
|
export declare function validateSourceCategoryModelDefaults(defaults?: Record<string, unknown>): void;
|
|
@@ -4,6 +4,8 @@ export interface ConfigHandlerDeps {
|
|
|
4
4
|
bundledSkillsDir: string;
|
|
5
5
|
bundledAgentsDir: string;
|
|
6
6
|
bundledCommandsDir: string;
|
|
7
|
+
/** Override for authenticated provider reader; for testing. */
|
|
8
|
+
getAuthenticatedProviders?: (rootDirOverride?: string) => ReadonlySet<string>;
|
|
7
9
|
}
|
|
8
10
|
export declare function toTitleCase(name: string): string;
|
|
9
11
|
export declare function formatAgentDescription(name: string, description: string | undefined): string;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema for the user-facing `systematic.json` / `systematic.jsonc` config.
|
|
3
|
+
*
|
|
4
|
+
* ## Zod 4 API notes (verified against zod@4.4.3 during implementation)
|
|
5
|
+
*
|
|
6
|
+
* - `z.toJSONSchema(schema, { target: 'draft-7' })` produces draft-07 JSON Schema.
|
|
7
|
+
* - `.default(value)` DOES round-trip into JSON Schema `default`.
|
|
8
|
+
* - `.meta({ description, examples })` attaches documentation metadata visible in
|
|
9
|
+
* JSON Schema output and via `schema.description`.
|
|
10
|
+
* - `z.object().strict()` rejects unknown keys with `unrecognized_keys` issues that
|
|
11
|
+
* include the offending key names and the path to the containing object.
|
|
12
|
+
* - `z.record(keySchema, valueSchema)` is the canonical 2-arg form (Zod 4 types
|
|
13
|
+
* require both key and value schemas). A single-arg overload works at runtime
|
|
14
|
+
* but is not reflected in the type declarations for this Zod 4 minor.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
export declare const AgentOverlaySchema: z.ZodObject<{
|
|
18
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
19
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
20
|
+
temperature: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
top_p: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
subagent: "subagent";
|
|
24
|
+
primary: "primary";
|
|
25
|
+
all: "all";
|
|
26
|
+
}>>;
|
|
27
|
+
color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
28
|
+
primary: "primary";
|
|
29
|
+
secondary: "secondary";
|
|
30
|
+
accent: "accent";
|
|
31
|
+
success: "success";
|
|
32
|
+
warning: "warning";
|
|
33
|
+
error: "error";
|
|
34
|
+
info: "info";
|
|
35
|
+
}>, z.ZodString]>>;
|
|
36
|
+
steps: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
disable: z.ZodOptional<z.ZodBoolean>;
|
|
39
|
+
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
40
|
+
permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
41
|
+
ask: "ask";
|
|
42
|
+
allow: "allow";
|
|
43
|
+
deny: "deny";
|
|
44
|
+
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
45
|
+
ask: "ask";
|
|
46
|
+
allow: "allow";
|
|
47
|
+
deny: "deny";
|
|
48
|
+
}>>]>>>;
|
|
49
|
+
}, z.core.$strict>;
|
|
50
|
+
export declare const CategoryOverlaySchema: z.ZodObject<{
|
|
51
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
52
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
53
|
+
temperature: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
top_p: z.ZodOptional<z.ZodNumber>;
|
|
55
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
56
|
+
subagent: "subagent";
|
|
57
|
+
primary: "primary";
|
|
58
|
+
all: "all";
|
|
59
|
+
}>>;
|
|
60
|
+
color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
61
|
+
primary: "primary";
|
|
62
|
+
secondary: "secondary";
|
|
63
|
+
accent: "accent";
|
|
64
|
+
success: "success";
|
|
65
|
+
warning: "warning";
|
|
66
|
+
error: "error";
|
|
67
|
+
info: "info";
|
|
68
|
+
}>, z.ZodString]>>;
|
|
69
|
+
steps: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
71
|
+
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
72
|
+
permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
73
|
+
ask: "ask";
|
|
74
|
+
allow: "allow";
|
|
75
|
+
deny: "deny";
|
|
76
|
+
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
77
|
+
ask: "ask";
|
|
78
|
+
allow: "allow";
|
|
79
|
+
deny: "deny";
|
|
80
|
+
}>>]>>>;
|
|
81
|
+
}, z.core.$strict>;
|
|
82
|
+
export declare const BootstrapSchema: z.ZodObject<{
|
|
83
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
84
|
+
file: z.ZodOptional<z.ZodString>;
|
|
85
|
+
}, z.core.$strict>;
|
|
86
|
+
export declare const SystematicConfigSchema: z.ZodObject<{
|
|
87
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
88
|
+
agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
89
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
90
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
91
|
+
temperature: z.ZodOptional<z.ZodNumber>;
|
|
92
|
+
top_p: z.ZodOptional<z.ZodNumber>;
|
|
93
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
94
|
+
subagent: "subagent";
|
|
95
|
+
primary: "primary";
|
|
96
|
+
all: "all";
|
|
97
|
+
}>>;
|
|
98
|
+
color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
99
|
+
primary: "primary";
|
|
100
|
+
secondary: "secondary";
|
|
101
|
+
accent: "accent";
|
|
102
|
+
success: "success";
|
|
103
|
+
warning: "warning";
|
|
104
|
+
error: "error";
|
|
105
|
+
info: "info";
|
|
106
|
+
}>, z.ZodString]>>;
|
|
107
|
+
steps: z.ZodOptional<z.ZodNumber>;
|
|
108
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
109
|
+
disable: z.ZodOptional<z.ZodBoolean>;
|
|
110
|
+
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
111
|
+
permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
112
|
+
ask: "ask";
|
|
113
|
+
allow: "allow";
|
|
114
|
+
deny: "deny";
|
|
115
|
+
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
116
|
+
ask: "ask";
|
|
117
|
+
allow: "allow";
|
|
118
|
+
deny: "deny";
|
|
119
|
+
}>>]>>>;
|
|
120
|
+
}, z.core.$strict>>>;
|
|
121
|
+
categories: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
122
|
+
model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
123
|
+
variant: z.ZodOptional<z.ZodString>;
|
|
124
|
+
temperature: z.ZodOptional<z.ZodNumber>;
|
|
125
|
+
top_p: z.ZodOptional<z.ZodNumber>;
|
|
126
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
127
|
+
subagent: "subagent";
|
|
128
|
+
primary: "primary";
|
|
129
|
+
all: "all";
|
|
130
|
+
}>>;
|
|
131
|
+
color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
132
|
+
primary: "primary";
|
|
133
|
+
secondary: "secondary";
|
|
134
|
+
accent: "accent";
|
|
135
|
+
success: "success";
|
|
136
|
+
warning: "warning";
|
|
137
|
+
error: "error";
|
|
138
|
+
info: "info";
|
|
139
|
+
}>, z.ZodString]>>;
|
|
140
|
+
steps: z.ZodOptional<z.ZodNumber>;
|
|
141
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
142
|
+
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
143
|
+
permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
|
|
144
|
+
ask: "ask";
|
|
145
|
+
allow: "allow";
|
|
146
|
+
deny: "deny";
|
|
147
|
+
}>, z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
148
|
+
ask: "ask";
|
|
149
|
+
allow: "allow";
|
|
150
|
+
deny: "deny";
|
|
151
|
+
}>>]>>>;
|
|
152
|
+
}, z.core.$strict>>>;
|
|
153
|
+
disabled_skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
154
|
+
disabled_agents: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
155
|
+
disabled_commands: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
156
|
+
bootstrap: z.ZodDefault<z.ZodObject<{
|
|
157
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
158
|
+
file: z.ZodOptional<z.ZodString>;
|
|
159
|
+
}, z.core.$strict>>;
|
|
160
|
+
}, z.core.$strict>;
|
|
161
|
+
export interface ValidationResult {
|
|
162
|
+
success: boolean;
|
|
163
|
+
data?: z.infer<typeof SystematicConfigSchema>;
|
|
164
|
+
errors?: readonly z.ZodIssue[];
|
|
165
|
+
}
|
|
166
|
+
export declare function validateConfig(input: unknown): ValidationResult;
|
|
167
|
+
export declare function assertSourceCategoryModelDefaults(defaults: Record<string, unknown>): void;
|
|
168
|
+
/**
|
|
169
|
+
* Overlay fields that require a project-or-higher trust source.
|
|
170
|
+
*
|
|
171
|
+
* This list is co-located with the schema definitions above so that any
|
|
172
|
+
* future field additions that need trust protection are added here at the
|
|
173
|
+
* same time. The regression tests in tests/unit/config-schema.test.ts
|
|
174
|
+
* assert that this list agrees with every field tagged `.meta({ trust:
|
|
175
|
+
* 'project-or-higher' })` in AgentOverlaySchema — preventing silent drift.
|
|
176
|
+
*
|
|
177
|
+
* Matches the hand-coded `SECURITY_OVERLAY_FIELDS` set in `src/lib/config.ts`.
|
|
178
|
+
*/
|
|
179
|
+
export declare const SECURITY_OVERLAY_FIELDS: readonly string[];
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
1
2
|
export interface BootstrapConfig {
|
|
2
3
|
enabled: boolean;
|
|
3
4
|
file?: string;
|
|
@@ -26,6 +27,26 @@ export interface SystematicConfig {
|
|
|
26
27
|
categories?: OverlayConfigMap;
|
|
27
28
|
}
|
|
28
29
|
export declare const DEFAULT_CONFIG: SystematicConfig;
|
|
30
|
+
interface RawSystematicConfig extends Omit<Partial<SystematicConfig>, 'agents' | 'categories'> {
|
|
31
|
+
agents?: unknown;
|
|
32
|
+
categories?: unknown;
|
|
33
|
+
}
|
|
34
|
+
interface ConfigSource {
|
|
35
|
+
path: string;
|
|
36
|
+
config: RawSystematicConfig;
|
|
37
|
+
trust: 'user' | 'project' | 'custom';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Type guard for errors thrown by the top-level config schema validator.
|
|
41
|
+
* Use this to distinguish schema validation failures from JSONC parse errors
|
|
42
|
+
* or file read errors.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isConfigSchemaError(err: unknown): err is Error & {
|
|
45
|
+
_tag: 'ConfigSchemaError';
|
|
46
|
+
filePath: string;
|
|
47
|
+
trust: ConfigSource['trust'];
|
|
48
|
+
issues: readonly z.core.$ZodIssue[];
|
|
49
|
+
};
|
|
29
50
|
export declare function loadConfig(projectDir: string): SystematicConfig;
|
|
30
51
|
export declare function loadConfigWithSources(projectDir: string): SourceAwareConfigResult;
|
|
31
52
|
export declare function getConfigPaths(projectDir: string): {
|
|
@@ -36,3 +57,4 @@ export declare function getConfigPaths(projectDir: string): {
|
|
|
36
57
|
userDir: string;
|
|
37
58
|
projectDir: string;
|
|
38
59
|
};
|
|
60
|
+
export {};
|