@fro.bot/systematic 2.13.2 → 2.14.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.
@@ -14,205 +14,742 @@ var __export = (target, all) => {
14
14
  });
15
15
  };
16
16
 
17
- // src/lib/config.ts
17
+ // src/lib/converter.ts
18
18
  import fs from "fs";
19
- import os from "os";
20
- import path from "path";
21
19
 
22
- // node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
23
- function createScanner(text, ignoreTrivia = false) {
24
- const len = text.length;
25
- let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
26
- function scanHexDigits(count, exact) {
27
- let digits = 0;
28
- let value2 = 0;
29
- while (digits < count || !exact) {
30
- let ch = text.charCodeAt(pos);
31
- if (ch >= 48 && ch <= 57) {
32
- value2 = value2 * 16 + ch - 48;
33
- } else if (ch >= 65 && ch <= 70) {
34
- value2 = value2 * 16 + ch - 65 + 10;
35
- } else if (ch >= 97 && ch <= 102) {
36
- value2 = value2 * 16 + ch - 97 + 10;
37
- } else {
38
- break;
39
- }
40
- pos++;
41
- digits++;
42
- }
43
- if (digits < count) {
44
- value2 = -1;
45
- }
46
- return value2;
20
+ // src/lib/frontmatter.ts
21
+ import yaml from "js-yaml";
22
+ function parseFrontmatter(content) {
23
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n?---\r?\n([\s\S]*)$/;
24
+ const match = content.match(frontmatterRegex);
25
+ if (!match) {
26
+ return {
27
+ data: {},
28
+ body: content,
29
+ hadFrontmatter: false,
30
+ parseError: false
31
+ };
47
32
  }
48
- function setPosition(newPosition) {
49
- pos = newPosition;
50
- value = "";
51
- tokenOffset = 0;
52
- token = 16;
53
- scanError = 0;
33
+ const yamlContent = match[1];
34
+ const body = match[2];
35
+ try {
36
+ const parsed = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA });
37
+ const data = parsed ?? {};
38
+ return { data, body, hadFrontmatter: true, parseError: false };
39
+ } catch {
40
+ return { data: {}, body, hadFrontmatter: true, parseError: true };
54
41
  }
55
- function scanNumber() {
56
- let start = pos;
57
- if (text.charCodeAt(pos) === 48) {
58
- pos++;
59
- } else {
60
- pos++;
61
- while (pos < text.length && isDigit(text.charCodeAt(pos))) {
62
- pos++;
63
- }
64
- }
65
- if (pos < text.length && text.charCodeAt(pos) === 46) {
66
- pos++;
67
- if (pos < text.length && isDigit(text.charCodeAt(pos))) {
68
- pos++;
69
- while (pos < text.length && isDigit(text.charCodeAt(pos))) {
70
- pos++;
71
- }
72
- } else {
73
- scanError = 3;
74
- return text.substring(start, pos);
75
- }
76
- }
77
- let end = pos;
78
- if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
79
- pos++;
80
- if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
81
- pos++;
82
- }
83
- if (pos < text.length && isDigit(text.charCodeAt(pos))) {
84
- pos++;
85
- while (pos < text.length && isDigit(text.charCodeAt(pos))) {
86
- pos++;
87
- }
88
- end = pos;
89
- } else {
90
- scanError = 3;
91
- }
92
- }
93
- return text.substring(start, end);
42
+ }
43
+ function formatFrontmatter(data) {
44
+ if (Object.keys(data).length === 0) {
45
+ return ["---", "---"].join(`
46
+ `);
94
47
  }
95
- function scanString() {
96
- let result = "", start = pos;
97
- while (true) {
98
- if (pos >= len) {
99
- result += text.substring(start, pos);
100
- scanError = 2;
101
- break;
102
- }
103
- const ch = text.charCodeAt(pos);
104
- if (ch === 34) {
105
- result += text.substring(start, pos);
106
- pos++;
107
- break;
108
- }
109
- if (ch === 92) {
110
- result += text.substring(start, pos);
111
- pos++;
112
- if (pos >= len) {
113
- scanError = 2;
114
- break;
115
- }
116
- const ch2 = text.charCodeAt(pos++);
117
- switch (ch2) {
118
- case 34:
119
- result += '"';
120
- break;
121
- case 92:
122
- result += "\\";
123
- break;
124
- case 47:
125
- result += "/";
126
- break;
127
- case 98:
128
- result += "\b";
129
- break;
130
- case 102:
131
- result += "\f";
132
- break;
133
- case 110:
134
- result += `
135
- `;
136
- break;
137
- case 114:
138
- result += "\r";
139
- break;
140
- case 116:
141
- result += "\t";
142
- break;
143
- case 117:
144
- const ch3 = scanHexDigits(4, true);
145
- if (ch3 >= 0) {
146
- result += String.fromCharCode(ch3);
147
- } else {
148
- scanError = 4;
149
- }
150
- break;
151
- default:
152
- scanError = 5;
153
- }
154
- start = pos;
155
- continue;
156
- }
157
- if (ch >= 0 && ch <= 31) {
158
- if (isLineBreak(ch)) {
159
- result += text.substring(start, pos);
160
- scanError = 2;
161
- break;
162
- } else {
163
- scanError = 6;
164
- }
165
- }
166
- pos++;
48
+ const yamlContent = yaml.dump(data, {
49
+ schema: yaml.JSON_SCHEMA,
50
+ lineWidth: -1,
51
+ noRefs: true
52
+ }).trimEnd();
53
+ return ["---", yamlContent, "---"].join(`
54
+ `);
55
+ }
56
+
57
+ // src/lib/validation.ts
58
+ function isRecord(value) {
59
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60
+ }
61
+ function isPermissionSetting(value) {
62
+ return value === "ask" || value === "allow" || value === "deny";
63
+ }
64
+ function isToolsMap(value) {
65
+ if (!isRecord(value))
66
+ return false;
67
+ return Object.values(value).every((entry) => typeof entry === "boolean");
68
+ }
69
+ function isAgentMode(value) {
70
+ return value === "subagent" || value === "primary" || value === "all";
71
+ }
72
+ function extractSimplePermission(data, key) {
73
+ if (!(key in data))
74
+ return;
75
+ const value = data[key];
76
+ return isPermissionSetting(value) ? value : null;
77
+ }
78
+ function extractBashPermission(data) {
79
+ if (!("bash" in data))
80
+ return;
81
+ const bash = data.bash;
82
+ if (isPermissionSetting(bash))
83
+ return bash;
84
+ if (isRecord(bash)) {
85
+ const entries = Object.entries(bash);
86
+ if (entries.every(([, setting]) => isPermissionSetting(setting))) {
87
+ return Object.fromEntries(entries);
167
88
  }
168
- return result;
169
89
  }
170
- function scanNext() {
171
- value = "";
172
- scanError = 0;
173
- tokenOffset = pos;
174
- lineStartOffset = lineNumber;
175
- prevTokenLineStartOffset = tokenLineStartOffset;
176
- if (pos >= len) {
177
- tokenOffset = len;
178
- return token = 17;
179
- }
180
- let code = text.charCodeAt(pos);
181
- if (isWhiteSpace(code)) {
182
- do {
183
- pos++;
184
- value += String.fromCharCode(code);
185
- code = text.charCodeAt(pos);
186
- } while (isWhiteSpace(code));
187
- return token = 15;
188
- }
189
- if (isLineBreak(code)) {
190
- pos++;
191
- value += String.fromCharCode(code);
192
- if (code === 13 && text.charCodeAt(pos) === 10) {
193
- pos++;
194
- value += `
195
- `;
196
- }
197
- lineNumber++;
198
- tokenLineStartOffset = pos;
199
- return token = 14;
200
- }
201
- switch (code) {
202
- case 123:
203
- pos++;
204
- return token = 1;
205
- case 125:
206
- pos++;
207
- return token = 2;
208
- case 91:
209
- pos++;
210
- return token = 3;
211
- case 93:
212
- pos++;
213
- return token = 4;
214
- case 58:
215
- pos++;
90
+ return null;
91
+ }
92
+ function buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory, task, skill) {
93
+ const permission = {};
94
+ if (edit)
95
+ permission.edit = edit;
96
+ if (bash)
97
+ permission.bash = bash;
98
+ if (webfetch)
99
+ permission.webfetch = webfetch;
100
+ if (doom_loop)
101
+ permission.doom_loop = doom_loop;
102
+ if (external_directory)
103
+ permission.external_directory = external_directory;
104
+ if (task)
105
+ permission.task = task;
106
+ if (skill)
107
+ permission.skill = skill;
108
+ return Object.keys(permission).length > 0 ? permission : undefined;
109
+ }
110
+ function normalizePermission(value) {
111
+ if (!isRecord(value))
112
+ return;
113
+ const bash = extractBashPermission(value);
114
+ if (bash === null)
115
+ return;
116
+ const edit = extractSimplePermission(value, "edit");
117
+ if (edit === null)
118
+ return;
119
+ const webfetch = extractSimplePermission(value, "webfetch");
120
+ if (webfetch === null)
121
+ return;
122
+ const doom_loop = extractSimplePermission(value, "doom_loop");
123
+ if (doom_loop === null)
124
+ return;
125
+ const external_directory = extractSimplePermission(value, "external_directory");
126
+ if (external_directory === null)
127
+ return;
128
+ const task = extractSimplePermission(value, "task");
129
+ if (task === null)
130
+ return;
131
+ const skill = extractSimplePermission(value, "skill");
132
+ if (skill === null)
133
+ return;
134
+ return buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory, task, skill);
135
+ }
136
+ function extractString(data, key, fallback = "") {
137
+ const value = data[key];
138
+ return typeof value === "string" ? value : fallback;
139
+ }
140
+ function extractNonEmptyString(data, key) {
141
+ const value = data[key];
142
+ if (typeof value !== "string")
143
+ return;
144
+ const trimmed = value.trim();
145
+ return trimmed !== "" ? trimmed : undefined;
146
+ }
147
+ function extractNumber(data, key) {
148
+ const value = data[key];
149
+ return typeof value === "number" ? value : undefined;
150
+ }
151
+ function extractBoolean(data, key) {
152
+ const value = data[key];
153
+ if (typeof value === "boolean")
154
+ return value;
155
+ if (typeof value === "string") {
156
+ const normalized = value.trim().toLowerCase();
157
+ if (normalized === "true")
158
+ return true;
159
+ if (normalized === "false")
160
+ return false;
161
+ }
162
+ return;
163
+ }
164
+
165
+ // src/lib/converter.ts
166
+ var CONVERTER_VERSION = 2;
167
+ var cache = new Map;
168
+ var TOOL_MAPPINGS = [
169
+ [/\bTask\s+tool\b/gi, "task tool"],
170
+ [/\bTask\s+([\w-]+)\s*:/g, "task $1:"],
171
+ [/\bTask\s+([\w-]+)\s*\(/g, "task $1("],
172
+ [/\bTask\s*\(/g, "task("],
173
+ [/\bTask\b(?=\s+to\s+\w)/g, "task"],
174
+ [/\bTodoWrite\b/g, "todowrite"],
175
+ [/\bAskUserQuestion\b/g, "question"],
176
+ [/\bWebSearch\b/g, "google_search"],
177
+ [/\bRead\b(?=\s+tool|\s+to\s+|\()/g, "read"],
178
+ [/\bWrite\b(?=\s+tool|\s+to\s+|\()/g, "write"],
179
+ [/\bEdit\b(?=\s+tool|\s+to\s+|\()/g, "edit"],
180
+ [/\bBash\b(?=\s+tool|\s+to\s+|\()/g, "bash"],
181
+ [/\bGrep\b(?=\s+tool|\s+to\s+|\()/g, "grep"],
182
+ [/\bGlob\b(?=\s+tool|\s+to\s+|\()/g, "glob"],
183
+ [/\bWebFetch\b/g, "webfetch"],
184
+ [/\bSkill\b(?=\s+tool|\s*\()/g, "skill"]
185
+ ];
186
+ var PATH_REPLACEMENTS = [
187
+ [/\.claude\/skills\//g, ".opencode/skills/"],
188
+ [/\.claude\/commands\//g, ".opencode/commands/"],
189
+ [/\.claude\/agents\//g, ".opencode/agents/"],
190
+ [/~\/\.claude\//g, "~/.config/opencode/"],
191
+ [/CLAUDE\.md/g, "AGENTS.md"],
192
+ [/\/compound-engineering:/g, "/systematic:"],
193
+ [/compound-engineering:/g, "systematic:"]
194
+ ];
195
+ var TOOL_NAME_MAP = {
196
+ task: "task",
197
+ todowrite: "todowrite",
198
+ askuserquestion: "question",
199
+ websearch: "google_search",
200
+ webfetch: "webfetch",
201
+ skill: "skill",
202
+ read: "read",
203
+ write: "write",
204
+ edit: "edit",
205
+ bash: "bash",
206
+ grep: "grep",
207
+ glob: "glob"
208
+ };
209
+ var PERMISSION_MODE_MAP = {
210
+ full: {
211
+ edit: "allow",
212
+ bash: "allow",
213
+ webfetch: "allow"
214
+ },
215
+ default: {
216
+ edit: "ask",
217
+ bash: "ask",
218
+ webfetch: "ask"
219
+ },
220
+ plan: {
221
+ edit: "deny",
222
+ bash: "deny",
223
+ webfetch: "ask"
224
+ },
225
+ bypassPermissions: {
226
+ edit: "allow",
227
+ bash: "allow",
228
+ webfetch: "allow"
229
+ }
230
+ };
231
+ function inferTemperature(name, description) {
232
+ const sample = `${name} ${description ?? ""}`.toLowerCase();
233
+ if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
234
+ return 0.1;
235
+ }
236
+ if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
237
+ return 0.2;
238
+ }
239
+ if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
240
+ return 0.3;
241
+ }
242
+ if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
243
+ return 0.6;
244
+ }
245
+ return 0.3;
246
+ }
247
+ var CODE_BLOCK_PATTERN = /```[\s\S]*?```|`[^`\n]+`/g;
248
+ function transformBody(body) {
249
+ const codeBlocks = [];
250
+ let placeholderIndex = 0;
251
+ const withPlaceholders = body.replace(CODE_BLOCK_PATTERN, (match) => {
252
+ codeBlocks.push(match);
253
+ return `__CODE_BLOCK_${placeholderIndex++}__`;
254
+ });
255
+ let result = withPlaceholders;
256
+ for (const [pattern, replacement] of TOOL_MAPPINGS) {
257
+ result = result.replace(pattern, replacement);
258
+ }
259
+ for (const [pattern, replacement] of PATH_REPLACEMENTS) {
260
+ result = result.replace(pattern, replacement);
261
+ }
262
+ for (let i = 0;i < codeBlocks.length; i++) {
263
+ result = result.replace(`__CODE_BLOCK_${i}__`, codeBlocks[i]);
264
+ }
265
+ return result;
266
+ }
267
+ function normalizeModel(model) {
268
+ if (model.includes("/"))
269
+ return model;
270
+ if (model === "inherit")
271
+ return model;
272
+ if (/^claude-/.test(model))
273
+ return `anthropic/${model}`;
274
+ if (/^(gpt-|o1-|o3-)/.test(model))
275
+ return `openai/${model}`;
276
+ if (/^gemini-/.test(model))
277
+ return `google/${model}`;
278
+ return `anthropic/${model}`;
279
+ }
280
+ function canonicalizeToolName(name) {
281
+ const lower = name.trim().toLowerCase();
282
+ return TOOL_NAME_MAP[lower] ?? lower;
283
+ }
284
+ function isValidSteps(value) {
285
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0;
286
+ }
287
+ function mapStepsField(data) {
288
+ if (data.steps !== undefined) {
289
+ if (isValidSteps(data.steps)) {
290
+ delete data.maxTurns;
291
+ delete data.maxSteps;
292
+ }
293
+ return;
294
+ }
295
+ const candidates = [];
296
+ if (isValidSteps(data.maxTurns))
297
+ candidates.push(data.maxTurns);
298
+ if (isValidSteps(data.maxSteps))
299
+ candidates.push(data.maxSteps);
300
+ if (candidates.length > 0) {
301
+ data.steps = Math.min(...candidates);
302
+ delete data.maxTurns;
303
+ delete data.maxSteps;
304
+ }
305
+ }
306
+ function mapToolsField(data) {
307
+ if (data.tools !== undefined && !Array.isArray(data.tools)) {
308
+ if (isToolsMap(data.tools)) {
309
+ mergeDisallowedTools(data);
310
+ }
311
+ return;
312
+ }
313
+ if (Array.isArray(data.tools)) {
314
+ const toolsMap = {};
315
+ for (const tool of data.tools) {
316
+ if (typeof tool === "string") {
317
+ toolsMap[canonicalizeToolName(tool)] = true;
318
+ }
319
+ }
320
+ if (Object.keys(toolsMap).length > 0) {
321
+ data.tools = toolsMap;
322
+ } else {
323
+ delete data.tools;
324
+ }
325
+ }
326
+ mergeDisallowedTools(data);
327
+ }
328
+ function mergeDisallowedTools(data) {
329
+ if (!Array.isArray(data.disallowedTools))
330
+ return;
331
+ const existing = isToolsMap(data.tools) ? data.tools : {};
332
+ for (const tool of data.disallowedTools) {
333
+ if (typeof tool === "string") {
334
+ existing[canonicalizeToolName(tool)] = false;
335
+ }
336
+ }
337
+ if (Object.keys(existing).length > 0) {
338
+ data.tools = existing;
339
+ }
340
+ delete data.disallowedTools;
341
+ }
342
+ function mapPermissionMode(data) {
343
+ if (data.permission !== undefined) {
344
+ const normalized = normalizePermission(data.permission);
345
+ if (normalized) {
346
+ data.permission = normalized;
347
+ delete data.permissionMode;
348
+ return;
349
+ }
350
+ }
351
+ if (typeof data.permissionMode !== "string")
352
+ return;
353
+ const mapped = PERMISSION_MODE_MAP[data.permissionMode];
354
+ data.permission = mapped ?? { edit: "ask", bash: "ask", webfetch: "ask" };
355
+ delete data.permissionMode;
356
+ }
357
+ function mapHiddenField(data) {
358
+ if (data["disable-model-invocation"] === true || data.disableModelInvocation === true) {
359
+ data.hidden = true;
360
+ delete data["disable-model-invocation"];
361
+ delete data.disableModelInvocation;
362
+ }
363
+ }
364
+ function normalizeModelField(data) {
365
+ if (typeof data.model === "string" && data.model !== "inherit") {
366
+ data.model = normalizeModel(data.model);
367
+ } else if (data.model === "inherit") {
368
+ delete data.model;
369
+ }
370
+ }
371
+ function transformAgentFrontmatter(data, agentMode) {
372
+ const result = { ...data };
373
+ result.mode = isAgentMode(data.mode) ? data.mode : agentMode;
374
+ const name = typeof data.name === "string" ? data.name : "";
375
+ const description = typeof data.description === "string" ? data.description : "";
376
+ if (description) {
377
+ result.description = description;
378
+ } else if (name) {
379
+ result.description = `${name} agent`;
380
+ }
381
+ normalizeModelField(result);
382
+ result.temperature = typeof data.temperature === "number" ? data.temperature : inferTemperature(name, description);
383
+ mapStepsField(result);
384
+ mapToolsField(result);
385
+ mapPermissionMode(result);
386
+ mapHiddenField(result);
387
+ return result;
388
+ }
389
+ function transformSkillFrontmatter(data) {
390
+ const result = { ...data };
391
+ normalizeModelField(result);
392
+ if (result.context === "fork") {
393
+ result.subtask = true;
394
+ }
395
+ return result;
396
+ }
397
+ function transformCommandFrontmatter(data) {
398
+ const result = { ...data };
399
+ normalizeModelField(result);
400
+ return result;
401
+ }
402
+ function convertContent(content, type, options = {}) {
403
+ if (content === "")
404
+ return "";
405
+ const { data, body, hadFrontmatter, parseError } = parseFrontmatter(content);
406
+ if (!hadFrontmatter) {
407
+ return options.skipBodyTransform ? content : transformBody(content);
408
+ }
409
+ if (parseError) {
410
+ return options.skipBodyTransform ? content : transformBody(content);
411
+ }
412
+ const shouldTransformBody = !options.skipBodyTransform;
413
+ const transformedBody = shouldTransformBody ? transformBody(body) : body;
414
+ if (type === "agent") {
415
+ const agentMode = options.agentMode ?? "subagent";
416
+ const transformedData = transformAgentFrontmatter(data, agentMode);
417
+ return `${formatFrontmatter(transformedData)}
418
+ ${transformedBody}`;
419
+ }
420
+ if (type === "skill") {
421
+ const transformedData = transformSkillFrontmatter(data);
422
+ return `${formatFrontmatter(transformedData)}
423
+ ${transformedBody}`;
424
+ }
425
+ if (type === "command") {
426
+ const transformedData = transformCommandFrontmatter(data);
427
+ return `${formatFrontmatter(transformedData)}
428
+ ${transformedBody}`;
429
+ }
430
+ return content;
431
+ }
432
+ function convertFileWithCache(filePath, type, options = {}) {
433
+ const fd = fs.openSync(filePath, "r");
434
+ try {
435
+ const stats = fs.fstatSync(fd);
436
+ const cacheKey = `${CONVERTER_VERSION}:${filePath}:${type}:${options.source ?? "bundled"}:${options.agentMode ?? "subagent"}:${options.skipBodyTransform ?? false}`;
437
+ const cached = cache.get(cacheKey);
438
+ if (cached != null && cached.mtimeMs === stats.mtimeMs) {
439
+ return cached.converted;
440
+ }
441
+ const content = fs.readFileSync(fd, "utf8");
442
+ const converted = convertContent(content, type, options);
443
+ cache.set(cacheKey, { mtimeMs: stats.mtimeMs, converted });
444
+ return converted;
445
+ } finally {
446
+ fs.closeSync(fd);
447
+ }
448
+ }
449
+
450
+ // src/lib/skills.ts
451
+ import fs3 from "fs";
452
+ import path2 from "path";
453
+
454
+ // src/lib/walk-dir.ts
455
+ import fs2 from "fs";
456
+ import path from "path";
457
+ function walkDir(rootDir, options = {}) {
458
+ const { maxDepth = 3, filter } = options;
459
+ const results = [];
460
+ if (!fs2.existsSync(rootDir))
461
+ return results;
462
+ function recurse(currentDir, depth, category) {
463
+ if (depth > maxDepth)
464
+ return;
465
+ const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
466
+ for (const entry of entries) {
467
+ const fullPath = path.join(currentDir, entry.name);
468
+ const walkEntry = {
469
+ path: fullPath,
470
+ name: entry.name,
471
+ isDirectory: entry.isDirectory(),
472
+ depth,
473
+ category
474
+ };
475
+ if (!filter || filter(walkEntry)) {
476
+ results.push(walkEntry);
477
+ }
478
+ if (entry.isDirectory()) {
479
+ recurse(fullPath, depth + 1, entry.name);
480
+ }
481
+ }
482
+ }
483
+ recurse(rootDir, 0);
484
+ return results;
485
+ }
486
+
487
+ // src/lib/skills.ts
488
+ function extractFrontmatter(filePath) {
489
+ try {
490
+ const content = fs3.readFileSync(filePath, "utf8");
491
+ const { data, parseError } = parseFrontmatter(content);
492
+ if (parseError) {
493
+ return { name: "", description: "" };
494
+ }
495
+ const metadataRaw = data.metadata;
496
+ let metadata;
497
+ if (isRecord(metadataRaw)) {
498
+ const entries = Object.entries(metadataRaw);
499
+ if (entries.every(([, v]) => typeof v === "string")) {
500
+ metadata = Object.fromEntries(entries);
501
+ }
502
+ }
503
+ const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
504
+ const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
505
+ return {
506
+ name: extractString(data, "name"),
507
+ description: extractString(data, "description"),
508
+ license: extractNonEmptyString(data, "license"),
509
+ compatibility: extractNonEmptyString(data, "compatibility"),
510
+ metadata,
511
+ disableModelInvocation: extractBoolean(data, "disable-model-invocation"),
512
+ userInvocable: extractBoolean(data, "user-invocable"),
513
+ subtask: data.context === "fork" ? true : extractBoolean(data, "subtask") ?? undefined,
514
+ agent: extractNonEmptyString(data, "agent"),
515
+ model: extractNonEmptyString(data, "model"),
516
+ argumentHint: argumentHint !== "" ? argumentHint : undefined,
517
+ allowedTools: extractNonEmptyString(data, "allowed-tools")
518
+ };
519
+ } catch {
520
+ return { name: "", description: "" };
521
+ }
522
+ }
523
+ function findSkillsInDir(dir, maxDepth = 3) {
524
+ const skills = [];
525
+ const entries = walkDir(dir, {
526
+ maxDepth,
527
+ filter: (e) => e.isDirectory
528
+ });
529
+ for (const entry of entries) {
530
+ const skillFile = path2.join(entry.path, "SKILL.md");
531
+ if (fs3.existsSync(skillFile)) {
532
+ const frontmatter = extractFrontmatter(skillFile);
533
+ skills.push({
534
+ path: entry.path,
535
+ skillFile,
536
+ name: frontmatter.name || entry.name,
537
+ description: frontmatter.description || "",
538
+ license: frontmatter.license,
539
+ compatibility: frontmatter.compatibility,
540
+ metadata: frontmatter.metadata,
541
+ disableModelInvocation: frontmatter.disableModelInvocation,
542
+ userInvocable: frontmatter.userInvocable,
543
+ subtask: frontmatter.subtask,
544
+ agent: frontmatter.agent,
545
+ model: frontmatter.model,
546
+ argumentHint: frontmatter.argumentHint,
547
+ allowedTools: frontmatter.allowedTools
548
+ });
549
+ }
550
+ }
551
+ return skills;
552
+ }
553
+
554
+ // src/lib/config.ts
555
+ import fs4 from "fs";
556
+ import os from "os";
557
+ import path3 from "path";
558
+
559
+ // node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
560
+ function createScanner(text, ignoreTrivia = false) {
561
+ const len = text.length;
562
+ let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
563
+ function scanHexDigits(count, exact) {
564
+ let digits = 0;
565
+ let value2 = 0;
566
+ while (digits < count || !exact) {
567
+ let ch = text.charCodeAt(pos);
568
+ if (ch >= 48 && ch <= 57) {
569
+ value2 = value2 * 16 + ch - 48;
570
+ } else if (ch >= 65 && ch <= 70) {
571
+ value2 = value2 * 16 + ch - 65 + 10;
572
+ } else if (ch >= 97 && ch <= 102) {
573
+ value2 = value2 * 16 + ch - 97 + 10;
574
+ } else {
575
+ break;
576
+ }
577
+ pos++;
578
+ digits++;
579
+ }
580
+ if (digits < count) {
581
+ value2 = -1;
582
+ }
583
+ return value2;
584
+ }
585
+ function setPosition(newPosition) {
586
+ pos = newPosition;
587
+ value = "";
588
+ tokenOffset = 0;
589
+ token = 16;
590
+ scanError = 0;
591
+ }
592
+ function scanNumber() {
593
+ let start = pos;
594
+ if (text.charCodeAt(pos) === 48) {
595
+ pos++;
596
+ } else {
597
+ pos++;
598
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
599
+ pos++;
600
+ }
601
+ }
602
+ if (pos < text.length && text.charCodeAt(pos) === 46) {
603
+ pos++;
604
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
605
+ pos++;
606
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
607
+ pos++;
608
+ }
609
+ } else {
610
+ scanError = 3;
611
+ return text.substring(start, pos);
612
+ }
613
+ }
614
+ let end = pos;
615
+ if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
616
+ pos++;
617
+ if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
618
+ pos++;
619
+ }
620
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
621
+ pos++;
622
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
623
+ pos++;
624
+ }
625
+ end = pos;
626
+ } else {
627
+ scanError = 3;
628
+ }
629
+ }
630
+ return text.substring(start, end);
631
+ }
632
+ function scanString() {
633
+ let result = "", start = pos;
634
+ while (true) {
635
+ if (pos >= len) {
636
+ result += text.substring(start, pos);
637
+ scanError = 2;
638
+ break;
639
+ }
640
+ const ch = text.charCodeAt(pos);
641
+ if (ch === 34) {
642
+ result += text.substring(start, pos);
643
+ pos++;
644
+ break;
645
+ }
646
+ if (ch === 92) {
647
+ result += text.substring(start, pos);
648
+ pos++;
649
+ if (pos >= len) {
650
+ scanError = 2;
651
+ break;
652
+ }
653
+ const ch2 = text.charCodeAt(pos++);
654
+ switch (ch2) {
655
+ case 34:
656
+ result += '"';
657
+ break;
658
+ case 92:
659
+ result += "\\";
660
+ break;
661
+ case 47:
662
+ result += "/";
663
+ break;
664
+ case 98:
665
+ result += "\b";
666
+ break;
667
+ case 102:
668
+ result += "\f";
669
+ break;
670
+ case 110:
671
+ result += `
672
+ `;
673
+ break;
674
+ case 114:
675
+ result += "\r";
676
+ break;
677
+ case 116:
678
+ result += "\t";
679
+ break;
680
+ case 117:
681
+ const ch3 = scanHexDigits(4, true);
682
+ if (ch3 >= 0) {
683
+ result += String.fromCharCode(ch3);
684
+ } else {
685
+ scanError = 4;
686
+ }
687
+ break;
688
+ default:
689
+ scanError = 5;
690
+ }
691
+ start = pos;
692
+ continue;
693
+ }
694
+ if (ch >= 0 && ch <= 31) {
695
+ if (isLineBreak(ch)) {
696
+ result += text.substring(start, pos);
697
+ scanError = 2;
698
+ break;
699
+ } else {
700
+ scanError = 6;
701
+ }
702
+ }
703
+ pos++;
704
+ }
705
+ return result;
706
+ }
707
+ function scanNext() {
708
+ value = "";
709
+ scanError = 0;
710
+ tokenOffset = pos;
711
+ lineStartOffset = lineNumber;
712
+ prevTokenLineStartOffset = tokenLineStartOffset;
713
+ if (pos >= len) {
714
+ tokenOffset = len;
715
+ return token = 17;
716
+ }
717
+ let code = text.charCodeAt(pos);
718
+ if (isWhiteSpace(code)) {
719
+ do {
720
+ pos++;
721
+ value += String.fromCharCode(code);
722
+ code = text.charCodeAt(pos);
723
+ } while (isWhiteSpace(code));
724
+ return token = 15;
725
+ }
726
+ if (isLineBreak(code)) {
727
+ pos++;
728
+ value += String.fromCharCode(code);
729
+ if (code === 13 && text.charCodeAt(pos) === 10) {
730
+ pos++;
731
+ value += `
732
+ `;
733
+ }
734
+ lineNumber++;
735
+ tokenLineStartOffset = pos;
736
+ return token = 14;
737
+ }
738
+ switch (code) {
739
+ case 123:
740
+ pos++;
741
+ return token = 1;
742
+ case 125:
743
+ pos++;
744
+ return token = 2;
745
+ case 91:
746
+ pos++;
747
+ return token = 3;
748
+ case 93:
749
+ pos++;
750
+ return token = 4;
751
+ case 58:
752
+ pos++;
216
753
  return token = 6;
217
754
  case 44:
218
755
  pos++;
@@ -1624,10 +2161,10 @@ function mergeDefs(...defs) {
1624
2161
  function cloneDef(schema) {
1625
2162
  return mergeDefs(schema._zod.def);
1626
2163
  }
1627
- function getElementAtPath(obj, path) {
1628
- if (!path)
2164
+ function getElementAtPath(obj, path3) {
2165
+ if (!path3)
1629
2166
  return obj;
1630
- return path.reduce((acc, key) => acc?.[key], obj);
2167
+ return path3.reduce((acc, key) => acc?.[key], obj);
1631
2168
  }
1632
2169
  function promiseAllObject(promisesObj) {
1633
2170
  const keys = Object.keys(promisesObj);
@@ -2035,11 +2572,11 @@ function explicitlyAborted(x, startIndex = 0) {
2035
2572
  }
2036
2573
  return false;
2037
2574
  }
2038
- function prefixIssues(path, issues) {
2575
+ function prefixIssues(path3, issues) {
2039
2576
  return issues.map((iss) => {
2040
2577
  var _a2;
2041
2578
  (_a2 = iss).path ?? (_a2.path = []);
2042
- iss.path.unshift(path);
2579
+ iss.path.unshift(path3);
2043
2580
  return iss;
2044
2581
  });
2045
2582
  }
@@ -2186,16 +2723,16 @@ function flattenError(error, mapper = (issue2) => issue2.message) {
2186
2723
  }
2187
2724
  function formatError(error, mapper = (issue2) => issue2.message) {
2188
2725
  const fieldErrors = { _errors: [] };
2189
- const processError = (error2, path = []) => {
2726
+ const processError = (error2, path3 = []) => {
2190
2727
  for (const issue2 of error2.issues) {
2191
2728
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2192
- issue2.errors.map((issues) => processError({ issues }, [...path, ...issue2.path]));
2729
+ issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
2193
2730
  } else if (issue2.code === "invalid_key") {
2194
- processError({ issues: issue2.issues }, [...path, ...issue2.path]);
2731
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2195
2732
  } else if (issue2.code === "invalid_element") {
2196
- processError({ issues: issue2.issues }, [...path, ...issue2.path]);
2733
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2197
2734
  } else {
2198
- const fullpath = [...path, ...issue2.path];
2735
+ const fullpath = [...path3, ...issue2.path];
2199
2736
  if (fullpath.length === 0) {
2200
2737
  fieldErrors._errors.push(mapper(issue2));
2201
2738
  } else {
@@ -2222,17 +2759,17 @@ function formatError(error, mapper = (issue2) => issue2.message) {
2222
2759
  }
2223
2760
  function treeifyError(error, mapper = (issue2) => issue2.message) {
2224
2761
  const result = { errors: [] };
2225
- const processError = (error2, path = []) => {
2762
+ const processError = (error2, path3 = []) => {
2226
2763
  var _a2, _b;
2227
2764
  for (const issue2 of error2.issues) {
2228
2765
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2229
- issue2.errors.map((issues) => processError({ issues }, [...path, ...issue2.path]));
2766
+ issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
2230
2767
  } else if (issue2.code === "invalid_key") {
2231
- processError({ issues: issue2.issues }, [...path, ...issue2.path]);
2768
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2232
2769
  } else if (issue2.code === "invalid_element") {
2233
- processError({ issues: issue2.issues }, [...path, ...issue2.path]);
2770
+ processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2234
2771
  } else {
2235
- const fullpath = [...path, ...issue2.path];
2772
+ const fullpath = [...path3, ...issue2.path];
2236
2773
  if (fullpath.length === 0) {
2237
2774
  result.errors.push(mapper(issue2));
2238
2775
  continue;
@@ -2264,8 +2801,8 @@ function treeifyError(error, mapper = (issue2) => issue2.message) {
2264
2801
  }
2265
2802
  function toDotPath(_path) {
2266
2803
  const segs = [];
2267
- const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2268
- for (const seg of path) {
2804
+ const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2805
+ for (const seg of path3) {
2269
2806
  if (typeof seg === "number")
2270
2807
  segs.push(`[${seg}]`);
2271
2808
  else if (typeof seg === "symbol")
@@ -14724,13 +15261,13 @@ function resolveRef(ref, ctx) {
14724
15261
  if (!ref.startsWith("#")) {
14725
15262
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14726
15263
  }
14727
- const path = ref.slice(1).split("/").filter(Boolean);
14728
- if (path.length === 0) {
15264
+ const path3 = ref.slice(1).split("/").filter(Boolean);
15265
+ if (path3.length === 0) {
14729
15266
  return ctx.rootSchema;
14730
15267
  }
14731
15268
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14732
- if (path[0] === defsKey) {
14733
- const key = path[1];
15269
+ if (path3[0] === defsKey) {
15270
+ const key = path3[1];
14734
15271
  if (!key || !ctx.defs[key]) {
14735
15272
  throw new Error(`Reference not found: ${ref}`);
14736
15273
  }
@@ -14981,1117 +15518,595 @@ function convertBaseSchema(schema, ctx) {
14981
15518
  const rest = items && typeof items === "object" && !Array.isArray(items) ? convertSchema(items, ctx) : undefined;
14982
15519
  if (rest) {
14983
15520
  zodSchema = z.tuple(tupleItems).rest(rest);
14984
- } else {
14985
- zodSchema = z.tuple(tupleItems);
14986
- }
14987
- if (typeof schema.minItems === "number") {
14988
- zodSchema = zodSchema.check(z.minLength(schema.minItems));
14989
- }
14990
- if (typeof schema.maxItems === "number") {
14991
- zodSchema = zodSchema.check(z.maxLength(schema.maxItems));
14992
- }
14993
- } else if (Array.isArray(items)) {
14994
- const tupleItems = items.map((item) => convertSchema(item, ctx));
14995
- const rest = schema.additionalItems && typeof schema.additionalItems === "object" ? convertSchema(schema.additionalItems, ctx) : undefined;
14996
- if (rest) {
14997
- zodSchema = z.tuple(tupleItems).rest(rest);
14998
- } else {
14999
- zodSchema = z.tuple(tupleItems);
15000
- }
15001
- if (typeof schema.minItems === "number") {
15002
- zodSchema = zodSchema.check(z.minLength(schema.minItems));
15003
- }
15004
- if (typeof schema.maxItems === "number") {
15005
- zodSchema = zodSchema.check(z.maxLength(schema.maxItems));
15006
- }
15007
- } else if (items !== undefined) {
15008
- const element = convertSchema(items, ctx);
15009
- let arraySchema = z.array(element);
15010
- if (typeof schema.minItems === "number") {
15011
- arraySchema = arraySchema.min(schema.minItems);
15012
- }
15013
- if (typeof schema.maxItems === "number") {
15014
- arraySchema = arraySchema.max(schema.maxItems);
15015
- }
15016
- zodSchema = arraySchema;
15017
- } else {
15018
- zodSchema = z.array(z.any());
15019
- }
15020
- break;
15021
- }
15022
- default:
15023
- throw new Error(`Unsupported type: ${type}`);
15024
- }
15025
- return zodSchema;
15026
- }
15027
- function convertSchema(schema, ctx) {
15028
- if (typeof schema === "boolean") {
15029
- return schema ? z.any() : z.never();
15030
- }
15031
- let baseSchema = convertBaseSchema(schema, ctx);
15032
- const hasExplicitType = schema.type || schema.enum !== undefined || schema.const !== undefined;
15033
- if (schema.anyOf && Array.isArray(schema.anyOf)) {
15034
- const options = schema.anyOf.map((s) => convertSchema(s, ctx));
15035
- const anyOfUnion = z.union(options);
15036
- baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
15037
- }
15038
- if (schema.oneOf && Array.isArray(schema.oneOf)) {
15039
- const options = schema.oneOf.map((s) => convertSchema(s, ctx));
15040
- const oneOfUnion = z.xor(options);
15041
- baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
15042
- }
15043
- if (schema.allOf && Array.isArray(schema.allOf)) {
15044
- if (schema.allOf.length === 0) {
15045
- baseSchema = hasExplicitType ? baseSchema : z.any();
15046
- } else {
15047
- let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0], ctx);
15048
- const startIdx = hasExplicitType ? 0 : 1;
15049
- for (let i = startIdx;i < schema.allOf.length; i++) {
15050
- result = z.intersection(result, convertSchema(schema.allOf[i], ctx));
15051
- }
15052
- baseSchema = result;
15053
- }
15054
- }
15055
- if (schema.nullable === true && ctx.version === "openapi-3.0") {
15056
- baseSchema = z.nullable(baseSchema);
15057
- }
15058
- if (schema.readOnly === true) {
15059
- baseSchema = z.readonly(baseSchema);
15060
- }
15061
- if (schema.default !== undefined) {
15062
- baseSchema = baseSchema.default(schema.default);
15063
- }
15064
- const extraMeta = {};
15065
- const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
15066
- for (const key of coreMetadataKeys) {
15067
- if (key in schema) {
15068
- extraMeta[key] = schema[key];
15069
- }
15070
- }
15071
- const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
15072
- for (const key of contentMetadataKeys) {
15073
- if (key in schema) {
15074
- extraMeta[key] = schema[key];
15075
- }
15076
- }
15077
- for (const key of Object.keys(schema)) {
15078
- if (!RECOGNIZED_KEYS.has(key)) {
15079
- extraMeta[key] = schema[key];
15080
- }
15081
- }
15082
- if (Object.keys(extraMeta).length > 0) {
15083
- ctx.registry.add(baseSchema, extraMeta);
15084
- }
15085
- if (schema.description) {
15086
- baseSchema = baseSchema.describe(schema.description);
15087
- }
15088
- return baseSchema;
15089
- }
15090
- function fromJSONSchema(schema, params) {
15091
- if (typeof schema === "boolean") {
15092
- return schema ? z.any() : z.never();
15093
- }
15094
- let normalized;
15095
- try {
15096
- normalized = JSON.parse(JSON.stringify(schema));
15097
- } catch {
15098
- throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
15099
- }
15100
- const version2 = detectVersion(normalized, params?.defaultTarget);
15101
- const defs = normalized.$defs || normalized.definitions || {};
15102
- const ctx = {
15103
- version: version2,
15104
- defs,
15105
- refs: new Map,
15106
- processing: new Set,
15107
- rootSchema: normalized,
15108
- registry: params?.registry ?? globalRegistry
15109
- };
15110
- return convertSchema(normalized, ctx);
15111
- }
15112
- // node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/coerce.js
15113
- var exports_coerce = {};
15114
- __export(exports_coerce, {
15115
- string: () => string3,
15116
- number: () => number3,
15117
- date: () => date4,
15118
- boolean: () => boolean3,
15119
- bigint: () => bigint3
15120
- });
15121
- function string3(params) {
15122
- return _coercedString(ZodString, params);
15123
- }
15124
- function number3(params) {
15125
- return _coercedNumber(ZodNumber, params);
15126
- }
15127
- function boolean3(params) {
15128
- return _coercedBoolean(ZodBoolean, params);
15129
- }
15130
- function bigint3(params) {
15131
- return _coercedBigint(ZodBigInt, params);
15132
- }
15133
- function date4(params) {
15134
- return _coercedDate(ZodDate, params);
15135
- }
15136
-
15137
- // node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
15138
- config(en_default());
15139
- // src/lib/agent-colors.ts
15140
- var OPENCODE_AGENT_COLOR_TOKENS = [
15141
- "primary",
15142
- "secondary",
15143
- "accent",
15144
- "success",
15145
- "warning",
15146
- "error",
15147
- "info"
15148
- ];
15149
-
15150
- // src/lib/config-schema.ts
15151
- var permissionSettingSchema = exports_external.enum(["ask", "allow", "deny"]);
15152
- var permissionRuleSchema = exports_external.union([
15153
- permissionSettingSchema,
15154
- exports_external.record(exports_external.string(), permissionSettingSchema)
15155
- ]);
15156
- var permissionSchema = exports_external.record(exports_external.string(), permissionRuleSchema).meta({
15157
- description: "Permission overrides per tool",
15158
- examples: [{ edit: "allow", bash: { curl: "allow", rm: "deny" } }]
15159
- });
15160
- var MODEL_FORMAT_MESSAGE = 'must be in provider/model format (e.g., "anthropic/claude-sonnet-4")';
15161
- var MODEL_FORMAT_REGEX = /^[^\s/]+\/\S+$/;
15162
- var modelSchema = exports_external.string().min(1).regex(MODEL_FORMAT_REGEX, MODEL_FORMAT_MESSAGE).nullable().meta({
15163
- description: "Model identifier in provider/model format, or null to inherit parent model",
15164
- examples: ["anthropic/claude-sonnet-4", null]
15165
- });
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
- description: "Model variant identifier",
15168
- examples: ["v2", "extended"]
15169
- });
15170
- var temperatureSchema = exports_external.number().min(0).meta({
15171
- description: "Sampling temperature (\u22650; 0 = deterministic)",
15172
- examples: [0.1, 0.7, 0]
15173
- });
15174
- var topPSchema = exports_external.number().min(0).max(1).meta({
15175
- description: "Nucleus sampling parameter (0 to 1)",
15176
- examples: [0.9, 0.1, 1]
15177
- });
15178
- var modeSchema = exports_external.enum(["subagent", "primary", "all"]).meta({
15179
- description: "Agent execution mode",
15180
- examples: ["subagent", "primary", "all"]
15181
- });
15182
- var colorSchema = exports_external.union([
15183
- exports_external.enum(OPENCODE_AGENT_COLOR_TOKENS),
15184
- exports_external.string().regex(/^#[0-9a-fA-F]{6}$/, "must be a 6-digit hex color (#RRGGBB)")
15185
- ]).meta({
15186
- description: "Agent color \u2014 named token from OpenCode or 6-digit hex color (#RRGGBB)",
15187
- examples: ["primary", "#ff6600"]
15188
- });
15189
- var stepsSchema = exports_external.number().int().positive().meta({
15190
- description: "Maximum execution steps (positive integer)",
15191
- examples: [10, 50]
15192
- });
15193
- var hiddenSchema = exports_external.boolean().meta({
15194
- description: "Hide agent from UI",
15195
- examples: [true, false]
15196
- });
15197
- var disableSchema = exports_external.boolean().meta({
15198
- description: "Disable this agent overlay",
15199
- examples: [true, false]
15200
- });
15201
- var skillsSchema = exports_external.array(exports_external.string().min(1)).meta({
15202
- description: "Skills enabled for this agent",
15203
- examples: [["ce:plan", "ce:review"]]
15204
- });
15205
- function trustAny(schema) {
15206
- return schema.meta({ trust: "any" });
15207
- }
15208
- function trustProtected(schema) {
15209
- return schema.meta({ trust: "project-or-higher" });
15210
- }
15211
- var AgentOverlaySchema = exports_external.object({
15212
- model: trustProtected(modelSchema).optional(),
15213
- variant: trustProtected(variantSchema).optional(),
15214
- temperature: trustAny(temperatureSchema).optional(),
15215
- top_p: trustAny(topPSchema).optional(),
15216
- mode: modeSchema.optional(),
15217
- color: colorSchema.optional(),
15218
- steps: stepsSchema.optional(),
15219
- hidden: hiddenSchema.optional(),
15220
- disable: disableSchema.optional(),
15221
- skills: trustProtected(skillsSchema).optional(),
15222
- permission: trustProtected(permissionSchema).optional()
15223
- }).strict().meta({
15224
- description: "Per-agent configuration overlay",
15225
- examples: [
15226
- {
15227
- model: "anthropic/claude-opus-4-7",
15228
- temperature: 0.1,
15229
- mode: "subagent"
15521
+ } else {
15522
+ zodSchema = z.tuple(tupleItems);
15523
+ }
15524
+ if (typeof schema.minItems === "number") {
15525
+ zodSchema = zodSchema.check(z.minLength(schema.minItems));
15526
+ }
15527
+ if (typeof schema.maxItems === "number") {
15528
+ zodSchema = zodSchema.check(z.maxLength(schema.maxItems));
15529
+ }
15530
+ } else if (Array.isArray(items)) {
15531
+ const tupleItems = items.map((item) => convertSchema(item, ctx));
15532
+ const rest = schema.additionalItems && typeof schema.additionalItems === "object" ? convertSchema(schema.additionalItems, ctx) : undefined;
15533
+ if (rest) {
15534
+ zodSchema = z.tuple(tupleItems).rest(rest);
15535
+ } else {
15536
+ zodSchema = z.tuple(tupleItems);
15537
+ }
15538
+ if (typeof schema.minItems === "number") {
15539
+ zodSchema = zodSchema.check(z.minLength(schema.minItems));
15540
+ }
15541
+ if (typeof schema.maxItems === "number") {
15542
+ zodSchema = zodSchema.check(z.maxLength(schema.maxItems));
15543
+ }
15544
+ } else if (items !== undefined) {
15545
+ const element = convertSchema(items, ctx);
15546
+ let arraySchema = z.array(element);
15547
+ if (typeof schema.minItems === "number") {
15548
+ arraySchema = arraySchema.min(schema.minItems);
15549
+ }
15550
+ if (typeof schema.maxItems === "number") {
15551
+ arraySchema = arraySchema.max(schema.maxItems);
15552
+ }
15553
+ zodSchema = arraySchema;
15554
+ } else {
15555
+ zodSchema = z.array(z.any());
15556
+ }
15557
+ break;
15230
15558
  }
15231
- ]
15232
- });
15233
- var CategoryOverlaySchema = exports_external.object({
15234
- model: trustProtected(modelSchema).optional(),
15235
- variant: trustProtected(variantSchema).optional(),
15236
- temperature: trustAny(temperatureSchema).optional(),
15237
- top_p: trustAny(topPSchema).optional(),
15238
- mode: modeSchema.optional(),
15239
- color: colorSchema.optional(),
15240
- steps: stepsSchema.optional(),
15241
- hidden: hiddenSchema.optional(),
15242
- skills: trustProtected(skillsSchema).optional(),
15243
- permission: trustProtected(permissionSchema).optional()
15244
- }).strict().meta({
15245
- description: "Per-category configuration overlay (same fields as agent minus disable)",
15246
- examples: [{ model: "anthropic/claude-opus-4-7", temperature: 0.1 }]
15247
- });
15248
- var BootstrapSchema = exports_external.object({
15249
- enabled: exports_external.boolean().default(true).meta({
15250
- description: "Enable bootstrap prompt injection into every conversation",
15251
- examples: [true, false]
15252
- }),
15253
- file: exports_external.string().optional().meta({
15254
- description: "Path to a custom bootstrap prompt file",
15255
- examples: ["~/.config/opencode/bootstrap.md"]
15256
- })
15257
- }).strict().meta({
15258
- description: "Bootstrap prompt configuration",
15259
- examples: [{ enabled: true }, { enabled: false }]
15260
- });
15261
- var SystematicConfigSchema = exports_external.object({
15262
- $schema: exports_external.string().url().optional().meta({
15263
- description: "JSON Schema URL for IDE autocomplete. The value is informational only \u2014 the loader does not fetch or validate against it. Add this to enable IDE schema activation and field-level autocomplete in editors that support JSON Schema (VSCode, Zed, IntelliJ).",
15264
- examples: [
15265
- "https://fro.bot/systematic/schemas/v2/systematic-config.schema.json"
15266
- ]
15267
- }),
15268
- agents: exports_external.record(exports_external.string(), AgentOverlaySchema).default({}).meta({
15269
- description: "Per-agent configuration overlays keyed by agent name",
15270
- examples: [{ "correctness-reviewer": { temperature: 0.1 } }, {}]
15271
- }),
15272
- categories: exports_external.record(exports_external.string(), CategoryOverlaySchema).default({}).meta({
15273
- description: "Per-category configuration overlays keyed by category name",
15274
- examples: [{ review: { model: "anthropic/claude-opus-4-7" } }, {}]
15275
- }),
15276
- disabled_skills: exports_external.array(exports_external.string()).default([]).meta({
15277
- description: "Array of skill names to disable globally",
15278
- examples: [["ce:plan", "ce:review"]]
15279
- }),
15280
- disabled_agents: exports_external.array(exports_external.string()).default([]).meta({
15281
- description: "Array of agent names to disable globally",
15282
- examples: [["previous-comments-reviewer", "cli-readiness-reviewer"]]
15283
- }),
15284
- disabled_commands: exports_external.array(exports_external.string()).default([]).meta({
15285
- description: "Array of command names to disable globally",
15286
- examples: [["deprecated-migration-helper"]]
15287
- }),
15288
- bootstrap: BootstrapSchema.default({ enabled: true }).meta({
15289
- description: "Bootstrap prompt configuration",
15290
- examples: [
15291
- { enabled: true },
15292
- { enabled: false, file: ".opencode/custom-prompt.md" }
15293
- ]
15294
- })
15295
- }).strict().meta({
15296
- description: "Systematic user configuration file (systematic.json / systematic.jsonc)",
15297
- examples: [{ disabled_skills: ["ce:plan"], bootstrap: { enabled: false } }]
15298
- });
15299
- var SECURITY_OVERLAY_FIELDS = [
15300
- "model",
15301
- "variant",
15302
- "skills",
15303
- "permission"
15304
- ];
15305
-
15306
- // src/lib/config.ts
15307
- var DEFAULT_CONFIG = {
15308
- disabled_skills: [],
15309
- disabled_agents: [],
15310
- disabled_commands: [],
15311
- bootstrap: {
15312
- enabled: true
15313
- },
15314
- agents: {},
15315
- categories: {}
15316
- };
15317
- var SECURITY_OVERLAY_FIELDS2 = new Set(SECURITY_OVERLAY_FIELDS);
15318
- function resolveConfigPath(dir, basename) {
15319
- const jsoncPath = path.join(dir, `${basename}.jsonc`);
15320
- if (fs.existsSync(jsoncPath))
15321
- return jsoncPath;
15322
- return path.join(dir, `${basename}.json`);
15323
- }
15324
- function isErrorWithCode(error51) {
15325
- return error51 instanceof Error && "code" in error51;
15326
- }
15327
- function loadJsoncFile(filePath) {
15328
- let content;
15329
- try {
15330
- content = fs.readFileSync(filePath, "utf-8");
15331
- } catch (error51) {
15332
- if (isErrorWithCode(error51) && error51.code === "ENOENT")
15333
- return null;
15334
- throw new Error(`Invalid Systematic config in ${filePath}: unable to read file`, { cause: error51 });
15335
- }
15336
- const errors3 = [];
15337
- const parsed = parse2(content, errors3);
15338
- if (errors3.length > 0) {
15339
- const error51 = errors3[0];
15340
- const message = error51 ? `${printParseErrorCode(error51.error)} at offset ${error51.offset}` : "unknown parse error";
15341
- throw new Error(`Invalid Systematic config in ${filePath}: JSONC parse error: ${message}`);
15342
- }
15343
- if (!isRecord(parsed)) {
15344
- throw new Error(`Invalid Systematic config in ${filePath}: root must be an object`);
15345
- }
15346
- return parsed;
15347
- }
15348
- function throwTopLevelConfigSchemaError(filePath, trust, issues) {
15349
- const issue2 = issues[0];
15350
- if (!issue2) {
15351
- throw Object.assign(new Error(`Invalid Systematic config in ${filePath}: schema validation failed`), { _tag: "ConfigSchemaError", filePath, trust, issues });
15559
+ default:
15560
+ throw new Error(`Unsupported type: ${type}`);
15352
15561
  }
15353
- const fieldPath = issue2.code === "unrecognized_keys" ? issue2.message.match(/"([^"]+)"/)?.[1] ?? issue2.path.join(".") : issue2.path.join(".");
15354
- const message = fieldPath ? `Invalid Systematic config in ${filePath}: ${fieldPath} ${issue2.message}` : `Invalid Systematic config in ${filePath}: ${issue2.message}`;
15355
- throw Object.assign(new Error(message), {
15356
- _tag: "ConfigSchemaError",
15357
- filePath,
15358
- trust,
15359
- issues
15360
- });
15562
+ return zodSchema;
15361
15563
  }
15362
- function loadConfigSource(filePath, trust) {
15363
- const rawConfig = loadJsoncFile(filePath);
15364
- if (!rawConfig)
15365
- return null;
15366
- const result = SystematicConfigSchema.safeParse(rawConfig);
15367
- if (!result.success) {
15368
- throwTopLevelConfigSchemaError(filePath, trust, result.error.issues);
15564
+ function convertSchema(schema, ctx) {
15565
+ if (typeof schema === "boolean") {
15566
+ return schema ? z.any() : z.never();
15369
15567
  }
15370
- return { path: filePath, config: rawConfig, trust };
15371
- }
15372
- function isRecord(value) {
15373
- return typeof value === "object" && value !== null && !Array.isArray(value);
15374
- }
15375
- function mergeArraysUnique(arr1, arr2) {
15376
- const set2 = new Set;
15377
- if (arr1)
15378
- for (const item of arr1)
15379
- set2.add(item);
15380
- if (arr2)
15381
- for (const item of arr2)
15382
- set2.add(item);
15383
- return Array.from(set2);
15384
- }
15385
- function loadConfig(projectDir) {
15386
- return loadConfigWithSources(projectDir).config;
15387
- }
15388
- function loadConfigWithSources(projectDir) {
15389
- const paths = getConfigPaths(projectDir);
15390
- const userSource = loadConfigSource(paths.userConfig, "user");
15391
- const projectSource = loadConfigSource(paths.projectConfig, "project");
15392
- const customSource = paths.customConfig ? loadConfigSource(paths.customConfig, "custom") : null;
15393
- const sources = [userSource, projectSource, customSource].filter((source) => source !== null);
15394
- const overlays = mergeOverlaySources(sources);
15395
- const userConfig = userSource?.config;
15396
- const projectConfig = projectSource?.config;
15397
- const customConfig = customSource?.config;
15398
- const result = {
15399
- disabled_skills: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_skills, userConfig?.disabled_skills), projectConfig?.disabled_skills), customConfig?.disabled_skills),
15400
- disabled_agents: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_agents, userConfig?.disabled_agents), projectConfig?.disabled_agents), customConfig?.disabled_agents),
15401
- disabled_commands: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_commands, userConfig?.disabled_commands), projectConfig?.disabled_commands), customConfig?.disabled_commands),
15402
- bootstrap: {
15403
- ...DEFAULT_CONFIG.bootstrap,
15404
- ...userConfig?.bootstrap,
15405
- ...projectConfig?.bootstrap,
15406
- ...customConfig?.bootstrap
15407
- },
15408
- agents: overlayValues(overlays.agents),
15409
- categories: overlayValues(overlays.categories)
15410
- };
15411
- return { config: result, overlays };
15412
- }
15413
- function mergeOverlaySources(sources) {
15414
- const result = {
15415
- agents: {},
15416
- categories: {}
15417
- };
15418
- for (const source of sources) {
15419
- mergeOverlayMap(result.agents, source, "agents");
15420
- mergeOverlayMap(result.categories, source, "categories");
15568
+ let baseSchema = convertBaseSchema(schema, ctx);
15569
+ const hasExplicitType = schema.type || schema.enum !== undefined || schema.const !== undefined;
15570
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
15571
+ const options = schema.anyOf.map((s) => convertSchema(s, ctx));
15572
+ const anyOfUnion = z.union(options);
15573
+ baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
15421
15574
  }
15422
- return result;
15423
- }
15424
- function mergeOverlayMap(target, source, mapKey) {
15425
- const overlayMap = source.config[mapKey];
15426
- if (overlayMap === undefined)
15427
- return;
15428
- if (!isRecord(overlayMap)) {
15429
- throwInvalidOverlay(source.path, mapKey);
15575
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
15576
+ const options = schema.oneOf.map((s) => convertSchema(s, ctx));
15577
+ const oneOfUnion = z.xor(options);
15578
+ baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
15430
15579
  }
15431
- for (const [key, value] of Object.entries(overlayMap)) {
15432
- const keyPath = `${mapKey}.${key}`;
15433
- if (!isRecord(value)) {
15434
- throwInvalidOverlay(source.path, keyPath);
15435
- }
15436
- if (source.trust === "project") {
15437
- rejectProjectSecurityOverlay(source.path, keyPath, value);
15580
+ if (schema.allOf && Array.isArray(schema.allOf)) {
15581
+ if (schema.allOf.length === 0) {
15582
+ baseSchema = hasExplicitType ? baseSchema : z.any();
15583
+ } else {
15584
+ let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0], ctx);
15585
+ const startIdx = hasExplicitType ? 0 : 1;
15586
+ for (let i = startIdx;i < schema.allOf.length; i++) {
15587
+ result = z.intersection(result, convertSchema(schema.allOf[i], ctx));
15588
+ }
15589
+ baseSchema = result;
15438
15590
  }
15439
- const previous = target[key];
15440
- const nextValue = source.trust === "project" && previous ? preserveSecurityFields(previous.value, value) : value;
15441
- target[key] = {
15442
- value: nextValue,
15443
- sourcePath: source.path,
15444
- keyPath
15445
- };
15446
15591
  }
15447
- }
15448
- function rejectProjectSecurityOverlay(sourcePath, keyPath, value) {
15449
- for (const field of SECURITY_OVERLAY_FIELDS2) {
15450
- if (Object.hasOwn(value, field)) {
15451
- throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath}.${field} is only valid in user config or OPENCODE_CONFIG_DIR config`);
15452
- }
15592
+ if (schema.nullable === true && ctx.version === "openapi-3.0") {
15593
+ baseSchema = z.nullable(baseSchema);
15453
15594
  }
15454
- }
15455
- function preserveSecurityFields(previous, next) {
15456
- const result = { ...next };
15457
- for (const field of SECURITY_OVERLAY_FIELDS2) {
15458
- if (Object.hasOwn(previous, field)) {
15459
- result[field] = previous[field];
15460
- }
15595
+ if (schema.readOnly === true) {
15596
+ baseSchema = z.readonly(baseSchema);
15461
15597
  }
15462
- return result;
15463
- }
15464
- function overlayValues(overlays) {
15465
- const result = {};
15466
- for (const [key, overlay] of Object.entries(overlays)) {
15467
- result[key] = overlay.value;
15598
+ if (schema.default !== undefined) {
15599
+ baseSchema = baseSchema.default(schema.default);
15468
15600
  }
15469
- return result;
15470
- }
15471
- function throwInvalidOverlay(sourcePath, keyPath) {
15472
- throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} must be an object`);
15473
- }
15474
- function getConfigPaths(projectDir) {
15475
- const homeDir = os.homedir();
15476
- const customConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
15477
- const result = {
15478
- userConfig: resolveConfigPath(path.join(homeDir, ".config/opencode"), "systematic"),
15479
- projectConfig: resolveConfigPath(path.join(projectDir, ".opencode"), "systematic"),
15480
- userDir: path.join(homeDir, ".config/opencode/systematic"),
15481
- projectDir: path.join(projectDir, ".opencode/systematic"),
15482
- ...customConfigDir && {
15483
- customConfig: resolveConfigPath(customConfigDir, "systematic"),
15484
- customDir: path.join(customConfigDir, "systematic")
15601
+ const extraMeta = {};
15602
+ const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
15603
+ for (const key of coreMetadataKeys) {
15604
+ if (key in schema) {
15605
+ extraMeta[key] = schema[key];
15485
15606
  }
15486
- };
15487
- return result;
15488
- }
15489
-
15490
- // src/lib/frontmatter.ts
15491
- import yaml from "js-yaml";
15492
- function parseFrontmatter(content) {
15493
- const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n?---\r?\n([\s\S]*)$/;
15494
- const match = content.match(frontmatterRegex);
15495
- if (!match) {
15496
- return {
15497
- data: {},
15498
- body: content,
15499
- hadFrontmatter: false,
15500
- parseError: false
15501
- };
15502
- }
15503
- const yamlContent = match[1];
15504
- const body = match[2];
15505
- try {
15506
- const parsed = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA });
15507
- const data = parsed ?? {};
15508
- return { data, body, hadFrontmatter: true, parseError: false };
15509
- } catch {
15510
- return { data: {}, body, hadFrontmatter: true, parseError: true };
15511
15607
  }
15512
- }
15513
- function formatFrontmatter(data) {
15514
- if (Object.keys(data).length === 0) {
15515
- return ["---", "---"].join(`
15516
- `);
15608
+ const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
15609
+ for (const key of contentMetadataKeys) {
15610
+ if (key in schema) {
15611
+ extraMeta[key] = schema[key];
15612
+ }
15517
15613
  }
15518
- const yamlContent = yaml.dump(data, {
15519
- schema: yaml.JSON_SCHEMA,
15520
- lineWidth: -1,
15521
- noRefs: true
15522
- }).trimEnd();
15523
- return ["---", yamlContent, "---"].join(`
15524
- `);
15525
- }
15526
-
15527
- // src/lib/validation.ts
15528
- function isRecord2(value) {
15529
- return typeof value === "object" && value !== null && !Array.isArray(value);
15530
- }
15531
- function isPermissionSetting(value) {
15532
- return value === "ask" || value === "allow" || value === "deny";
15533
- }
15534
- function isToolsMap(value) {
15535
- if (!isRecord2(value))
15536
- return false;
15537
- return Object.values(value).every((entry) => typeof entry === "boolean");
15538
- }
15539
- function isAgentMode(value) {
15540
- return value === "subagent" || value === "primary" || value === "all";
15541
- }
15542
- function extractSimplePermission(data, key) {
15543
- if (!(key in data))
15544
- return;
15545
- const value = data[key];
15546
- return isPermissionSetting(value) ? value : null;
15547
- }
15548
- function extractBashPermission(data) {
15549
- if (!("bash" in data))
15550
- return;
15551
- const bash = data.bash;
15552
- if (isPermissionSetting(bash))
15553
- return bash;
15554
- if (isRecord2(bash)) {
15555
- const entries = Object.entries(bash);
15556
- if (entries.every(([, setting]) => isPermissionSetting(setting))) {
15557
- return Object.fromEntries(entries);
15614
+ for (const key of Object.keys(schema)) {
15615
+ if (!RECOGNIZED_KEYS.has(key)) {
15616
+ extraMeta[key] = schema[key];
15558
15617
  }
15559
15618
  }
15560
- return null;
15561
- }
15562
- function buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory, task, skill) {
15563
- const permission = {};
15564
- if (edit)
15565
- permission.edit = edit;
15566
- if (bash)
15567
- permission.bash = bash;
15568
- if (webfetch)
15569
- permission.webfetch = webfetch;
15570
- if (doom_loop)
15571
- permission.doom_loop = doom_loop;
15572
- if (external_directory)
15573
- permission.external_directory = external_directory;
15574
- if (task)
15575
- permission.task = task;
15576
- if (skill)
15577
- permission.skill = skill;
15578
- return Object.keys(permission).length > 0 ? permission : undefined;
15579
- }
15580
- function normalizePermission(value) {
15581
- if (!isRecord2(value))
15582
- return;
15583
- const bash = extractBashPermission(value);
15584
- if (bash === null)
15585
- return;
15586
- const edit = extractSimplePermission(value, "edit");
15587
- if (edit === null)
15588
- return;
15589
- const webfetch = extractSimplePermission(value, "webfetch");
15590
- if (webfetch === null)
15591
- return;
15592
- const doom_loop = extractSimplePermission(value, "doom_loop");
15593
- if (doom_loop === null)
15594
- return;
15595
- const external_directory = extractSimplePermission(value, "external_directory");
15596
- if (external_directory === null)
15597
- return;
15598
- const task = extractSimplePermission(value, "task");
15599
- if (task === null)
15600
- return;
15601
- const skill = extractSimplePermission(value, "skill");
15602
- if (skill === null)
15603
- return;
15604
- return buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory, task, skill);
15605
- }
15606
- function extractString(data, key, fallback = "") {
15607
- const value = data[key];
15608
- return typeof value === "string" ? value : fallback;
15609
- }
15610
- function extractNonEmptyString(data, key) {
15611
- const value = data[key];
15612
- if (typeof value !== "string")
15613
- return;
15614
- const trimmed = value.trim();
15615
- return trimmed !== "" ? trimmed : undefined;
15616
- }
15617
- function extractNumber(data, key) {
15618
- const value = data[key];
15619
- return typeof value === "number" ? value : undefined;
15620
- }
15621
- function extractBoolean(data, key) {
15622
- const value = data[key];
15623
- if (typeof value === "boolean")
15624
- return value;
15625
- if (typeof value === "string") {
15626
- const normalized = value.trim().toLowerCase();
15627
- if (normalized === "true")
15628
- return true;
15629
- if (normalized === "false")
15630
- return false;
15619
+ if (Object.keys(extraMeta).length > 0) {
15620
+ ctx.registry.add(baseSchema, extraMeta);
15631
15621
  }
15632
- return;
15633
- }
15634
-
15635
- // src/lib/walk-dir.ts
15636
- import fs2 from "fs";
15637
- import path2 from "path";
15638
- function walkDir(rootDir, options = {}) {
15639
- const { maxDepth = 3, filter } = options;
15640
- const results = [];
15641
- if (!fs2.existsSync(rootDir))
15642
- return results;
15643
- function recurse(currentDir, depth, category) {
15644
- if (depth > maxDepth)
15645
- return;
15646
- const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
15647
- for (const entry of entries) {
15648
- const fullPath = path2.join(currentDir, entry.name);
15649
- const walkEntry = {
15650
- path: fullPath,
15651
- name: entry.name,
15652
- isDirectory: entry.isDirectory(),
15653
- depth,
15654
- category
15655
- };
15656
- if (!filter || filter(walkEntry)) {
15657
- results.push(walkEntry);
15658
- }
15659
- if (entry.isDirectory()) {
15660
- recurse(fullPath, depth + 1, entry.name);
15661
- }
15662
- }
15622
+ if (schema.description) {
15623
+ baseSchema = baseSchema.describe(schema.description);
15663
15624
  }
15664
- recurse(rootDir, 0);
15665
- return results;
15666
- }
15667
-
15668
- // src/lib/agents.ts
15669
- function findAgentsInDir(dir, maxDepth = 2) {
15670
- const entries = walkDir(dir, {
15671
- maxDepth,
15672
- filter: (e) => !e.isDirectory && e.name.endsWith(".md")
15673
- });
15674
- return entries.map((entry) => ({
15675
- name: entry.name.replace(/\.md$/, ""),
15676
- file: entry.path,
15677
- category: entry.category
15678
- }));
15625
+ return baseSchema;
15679
15626
  }
15680
- function extractAgentFrontmatter(content) {
15681
- const { data, parseError, body } = parseFrontmatter(content);
15682
- if (parseError) {
15683
- return { name: "", description: "", prompt: body.trim() };
15627
+ function fromJSONSchema(schema, params) {
15628
+ if (typeof schema === "boolean") {
15629
+ return schema ? z.any() : z.never();
15630
+ }
15631
+ let normalized;
15632
+ try {
15633
+ normalized = JSON.parse(JSON.stringify(schema));
15634
+ } catch {
15635
+ throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
15684
15636
  }
15685
- return {
15686
- name: extractString(data, "name"),
15687
- description: extractString(data, "description"),
15688
- prompt: body.trim(),
15689
- model: extractNonEmptyString(data, "model"),
15690
- variant: extractNonEmptyString(data, "variant"),
15691
- temperature: extractNumber(data, "temperature"),
15692
- top_p: extractNumber(data, "top_p"),
15693
- tools: isToolsMap(data.tools) ? data.tools : undefined,
15694
- disable: extractBoolean(data, "disable"),
15695
- mode: isAgentMode(data.mode) ? data.mode : undefined,
15696
- color: extractNonEmptyString(data, "color"),
15697
- steps: extractNumber(data, "steps"),
15698
- hidden: extractBoolean(data, "hidden") ?? undefined,
15699
- permission: normalizePermission(data.permission)
15637
+ const version2 = detectVersion(normalized, params?.defaultTarget);
15638
+ const defs = normalized.$defs || normalized.definitions || {};
15639
+ const ctx = {
15640
+ version: version2,
15641
+ defs,
15642
+ refs: new Map,
15643
+ processing: new Set,
15644
+ rootSchema: normalized,
15645
+ registry: params?.registry ?? globalRegistry
15700
15646
  };
15647
+ return convertSchema(normalized, ctx);
15701
15648
  }
15702
-
15703
- // src/lib/commands.ts
15704
- function findCommandsInDir(dir, maxDepth = 2) {
15705
- const entries = walkDir(dir, {
15706
- maxDepth,
15707
- filter: (e) => !e.isDirectory && e.name.endsWith(".md")
15708
- });
15709
- return entries.map((entry) => {
15710
- const baseName = entry.name.replace(/\.md$/, "");
15711
- const commandName = entry.category ? `/${entry.category}:${baseName}` : `/${baseName}`;
15712
- return {
15713
- name: commandName,
15714
- file: entry.path,
15715
- category: entry.category
15716
- };
15717
- });
15649
+ // node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/coerce.js
15650
+ var exports_coerce = {};
15651
+ __export(exports_coerce, {
15652
+ string: () => string3,
15653
+ number: () => number3,
15654
+ date: () => date4,
15655
+ boolean: () => boolean3,
15656
+ bigint: () => bigint3
15657
+ });
15658
+ function string3(params) {
15659
+ return _coercedString(ZodString, params);
15718
15660
  }
15719
- function extractCommandFrontmatter(content) {
15720
- const { data, parseError } = parseFrontmatter(content);
15721
- if (parseError) {
15722
- return {
15723
- name: "",
15724
- description: "",
15725
- argumentHint: "",
15726
- agent: undefined,
15727
- model: undefined,
15728
- subtask: undefined
15729
- };
15730
- }
15731
- const argumentHintRaw = extractString(data, "argument-hint");
15732
- return {
15733
- name: extractString(data, "name"),
15734
- description: extractString(data, "description"),
15735
- argumentHint: argumentHintRaw.replace(/^["']|["']$/g, ""),
15736
- agent: extractNonEmptyString(data, "agent"),
15737
- model: extractNonEmptyString(data, "model"),
15738
- subtask: extractBoolean(data, "subtask")
15739
- };
15661
+ function number3(params) {
15662
+ return _coercedNumber(ZodNumber, params);
15663
+ }
15664
+ function boolean3(params) {
15665
+ return _coercedBoolean(ZodBoolean, params);
15666
+ }
15667
+ function bigint3(params) {
15668
+ return _coercedBigint(ZodBigInt, params);
15669
+ }
15670
+ function date4(params) {
15671
+ return _coercedDate(ZodDate, params);
15740
15672
  }
15741
15673
 
15742
- // src/lib/converter.ts
15743
- import fs3 from "fs";
15744
- var CONVERTER_VERSION = 2;
15745
- var cache = new Map;
15746
- var TOOL_MAPPINGS = [
15747
- [/\bTask\s+tool\b/gi, "task tool"],
15748
- [/\bTask\s+([\w-]+)\s*:/g, "task $1:"],
15749
- [/\bTask\s+([\w-]+)\s*\(/g, "task $1("],
15750
- [/\bTask\s*\(/g, "task("],
15751
- [/\bTask\b(?=\s+to\s+\w)/g, "task"],
15752
- [/\bTodoWrite\b/g, "todowrite"],
15753
- [/\bAskUserQuestion\b/g, "question"],
15754
- [/\bWebSearch\b/g, "google_search"],
15755
- [/\bRead\b(?=\s+tool|\s+to\s+|\()/g, "read"],
15756
- [/\bWrite\b(?=\s+tool|\s+to\s+|\()/g, "write"],
15757
- [/\bEdit\b(?=\s+tool|\s+to\s+|\()/g, "edit"],
15758
- [/\bBash\b(?=\s+tool|\s+to\s+|\()/g, "bash"],
15759
- [/\bGrep\b(?=\s+tool|\s+to\s+|\()/g, "grep"],
15760
- [/\bGlob\b(?=\s+tool|\s+to\s+|\()/g, "glob"],
15761
- [/\bWebFetch\b/g, "webfetch"],
15762
- [/\bSkill\b(?=\s+tool|\s*\()/g, "skill"]
15674
+ // node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
15675
+ config(en_default());
15676
+ // src/lib/agent-colors.ts
15677
+ var OPENCODE_AGENT_COLOR_TOKENS = [
15678
+ "primary",
15679
+ "secondary",
15680
+ "accent",
15681
+ "success",
15682
+ "warning",
15683
+ "error",
15684
+ "info"
15763
15685
  ];
15764
- var PATH_REPLACEMENTS = [
15765
- [/\.claude\/skills\//g, ".opencode/skills/"],
15766
- [/\.claude\/commands\//g, ".opencode/commands/"],
15767
- [/\.claude\/agents\//g, ".opencode/agents/"],
15768
- [/~\/\.claude\//g, "~/.config/opencode/"],
15769
- [/CLAUDE\.md/g, "AGENTS.md"],
15770
- [/\/compound-engineering:/g, "/systematic:"],
15771
- [/compound-engineering:/g, "systematic:"]
15686
+
15687
+ // src/lib/config-schema.ts
15688
+ var permissionSettingSchema = exports_external.enum(["ask", "allow", "deny"]);
15689
+ var permissionRuleSchema = exports_external.union([
15690
+ permissionSettingSchema,
15691
+ exports_external.record(exports_external.string(), permissionSettingSchema)
15692
+ ]);
15693
+ var permissionSchema = exports_external.record(exports_external.string(), permissionRuleSchema).meta({
15694
+ description: "Permission overrides per tool",
15695
+ examples: [{ edit: "allow", bash: { curl: "allow", rm: "deny" } }]
15696
+ });
15697
+ var MODEL_FORMAT_MESSAGE = 'must be in provider/model format (e.g., "anthropic/claude-sonnet-4")';
15698
+ var MODEL_FORMAT_REGEX = /^[^\s/]+\/\S+$/;
15699
+ var modelSchema = exports_external.string().min(1).regex(MODEL_FORMAT_REGEX, MODEL_FORMAT_MESSAGE).nullable().meta({
15700
+ description: "Model identifier in provider/model format, or null to inherit parent model",
15701
+ examples: ["anthropic/claude-sonnet-4", null]
15702
+ });
15703
+ 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({
15704
+ description: "Model variant identifier",
15705
+ examples: ["v2", "extended"]
15706
+ });
15707
+ var temperatureSchema = exports_external.number().min(0).meta({
15708
+ description: "Sampling temperature (\u22650; 0 = deterministic)",
15709
+ examples: [0.1, 0.7, 0]
15710
+ });
15711
+ var topPSchema = exports_external.number().min(0).max(1).meta({
15712
+ description: "Nucleus sampling parameter (0 to 1)",
15713
+ examples: [0.9, 0.1, 1]
15714
+ });
15715
+ var modeSchema = exports_external.enum(["subagent", "primary", "all"]).meta({
15716
+ description: "Agent execution mode",
15717
+ examples: ["subagent", "primary", "all"]
15718
+ });
15719
+ var colorSchema = exports_external.union([
15720
+ exports_external.enum(OPENCODE_AGENT_COLOR_TOKENS),
15721
+ exports_external.string().regex(/^#[0-9a-fA-F]{6}$/, "must be a 6-digit hex color (#RRGGBB)")
15722
+ ]).meta({
15723
+ description: "Agent color \u2014 named token from OpenCode or 6-digit hex color (#RRGGBB)",
15724
+ examples: ["primary", "#ff6600"]
15725
+ });
15726
+ var stepsSchema = exports_external.number().int().positive().meta({
15727
+ description: "Maximum execution steps (positive integer)",
15728
+ examples: [10, 50]
15729
+ });
15730
+ var hiddenSchema = exports_external.boolean().meta({
15731
+ description: "Hide agent from UI",
15732
+ examples: [true, false]
15733
+ });
15734
+ var disableSchema = exports_external.boolean().meta({
15735
+ description: "Disable this agent overlay",
15736
+ examples: [true, false]
15737
+ });
15738
+ var skillsSchema = exports_external.array(exports_external.string().min(1)).meta({
15739
+ description: "Skills enabled for this agent",
15740
+ examples: [["ce:plan", "ce:review"]]
15741
+ });
15742
+ function trustAny(schema) {
15743
+ return schema.meta({ trust: "any" });
15744
+ }
15745
+ function trustProtected(schema) {
15746
+ return schema.meta({ trust: "project-or-higher" });
15747
+ }
15748
+ function enforceVariantHasExplicitModel(overlay, ctx) {
15749
+ if (overlay.variant === undefined)
15750
+ return;
15751
+ if (typeof overlay.model === "string")
15752
+ return;
15753
+ ctx.addIssue({
15754
+ code: "custom",
15755
+ path: ["variant"],
15756
+ message: "variant requires a non-null model in the same overlay; remove variant or set model explicitly"
15757
+ });
15758
+ }
15759
+ var AgentOverlaySchema = exports_external.object({
15760
+ model: trustProtected(modelSchema).optional(),
15761
+ variant: trustProtected(variantSchema).optional(),
15762
+ temperature: trustAny(temperatureSchema).optional(),
15763
+ top_p: trustAny(topPSchema).optional(),
15764
+ mode: modeSchema.optional(),
15765
+ color: colorSchema.optional(),
15766
+ steps: stepsSchema.optional(),
15767
+ hidden: hiddenSchema.optional(),
15768
+ disable: disableSchema.optional(),
15769
+ skills: trustProtected(skillsSchema).optional(),
15770
+ permission: trustProtected(permissionSchema).optional()
15771
+ }).strict().superRefine(enforceVariantHasExplicitModel).meta({
15772
+ description: "Per-agent configuration overlay",
15773
+ examples: [
15774
+ {
15775
+ model: "anthropic/claude-opus-4-7",
15776
+ temperature: 0.1,
15777
+ mode: "subagent"
15778
+ }
15779
+ ]
15780
+ });
15781
+ var CategoryOverlaySchema = exports_external.object({
15782
+ model: trustProtected(modelSchema).optional(),
15783
+ variant: trustProtected(variantSchema).optional(),
15784
+ temperature: trustAny(temperatureSchema).optional(),
15785
+ top_p: trustAny(topPSchema).optional(),
15786
+ mode: modeSchema.optional(),
15787
+ color: colorSchema.optional(),
15788
+ steps: stepsSchema.optional(),
15789
+ hidden: hiddenSchema.optional(),
15790
+ skills: trustProtected(skillsSchema).optional(),
15791
+ permission: trustProtected(permissionSchema).optional()
15792
+ }).strict().superRefine(enforceVariantHasExplicitModel).meta({
15793
+ description: "Per-category configuration overlay (same fields as agent minus disable)",
15794
+ examples: [{ model: "anthropic/claude-opus-4-7", temperature: 0.1 }]
15795
+ });
15796
+ var BootstrapSchema = exports_external.object({
15797
+ enabled: exports_external.boolean().default(true).meta({
15798
+ description: "Enable bootstrap prompt injection into every conversation",
15799
+ examples: [true, false]
15800
+ }),
15801
+ file: exports_external.string().optional().meta({
15802
+ description: "Path to a custom bootstrap prompt file",
15803
+ examples: ["~/.config/opencode/bootstrap.md"]
15804
+ })
15805
+ }).strict().meta({
15806
+ description: "Bootstrap prompt configuration",
15807
+ examples: [{ enabled: true }, { enabled: false }]
15808
+ });
15809
+ var SystematicConfigSchema = exports_external.object({
15810
+ $schema: exports_external.string().url().optional().meta({
15811
+ description: "JSON Schema URL for IDE autocomplete. The value is informational only \u2014 the loader does not fetch or validate against it. Add this to enable IDE schema activation and field-level autocomplete in editors that support JSON Schema (VSCode, Zed, IntelliJ).",
15812
+ examples: [
15813
+ "https://fro.bot/systematic/schemas/v2/systematic-config.schema.json"
15814
+ ]
15815
+ }),
15816
+ agents: exports_external.record(exports_external.string(), AgentOverlaySchema).default({}).meta({
15817
+ description: "Per-agent configuration overlays keyed by agent name",
15818
+ examples: [{ "correctness-reviewer": { temperature: 0.1 } }, {}]
15819
+ }),
15820
+ categories: exports_external.record(exports_external.string(), CategoryOverlaySchema).default({}).meta({
15821
+ description: "Per-category configuration overlays keyed by category name",
15822
+ examples: [{ review: { model: "anthropic/claude-opus-4-7" } }, {}]
15823
+ }),
15824
+ disabled_skills: exports_external.array(exports_external.string()).default([]).meta({
15825
+ description: "Array of skill names to disable globally",
15826
+ examples: [["ce:plan", "ce:review"]]
15827
+ }),
15828
+ disabled_agents: exports_external.array(exports_external.string()).default([]).meta({
15829
+ description: "Array of agent names to disable globally",
15830
+ examples: [["previous-comments-reviewer", "cli-readiness-reviewer"]]
15831
+ }),
15832
+ disabled_commands: exports_external.array(exports_external.string()).default([]).meta({
15833
+ description: "Array of command names to disable globally",
15834
+ examples: [["deprecated-migration-helper"]]
15835
+ }),
15836
+ bootstrap: BootstrapSchema.default({ enabled: true }).meta({
15837
+ description: "Bootstrap prompt configuration",
15838
+ examples: [
15839
+ { enabled: true },
15840
+ { enabled: false, file: ".opencode/custom-prompt.md" }
15841
+ ]
15842
+ })
15843
+ }).strict().meta({
15844
+ description: "Systematic user configuration file (systematic.json / systematic.jsonc)",
15845
+ examples: [{ disabled_skills: ["ce:plan"], bootstrap: { enabled: false } }]
15846
+ });
15847
+ var SECURITY_OVERLAY_FIELDS = [
15848
+ "model",
15849
+ "variant",
15850
+ "skills",
15851
+ "permission"
15772
15852
  ];
15773
- var TOOL_NAME_MAP = {
15774
- task: "task",
15775
- todowrite: "todowrite",
15776
- askuserquestion: "question",
15777
- websearch: "google_search",
15778
- webfetch: "webfetch",
15779
- skill: "skill",
15780
- read: "read",
15781
- write: "write",
15782
- edit: "edit",
15783
- bash: "bash",
15784
- grep: "grep",
15785
- glob: "glob"
15786
- };
15787
- var PERMISSION_MODE_MAP = {
15788
- full: {
15789
- edit: "allow",
15790
- bash: "allow",
15791
- webfetch: "allow"
15792
- },
15793
- default: {
15794
- edit: "ask",
15795
- bash: "ask",
15796
- webfetch: "ask"
15797
- },
15798
- plan: {
15799
- edit: "deny",
15800
- bash: "deny",
15801
- webfetch: "ask"
15853
+
15854
+ // src/lib/config.ts
15855
+ var DEFAULT_CONFIG = {
15856
+ disabled_skills: [],
15857
+ disabled_agents: [],
15858
+ disabled_commands: [],
15859
+ bootstrap: {
15860
+ enabled: true
15802
15861
  },
15803
- bypassPermissions: {
15804
- edit: "allow",
15805
- bash: "allow",
15806
- webfetch: "allow"
15807
- }
15862
+ agents: {},
15863
+ categories: {}
15808
15864
  };
15809
- function inferTemperature(name, description) {
15810
- const sample = `${name} ${description ?? ""}`.toLowerCase();
15811
- if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
15812
- return 0.1;
15813
- }
15814
- if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
15815
- return 0.2;
15865
+ var SECURITY_OVERLAY_FIELDS2 = new Set(SECURITY_OVERLAY_FIELDS);
15866
+ function resolveConfigPath(dir, basename) {
15867
+ const jsoncPath = path3.join(dir, `${basename}.jsonc`);
15868
+ if (fs4.existsSync(jsoncPath))
15869
+ return jsoncPath;
15870
+ return path3.join(dir, `${basename}.json`);
15871
+ }
15872
+ function isErrorWithCode(error51) {
15873
+ return error51 instanceof Error && "code" in error51;
15874
+ }
15875
+ function loadJsoncFile(filePath) {
15876
+ let content;
15877
+ try {
15878
+ content = fs4.readFileSync(filePath, "utf-8");
15879
+ } catch (error51) {
15880
+ if (isErrorWithCode(error51) && error51.code === "ENOENT")
15881
+ return null;
15882
+ throw new Error(`Invalid Systematic config in ${filePath}: unable to read file`, { cause: error51 });
15816
15883
  }
15817
- if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
15818
- return 0.3;
15884
+ const errors3 = [];
15885
+ const parsed = parse2(content, errors3);
15886
+ if (errors3.length > 0) {
15887
+ const error51 = errors3[0];
15888
+ const message = error51 ? `${printParseErrorCode(error51.error)} at offset ${error51.offset}` : "unknown parse error";
15889
+ throw new Error(`Invalid Systematic config in ${filePath}: JSONC parse error: ${message}`);
15819
15890
  }
15820
- if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
15821
- return 0.6;
15891
+ if (!isRecord2(parsed)) {
15892
+ throw new Error(`Invalid Systematic config in ${filePath}: root must be an object`);
15822
15893
  }
15823
- return 0.3;
15894
+ return parsed;
15824
15895
  }
15825
- var CODE_BLOCK_PATTERN = /```[\s\S]*?```|`[^`\n]+`/g;
15826
- function transformBody(body) {
15827
- const codeBlocks = [];
15828
- let placeholderIndex = 0;
15829
- const withPlaceholders = body.replace(CODE_BLOCK_PATTERN, (match) => {
15830
- codeBlocks.push(match);
15831
- return `__CODE_BLOCK_${placeholderIndex++}__`;
15832
- });
15833
- let result = withPlaceholders;
15834
- for (const [pattern, replacement] of TOOL_MAPPINGS) {
15835
- result = result.replace(pattern, replacement);
15836
- }
15837
- for (const [pattern, replacement] of PATH_REPLACEMENTS) {
15838
- result = result.replace(pattern, replacement);
15839
- }
15840
- for (let i = 0;i < codeBlocks.length; i++) {
15841
- result = result.replace(`__CODE_BLOCK_${i}__`, codeBlocks[i]);
15896
+ function throwTopLevelConfigSchemaError(filePath, trust, issues) {
15897
+ const issue2 = issues[0];
15898
+ if (!issue2) {
15899
+ throw Object.assign(new Error(`Invalid Systematic config in ${filePath}: schema validation failed`), { _tag: "ConfigSchemaError", filePath, trust, issues });
15842
15900
  }
15843
- return result;
15901
+ const fieldPath = issue2.code === "unrecognized_keys" ? issue2.message.match(/"([^"]+)"/)?.[1] ?? issue2.path.join(".") : issue2.path.join(".");
15902
+ const message = fieldPath ? `Invalid Systematic config in ${filePath}: ${fieldPath} ${issue2.message}` : `Invalid Systematic config in ${filePath}: ${issue2.message}`;
15903
+ throw Object.assign(new Error(message), {
15904
+ _tag: "ConfigSchemaError",
15905
+ filePath,
15906
+ trust,
15907
+ issues
15908
+ });
15844
15909
  }
15845
- function normalizeModel(model) {
15846
- if (model.includes("/"))
15847
- return model;
15848
- if (model === "inherit")
15849
- return model;
15850
- if (/^claude-/.test(model))
15851
- return `anthropic/${model}`;
15852
- if (/^(gpt-|o1-|o3-)/.test(model))
15853
- return `openai/${model}`;
15854
- if (/^gemini-/.test(model))
15855
- return `google/${model}`;
15856
- return `anthropic/${model}`;
15910
+ function loadConfigSource(filePath, trust) {
15911
+ const rawConfig = loadJsoncFile(filePath);
15912
+ if (!rawConfig)
15913
+ return null;
15914
+ const result = SystematicConfigSchema.safeParse(rawConfig);
15915
+ if (!result.success) {
15916
+ throwTopLevelConfigSchemaError(filePath, trust, result.error.issues);
15917
+ }
15918
+ return { path: filePath, config: rawConfig, trust };
15857
15919
  }
15858
- function canonicalizeToolName(name) {
15859
- const lower = name.trim().toLowerCase();
15860
- return TOOL_NAME_MAP[lower] ?? lower;
15920
+ function isRecord2(value) {
15921
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15861
15922
  }
15862
- function isValidSteps(value) {
15863
- return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0;
15923
+ function mergeArraysUnique(arr1, arr2) {
15924
+ const set2 = new Set;
15925
+ if (arr1)
15926
+ for (const item of arr1)
15927
+ set2.add(item);
15928
+ if (arr2)
15929
+ for (const item of arr2)
15930
+ set2.add(item);
15931
+ return Array.from(set2);
15864
15932
  }
15865
- function mapStepsField(data) {
15866
- if (data.steps !== undefined) {
15867
- if (isValidSteps(data.steps)) {
15868
- delete data.maxTurns;
15869
- delete data.maxSteps;
15870
- }
15871
- return;
15872
- }
15873
- const candidates = [];
15874
- if (isValidSteps(data.maxTurns))
15875
- candidates.push(data.maxTurns);
15876
- if (isValidSteps(data.maxSteps))
15877
- candidates.push(data.maxSteps);
15878
- if (candidates.length > 0) {
15879
- data.steps = Math.min(...candidates);
15880
- delete data.maxTurns;
15881
- delete data.maxSteps;
15882
- }
15933
+ function loadConfig(projectDir) {
15934
+ return loadConfigWithSources(projectDir).config;
15883
15935
  }
15884
- function mapToolsField(data) {
15885
- if (data.tools !== undefined && !Array.isArray(data.tools)) {
15886
- if (isToolsMap(data.tools)) {
15887
- mergeDisallowedTools(data);
15888
- }
15889
- return;
15890
- }
15891
- if (Array.isArray(data.tools)) {
15892
- const toolsMap = {};
15893
- for (const tool of data.tools) {
15894
- if (typeof tool === "string") {
15895
- toolsMap[canonicalizeToolName(tool)] = true;
15896
- }
15897
- }
15898
- if (Object.keys(toolsMap).length > 0) {
15899
- data.tools = toolsMap;
15900
- } else {
15901
- delete data.tools;
15902
- }
15903
- }
15904
- mergeDisallowedTools(data);
15936
+ function loadConfigWithSources(projectDir) {
15937
+ const paths = getConfigPaths(projectDir);
15938
+ const userSource = loadConfigSource(paths.userConfig, "user");
15939
+ const projectSource = loadConfigSource(paths.projectConfig, "project");
15940
+ const customSource = paths.customConfig ? loadConfigSource(paths.customConfig, "custom") : null;
15941
+ const sources = [userSource, projectSource, customSource].filter((source) => source !== null);
15942
+ const overlays = mergeOverlaySources(sources);
15943
+ const userConfig = userSource?.config;
15944
+ const projectConfig = projectSource?.config;
15945
+ const customConfig = customSource?.config;
15946
+ const result = {
15947
+ disabled_skills: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_skills, userConfig?.disabled_skills), projectConfig?.disabled_skills), customConfig?.disabled_skills),
15948
+ disabled_agents: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_agents, userConfig?.disabled_agents), projectConfig?.disabled_agents), customConfig?.disabled_agents),
15949
+ disabled_commands: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_commands, userConfig?.disabled_commands), projectConfig?.disabled_commands), customConfig?.disabled_commands),
15950
+ bootstrap: {
15951
+ ...DEFAULT_CONFIG.bootstrap,
15952
+ ...userConfig?.bootstrap,
15953
+ ...projectConfig?.bootstrap,
15954
+ ...customConfig?.bootstrap
15955
+ },
15956
+ agents: overlayValues(overlays.agents),
15957
+ categories: overlayValues(overlays.categories)
15958
+ };
15959
+ return { config: result, overlays };
15905
15960
  }
15906
- function mergeDisallowedTools(data) {
15907
- if (!Array.isArray(data.disallowedTools))
15908
- return;
15909
- const existing = isToolsMap(data.tools) ? data.tools : {};
15910
- for (const tool of data.disallowedTools) {
15911
- if (typeof tool === "string") {
15912
- existing[canonicalizeToolName(tool)] = false;
15913
- }
15914
- }
15915
- if (Object.keys(existing).length > 0) {
15916
- data.tools = existing;
15961
+ function mergeOverlaySources(sources) {
15962
+ const result = {
15963
+ agents: {},
15964
+ categories: {}
15965
+ };
15966
+ for (const source of sources) {
15967
+ mergeOverlayMap(result.agents, source, "agents");
15968
+ mergeOverlayMap(result.categories, source, "categories");
15917
15969
  }
15918
- delete data.disallowedTools;
15970
+ return result;
15919
15971
  }
15920
- function mapPermissionMode(data) {
15921
- if (data.permission !== undefined) {
15922
- const normalized = normalizePermission(data.permission);
15923
- if (normalized) {
15924
- data.permission = normalized;
15925
- delete data.permissionMode;
15926
- return;
15927
- }
15928
- }
15929
- if (typeof data.permissionMode !== "string")
15972
+ function mergeOverlayMap(target, source, mapKey) {
15973
+ const overlayMap = source.config[mapKey];
15974
+ if (overlayMap === undefined)
15930
15975
  return;
15931
- const mapped = PERMISSION_MODE_MAP[data.permissionMode];
15932
- data.permission = mapped ?? { edit: "ask", bash: "ask", webfetch: "ask" };
15933
- delete data.permissionMode;
15934
- }
15935
- function mapHiddenField(data) {
15936
- if (data["disable-model-invocation"] === true || data.disableModelInvocation === true) {
15937
- data.hidden = true;
15938
- delete data["disable-model-invocation"];
15939
- delete data.disableModelInvocation;
15976
+ if (!isRecord2(overlayMap)) {
15977
+ throwInvalidOverlay(source.path, mapKey);
15940
15978
  }
15941
- }
15942
- function normalizeModelField(data) {
15943
- if (typeof data.model === "string" && data.model !== "inherit") {
15944
- data.model = normalizeModel(data.model);
15945
- } else if (data.model === "inherit") {
15946
- delete data.model;
15979
+ for (const [key, value] of Object.entries(overlayMap)) {
15980
+ const keyPath = `${mapKey}.${key}`;
15981
+ if (!isRecord2(value)) {
15982
+ throwInvalidOverlay(source.path, keyPath);
15983
+ }
15984
+ if (source.trust === "project") {
15985
+ rejectProjectSecurityOverlay(source.path, keyPath, value);
15986
+ }
15987
+ const previous = target[key];
15988
+ const nextValue = source.trust === "project" && previous ? preserveSecurityFields(previous.value, value) : value;
15989
+ target[key] = {
15990
+ value: nextValue,
15991
+ sourcePath: source.path,
15992
+ keyPath
15993
+ };
15947
15994
  }
15948
15995
  }
15949
- function transformAgentFrontmatter(data, agentMode) {
15950
- const result = { ...data };
15951
- result.mode = isAgentMode(data.mode) ? data.mode : agentMode;
15952
- const name = typeof data.name === "string" ? data.name : "";
15953
- const description = typeof data.description === "string" ? data.description : "";
15954
- if (description) {
15955
- result.description = description;
15956
- } else if (name) {
15957
- result.description = `${name} agent`;
15996
+ function rejectProjectSecurityOverlay(sourcePath, keyPath, value) {
15997
+ for (const field of SECURITY_OVERLAY_FIELDS2) {
15998
+ if (Object.hasOwn(value, field)) {
15999
+ throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath}.${field} is only valid in user config or OPENCODE_CONFIG_DIR config`);
16000
+ }
15958
16001
  }
15959
- normalizeModelField(result);
15960
- result.temperature = typeof data.temperature === "number" ? data.temperature : inferTemperature(name, description);
15961
- mapStepsField(result);
15962
- mapToolsField(result);
15963
- mapPermissionMode(result);
15964
- mapHiddenField(result);
15965
- return result;
15966
16002
  }
15967
- function transformSkillFrontmatter(data) {
15968
- const result = { ...data };
15969
- normalizeModelField(result);
15970
- if (result.context === "fork") {
15971
- result.subtask = true;
16003
+ function preserveSecurityFields(previous, next) {
16004
+ const result = { ...next };
16005
+ for (const field of SECURITY_OVERLAY_FIELDS2) {
16006
+ if (Object.hasOwn(previous, field)) {
16007
+ result[field] = previous[field];
16008
+ }
15972
16009
  }
15973
16010
  return result;
15974
16011
  }
15975
- function transformCommandFrontmatter(data) {
15976
- const result = { ...data };
15977
- normalizeModelField(result);
16012
+ function overlayValues(overlays) {
16013
+ const result = {};
16014
+ for (const [key, overlay] of Object.entries(overlays)) {
16015
+ result[key] = overlay.value;
16016
+ }
15978
16017
  return result;
15979
16018
  }
15980
- function convertContent(content, type, options = {}) {
15981
- if (content === "")
15982
- return "";
15983
- const { data, body, hadFrontmatter, parseError } = parseFrontmatter(content);
15984
- if (!hadFrontmatter) {
15985
- return options.skipBodyTransform ? content : transformBody(content);
15986
- }
15987
- if (parseError) {
15988
- return options.skipBodyTransform ? content : transformBody(content);
15989
- }
15990
- const shouldTransformBody = !options.skipBodyTransform;
15991
- const transformedBody = shouldTransformBody ? transformBody(body) : body;
15992
- if (type === "agent") {
15993
- const agentMode = options.agentMode ?? "subagent";
15994
- const transformedData = transformAgentFrontmatter(data, agentMode);
15995
- return `${formatFrontmatter(transformedData)}
15996
- ${transformedBody}`;
15997
- }
15998
- if (type === "skill") {
15999
- const transformedData = transformSkillFrontmatter(data);
16000
- return `${formatFrontmatter(transformedData)}
16001
- ${transformedBody}`;
16002
- }
16003
- if (type === "command") {
16004
- const transformedData = transformCommandFrontmatter(data);
16005
- return `${formatFrontmatter(transformedData)}
16006
- ${transformedBody}`;
16007
- }
16008
- return content;
16019
+ function throwInvalidOverlay(sourcePath, keyPath) {
16020
+ throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} must be an object`);
16009
16021
  }
16010
- function convertFileWithCache(filePath, type, options = {}) {
16011
- const fd = fs3.openSync(filePath, "r");
16012
- try {
16013
- const stats = fs3.fstatSync(fd);
16014
- const cacheKey = `${CONVERTER_VERSION}:${filePath}:${type}:${options.source ?? "bundled"}:${options.agentMode ?? "subagent"}:${options.skipBodyTransform ?? false}`;
16015
- const cached2 = cache.get(cacheKey);
16016
- if (cached2 != null && cached2.mtimeMs === stats.mtimeMs) {
16017
- return cached2.converted;
16022
+ function getConfigPaths(projectDir) {
16023
+ const homeDir = os.homedir();
16024
+ const customConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
16025
+ const result = {
16026
+ userConfig: resolveConfigPath(path3.join(homeDir, ".config/opencode"), "systematic"),
16027
+ projectConfig: resolveConfigPath(path3.join(projectDir, ".opencode"), "systematic"),
16028
+ userDir: path3.join(homeDir, ".config/opencode/systematic"),
16029
+ projectDir: path3.join(projectDir, ".opencode/systematic"),
16030
+ ...customConfigDir && {
16031
+ customConfig: resolveConfigPath(customConfigDir, "systematic"),
16032
+ customDir: path3.join(customConfigDir, "systematic")
16018
16033
  }
16019
- const content = fs3.readFileSync(fd, "utf8");
16020
- const converted = convertContent(content, type, options);
16021
- cache.set(cacheKey, { mtimeMs: stats.mtimeMs, converted });
16022
- return converted;
16023
- } finally {
16024
- fs3.closeSync(fd);
16025
- }
16034
+ };
16035
+ return result;
16026
16036
  }
16027
16037
 
16028
- // src/lib/skills.ts
16029
- import fs4 from "fs";
16030
- import path3 from "path";
16031
- function extractFrontmatter(filePath) {
16032
- try {
16033
- const content = fs4.readFileSync(filePath, "utf8");
16034
- const { data, parseError } = parseFrontmatter(content);
16035
- if (parseError) {
16036
- return { name: "", description: "" };
16037
- }
16038
- const metadataRaw = data.metadata;
16039
- let metadata;
16040
- if (isRecord2(metadataRaw)) {
16041
- const entries = Object.entries(metadataRaw);
16042
- if (entries.every(([, v]) => typeof v === "string")) {
16043
- metadata = Object.fromEntries(entries);
16044
- }
16045
- }
16046
- const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
16047
- const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
16048
- return {
16049
- name: extractString(data, "name"),
16050
- description: extractString(data, "description"),
16051
- license: extractNonEmptyString(data, "license"),
16052
- compatibility: extractNonEmptyString(data, "compatibility"),
16053
- metadata,
16054
- disableModelInvocation: extractBoolean(data, "disable-model-invocation"),
16055
- userInvocable: extractBoolean(data, "user-invocable"),
16056
- subtask: data.context === "fork" ? true : extractBoolean(data, "subtask") ?? undefined,
16057
- agent: extractNonEmptyString(data, "agent"),
16058
- model: extractNonEmptyString(data, "model"),
16059
- argumentHint: argumentHint !== "" ? argumentHint : undefined,
16060
- allowedTools: extractNonEmptyString(data, "allowed-tools")
16061
- };
16062
- } catch {
16063
- return { name: "", description: "" };
16038
+ // src/lib/agents.ts
16039
+ function findAgentsInDir(dir, maxDepth = 2) {
16040
+ const entries = walkDir(dir, {
16041
+ maxDepth,
16042
+ filter: (e) => !e.isDirectory && e.name.endsWith(".md")
16043
+ });
16044
+ return entries.map((entry) => ({
16045
+ name: entry.name.replace(/\.md$/, ""),
16046
+ file: entry.path,
16047
+ category: entry.category
16048
+ }));
16049
+ }
16050
+ function extractAgentFrontmatter(content) {
16051
+ const { data, parseError, body } = parseFrontmatter(content);
16052
+ if (parseError) {
16053
+ return { name: "", description: "", prompt: body.trim() };
16064
16054
  }
16055
+ return {
16056
+ name: extractString(data, "name"),
16057
+ description: extractString(data, "description"),
16058
+ prompt: body.trim(),
16059
+ model: extractNonEmptyString(data, "model"),
16060
+ variant: extractNonEmptyString(data, "variant"),
16061
+ temperature: extractNumber(data, "temperature"),
16062
+ top_p: extractNumber(data, "top_p"),
16063
+ tools: isToolsMap(data.tools) ? data.tools : undefined,
16064
+ disable: extractBoolean(data, "disable"),
16065
+ mode: isAgentMode(data.mode) ? data.mode : undefined,
16066
+ color: extractNonEmptyString(data, "color"),
16067
+ steps: extractNumber(data, "steps"),
16068
+ hidden: extractBoolean(data, "hidden") ?? undefined,
16069
+ permission: normalizePermission(data.permission)
16070
+ };
16065
16071
  }
16066
- function findSkillsInDir(dir, maxDepth = 3) {
16067
- const skills = [];
16072
+
16073
+ // src/lib/commands.ts
16074
+ function findCommandsInDir(dir, maxDepth = 2) {
16068
16075
  const entries = walkDir(dir, {
16069
16076
  maxDepth,
16070
- filter: (e) => e.isDirectory
16077
+ filter: (e) => !e.isDirectory && e.name.endsWith(".md")
16071
16078
  });
16072
- for (const entry of entries) {
16073
- const skillFile = path3.join(entry.path, "SKILL.md");
16074
- if (fs4.existsSync(skillFile)) {
16075
- const frontmatter = extractFrontmatter(skillFile);
16076
- skills.push({
16077
- path: entry.path,
16078
- skillFile,
16079
- name: frontmatter.name || entry.name,
16080
- description: frontmatter.description || "",
16081
- license: frontmatter.license,
16082
- compatibility: frontmatter.compatibility,
16083
- metadata: frontmatter.metadata,
16084
- disableModelInvocation: frontmatter.disableModelInvocation,
16085
- userInvocable: frontmatter.userInvocable,
16086
- subtask: frontmatter.subtask,
16087
- agent: frontmatter.agent,
16088
- model: frontmatter.model,
16089
- argumentHint: frontmatter.argumentHint,
16090
- allowedTools: frontmatter.allowedTools
16091
- });
16092
- }
16079
+ return entries.map((entry) => {
16080
+ const baseName = entry.name.replace(/\.md$/, "");
16081
+ const commandName = entry.category ? `/${entry.category}:${baseName}` : `/${baseName}`;
16082
+ return {
16083
+ name: commandName,
16084
+ file: entry.path,
16085
+ category: entry.category
16086
+ };
16087
+ });
16088
+ }
16089
+ function extractCommandFrontmatter(content) {
16090
+ const { data, parseError } = parseFrontmatter(content);
16091
+ if (parseError) {
16092
+ return {
16093
+ name: "",
16094
+ description: "",
16095
+ argumentHint: "",
16096
+ agent: undefined,
16097
+ model: undefined,
16098
+ subtask: undefined
16099
+ };
16093
16100
  }
16094
- return skills;
16101
+ const argumentHintRaw = extractString(data, "argument-hint");
16102
+ return {
16103
+ name: extractString(data, "name"),
16104
+ description: extractString(data, "description"),
16105
+ argumentHint: argumentHintRaw.replace(/^["']|["']$/g, ""),
16106
+ agent: extractNonEmptyString(data, "agent"),
16107
+ model: extractNonEmptyString(data, "model"),
16108
+ subtask: extractBoolean(data, "subtask")
16109
+ };
16095
16110
  }
16096
16111
 
16097
- export { parseFrontmatter, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, isRecord2 as isRecord, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir };
16112
+ export { parseFrontmatter, isRecord, convertContent, convertFileWithCache, findSkillsInDir, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter };