thoth-agents 0.12.0 → 0.13.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.
Files changed (43) hide show
  1. package/README.md +11 -2
  2. package/dist/{chunk-SEF6PFM6.js → chunk-5MYXVRZZ.js} +132 -157
  3. package/dist/chunk-6O4GKDS7.js +207 -0
  4. package/dist/{chunk-QOA2AETX.js → chunk-NOLDFT7O.js} +3 -3
  5. package/dist/{chunk-7F33WUSD.js → chunk-VVYAQTYE.js} +2 -2
  6. package/dist/{chunk-NY2IPJ7V.js → chunk-YCWGTDFE.js} +531 -233
  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 +1384 -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,10 +950,13 @@ function detectCurrentConfig() {
648
950
 
649
951
  // src/config/loader.ts
650
952
  var PROMPTS_DIR_NAME = "thoth-agents";
651
- function loadConfigFromPath(configPath) {
953
+ function loadConfigFromPath(configPath, unsupportedKeyPaths) {
652
954
  try {
653
- const content = fs.readFileSync(configPath, "utf-8");
955
+ const content = fs2.readFileSync(configPath, "utf-8");
654
956
  const rawConfig = JSON.parse(stripJsonComments(content));
957
+ if (rawConfig !== null && typeof rawConfig === "object" && ("preset" in rawConfig || "presets" in rawConfig)) {
958
+ unsupportedKeyPaths.add(configPath);
959
+ }
655
960
  const result = PluginConfigSchema.safeParse(rawConfig);
656
961
  if (!result.success) {
657
962
  console.warn(`[thoth-agents] Invalid config at ${configPath}:`);
@@ -672,10 +977,10 @@ function loadConfigFromPath(configPath) {
672
977
  function findConfigPath(basePath) {
673
978
  const jsoncPath = `${basePath}.jsonc`;
674
979
  const jsonPath = `${basePath}.json`;
675
- if (fs.existsSync(jsoncPath)) {
980
+ if (fs2.existsSync(jsoncPath)) {
676
981
  return jsoncPath;
677
982
  }
678
- if (fs.existsSync(jsonPath)) {
983
+ if (fs2.existsSync(jsonPath)) {
679
984
  return jsonPath;
680
985
  }
681
986
  return null;
@@ -699,16 +1004,17 @@ function deepMerge(base, override) {
699
1004
  return result;
700
1005
  }
701
1006
  function loadPluginConfig(directory) {
702
- const userConfigBasePath = path.join(getConfigDir(), "thoth-agents");
703
- const projectConfigBasePath = path.join(
1007
+ const userConfigBasePath = path2.join(getConfigDir(), "thoth-agents");
1008
+ const projectConfigBasePath = path2.join(
704
1009
  directory,
705
1010
  ".opencode",
706
1011
  "thoth-agents"
707
1012
  );
708
1013
  const userConfigPath = findConfigPath(userConfigBasePath);
709
1014
  const projectConfigPath = findConfigPath(projectConfigBasePath);
710
- let config = userConfigPath ? loadConfigFromPath(userConfigPath) ?? {} : {};
711
- const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath) : null;
1015
+ const unsupportedKeyPaths = /* @__PURE__ */ new Set();
1016
+ let config = userConfigPath ? loadConfigFromPath(userConfigPath, unsupportedKeyPaths) ?? {} : {};
1017
+ const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath, unsupportedKeyPaths) : null;
712
1018
  if (projectConfig) {
713
1019
  config = {
714
1020
  ...config,
@@ -719,51 +1025,31 @@ function loadPluginConfig(directory) {
719
1025
  claudeCode: deepMerge(config.claudeCode, projectConfig.claudeCode)
720
1026
  };
721
1027
  }
722
- const envPreset = process.env.THOTH_AGENTS_PRESET;
723
- if (envPreset) {
724
- config.preset = envPreset;
725
- }
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
- }
1028
+ if (unsupportedKeyPaths.size > 0) {
1029
+ console.warn(
1030
+ `[thoth-agents] Config keys preset/presets are unsupported and ignored (${[...unsupportedKeyPaths].join(", ")}).`
1031
+ );
737
1032
  }
738
1033
  return config;
739
1034
  }
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];
1035
+ function loadAgentPrompt(agentName) {
1036
+ const promptsDir = path2.join(getConfigDir(), PROMPTS_DIR_NAME);
744
1037
  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
- }
1038
+ const readPrompt = (fileName, errorPrefix) => {
1039
+ const promptPath = path2.join(promptsDir, fileName);
1040
+ if (!fs2.existsSync(promptPath)) return void 0;
1041
+ try {
1042
+ return fs2.readFileSync(promptPath, "utf-8");
1043
+ } catch (error) {
1044
+ console.warn(
1045
+ `[thoth-agents] ${errorPrefix} ${promptPath}:`,
1046
+ error instanceof Error ? error.message : String(error)
1047
+ );
1048
+ return void 0;
759
1049
  }
760
- return void 0;
761
1050
  };
762
- result.prompt = readFirstPrompt(
763
- `${agentName}.md`,
764
- "Error reading prompt file"
765
- );
766
- result.appendPrompt = readFirstPrompt(
1051
+ result.prompt = readPrompt(`${agentName}.md`, "Error reading prompt file");
1052
+ result.appendPrompt = readPrompt(
767
1053
  `${agentName}_append.md`,
768
1054
  "Error reading append prompt file"
769
1055
  );
@@ -774,7 +1060,7 @@ function loadAgentPrompt(agentName, preset) {
774
1060
  var text = { type: "string" };
775
1061
  var texts = { type: "array", items: text };
776
1062
  var nullableText = { type: ["string", "null"] };
777
- function object(properties) {
1063
+ function object2(properties) {
778
1064
  return {
779
1065
  type: "object",
780
1066
  properties,
@@ -785,7 +1071,7 @@ function object(properties) {
785
1071
  function array(items) {
786
1072
  return { type: "array", items };
787
1073
  }
788
- var common = object({
1074
+ var common = object2({
789
1075
  schemaVersion: { type: "number", const: 1 },
790
1076
  role: {
791
1077
  type: "string",
@@ -797,7 +1083,7 @@ var common = object({
797
1083
  },
798
1084
  conclusion: text,
799
1085
  evidence: array(
800
- object({
1086
+ object2({
801
1087
  id: { type: "string", minLength: 1 },
802
1088
  kind: text,
803
1089
  locator: text,
@@ -807,7 +1093,7 @@ var common = object({
807
1093
  })
808
1094
  ),
809
1095
  verification: array(
810
- object({
1096
+ object2({
811
1097
  check: text,
812
1098
  method: text,
813
1099
  outcome: { type: "string", enum: ["pass", "fail", "not-run"] },
@@ -816,7 +1102,7 @@ var common = object({
816
1102
  })
817
1103
  ),
818
1104
  risks: array(
819
- object({
1105
+ object2({
820
1106
  description: text,
821
1107
  severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
822
1108
  blocking: { type: "boolean" },
@@ -824,11 +1110,11 @@ var common = object({
824
1110
  })
825
1111
  ),
826
1112
  openQuestions: array(
827
- object({ question: text, owner: text, options: texts, facts: texts })
1113
+ object2({ question: text, owner: text, options: texts, facts: texts })
828
1114
  )
829
1115
  });
830
1116
  var review = {
831
- ...object({
1117
+ ...object2({
832
1118
  kind: {
833
1119
  type: "string",
834
1120
  enum: ["plan-review", "final-verification"]
@@ -844,7 +1130,7 @@ var SPECIALIST_RESULT_CONTRACT = {
844
1130
  fence: "thoth-result",
845
1131
  common,
846
1132
  roles: {
847
- oracle: object({ nextAction: nullableText, review })
1133
+ oracle: object2({ nextAction: nullableText, review })
848
1134
  },
849
1135
  reviewKinds: {
850
1136
  "plan-review": { verdicts: ["OKAY", "REJECT"] },
@@ -2411,6 +2697,7 @@ function composeAgentPrompt({
2411
2697
  }
2412
2698
 
2413
2699
  export {
2700
+ PROFILE_ROLES,
2414
2701
  SUBAGENT_NAMES,
2415
2702
  ALL_AGENT_NAMES,
2416
2703
  LEGACY_OPENAI_PRESET,
@@ -2419,15 +2706,26 @@ export {
2419
2706
  DEFAULT_MODELS,
2420
2707
  getDefaultOpenCodeModel,
2421
2708
  getDefaultOpenCodeVariant,
2422
- findPackageRoot,
2423
- isValidPackageVersion,
2424
- resolveExecutingPackageVersion,
2709
+ resolveRoleProfile,
2710
+ mergeProfileModelChain,
2711
+ formatModelString,
2712
+ validateProfileName,
2713
+ ProfileConflictError,
2714
+ ModelProfileStore,
2425
2715
  getConfigDir,
2426
2716
  getOpenCodeManagedModelStatePath,
2427
2717
  getExistingLiteConfigPath,
2428
2718
  getExistingConfigPath,
2429
2719
  ensureConfigDir,
2720
+ MODEL_MAPPINGS,
2430
2721
  generateLiteConfig,
2722
+ openCodeProfileStore,
2723
+ activeOpenCodeProfile,
2724
+ effectiveOpenCodeProfile,
2725
+ withoutLegacyPresets,
2726
+ findPackageRoot,
2727
+ isValidPackageVersion,
2728
+ resolveExecutingPackageVersion,
2431
2729
  parseConfig,
2432
2730
  writeConfig,
2433
2731
  pluginEntries,