@fro.bot/systematic 2.11.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/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-mfy9dbdx.js";
16
+ } from "./index-g602hys2.js";
16
17
 
17
18
  // src/index.ts
18
19
  import fs4 from "fs";
@@ -88,19 +89,6 @@ var SOURCE_CATEGORY_MODEL_DEFAULTS = {
88
89
  review: ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
89
90
  workflow: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"]
90
91
  };
91
- var ALLOWED_OVERLAY_FIELDS = new Set([
92
- "model",
93
- "variant",
94
- "temperature",
95
- "top_p",
96
- "permission",
97
- "mode",
98
- "color",
99
- "steps",
100
- "hidden",
101
- "disable",
102
- "skills"
103
- ]);
104
92
  function buildBundledAgentInventory(agentsDir, disabledAgents) {
105
93
  const categories = readCategoryDirs(agentsDir);
106
94
  const agentsByQualifiedId = {};
@@ -221,17 +209,7 @@ function assertSourceCategoryModelCoverage(categories) {
221
209
  }
222
210
  }
223
211
  function validateSourceCategoryModelDefaults(defaults = SOURCE_CATEGORY_MODEL_DEFAULTS) {
224
- for (const [category, value] of Object.entries(defaults)) {
225
- if (!Array.isArray(value)) {
226
- throw new Error(`Source category model defaults: ${category} must be a non-empty array of provider/model strings`);
227
- }
228
- if (value.length === 0) {
229
- throw new Error(`Source category model defaults: ${category} must be a non-empty array of provider/model strings`);
230
- }
231
- for (const [index, model] of value.entries()) {
232
- validateModel("source category model defaults", `source category model defaults.${category}[${index}]`, model);
233
- }
234
- }
212
+ assertSourceCategoryModelDefaults(defaults);
235
213
  }
236
214
  function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
237
215
  const result = [];
@@ -281,150 +259,35 @@ function validateCategoryOverlays(inventory, overlays, enabledSkills) {
281
259
  }
282
260
  return result;
283
261
  }
284
- function validateOverlayFields(overlay, targetType, enabledSkills) {
285
- if (Object.hasOwn(overlay.value, "skills") && hasPermissionSkill(overlay.value.permission)) {
286
- throwConfigError(overlay.sourcePath, overlay.keyPath, "cannot set both skills and permission.skill in the same overlay object");
287
- }
288
- for (const [field, value] of Object.entries(overlay.value)) {
289
- const keyPath = `${overlay.keyPath}.${field}`;
290
- if (!ALLOWED_OVERLAY_FIELDS.has(field)) {
291
- throwConfigError(overlay.sourcePath, keyPath, `unsupported agent overlay field "${field}"`);
292
- }
293
- if (targetType === "category" && field === "disable") {
294
- throwConfigError(overlay.sourcePath, keyPath, "disable is only valid for exact agent overlays");
295
- }
296
- validateOverlayFieldValue(overlay.sourcePath, keyPath, field, value, enabledSkills);
297
- }
298
- }
299
262
  function hasPermissionSkill(permission) {
300
263
  return isRecord(permission) && Object.hasOwn(permission, "skill");
301
264
  }
302
- function validateOverlayFieldValue(sourcePath, keyPath, field, value, enabledSkills) {
303
- switch (field) {
304
- case "model":
305
- if (value === null)
306
- return;
307
- validateModel(sourcePath, keyPath, value);
308
- return;
309
- case "variant":
310
- validateNonEmptyString(sourcePath, keyPath, value);
311
- return;
312
- case "temperature":
313
- validateTemperature(sourcePath, keyPath, value);
314
- return;
315
- case "top_p":
316
- validateTopP(sourcePath, keyPath, value);
317
- return;
318
- case "permission":
319
- validatePermission(sourcePath, keyPath, value);
320
- return;
321
- case "mode":
322
- validateMode(sourcePath, keyPath, value);
323
- return;
324
- case "color":
325
- validateColor(sourcePath, keyPath, value);
326
- return;
327
- case "steps":
328
- validatePositiveInteger(sourcePath, keyPath, value);
329
- return;
330
- case "hidden":
331
- case "disable":
332
- validateBoolean(sourcePath, keyPath, value);
333
- return;
334
- case "skills":
335
- validateSkills(sourcePath, keyPath, value, enabledSkills);
336
- return;
337
- }
338
- }
339
- function validateModel(sourcePath, keyPath, value) {
340
- if (typeof value !== "string") {
341
- throwConfigError(sourcePath, keyPath, "must be a provider/model string");
342
- }
343
- if (value !== value.trim() || /\s/.test(value)) {
344
- throwConfigError(sourcePath, keyPath, "must be a provider/model string");
345
- }
346
- const slashIndex = value.indexOf("/");
347
- if (value === "" || slashIndex <= 0 || slashIndex === value.length - 1) {
348
- throwConfigError(sourcePath, keyPath, "must be a provider/model string");
349
- }
350
- }
351
- function validateNonEmptyString(sourcePath, keyPath, value) {
352
- if (typeof value !== "string" || value === "" || value !== value.trim()) {
353
- throwConfigError(sourcePath, keyPath, "must be a non-empty string");
354
- }
355
- }
356
- function validateTemperature(sourcePath, keyPath, value) {
357
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
358
- throwConfigError(sourcePath, keyPath, "must be a non-negative finite number");
359
- }
360
- }
361
- function validateTopP(sourcePath, keyPath, value) {
362
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
363
- throwConfigError(sourcePath, keyPath, "must be a number from 0 to 1");
364
- }
365
- }
366
- function validatePositiveInteger(sourcePath, keyPath, value) {
367
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
368
- throwConfigError(sourcePath, keyPath, "must be a positive integer");
369
- }
370
- }
371
- function validateBoolean(sourcePath, keyPath, value) {
372
- if (typeof value !== "boolean") {
373
- throwConfigError(sourcePath, keyPath, "must be a boolean");
374
- }
375
- }
376
- function validateMode(sourcePath, keyPath, value) {
377
- if (!isAgentMode(value)) {
378
- 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");
379
268
  }
269
+ const result = parseOverlayShape(overlay, targetType);
270
+ validateOverlaySkills(overlay, result.skills, enabledSkills);
380
271
  }
381
- function validateColor(sourcePath, keyPath, value) {
382
- if (typeof value !== "string" || value !== value.trim() || !isOpenCodeColor(value)) {
383
- throwConfigError(sourcePath, keyPath, "must be an OpenCode-compatible color string");
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]);
384
277
  }
278
+ return { skills: result.data.skills };
385
279
  }
386
- function isOpenCodeColor(value) {
387
- if (value === "")
388
- return false;
389
- if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value))
390
- return true;
391
- 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);
392
284
  }
393
- function validateSkills(sourcePath, keyPath, value, enabledSkills) {
394
- if (!Array.isArray(value) || !value.every((skill) => typeof skill === "string" && skill !== "" && skill === skill.trim())) {
395
- throwConfigError(sourcePath, keyPath, "must be an array of non-empty strings");
396
- }
397
- if (!enabledSkills)
285
+ function validateOverlaySkills(overlay, skills, enabledSkills) {
286
+ if (!enabledSkills || !skills)
398
287
  return;
399
- for (const skill of value) {
288
+ for (const skill of skills) {
400
289
  if (!enabledSkills.has(skill)) {
401
- throwConfigError(sourcePath, keyPath, `unknown or disabled skill "${skill}"`);
402
- }
403
- }
404
- }
405
- function validatePermission(sourcePath, keyPath, value) {
406
- if (!isRecord(value)) {
407
- throwConfigError(sourcePath, keyPath, "must be an object");
408
- }
409
- for (const [toolKey, rule] of Object.entries(value)) {
410
- if (toolKey.trim() === "") {
411
- throwConfigError(sourcePath, `${keyPath}.${toolKey}`, "must use a non-empty tool key");
412
- }
413
- validatePermissionRule(sourcePath, `${keyPath}.${toolKey}`, rule);
414
- }
415
- }
416
- function validatePermissionRule(sourcePath, keyPath, value) {
417
- if (isPermissionSetting(value))
418
- return;
419
- if (!isRecord(value)) {
420
- throwConfigError(sourcePath, keyPath, "must be ask, allow, deny, or an object of pattern rules");
421
- }
422
- for (const [pattern, setting] of Object.entries(value)) {
423
- if (pattern.trim() === "") {
424
- throwConfigError(sourcePath, `${keyPath}.${pattern}`, "must use a non-empty permission pattern");
425
- }
426
- if (!isPermissionSetting(setting)) {
427
- throwConfigError(sourcePath, `${keyPath}.${pattern}`, "must be ask, allow, or deny");
290
+ throwConfigError(overlay.sourcePath, `${overlay.keyPath}.skills`, `unknown or disabled skill "${skill}"`);
428
291
  }
429
292
  }
430
293
  }
@@ -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;
@@ -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[];
@@ -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 {};