thoth-agents 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +11 -2
  2. package/dist/{chunk-7F33WUSD.js → chunk-422L76R5.js} +2 -2
  3. package/dist/chunk-6O4GKDS7.js +207 -0
  4. package/dist/{chunk-SEF6PFM6.js → chunk-A7IGKE4R.js} +132 -157
  5. package/dist/{chunk-NY2IPJ7V.js → chunk-HFBCW4MD.js} +554 -236
  6. package/dist/{chunk-QOA2AETX.js → chunk-KUA3F4D6.js} +3 -3
  7. package/dist/cli/index.js +6 -6
  8. package/dist/cli/opencode-profiles.d.ts +10 -0
  9. package/dist/cli/tui/index.js +3 -3
  10. package/dist/config/loader.d.ts +1 -4
  11. package/dist/config/schema.d.ts +0 -22
  12. package/dist/index.js +824 -30
  13. package/dist/opencode/context.d.ts +16 -0
  14. package/dist/opencode/profile-rpc-handlers.d.ts +3 -0
  15. package/dist/opencode/profile-rpc.d.ts +688 -0
  16. package/dist/opencode/profile-runtime.d.ts +3 -0
  17. package/dist/opencode/profile-sessions.d.ts +18 -0
  18. package/dist/opencode/profiles/editor.d.ts +44 -0
  19. package/dist/opencode/profiles/index.d.ts +18 -0
  20. package/dist/opencode/profiles/resolver.d.ts +12 -0
  21. package/dist/opencode/profiles/store.d.ts +63 -0
  22. package/dist/opencode/profiles/types.d.ts +26 -0
  23. package/dist/opencode/tool-overrides/evaluator.d.ts +13 -0
  24. package/dist/opencode/tool-overrides/index.d.ts +5 -0
  25. package/dist/opencode/tool-overrides/store.d.ts +38 -0
  26. package/dist/opencode/tool-overrides/types.d.ts +32 -0
  27. package/dist/opencode/tool-overrides/validation.d.ts +5 -0
  28. package/dist/opencode/tool-overrides/wildcard.d.ts +3 -0
  29. package/dist/opencode/tool-rpc-handlers.d.ts +3 -0
  30. package/dist/opencode/tool-rpc.d.ts +428 -0
  31. package/dist/pi.js +2 -2
  32. package/dist/tui/fixtures.d.ts +2 -0
  33. package/dist/tui/flow.d.ts +16 -0
  34. package/dist/tui/index.d.ts +7 -0
  35. package/dist/tui/index.js +1386 -0
  36. package/dist/tui/model.d.ts +84 -0
  37. package/dist/tui/tools/fixtures.d.ts +3 -0
  38. package/dist/tui/tools/flow.d.ts +11 -0
  39. package/dist/tui/tools/model.d.ts +97 -0
  40. package/package.json +5 -1
  41. package/plugin/.claude-plugin/plugin.json +1 -1
  42. package/plugin/.codex-plugin/plugin.json +1 -1
  43. package/thoth-agents.schema.json +0 -70
@@ -19,6 +19,108 @@ function findPackageRoot(startDir) {
19
19
  }
20
20
  }
21
21
 
22
+ // src/config/schema.ts
23
+ import { z } from "zod";
24
+ var AGENT_NAMES = [
25
+ "orchestrator",
26
+ "oracle",
27
+ "designer",
28
+ "explorer",
29
+ "librarian",
30
+ "worker"
31
+ ];
32
+ var FALLBACK_AGENT_NAMES = [...AGENT_NAMES];
33
+ var MANUAL_AGENT_NAMES = [...AGENT_NAMES];
34
+ var ProviderModelIdSchema = z.string().regex(
35
+ /^[^/\s]+\/[^\s]+$/,
36
+ "Expected provider/model format (provider/.../model)"
37
+ );
38
+ var ManualAgentPlanSchema = z.object({
39
+ primary: ProviderModelIdSchema,
40
+ fallback1: ProviderModelIdSchema,
41
+ fallback2: ProviderModelIdSchema,
42
+ fallback3: ProviderModelIdSchema
43
+ }).superRefine((value, ctx) => {
44
+ const unique = /* @__PURE__ */ new Set([
45
+ value.primary,
46
+ value.fallback1,
47
+ value.fallback2,
48
+ value.fallback3
49
+ ]);
50
+ if (unique.size !== 4) {
51
+ ctx.addIssue({
52
+ code: z.ZodIssueCode.custom,
53
+ message: "primary and fallbacks must be unique per agent"
54
+ });
55
+ }
56
+ });
57
+ var ManualPlanSchema = z.object({
58
+ orchestrator: ManualAgentPlanSchema,
59
+ oracle: ManualAgentPlanSchema,
60
+ designer: ManualAgentPlanSchema,
61
+ explorer: ManualAgentPlanSchema,
62
+ librarian: ManualAgentPlanSchema,
63
+ worker: ManualAgentPlanSchema
64
+ }).strict();
65
+ var AgentModelChainSchema = z.array(z.string()).min(1);
66
+ var SupportedAgentRecordKeySchema = z.string().regex(/^(?!quick$|deep$).+$/, "Unsupported removed agent role");
67
+ var FallbackChainsSchema = z.record(
68
+ SupportedAgentRecordKeySchema,
69
+ AgentModelChainSchema
70
+ );
71
+ var AgentOverrideConfigSchema = z.object({
72
+ model: z.union([
73
+ z.string(),
74
+ z.array(
75
+ z.union([
76
+ z.string(),
77
+ z.object({
78
+ id: z.string(),
79
+ variant: z.string().optional()
80
+ })
81
+ ])
82
+ )
83
+ ]).optional(),
84
+ /** Pi specialist catalog filter; omitted discovers all, [] discovers none. */
85
+ skills: z.array(z.string().trim().min(1)).optional(),
86
+ temperature: z.number().min(0).max(2).optional(),
87
+ steps: z.number().int().min(1).optional(),
88
+ variant: z.string().optional().catch(void 0)
89
+ });
90
+ var AgentNameSchema = z.enum(AGENT_NAMES);
91
+ var McpNameSchema = z.enum(["exa", "context7", "grep_app"]);
92
+ var CodexGenerationConfigSchema = z.object({
93
+ enabled: z.boolean().default(false),
94
+ outputRoot: z.string().optional(),
95
+ dryRun: z.boolean().default(true)
96
+ });
97
+ var ClaudeCodeGenerationConfigSchema = z.object({
98
+ enabled: z.boolean().default(false),
99
+ outputRoot: z.string().optional(),
100
+ dryRun: z.boolean().default(true)
101
+ });
102
+ var FailoverConfigSchema = z.object({
103
+ enabled: z.boolean().default(true),
104
+ timeoutMs: z.number().min(0).default(15e3),
105
+ retryDelayMs: z.number().min(0).default(500),
106
+ chains: FallbackChainsSchema.default({})
107
+ });
108
+ var PluginConfigSchema = z.object({
109
+ $schema: z.string().optional(),
110
+ setDefaultAgent: z.boolean().optional(),
111
+ scoringEngineVersion: z.enum(["v1", "v2-shadow", "v2"]).optional(),
112
+ balanceProviderUsage: z.boolean().optional(),
113
+ manualPlan: ManualPlanSchema.optional(),
114
+ agents: z.record(SupportedAgentRecordKeySchema, AgentOverrideConfigSchema).optional(),
115
+ disabled_mcps: z.array(z.string()).optional(),
116
+ fallback: FailoverConfigSchema.optional(),
117
+ codex: CodexGenerationConfigSchema.optional(),
118
+ claudeCode: ClaudeCodeGenerationConfigSchema.optional()
119
+ });
120
+
121
+ // src/opencode/profiles/types.ts
122
+ var PROFILE_ROLES = AgentNameSchema.options;
123
+
22
124
  // src/config/constants.ts
23
125
  var SUBAGENT_NAMES = [
24
126
  "explorer",
@@ -88,6 +190,302 @@ function getDefaultOpenCodeVariant(name) {
88
190
  var DEFAULT_TIMEOUT_MS = 2 * 60 * 1e3;
89
191
  var MAX_POLL_TIME_MS = 5 * 60 * 1e3;
90
192
 
193
+ // src/opencode/profiles/resolver.ts
194
+ function configuredModelChain(config) {
195
+ const models = config?.model;
196
+ return (typeof models === "string" ? [models] : models ?? []).map(
197
+ (entry) => {
198
+ const value = typeof entry === "string" ? { id: entry } : { ...entry };
199
+ const variant = Array.isArray(models) ? value.variant : config?.variant;
200
+ return { ...value, ...variant ? { variant } : {} };
201
+ }
202
+ );
203
+ }
204
+ function resolveModelPair(config, inherited = {}) {
205
+ const first = configuredModelChain(config)[0];
206
+ const model = first?.id ?? inherited.model;
207
+ const variant = config?.model !== void 0 ? first?.variant : config?.variant ?? inherited.variant;
208
+ return { ...model ? { model } : {}, ...variant ? { variant } : {} };
209
+ }
210
+ function resolveRoleProfile(role, profile, roster) {
211
+ return resolveModelPair(profile.agents[role], resolveModelPair(roster[role]));
212
+ }
213
+ function defaultModelProfile(roster) {
214
+ return {
215
+ agents: Object.fromEntries(
216
+ PROFILE_ROLES.map((role) => [
217
+ role,
218
+ resolveRoleProfile(role, { agents: {} }, roster)
219
+ ])
220
+ )
221
+ };
222
+ }
223
+ function mergeProfileModelChain(profile, configured) {
224
+ const entries = profile.model ? [
225
+ {
226
+ id: profile.model,
227
+ ...profile.variant ? { variant: profile.variant } : {}
228
+ },
229
+ ...configured
230
+ ] : configured;
231
+ const seen = /* @__PURE__ */ new Set();
232
+ return entries.filter(({ id }) => {
233
+ if (seen.has(id)) return false;
234
+ seen.add(id);
235
+ return true;
236
+ }).map((entry) => ({ ...entry }));
237
+ }
238
+ function formatModelString(profile) {
239
+ return profile.model ? `${profile.model}${profile.variant ? `#${profile.variant}` : ""}` : void 0;
240
+ }
241
+
242
+ // src/opencode/profiles/store.ts
243
+ import { randomUUID } from "crypto";
244
+ import fs from "fs";
245
+ import os from "os";
246
+ import path from "path";
247
+ import { isDeepStrictEqual } from "util";
248
+ var DEFAULT_PROFILE = "default";
249
+ function modelProfilePaths(options) {
250
+ if (options.paths) return { ...options.paths };
251
+ const env = options.env ?? process.env;
252
+ const configDir = options.configDir ?? (env.OPENCODE_CONFIG_DIR?.trim() || path.join(
253
+ env.XDG_CONFIG_HOME || path.join(options.homeDir ?? os.homedir(), ".config"),
254
+ "opencode"
255
+ ));
256
+ return {
257
+ global: path.join(configDir, "thoth", "profiles.json"),
258
+ project: path.join(options.cwd, ".opencode", "thoth", "profiles.json")
259
+ };
260
+ }
261
+ function object(value) {
262
+ return value !== null && typeof value === "object" && !Array.isArray(value);
263
+ }
264
+ function validateProfileName(value, writable = false) {
265
+ if (typeof value !== "string" || !value.trim())
266
+ throw new Error("Profile name must be non-empty");
267
+ const name = value.trim();
268
+ if (writable && name === DEFAULT_PROFILE)
269
+ throw new Error("default is a reserved read-only virtual profile");
270
+ return name;
271
+ }
272
+ function parseRoleProfile(value) {
273
+ if (!object(value)) throw new Error("Role override must be an object");
274
+ if (value.model !== void 0 && (typeof value.model !== "string" || !/^[^/\s#]+\/[^\s#]+$/.test(value.model)))
275
+ throw new Error("Invalid model; expected provider/id without #variant");
276
+ if (value.variant !== void 0 && (typeof value.variant !== "string" || !/^[^\s#]+$/.test(value.variant)))
277
+ throw new Error(
278
+ "Invalid variant; expected a non-empty string without whitespace or #"
279
+ );
280
+ return {
281
+ ...value.model !== void 0 ? { model: value.model } : {},
282
+ ...value.variant !== void 0 ? { variant: value.variant } : {}
283
+ };
284
+ }
285
+ function parseNamedModelProfile(value) {
286
+ if (!object(value) || !object(value.agents))
287
+ throw new Error("Profile must contain an agents object");
288
+ const agents = {};
289
+ for (const [role, override] of Object.entries(value.agents)) {
290
+ const canonical = PROFILE_ROLES.find((name) => name === role);
291
+ if (!canonical) throw new Error(`Unknown profile role: ${role}`);
292
+ agents[canonical] = parseRoleProfile(override);
293
+ }
294
+ return { agents };
295
+ }
296
+ function parseModelProfilesDocument(raw) {
297
+ if (!object(raw) || raw.version !== 1 || !object(raw.profiles))
298
+ throw new Error("Expected version 1 and a profiles object");
299
+ const profiles = /* @__PURE__ */ Object.create(null);
300
+ for (const [key, value] of Object.entries(raw.profiles)) {
301
+ const name = validateProfileName(key, true);
302
+ if (Object.hasOwn(profiles, name))
303
+ throw new Error(`Duplicate profile: ${name}`);
304
+ profiles[name] = parseNamedModelProfile(value);
305
+ }
306
+ return {
307
+ version: 1,
308
+ ...raw.active !== void 0 ? { active: validateProfileName(raw.active) } : {},
309
+ profiles
310
+ };
311
+ }
312
+ var emptyDocument = () => ({
313
+ version: 1,
314
+ profiles: /* @__PURE__ */ Object.create(null)
315
+ });
316
+ var ProfileConflictError = class extends Error {
317
+ constructor(scope, profileName) {
318
+ super(
319
+ `Profile changed on disk: ${scope}:${profileName}; reload before saving.`
320
+ );
321
+ this.scope = scope;
322
+ this.profileName = profileName;
323
+ this.name = "ProfileConflictError";
324
+ }
325
+ scope;
326
+ profileName;
327
+ code = "PROFILE_CONFLICT";
328
+ };
329
+ var ModelProfileStore = class {
330
+ constructor(options) {
331
+ this.options = options;
332
+ this.paths = modelProfilePaths(options);
333
+ }
334
+ options;
335
+ paths;
336
+ defaultProfile() {
337
+ return defaultModelProfile(this.options.roster ?? {});
338
+ }
339
+ load(scope) {
340
+ try {
341
+ return parseModelProfilesDocument(
342
+ JSON.parse(fs.readFileSync(this.paths[scope], "utf8"))
343
+ );
344
+ } catch (error) {
345
+ if (error.code === "ENOENT")
346
+ return emptyDocument();
347
+ throw new Error(
348
+ `Cannot read model profiles ${this.paths[scope]}: ${String(error)}`,
349
+ { cause: error }
350
+ );
351
+ }
352
+ }
353
+ read(scope, warnings = []) {
354
+ try {
355
+ return this.load(scope);
356
+ } catch (error) {
357
+ const warning = String(error);
358
+ warnings.push(warning);
359
+ this.options.warn?.(warning);
360
+ return emptyDocument();
361
+ }
362
+ }
363
+ lookup(name, scope) {
364
+ const key = validateProfileName(name);
365
+ if (key === DEFAULT_PROFILE) return { profile: this.defaultProfile() };
366
+ for (const source of scope ? [scope] : ["project", "global"]) {
367
+ const profiles = this.read(source).profiles;
368
+ if (Object.hasOwn(profiles, key))
369
+ return { profile: profiles[key], scope: source };
370
+ }
371
+ return void 0;
372
+ }
373
+ write(scope, document) {
374
+ const validated = parseModelProfilesDocument(document);
375
+ const file = this.paths[scope];
376
+ fs.mkdirSync(path.dirname(file), { recursive: true });
377
+ const temp = `${file}.${randomUUID()}.tmp`;
378
+ try {
379
+ fs.writeFileSync(temp, `${JSON.stringify(validated, null, 2)}
380
+ `, {
381
+ encoding: "utf8",
382
+ flag: "wx"
383
+ });
384
+ fs.renameSync(temp, file);
385
+ } finally {
386
+ if (fs.existsSync(temp)) fs.unlinkSync(temp);
387
+ }
388
+ }
389
+ /** Re-read before writing. An expected baseline enables same-profile conflict detection. */
390
+ save(scope, name, value, options = {}) {
391
+ const key = validateProfileName(name, true);
392
+ const next = parseNamedModelProfile(value);
393
+ const document = this.load(scope);
394
+ const existing = Object.hasOwn(document.profiles, key) ? document.profiles[key] : null;
395
+ if (Object.hasOwn(options, "expected") && !isDeepStrictEqual(existing, options.expected))
396
+ throw new ProfileConflictError(scope, key);
397
+ document.profiles[key] = next;
398
+ this.write(scope, document);
399
+ }
400
+ create(scope, name, value) {
401
+ const key = validateProfileName(name, true);
402
+ if (Object.hasOwn(this.load(scope).profiles, key))
403
+ throw new Error(`Profile already exists: ${key}`);
404
+ this.save(scope, key, value, { expected: null });
405
+ }
406
+ /** Pins that actually resolved to a global profile follow rename/delete. */
407
+ projectPointsToGlobal(name) {
408
+ const project = this.load("project");
409
+ return project.active === name && !Object.hasOwn(project.profiles, name);
410
+ }
411
+ rename(scope, source, target) {
412
+ const from = validateProfileName(source, true);
413
+ const to = validateProfileName(target, true);
414
+ const document = this.load(scope);
415
+ if (!Object.hasOwn(document.profiles, from))
416
+ throw new Error(`Profile not found: ${from}`);
417
+ if (Object.hasOwn(document.profiles, to))
418
+ throw new Error(`Profile already exists: ${to}`);
419
+ const followPin = scope === "global" && this.projectPointsToGlobal(from);
420
+ document.profiles[to] = document.profiles[from];
421
+ delete document.profiles[from];
422
+ if (document.active === from) document.active = to;
423
+ this.write(scope, document);
424
+ if (followPin) this.setProjectPin(to);
425
+ }
426
+ delete(scope, name) {
427
+ const key = validateProfileName(name, true);
428
+ const document = this.load(scope);
429
+ if (!Object.hasOwn(document.profiles, key))
430
+ throw new Error(`Profile not found: ${key}`);
431
+ const clearPin = scope === "global" && this.projectPointsToGlobal(key);
432
+ delete document.profiles[key];
433
+ const clearedActive = document.active === key;
434
+ if (clearedActive) delete document.active;
435
+ this.write(scope, document);
436
+ if (clearPin) this.setProjectPin();
437
+ return { clearedActive };
438
+ }
439
+ setActive(scope, name) {
440
+ const document = this.load(scope);
441
+ if (name === void 0) delete document.active;
442
+ else {
443
+ const key = validateProfileName(name);
444
+ if (key !== DEFAULT_PROFILE && !(scope === "global" ? Object.hasOwn(document.profiles, key) : this.lookup(key)))
445
+ throw new Error(`Profile not found: ${key}`);
446
+ document.active = key;
447
+ }
448
+ this.write(scope, document);
449
+ }
450
+ setGlobalActive(name) {
451
+ this.setActive("global", name);
452
+ }
453
+ setProjectPin(name) {
454
+ this.setActive("project", name);
455
+ }
456
+ resolve(sessionSelection) {
457
+ const warnings = [];
458
+ const documents = {
459
+ global: this.read("global", warnings),
460
+ project: this.read("project", warnings)
461
+ };
462
+ for (const [level, name] of [
463
+ ["session", sessionSelection],
464
+ ["project", documents.project.active],
465
+ ["global", documents.global.active]
466
+ ]) {
467
+ if (!name) continue;
468
+ if (name === DEFAULT_PROFILE)
469
+ return { name, profile: this.defaultProfile(), warnings };
470
+ const scope = Object.hasOwn(documents.project.profiles, name) ? "project" : "global";
471
+ if (Object.hasOwn(documents[scope].profiles, name) && (level !== "global" || Object.hasOwn(documents.global.profiles, name)))
472
+ return {
473
+ name,
474
+ scope,
475
+ profile: documents[scope].profiles[name],
476
+ warnings
477
+ };
478
+ const warning = `Missing ${level} model profile "${name}"; falling back.`;
479
+ warnings.push(warning);
480
+ this.options.warn?.(warning);
481
+ }
482
+ return { name: DEFAULT_PROFILE, profile: this.defaultProfile(), warnings };
483
+ }
484
+ };
485
+
486
+ // src/opencode/profiles/editor.ts
487
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
488
+
91
489
  // src/cli/package-version.ts
92
490
  import { readFileSync } from "fs";
93
491
  import { dirname as dirname2, join as join2 } from "path";
@@ -181,8 +579,8 @@ function resolveExecutingPackageVersion(options = {}) {
181
579
  }
182
580
 
183
581
  // src/config/loader.ts
184
- import * as fs from "fs";
185
- import * as path from "path";
582
+ import * as fs2 from "fs";
583
+ import * as path2 from "path";
186
584
 
187
585
  // src/cli/config-io.ts
188
586
  import {
@@ -259,111 +657,6 @@ function ensureOpenCodeConfigDir() {
259
657
  }
260
658
  }
261
659
 
262
- // src/config/schema.ts
263
- import { z } from "zod";
264
- var AGENT_NAMES = [
265
- "orchestrator",
266
- "oracle",
267
- "designer",
268
- "explorer",
269
- "librarian",
270
- "worker"
271
- ];
272
- var FALLBACK_AGENT_NAMES = [...AGENT_NAMES];
273
- var MANUAL_AGENT_NAMES = [...AGENT_NAMES];
274
- var ProviderModelIdSchema = z.string().regex(
275
- /^[^/\s]+\/[^\s]+$/,
276
- "Expected provider/model format (provider/.../model)"
277
- );
278
- var ManualAgentPlanSchema = z.object({
279
- primary: ProviderModelIdSchema,
280
- fallback1: ProviderModelIdSchema,
281
- fallback2: ProviderModelIdSchema,
282
- fallback3: ProviderModelIdSchema
283
- }).superRefine((value, ctx) => {
284
- const unique = /* @__PURE__ */ new Set([
285
- value.primary,
286
- value.fallback1,
287
- value.fallback2,
288
- value.fallback3
289
- ]);
290
- if (unique.size !== 4) {
291
- ctx.addIssue({
292
- code: z.ZodIssueCode.custom,
293
- message: "primary and fallbacks must be unique per agent"
294
- });
295
- }
296
- });
297
- var ManualPlanSchema = z.object({
298
- orchestrator: ManualAgentPlanSchema,
299
- oracle: ManualAgentPlanSchema,
300
- designer: ManualAgentPlanSchema,
301
- explorer: ManualAgentPlanSchema,
302
- librarian: ManualAgentPlanSchema,
303
- worker: ManualAgentPlanSchema
304
- }).strict();
305
- var AgentModelChainSchema = z.array(z.string()).min(1);
306
- var SupportedAgentRecordKeySchema = z.string().regex(/^(?!quick$|deep$).+$/, "Unsupported removed agent role");
307
- var FallbackChainsSchema = z.record(
308
- SupportedAgentRecordKeySchema,
309
- AgentModelChainSchema
310
- );
311
- var AgentOverrideConfigSchema = z.object({
312
- model: z.union([
313
- z.string(),
314
- z.array(
315
- z.union([
316
- z.string(),
317
- z.object({
318
- id: z.string(),
319
- variant: z.string().optional()
320
- })
321
- ])
322
- )
323
- ]).optional(),
324
- /** Pi specialist catalog filter; omitted discovers all, [] discovers none. */
325
- skills: z.array(z.string().trim().min(1)).optional(),
326
- temperature: z.number().min(0).max(2).optional(),
327
- steps: z.number().int().min(1).optional(),
328
- variant: z.string().optional().catch(void 0)
329
- });
330
- var PresetSchema = z.record(
331
- SupportedAgentRecordKeySchema,
332
- AgentOverrideConfigSchema
333
- );
334
- var AgentNameSchema = z.enum(AGENT_NAMES);
335
- var McpNameSchema = z.enum(["exa", "context7", "grep_app"]);
336
- var CodexGenerationConfigSchema = z.object({
337
- enabled: z.boolean().default(false),
338
- outputRoot: z.string().optional(),
339
- dryRun: z.boolean().default(true)
340
- });
341
- var ClaudeCodeGenerationConfigSchema = z.object({
342
- enabled: z.boolean().default(false),
343
- outputRoot: z.string().optional(),
344
- dryRun: z.boolean().default(true)
345
- });
346
- var FailoverConfigSchema = z.object({
347
- enabled: z.boolean().default(true),
348
- timeoutMs: z.number().min(0).default(15e3),
349
- retryDelayMs: z.number().min(0).default(500),
350
- chains: FallbackChainsSchema.default({})
351
- });
352
- var PluginConfigSchema = z.object({
353
- $schema: z.string().optional(),
354
- preset: z.string().optional(),
355
- setDefaultAgent: z.boolean().optional(),
356
- scoringEngineVersion: z.enum(["v1", "v2-shadow", "v2"]).optional(),
357
- balanceProviderUsage: z.boolean().optional(),
358
- manualPlan: ManualPlanSchema.optional(),
359
- presets: z.record(z.string(), PresetSchema).optional(),
360
- agents: z.record(SupportedAgentRecordKeySchema, AgentOverrideConfigSchema).optional(),
361
- disabled_mcps: z.array(z.string()).optional(),
362
- fallback: FailoverConfigSchema.optional(),
363
- codex: CodexGenerationConfigSchema.optional(),
364
- claudeCode: ClaudeCodeGenerationConfigSchema.optional()
365
- });
366
-
367
660
  // src/config/utils.ts
368
661
  function getAgentOverride(config, name) {
369
662
  return config?.agents?.[name];
@@ -399,28 +692,74 @@ var MODEL_MAPPINGS = {
399
692
  openai: buildOpenAIModelMappings()
400
693
  };
401
694
  function generateLiteConfig(_installConfig) {
402
- const config = {
403
- $schema: THOTH_AGENTS_CONFIG_SCHEMA_URL,
404
- preset: "openai",
405
- presets: {}
406
- };
407
- const createAgentConfig = (modelInfo) => {
408
- return {
409
- model: modelInfo.model,
410
- variant: modelInfo.variant
411
- };
695
+ return { $schema: THOTH_AGENTS_CONFIG_SCHEMA_URL };
696
+ }
697
+
698
+ // src/cli/opencode-profiles.ts
699
+ function openCodeProfileStore(roster) {
700
+ return new ModelProfileStore({
701
+ cwd: process.cwd(),
702
+ configDir: getConfigDir(),
703
+ roster
704
+ });
705
+ }
706
+ function activeOpenCodeProfile() {
707
+ const store = openCodeProfileStore();
708
+ const document = store.read("global");
709
+ return {
710
+ name: document.active,
711
+ profile: document.active ? store.lookup(document.active, "global")?.profile : void 0
412
712
  };
413
- const buildPreset = (mappingName) => {
414
- const mapping = MODEL_MAPPINGS[mappingName];
415
- return Object.fromEntries(
416
- Object.entries(mapping).map(([agentName, modelInfo]) => [
417
- agentName,
418
- createAgentConfig(modelInfo)
713
+ }
714
+ function effectiveOpenCodeProfile(config) {
715
+ const root = config?.agents ?? {};
716
+ const roster = Object.fromEntries(
717
+ PROFILE_ROLES.map((role) => [
718
+ role,
719
+ resolveModelPair(root[role], MODEL_MAPPINGS.openai[role])
720
+ ])
721
+ );
722
+ const selected = activeOpenCodeProfile().profile ?? { agents: {} };
723
+ return {
724
+ agents: Object.fromEntries(
725
+ PROFILE_ROLES.map((role) => [
726
+ role,
727
+ resolveRoleProfile(role, selected, roster)
419
728
  ])
420
- );
729
+ )
421
730
  };
422
- config.presets.openai = buildPreset("openai");
423
- return config;
731
+ }
732
+ function syncOpenCodeProfile(reset = false) {
733
+ const store = openCodeProfileStore();
734
+ const document = store.read("global");
735
+ const names = ["openai"];
736
+ if (document.active === "agents" && document.profiles.agents)
737
+ names.push("agents");
738
+ for (const name of names) {
739
+ const previous = document.profiles[name]?.agents ?? {};
740
+ const agents = Object.fromEntries(
741
+ PROFILE_ROLES.map((role) => {
742
+ const current = previous[role];
743
+ const legacy = LEGACY_OPENAI_PRESET[role];
744
+ const advance = current?.model === `openai/${legacy.model}` && current?.variant === legacy.effort;
745
+ return [
746
+ role,
747
+ reset && name === "openai" || advance ? MODEL_MAPPINGS.openai[role] : resolveRoleProfile(
748
+ role,
749
+ { agents: previous },
750
+ MODEL_MAPPINGS.openai
751
+ )
752
+ ];
753
+ })
754
+ );
755
+ store.save("global", name, { agents });
756
+ }
757
+ if (!document.active || !store.lookup(document.active, "global"))
758
+ store.setGlobalActive("openai");
759
+ }
760
+ function withoutLegacyPresets(config) {
761
+ const { preset: _preset, presets: _presets, ...rest } = config;
762
+ return rest;
424
763
  }
425
764
 
426
765
  // src/cli/config-io.ts
@@ -436,23 +775,23 @@ function stripJsonComments(json) {
436
775
  (match, comma, closing) => comma ? closing : match
437
776
  );
438
777
  }
439
- function parseConfigFile(path2) {
778
+ function parseConfigFile(path3) {
440
779
  try {
441
- if (!existsSync3(path2)) return { config: null };
442
- const stat = statSync(path2);
780
+ if (!existsSync3(path3)) return { config: null };
781
+ const stat = statSync(path3);
443
782
  if (stat.size === 0) return { config: null };
444
- const content = readFileSync2(path2, "utf-8");
783
+ const content = readFileSync2(path3, "utf-8");
445
784
  if (content.trim().length === 0) return { config: null };
446
785
  return { config: JSON.parse(stripJsonComments(content)) };
447
786
  } catch (err) {
448
787
  return { config: null, error: String(err) };
449
788
  }
450
789
  }
451
- function parseConfig(path2) {
452
- const result = parseConfigFile(path2);
790
+ function parseConfig(path3) {
791
+ const result = parseConfigFile(path3);
453
792
  if (result.config || result.error) return result;
454
- if (path2.endsWith(".json")) {
455
- const jsoncPath = path2.replace(/\.json$/, ".jsonc");
793
+ if (path3.endsWith(".json")) {
794
+ const jsoncPath = path3.replace(/\.json$/, ".jsonc");
456
795
  return parseConfigFile(jsoncPath);
457
796
  }
458
797
  return { config: null };
@@ -556,43 +895,6 @@ function configObject(value) {
556
895
  throw new Error("Expected an object in managed lite configuration.");
557
896
  return value;
558
897
  }
559
- function mergeLiteConfig(defaults, installed) {
560
- const presets = { ...configObject(installed.presets) };
561
- const shipped = configObject(configObject(defaults.presets).openai);
562
- const activeName = typeof installed.preset === "string" ? installed.preset : "openai";
563
- const rootAgents = { ...configObject(installed.agents) };
564
- for (const name of /* @__PURE__ */ new Set(["openai", activeName])) {
565
- const roles = { ...configObject(presets[name]) };
566
- for (const [role, legacy] of Object.entries(LEGACY_OPENAI_PRESET)) {
567
- const rootRole = name === activeName ? rootAgents[role] : void 0;
568
- const current = {
569
- ...configObject(roles[role]),
570
- ...configObject(rootRole)
571
- };
572
- const advancesLegacy = current.model === `openai/${legacy.model}` && current.variant === legacy.effort;
573
- if (roles[role] === void 0 && rootRole === void 0) {
574
- roles[role] = shipped[role];
575
- } else if (advancesLegacy) {
576
- roles[role] = { ...current, ...configObject(shipped[role]) };
577
- if (rootRole !== void 0)
578
- rootAgents[role] = {
579
- ...configObject(rootRole),
580
- ...configObject(shipped[role])
581
- };
582
- } else {
583
- roles[role] = current;
584
- }
585
- }
586
- presets[name] = roles;
587
- }
588
- return {
589
- ...defaults,
590
- ...installed,
591
- preset: activeName,
592
- presets,
593
- ...installed.agents !== void 0 ? { agents: rootAgents } : {}
594
- };
595
- }
596
898
  function writeLiteConfig(installConfig, targetPath) {
597
899
  const configPath = targetPath ?? getLiteConfig();
598
900
  try {
@@ -601,8 +903,10 @@ function writeLiteConfig(installConfig, targetPath) {
601
903
  if (!installConfig.reset && existsSync3(configPath)) {
602
904
  const { config: installed, error } = parseConfigFile(configPath);
603
905
  if (error) throw new Error(`Failed to parse lite config: ${error}`);
604
- if (installed) config = mergeLiteConfig(config, configObject(installed));
906
+ if (installed) config = { ...config, ...configObject(installed) };
605
907
  }
908
+ config = withoutLegacyPresets(config);
909
+ syncOpenCodeProfile(installConfig.reset);
606
910
  const tmpPath = `${configPath}.tmp`;
607
911
  const bakPath = `${configPath}.bak`;
608
912
  const content = `${JSON.stringify(config, null, 2)}
@@ -635,9 +939,7 @@ function detectCurrentConfig() {
635
939
  const { config: liteConfig } = parseConfig(getLiteConfig());
636
940
  if (liteConfig && typeof liteConfig === "object") {
637
941
  const configObj = liteConfig;
638
- const presetName = configObj.preset;
639
- const presets = configObj.presets;
640
- const agents = presets?.[presetName];
942
+ const agents = effectiveOpenCodeProfile(configObj).agents;
641
943
  if (agents) {
642
944
  const models = Object.values(agents).map((a) => a?.model).filter(Boolean);
643
945
  result.hasOpenAI = models.some((m) => m?.startsWith("openai/"));
@@ -648,17 +950,31 @@ function detectCurrentConfig() {
648
950
 
649
951
  // src/config/loader.ts
650
952
  var PROMPTS_DIR_NAME = "thoth-agents";
651
- function loadConfigFromPath(configPath) {
953
+ function withoutInjectedDefaults(parsed, raw) {
954
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || raw === null || typeof raw !== "object") {
955
+ return parsed;
956
+ }
957
+ return Object.fromEntries(
958
+ Object.entries(parsed).filter(([key]) => Object.hasOwn(raw, key)).map(([key, value]) => [
959
+ key,
960
+ withoutInjectedDefaults(value, raw[key])
961
+ ])
962
+ );
963
+ }
964
+ function loadConfigFromPath(configPath, unsupportedKeyPaths) {
652
965
  try {
653
- const content = fs.readFileSync(configPath, "utf-8");
966
+ const content = fs2.readFileSync(configPath, "utf-8");
654
967
  const rawConfig = JSON.parse(stripJsonComments(content));
968
+ if (rawConfig !== null && typeof rawConfig === "object" && ("preset" in rawConfig || "presets" in rawConfig)) {
969
+ unsupportedKeyPaths.add(configPath);
970
+ }
655
971
  const result = PluginConfigSchema.safeParse(rawConfig);
656
972
  if (!result.success) {
657
973
  console.warn(`[thoth-agents] Invalid config at ${configPath}:`);
658
974
  console.warn(result.error.format());
659
975
  return null;
660
976
  }
661
- return result.data;
977
+ return withoutInjectedDefaults(result.data, rawConfig);
662
978
  } catch (error) {
663
979
  if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
664
980
  console.warn(
@@ -672,10 +988,10 @@ function loadConfigFromPath(configPath) {
672
988
  function findConfigPath(basePath) {
673
989
  const jsoncPath = `${basePath}.jsonc`;
674
990
  const jsonPath = `${basePath}.json`;
675
- if (fs.existsSync(jsoncPath)) {
991
+ if (fs2.existsSync(jsoncPath)) {
676
992
  return jsoncPath;
677
993
  }
678
- if (fs.existsSync(jsonPath)) {
994
+ if (fs2.existsSync(jsonPath)) {
679
995
  return jsonPath;
680
996
  }
681
997
  return null;
@@ -683,9 +999,9 @@ function findConfigPath(basePath) {
683
999
  function deepMerge(base, override) {
684
1000
  if (!base) return override;
685
1001
  if (!override) return base;
686
- const result = { ...base };
1002
+ const result = Object.assign(/* @__PURE__ */ Object.create(null), base);
687
1003
  for (const key of Object.keys(override)) {
688
- const baseVal = base[key];
1004
+ const baseVal = Object.hasOwn(base, key) ? base[key] : void 0;
689
1005
  const overrideVal = override[key];
690
1006
  if (typeof baseVal === "object" && baseVal !== null && typeof overrideVal === "object" && overrideVal !== null && !Array.isArray(baseVal) && !Array.isArray(overrideVal)) {
691
1007
  result[key] = deepMerge(
@@ -699,16 +1015,17 @@ function deepMerge(base, override) {
699
1015
  return result;
700
1016
  }
701
1017
  function loadPluginConfig(directory) {
702
- const userConfigBasePath = path.join(getConfigDir(), "thoth-agents");
703
- const projectConfigBasePath = path.join(
1018
+ const userConfigBasePath = path2.join(getConfigDir(), "thoth-agents");
1019
+ const projectConfigBasePath = path2.join(
704
1020
  directory,
705
1021
  ".opencode",
706
1022
  "thoth-agents"
707
1023
  );
708
1024
  const userConfigPath = findConfigPath(userConfigBasePath);
709
1025
  const projectConfigPath = findConfigPath(projectConfigBasePath);
710
- let config = userConfigPath ? loadConfigFromPath(userConfigPath) ?? {} : {};
711
- const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath) : null;
1026
+ const unsupportedKeyPaths = /* @__PURE__ */ new Set();
1027
+ let config = userConfigPath ? loadConfigFromPath(userConfigPath, unsupportedKeyPaths) ?? {} : {};
1028
+ const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath, unsupportedKeyPaths) : null;
712
1029
  if (projectConfig) {
713
1030
  config = {
714
1031
  ...config,
@@ -719,51 +1036,40 @@ function loadPluginConfig(directory) {
719
1036
  claudeCode: deepMerge(config.claudeCode, projectConfig.claudeCode)
720
1037
  };
721
1038
  }
722
- const envPreset = process.env.THOTH_AGENTS_PRESET;
723
- if (envPreset) {
724
- config.preset = envPreset;
1039
+ if (unsupportedKeyPaths.size > 0) {
1040
+ console.warn(
1041
+ `[thoth-agents] Config keys preset/presets are unsupported and ignored (${[...unsupportedKeyPaths].join(", ")}).`
1042
+ );
725
1043
  }
726
- if (config.preset) {
727
- const preset = config.presets?.[config.preset];
728
- if (preset) {
729
- config.agents = deepMerge(preset, config.agents);
730
- } else {
731
- const presetSource = envPreset === config.preset ? "environment variable" : "config file";
732
- const availablePresets = config.presets ? Object.keys(config.presets).join(", ") : "none";
733
- console.warn(
734
- `[thoth-agents] Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`
735
- );
736
- }
1044
+ const result = PluginConfigSchema.safeParse(config);
1045
+ if (!result.success) {
1046
+ const sourcePaths = [userConfigPath, projectConfigPath].filter(Boolean);
1047
+ console.warn(
1048
+ `[thoth-agents] Invalid merged config from ${sourcePaths.join(", ")}:`
1049
+ );
1050
+ console.warn(result.error.format());
1051
+ return {};
737
1052
  }
738
- return config;
1053
+ return result.data;
739
1054
  }
740
- function loadAgentPrompt(agentName, preset) {
741
- const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : void 0;
742
- const promptsDir = path.join(getConfigDir(), PROMPTS_DIR_NAME);
743
- const promptSearchDirs = presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
1055
+ function loadAgentPrompt(agentName) {
1056
+ const promptsDir = path2.join(getConfigDir(), PROMPTS_DIR_NAME);
744
1057
  const result = {};
745
- const readFirstPrompt = (fileName, errorPrefix) => {
746
- for (const dir of promptSearchDirs) {
747
- const promptPath = path.join(dir, fileName);
748
- if (!fs.existsSync(promptPath)) {
749
- continue;
750
- }
751
- try {
752
- return fs.readFileSync(promptPath, "utf-8");
753
- } catch (error) {
754
- console.warn(
755
- `[thoth-agents] ${errorPrefix} ${promptPath}:`,
756
- error instanceof Error ? error.message : String(error)
757
- );
758
- }
1058
+ const readPrompt = (fileName, errorPrefix) => {
1059
+ const promptPath = path2.join(promptsDir, fileName);
1060
+ if (!fs2.existsSync(promptPath)) return void 0;
1061
+ try {
1062
+ return fs2.readFileSync(promptPath, "utf-8");
1063
+ } catch (error) {
1064
+ console.warn(
1065
+ `[thoth-agents] ${errorPrefix} ${promptPath}:`,
1066
+ error instanceof Error ? error.message : String(error)
1067
+ );
1068
+ return void 0;
759
1069
  }
760
- return void 0;
761
1070
  };
762
- result.prompt = readFirstPrompt(
763
- `${agentName}.md`,
764
- "Error reading prompt file"
765
- );
766
- result.appendPrompt = readFirstPrompt(
1071
+ result.prompt = readPrompt(`${agentName}.md`, "Error reading prompt file");
1072
+ result.appendPrompt = readPrompt(
767
1073
  `${agentName}_append.md`,
768
1074
  "Error reading append prompt file"
769
1075
  );
@@ -774,7 +1080,7 @@ function loadAgentPrompt(agentName, preset) {
774
1080
  var text = { type: "string" };
775
1081
  var texts = { type: "array", items: text };
776
1082
  var nullableText = { type: ["string", "null"] };
777
- function object(properties) {
1083
+ function object2(properties) {
778
1084
  return {
779
1085
  type: "object",
780
1086
  properties,
@@ -785,7 +1091,7 @@ function object(properties) {
785
1091
  function array(items) {
786
1092
  return { type: "array", items };
787
1093
  }
788
- var common = object({
1094
+ var common = object2({
789
1095
  schemaVersion: { type: "number", const: 1 },
790
1096
  role: {
791
1097
  type: "string",
@@ -797,7 +1103,7 @@ var common = object({
797
1103
  },
798
1104
  conclusion: text,
799
1105
  evidence: array(
800
- object({
1106
+ object2({
801
1107
  id: { type: "string", minLength: 1 },
802
1108
  kind: text,
803
1109
  locator: text,
@@ -807,7 +1113,7 @@ var common = object({
807
1113
  })
808
1114
  ),
809
1115
  verification: array(
810
- object({
1116
+ object2({
811
1117
  check: text,
812
1118
  method: text,
813
1119
  outcome: { type: "string", enum: ["pass", "fail", "not-run"] },
@@ -816,7 +1122,7 @@ var common = object({
816
1122
  })
817
1123
  ),
818
1124
  risks: array(
819
- object({
1125
+ object2({
820
1126
  description: text,
821
1127
  severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
822
1128
  blocking: { type: "boolean" },
@@ -824,11 +1130,11 @@ var common = object({
824
1130
  })
825
1131
  ),
826
1132
  openQuestions: array(
827
- object({ question: text, owner: text, options: texts, facts: texts })
1133
+ object2({ question: text, owner: text, options: texts, facts: texts })
828
1134
  )
829
1135
  });
830
1136
  var review = {
831
- ...object({
1137
+ ...object2({
832
1138
  kind: {
833
1139
  type: "string",
834
1140
  enum: ["plan-review", "final-verification"]
@@ -844,7 +1150,7 @@ var SPECIALIST_RESULT_CONTRACT = {
844
1150
  fence: "thoth-result",
845
1151
  common,
846
1152
  roles: {
847
- oracle: object({ nextAction: nullableText, review })
1153
+ oracle: object2({ nextAction: nullableText, review })
848
1154
  },
849
1155
  reviewKinds: {
850
1156
  "plan-review": { verdicts: ["OKAY", "REJECT"] },
@@ -2411,6 +2717,7 @@ function composeAgentPrompt({
2411
2717
  }
2412
2718
 
2413
2719
  export {
2720
+ PROFILE_ROLES,
2414
2721
  SUBAGENT_NAMES,
2415
2722
  ALL_AGENT_NAMES,
2416
2723
  LEGACY_OPENAI_PRESET,
@@ -2419,15 +2726,26 @@ export {
2419
2726
  DEFAULT_MODELS,
2420
2727
  getDefaultOpenCodeModel,
2421
2728
  getDefaultOpenCodeVariant,
2422
- findPackageRoot,
2423
- isValidPackageVersion,
2424
- resolveExecutingPackageVersion,
2729
+ resolveRoleProfile,
2730
+ mergeProfileModelChain,
2731
+ formatModelString,
2732
+ validateProfileName,
2733
+ ProfileConflictError,
2734
+ ModelProfileStore,
2425
2735
  getConfigDir,
2426
2736
  getOpenCodeManagedModelStatePath,
2427
2737
  getExistingLiteConfigPath,
2428
2738
  getExistingConfigPath,
2429
2739
  ensureConfigDir,
2740
+ MODEL_MAPPINGS,
2430
2741
  generateLiteConfig,
2742
+ openCodeProfileStore,
2743
+ activeOpenCodeProfile,
2744
+ effectiveOpenCodeProfile,
2745
+ withoutLegacyPresets,
2746
+ findPackageRoot,
2747
+ isValidPackageVersion,
2748
+ resolveExecutingPackageVersion,
2431
2749
  parseConfig,
2432
2750
  writeConfig,
2433
2751
  pluginEntries,