thoth-agents 0.11.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.
- package/README.md +16 -5
- package/dist/agents/agent-config.d.ts +13 -0
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/orchestrator.d.ts +1 -1
- package/dist/{chunk-I5TXGNSG.js → chunk-5MYXVRZZ.js} +634 -366
- package/dist/chunk-6O4GKDS7.js +207 -0
- package/dist/{chunk-RWQPMWEM.js → chunk-JLU3FSBT.js} +17 -2
- package/dist/chunk-MIDGZE34.js +107 -0
- package/dist/{chunk-HACUJFKI.js → chunk-NOLDFT7O.js} +4 -230
- package/dist/{chunk-5WPP6IY7.js → chunk-VVYAQTYE.js} +2 -2
- package/dist/{chunk-DNPKVFOB.js → chunk-YCWGTDFE.js} +604 -315
- package/dist/cli/config-io.d.ts +2 -2
- package/dist/cli/index.js +94 -41
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/opencode-profiles.d.ts +10 -0
- package/dist/cli/opencode-skill-consent.d.ts +5 -0
- package/dist/cli/operations/opencode.d.ts +2 -1
- package/dist/cli/operations/types.d.ts +1 -0
- package/dist/cli/owned-skills.d.ts +16 -0
- package/dist/cli/paths.d.ts +1 -1
- package/dist/cli/providers.d.ts +1 -1
- package/dist/cli/system.d.ts +3 -1
- package/dist/cli/thoth-mem-install.d.ts +10 -10
- package/dist/cli/tui/index.js +53 -5
- package/dist/cli/tui/operations.d.ts +2 -0
- package/dist/cli/types.d.ts +2 -4
- package/dist/config/loader.d.ts +2 -5
- package/dist/config/schema.d.ts +0 -53
- package/dist/harness/adapters/opencode.d.ts +1 -1
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -8
- package/dist/hooks/foreground-fallback/index.d.ts +14 -69
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/json-error-recovery/hook.d.ts +2 -15
- package/dist/hooks/oracle-gate-verdict/hook.d.ts +4 -11
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1603 -1408
- package/dist/opencode/agents.d.ts +3 -0
- package/dist/opencode/command.d.ts +3 -0
- package/dist/opencode/context.d.ts +36 -0
- package/dist/opencode/delegation-hooks.d.ts +3 -0
- package/dist/opencode/mcp.d.ts +3 -0
- package/dist/opencode/permission-rules.d.ts +9 -0
- package/dist/opencode/permissions.d.ts +3 -0
- package/dist/opencode/profile-rpc-handlers.d.ts +3 -0
- package/dist/opencode/profile-rpc.d.ts +688 -0
- package/dist/opencode/profile-runtime.d.ts +3 -0
- package/dist/opencode/profile-sessions.d.ts +18 -0
- package/dist/opencode/profiles/editor.d.ts +44 -0
- package/dist/opencode/profiles/index.d.ts +18 -0
- package/dist/opencode/profiles/resolver.d.ts +12 -0
- package/dist/opencode/profiles/store.d.ts +63 -0
- package/dist/opencode/profiles/types.d.ts +26 -0
- package/dist/opencode/session-hooks.d.ts +3 -0
- package/dist/opencode/session-state.d.ts +39 -0
- package/dist/opencode/skills.d.ts +3 -0
- package/dist/opencode/tool-overrides/evaluator.d.ts +13 -0
- package/dist/opencode/tool-overrides/index.d.ts +5 -0
- package/dist/opencode/tool-overrides/store.d.ts +38 -0
- package/dist/opencode/tool-overrides/types.d.ts +32 -0
- package/dist/opencode/tool-overrides/validation.d.ts +5 -0
- package/dist/opencode/tool-overrides/wildcard.d.ts +3 -0
- package/dist/opencode/tool-rpc-handlers.d.ts +3 -0
- package/dist/opencode/tool-rpc.d.ts +428 -0
- package/dist/opencode/tools.d.ts +3 -0
- package/dist/pi.js +3 -3
- package/dist/tools/ast-grep/cli.d.ts +1 -0
- package/dist/tools/ast-grep/index.d.ts +108 -2
- package/dist/tools/ast-grep/tools.d.ts +107 -3
- package/dist/tools/define-tool.d.ts +23 -0
- package/dist/tools/lsp/tools.d.ts +97 -5
- package/dist/tui/fixtures.d.ts +2 -0
- package/dist/tui/flow.d.ts +16 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.js +1384 -0
- package/dist/tui/model.d.ts +84 -0
- package/dist/tui/tools/fixtures.d.ts +3 -0
- package/dist/tui/tools/flow.d.ts +11 -0
- package/dist/tui/tools/model.d.ts +97 -0
- package/dist/utils/index.d.ts +0 -1
- package/package.json +18 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/skills/thoth-sdd/SKILL.md +8 -2
- package/plugin/skills/thoth-sdd/scripts/validate-result.mjs +28 -2
- package/skills/thoth-sdd/SKILL.md +8 -2
- package/skills/thoth-sdd/scripts/validate-result.mjs +28 -2
- package/thoth-agents.schema.json +0 -96
- package/dist/chunk-62L3ORXN.js +0 -30
- package/dist/delegation/project-id.d.ts +0 -10
- package/dist/hooks/auto-update-checker/checker.d.ts +0 -23
- package/dist/hooks/auto-update-checker/constants.d.ts +0 -11
- package/dist/hooks/auto-update-checker/index.d.ts +0 -17
- package/dist/hooks/auto-update-checker/types.d.ts +0 -22
- package/dist/utils/tmux-session-manager.d.ts +0 -63
- package/dist/utils/tmux.d.ts +0 -32
|
@@ -19,19 +19,107 @@ function findPackageRoot(startDir) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// src/config/
|
|
23
|
-
import
|
|
24
|
-
|
|
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
|
+
});
|
|
25
120
|
|
|
26
|
-
// src/
|
|
27
|
-
|
|
28
|
-
copyFileSync,
|
|
29
|
-
existsSync as existsSync3,
|
|
30
|
-
readFileSync as readFileSync2,
|
|
31
|
-
renameSync,
|
|
32
|
-
statSync,
|
|
33
|
-
writeFileSync
|
|
34
|
-
} from "fs";
|
|
121
|
+
// src/opencode/profiles/types.ts
|
|
122
|
+
var PROFILE_ROLES = AgentNameSchema.options;
|
|
35
123
|
|
|
36
124
|
// src/config/constants.ts
|
|
37
125
|
var SUBAGENT_NAMES = [
|
|
@@ -99,10 +187,305 @@ function getDefaultOpenCodeVariant(name) {
|
|
|
99
187
|
}
|
|
100
188
|
return CONFIRMED_OPENAI_SUBAGENT_PRESET[name].effort;
|
|
101
189
|
}
|
|
102
|
-
var POLL_INTERVAL_BACKGROUND_MS = 2e3;
|
|
103
190
|
var DEFAULT_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
104
191
|
var MAX_POLL_TIME_MS = 5 * 60 * 1e3;
|
|
105
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
|
+
|
|
106
489
|
// src/cli/package-version.ts
|
|
107
490
|
import { readFileSync } from "fs";
|
|
108
491
|
import { dirname as dirname2, join as join2 } from "path";
|
|
@@ -195,27 +578,41 @@ function resolveExecutingPackageVersion(options = {}) {
|
|
|
195
578
|
return { ok: true, version, packageRoot };
|
|
196
579
|
}
|
|
197
580
|
|
|
581
|
+
// src/config/loader.ts
|
|
582
|
+
import * as fs2 from "fs";
|
|
583
|
+
import * as path2 from "path";
|
|
584
|
+
|
|
585
|
+
// src/cli/config-io.ts
|
|
586
|
+
import {
|
|
587
|
+
copyFileSync,
|
|
588
|
+
existsSync as existsSync3,
|
|
589
|
+
readFileSync as readFileSync2,
|
|
590
|
+
renameSync,
|
|
591
|
+
statSync,
|
|
592
|
+
writeFileSync
|
|
593
|
+
} from "fs";
|
|
594
|
+
|
|
198
595
|
// src/cli/paths.ts
|
|
199
596
|
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
200
597
|
import { homedir } from "os";
|
|
201
598
|
import { dirname as dirname3, join as join3 } from "path";
|
|
202
|
-
function getDefaultOpenCodeConfigDir() {
|
|
203
|
-
const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join3(
|
|
599
|
+
function getDefaultOpenCodeConfigDir(homeDir) {
|
|
600
|
+
const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join3(homeDir, ".config");
|
|
204
601
|
return join3(userConfigDir, "opencode");
|
|
205
602
|
}
|
|
206
603
|
function getCustomOpenCodeConfigDir() {
|
|
207
604
|
const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
208
605
|
return configDir || void 0;
|
|
209
606
|
}
|
|
210
|
-
function getConfigDir() {
|
|
607
|
+
function getConfigDir(homeDir = homedir()) {
|
|
211
608
|
const customConfigDir = getCustomOpenCodeConfigDir();
|
|
212
609
|
if (customConfigDir) {
|
|
213
610
|
return customConfigDir;
|
|
214
611
|
}
|
|
215
|
-
return getDefaultOpenCodeConfigDir();
|
|
612
|
+
return getDefaultOpenCodeConfigDir(homeDir);
|
|
216
613
|
}
|
|
217
614
|
function getOpenCodeConfigPaths() {
|
|
218
|
-
const configDir =
|
|
615
|
+
const configDir = getConfigDir();
|
|
219
616
|
return [join3(configDir, "opencode.json"), join3(configDir, "opencode.jsonc")];
|
|
220
617
|
}
|
|
221
618
|
function getConfigJson() {
|
|
@@ -260,130 +657,6 @@ function ensureOpenCodeConfigDir() {
|
|
|
260
657
|
}
|
|
261
658
|
}
|
|
262
659
|
|
|
263
|
-
// src/config/schema.ts
|
|
264
|
-
import { z } from "zod";
|
|
265
|
-
var AGENT_NAMES = [
|
|
266
|
-
"orchestrator",
|
|
267
|
-
"oracle",
|
|
268
|
-
"designer",
|
|
269
|
-
"explorer",
|
|
270
|
-
"librarian",
|
|
271
|
-
"worker"
|
|
272
|
-
];
|
|
273
|
-
var FALLBACK_AGENT_NAMES = [...AGENT_NAMES];
|
|
274
|
-
var MANUAL_AGENT_NAMES = [...AGENT_NAMES];
|
|
275
|
-
var ProviderModelIdSchema = z.string().regex(
|
|
276
|
-
/^[^/\s]+\/[^\s]+$/,
|
|
277
|
-
"Expected provider/model format (provider/.../model)"
|
|
278
|
-
);
|
|
279
|
-
var ManualAgentPlanSchema = z.object({
|
|
280
|
-
primary: ProviderModelIdSchema,
|
|
281
|
-
fallback1: ProviderModelIdSchema,
|
|
282
|
-
fallback2: ProviderModelIdSchema,
|
|
283
|
-
fallback3: ProviderModelIdSchema
|
|
284
|
-
}).superRefine((value, ctx) => {
|
|
285
|
-
const unique = /* @__PURE__ */ new Set([
|
|
286
|
-
value.primary,
|
|
287
|
-
value.fallback1,
|
|
288
|
-
value.fallback2,
|
|
289
|
-
value.fallback3
|
|
290
|
-
]);
|
|
291
|
-
if (unique.size !== 4) {
|
|
292
|
-
ctx.addIssue({
|
|
293
|
-
code: z.ZodIssueCode.custom,
|
|
294
|
-
message: "primary and fallbacks must be unique per agent"
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
var ManualPlanSchema = z.object({
|
|
299
|
-
orchestrator: ManualAgentPlanSchema,
|
|
300
|
-
oracle: ManualAgentPlanSchema,
|
|
301
|
-
designer: ManualAgentPlanSchema,
|
|
302
|
-
explorer: ManualAgentPlanSchema,
|
|
303
|
-
librarian: ManualAgentPlanSchema,
|
|
304
|
-
worker: ManualAgentPlanSchema
|
|
305
|
-
}).strict();
|
|
306
|
-
var AgentModelChainSchema = z.array(z.string()).min(1);
|
|
307
|
-
var SupportedAgentRecordKeySchema = z.string().regex(/^(?!quick$|deep$).+$/, "Unsupported removed agent role");
|
|
308
|
-
var FallbackChainsSchema = z.record(
|
|
309
|
-
SupportedAgentRecordKeySchema,
|
|
310
|
-
AgentModelChainSchema
|
|
311
|
-
);
|
|
312
|
-
var AgentOverrideConfigSchema = z.object({
|
|
313
|
-
model: z.union([
|
|
314
|
-
z.string(),
|
|
315
|
-
z.array(
|
|
316
|
-
z.union([
|
|
317
|
-
z.string(),
|
|
318
|
-
z.object({
|
|
319
|
-
id: z.string(),
|
|
320
|
-
variant: z.string().optional()
|
|
321
|
-
})
|
|
322
|
-
])
|
|
323
|
-
)
|
|
324
|
-
]).optional(),
|
|
325
|
-
/** Pi specialist catalog filter; omitted discovers all, [] discovers none. */
|
|
326
|
-
skills: z.array(z.string().trim().min(1)).optional(),
|
|
327
|
-
temperature: z.number().min(0).max(2).optional(),
|
|
328
|
-
steps: z.number().int().min(1).optional(),
|
|
329
|
-
variant: z.string().optional().catch(void 0)
|
|
330
|
-
});
|
|
331
|
-
var TmuxLayoutSchema = z.enum([
|
|
332
|
-
"main-horizontal",
|
|
333
|
-
// Main pane on top, agents stacked below
|
|
334
|
-
"main-vertical",
|
|
335
|
-
// Main pane on left, agents stacked on right
|
|
336
|
-
"tiled",
|
|
337
|
-
// All panes equal size grid
|
|
338
|
-
"even-horizontal",
|
|
339
|
-
// All panes side by side
|
|
340
|
-
"even-vertical"
|
|
341
|
-
// All panes stacked vertically
|
|
342
|
-
]);
|
|
343
|
-
var TmuxConfigSchema = z.object({
|
|
344
|
-
enabled: z.boolean().default(false),
|
|
345
|
-
layout: TmuxLayoutSchema.default("main-vertical"),
|
|
346
|
-
main_pane_size: z.number().min(20).max(80).default(60)
|
|
347
|
-
// percentage for main pane
|
|
348
|
-
});
|
|
349
|
-
var PresetSchema = z.record(
|
|
350
|
-
SupportedAgentRecordKeySchema,
|
|
351
|
-
AgentOverrideConfigSchema
|
|
352
|
-
);
|
|
353
|
-
var AgentNameSchema = z.enum(AGENT_NAMES);
|
|
354
|
-
var McpNameSchema = z.enum(["exa", "context7", "grep_app"]);
|
|
355
|
-
var CodexGenerationConfigSchema = z.object({
|
|
356
|
-
enabled: z.boolean().default(false),
|
|
357
|
-
outputRoot: z.string().optional(),
|
|
358
|
-
dryRun: z.boolean().default(true)
|
|
359
|
-
});
|
|
360
|
-
var ClaudeCodeGenerationConfigSchema = z.object({
|
|
361
|
-
enabled: z.boolean().default(false),
|
|
362
|
-
outputRoot: z.string().optional(),
|
|
363
|
-
dryRun: z.boolean().default(true)
|
|
364
|
-
});
|
|
365
|
-
var FailoverConfigSchema = z.object({
|
|
366
|
-
enabled: z.boolean().default(true),
|
|
367
|
-
timeoutMs: z.number().min(0).default(15e3),
|
|
368
|
-
retryDelayMs: z.number().min(0).default(500),
|
|
369
|
-
chains: FallbackChainsSchema.default({})
|
|
370
|
-
});
|
|
371
|
-
var PluginConfigSchema = z.object({
|
|
372
|
-
$schema: z.string().optional(),
|
|
373
|
-
preset: z.string().optional(),
|
|
374
|
-
setDefaultAgent: z.boolean().optional(),
|
|
375
|
-
scoringEngineVersion: z.enum(["v1", "v2-shadow", "v2"]).optional(),
|
|
376
|
-
balanceProviderUsage: z.boolean().optional(),
|
|
377
|
-
manualPlan: ManualPlanSchema.optional(),
|
|
378
|
-
presets: z.record(z.string(), PresetSchema).optional(),
|
|
379
|
-
agents: z.record(SupportedAgentRecordKeySchema, AgentOverrideConfigSchema).optional(),
|
|
380
|
-
disabled_mcps: z.array(z.string()).optional(),
|
|
381
|
-
tmux: TmuxConfigSchema.optional(),
|
|
382
|
-
fallback: FailoverConfigSchema.optional(),
|
|
383
|
-
codex: CodexGenerationConfigSchema.optional(),
|
|
384
|
-
claudeCode: ClaudeCodeGenerationConfigSchema.optional()
|
|
385
|
-
});
|
|
386
|
-
|
|
387
660
|
// src/config/utils.ts
|
|
388
661
|
function getAgentOverride(config, name) {
|
|
389
662
|
return config?.agents?.[name];
|
|
@@ -418,36 +691,75 @@ function buildOpenAIModelMappings() {
|
|
|
418
691
|
var MODEL_MAPPINGS = {
|
|
419
692
|
openai: buildOpenAIModelMappings()
|
|
420
693
|
};
|
|
421
|
-
function generateLiteConfig(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
694
|
+
function generateLiteConfig(_installConfig) {
|
|
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
|
|
432
712
|
};
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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)
|
|
439
728
|
])
|
|
440
|
-
)
|
|
729
|
+
)
|
|
441
730
|
};
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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 });
|
|
449
756
|
}
|
|
450
|
-
|
|
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;
|
|
451
763
|
}
|
|
452
764
|
|
|
453
765
|
// src/cli/config-io.ts
|
|
@@ -463,23 +775,23 @@ function stripJsonComments(json) {
|
|
|
463
775
|
(match, comma, closing) => comma ? closing : match
|
|
464
776
|
);
|
|
465
777
|
}
|
|
466
|
-
function parseConfigFile(
|
|
778
|
+
function parseConfigFile(path3) {
|
|
467
779
|
try {
|
|
468
|
-
if (!existsSync3(
|
|
469
|
-
const stat = statSync(
|
|
780
|
+
if (!existsSync3(path3)) return { config: null };
|
|
781
|
+
const stat = statSync(path3);
|
|
470
782
|
if (stat.size === 0) return { config: null };
|
|
471
|
-
const content = readFileSync2(
|
|
783
|
+
const content = readFileSync2(path3, "utf-8");
|
|
472
784
|
if (content.trim().length === 0) return { config: null };
|
|
473
785
|
return { config: JSON.parse(stripJsonComments(content)) };
|
|
474
786
|
} catch (err) {
|
|
475
787
|
return { config: null, error: String(err) };
|
|
476
788
|
}
|
|
477
789
|
}
|
|
478
|
-
function parseConfig(
|
|
479
|
-
const result = parseConfigFile(
|
|
790
|
+
function parseConfig(path3) {
|
|
791
|
+
const result = parseConfigFile(path3);
|
|
480
792
|
if (result.config || result.error) return result;
|
|
481
|
-
if (
|
|
482
|
-
const jsoncPath =
|
|
793
|
+
if (path3.endsWith(".json")) {
|
|
794
|
+
const jsoncPath = path3.replace(/\.json$/, ".jsonc");
|
|
483
795
|
return parseConfigFile(jsoncPath);
|
|
484
796
|
}
|
|
485
797
|
return { config: null };
|
|
@@ -500,24 +812,32 @@ function writeConfig(configPath, config) {
|
|
|
500
812
|
writeFileSync(tmpPath, content);
|
|
501
813
|
renameSync(tmpPath, configPath);
|
|
502
814
|
}
|
|
815
|
+
function pluginEntries(value) {
|
|
816
|
+
return Array.isArray(value) ? value : [];
|
|
817
|
+
}
|
|
818
|
+
function isManagedOpenCodePlugin(entry) {
|
|
819
|
+
const name = typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : entry && typeof entry === "object" && "package" in entry ? entry.package : void 0;
|
|
820
|
+
return typeof name === "string" && (name === PACKAGE_NAME || name.startsWith(`${PACKAGE_NAME}@`));
|
|
821
|
+
}
|
|
822
|
+
function hasManagedPluginOptions(entry) {
|
|
823
|
+
if (!isManagedOpenCodePlugin(entry)) return false;
|
|
824
|
+
const options = Array.isArray(entry) ? entry[1] : entry && typeof entry === "object" && "options" in entry ? entry.options : void 0;
|
|
825
|
+
return !!options && typeof options === "object" && Object.keys(options).length > 0;
|
|
826
|
+
}
|
|
503
827
|
function mergeOpenCodeMainConfig(config, update) {
|
|
504
828
|
if (update.ensurePlugin) {
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
829
|
+
const legacy = pluginEntries(config.plugin).filter(
|
|
830
|
+
(entry) => !isManagedOpenCodePlugin(entry)
|
|
831
|
+
);
|
|
832
|
+
if (legacy.length > 0) config.plugin = legacy;
|
|
833
|
+
else delete config.plugin;
|
|
834
|
+
config.plugins = [
|
|
835
|
+
...pluginEntries(config.plugins).filter(
|
|
836
|
+
(entry) => !isManagedOpenCodePlugin(entry)
|
|
509
837
|
),
|
|
510
838
|
`${PACKAGE_NAME}@${update.pluginVersion}`
|
|
511
839
|
];
|
|
512
840
|
}
|
|
513
|
-
if (update.disableDefaults) {
|
|
514
|
-
const agent = config.agent && typeof config.agent === "object" ? { ...config.agent } : {};
|
|
515
|
-
const explore = agent.explore && typeof agent.explore === "object" ? { ...agent.explore } : {};
|
|
516
|
-
const general = agent.general && typeof agent.general === "object" ? { ...agent.general } : {};
|
|
517
|
-
agent.explore = { ...explore, disable: true };
|
|
518
|
-
agent.general = { ...general, disable: true };
|
|
519
|
-
config.agent = agent;
|
|
520
|
-
}
|
|
521
841
|
return config;
|
|
522
842
|
}
|
|
523
843
|
function updateOpenCodeMainConfig(update) {
|
|
@@ -547,10 +867,19 @@ function updateOpenCodeMainConfig(update) {
|
|
|
547
867
|
error: `Failed to parse config: ${error}`
|
|
548
868
|
};
|
|
549
869
|
}
|
|
870
|
+
const removedOptions = update.ensurePlugin && [
|
|
871
|
+
...pluginEntries(parsedConfig?.plugin),
|
|
872
|
+
...pluginEntries(parsedConfig?.plugins)
|
|
873
|
+
].some(hasManagedPluginOptions);
|
|
550
874
|
writeConfig(
|
|
551
875
|
configPath,
|
|
552
876
|
mergeOpenCodeMainConfig(parsedConfig ?? {}, update)
|
|
553
877
|
);
|
|
878
|
+
if (removedOptions) {
|
|
879
|
+
console.warn(
|
|
880
|
+
`[config-manager] Removed non-empty thoth-agents plugin options while pinning the v2 plugin. Original entries are preserved in ${configPath}.bak; configure the plugin via thoth-agents.json instead.`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
554
883
|
return { success: true, configPath };
|
|
555
884
|
} catch (err) {
|
|
556
885
|
return {
|
|
@@ -566,43 +895,6 @@ function configObject(value) {
|
|
|
566
895
|
throw new Error("Expected an object in managed lite configuration.");
|
|
567
896
|
return value;
|
|
568
897
|
}
|
|
569
|
-
function mergeLiteConfig(defaults, installed) {
|
|
570
|
-
const presets = { ...configObject(installed.presets) };
|
|
571
|
-
const shipped = configObject(configObject(defaults.presets).openai);
|
|
572
|
-
const activeName = typeof installed.preset === "string" ? installed.preset : "openai";
|
|
573
|
-
const rootAgents = { ...configObject(installed.agents) };
|
|
574
|
-
for (const name of /* @__PURE__ */ new Set(["openai", activeName])) {
|
|
575
|
-
const roles = { ...configObject(presets[name]) };
|
|
576
|
-
for (const [role, legacy] of Object.entries(LEGACY_OPENAI_PRESET)) {
|
|
577
|
-
const rootRole = name === activeName ? rootAgents[role] : void 0;
|
|
578
|
-
const current = {
|
|
579
|
-
...configObject(roles[role]),
|
|
580
|
-
...configObject(rootRole)
|
|
581
|
-
};
|
|
582
|
-
const advancesLegacy = current.model === `openai/${legacy.model}` && current.variant === legacy.effort;
|
|
583
|
-
if (roles[role] === void 0 && rootRole === void 0) {
|
|
584
|
-
roles[role] = shipped[role];
|
|
585
|
-
} else if (advancesLegacy) {
|
|
586
|
-
roles[role] = { ...current, ...configObject(shipped[role]) };
|
|
587
|
-
if (rootRole !== void 0)
|
|
588
|
-
rootAgents[role] = {
|
|
589
|
-
...configObject(rootRole),
|
|
590
|
-
...configObject(shipped[role])
|
|
591
|
-
};
|
|
592
|
-
} else {
|
|
593
|
-
roles[role] = current;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
presets[name] = roles;
|
|
597
|
-
}
|
|
598
|
-
return {
|
|
599
|
-
...defaults,
|
|
600
|
-
...installed,
|
|
601
|
-
preset: activeName,
|
|
602
|
-
presets,
|
|
603
|
-
...installed.agents !== void 0 ? { agents: rootAgents } : {}
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
898
|
function writeLiteConfig(installConfig, targetPath) {
|
|
607
899
|
const configPath = targetPath ?? getLiteConfig();
|
|
608
900
|
try {
|
|
@@ -611,8 +903,10 @@ function writeLiteConfig(installConfig, targetPath) {
|
|
|
611
903
|
if (!installConfig.reset && existsSync3(configPath)) {
|
|
612
904
|
const { config: installed, error } = parseConfigFile(configPath);
|
|
613
905
|
if (error) throw new Error(`Failed to parse lite config: ${error}`);
|
|
614
|
-
if (installed) config =
|
|
906
|
+
if (installed) config = { ...config, ...configObject(installed) };
|
|
615
907
|
}
|
|
908
|
+
config = withoutLegacyPresets(config);
|
|
909
|
+
syncOpenCodeProfile(installConfig.reset);
|
|
616
910
|
const tmpPath = `${configPath}.tmp`;
|
|
617
911
|
const bakPath = `${configPath}.bak`;
|
|
618
912
|
const content = `${JSON.stringify(config, null, 2)}
|
|
@@ -634,37 +928,35 @@ function writeLiteConfig(installConfig, targetPath) {
|
|
|
634
928
|
function detectCurrentConfig() {
|
|
635
929
|
const result = {
|
|
636
930
|
isInstalled: false,
|
|
637
|
-
hasOpenAI: false
|
|
638
|
-
hasTmux: false
|
|
931
|
+
hasOpenAI: false
|
|
639
932
|
};
|
|
640
933
|
const { config } = parseConfig(getExistingConfigPath());
|
|
641
934
|
if (!config) return result;
|
|
642
|
-
|
|
643
|
-
|
|
935
|
+
result.isInstalled = [
|
|
936
|
+
...pluginEntries(config.plugin),
|
|
937
|
+
...pluginEntries(config.plugins)
|
|
938
|
+
].some(isManagedOpenCodePlugin);
|
|
644
939
|
const { config: liteConfig } = parseConfig(getLiteConfig());
|
|
645
940
|
if (liteConfig && typeof liteConfig === "object") {
|
|
646
941
|
const configObj = liteConfig;
|
|
647
|
-
const
|
|
648
|
-
const presets = configObj.presets;
|
|
649
|
-
const agents = presets?.[presetName];
|
|
942
|
+
const agents = effectiveOpenCodeProfile(configObj).agents;
|
|
650
943
|
if (agents) {
|
|
651
944
|
const models = Object.values(agents).map((a) => a?.model).filter(Boolean);
|
|
652
945
|
result.hasOpenAI = models.some((m) => m?.startsWith("openai/"));
|
|
653
946
|
}
|
|
654
|
-
if (configObj.tmux && typeof configObj.tmux === "object") {
|
|
655
|
-
const tmuxConfig = configObj.tmux;
|
|
656
|
-
result.hasTmux = tmuxConfig.enabled === true;
|
|
657
|
-
}
|
|
658
947
|
}
|
|
659
948
|
return result;
|
|
660
949
|
}
|
|
661
950
|
|
|
662
951
|
// src/config/loader.ts
|
|
663
952
|
var PROMPTS_DIR_NAME = "thoth-agents";
|
|
664
|
-
function loadConfigFromPath(configPath) {
|
|
953
|
+
function loadConfigFromPath(configPath, unsupportedKeyPaths) {
|
|
665
954
|
try {
|
|
666
|
-
const content =
|
|
955
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
667
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
|
+
}
|
|
668
960
|
const result = PluginConfigSchema.safeParse(rawConfig);
|
|
669
961
|
if (!result.success) {
|
|
670
962
|
console.warn(`[thoth-agents] Invalid config at ${configPath}:`);
|
|
@@ -685,10 +977,10 @@ function loadConfigFromPath(configPath) {
|
|
|
685
977
|
function findConfigPath(basePath) {
|
|
686
978
|
const jsoncPath = `${basePath}.jsonc`;
|
|
687
979
|
const jsonPath = `${basePath}.json`;
|
|
688
|
-
if (
|
|
980
|
+
if (fs2.existsSync(jsoncPath)) {
|
|
689
981
|
return jsoncPath;
|
|
690
982
|
}
|
|
691
|
-
if (
|
|
983
|
+
if (fs2.existsSync(jsonPath)) {
|
|
692
984
|
return jsonPath;
|
|
693
985
|
}
|
|
694
986
|
return null;
|
|
@@ -712,72 +1004,52 @@ function deepMerge(base, override) {
|
|
|
712
1004
|
return result;
|
|
713
1005
|
}
|
|
714
1006
|
function loadPluginConfig(directory) {
|
|
715
|
-
const userConfigBasePath =
|
|
716
|
-
const projectConfigBasePath =
|
|
1007
|
+
const userConfigBasePath = path2.join(getConfigDir(), "thoth-agents");
|
|
1008
|
+
const projectConfigBasePath = path2.join(
|
|
717
1009
|
directory,
|
|
718
1010
|
".opencode",
|
|
719
1011
|
"thoth-agents"
|
|
720
1012
|
);
|
|
721
1013
|
const userConfigPath = findConfigPath(userConfigBasePath);
|
|
722
1014
|
const projectConfigPath = findConfigPath(projectConfigBasePath);
|
|
723
|
-
|
|
724
|
-
|
|
1015
|
+
const unsupportedKeyPaths = /* @__PURE__ */ new Set();
|
|
1016
|
+
let config = userConfigPath ? loadConfigFromPath(userConfigPath, unsupportedKeyPaths) ?? {} : {};
|
|
1017
|
+
const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath, unsupportedKeyPaths) : null;
|
|
725
1018
|
if (projectConfig) {
|
|
726
1019
|
config = {
|
|
727
1020
|
...config,
|
|
728
1021
|
...projectConfig,
|
|
729
1022
|
agents: deepMerge(config.agents, projectConfig.agents),
|
|
730
|
-
tmux: deepMerge(config.tmux, projectConfig.tmux),
|
|
731
1023
|
fallback: deepMerge(config.fallback, projectConfig.fallback),
|
|
732
1024
|
codex: deepMerge(config.codex, projectConfig.codex),
|
|
733
1025
|
claudeCode: deepMerge(config.claudeCode, projectConfig.claudeCode)
|
|
734
1026
|
};
|
|
735
1027
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (config.preset) {
|
|
741
|
-
const preset = config.presets?.[config.preset];
|
|
742
|
-
if (preset) {
|
|
743
|
-
config.agents = deepMerge(preset, config.agents);
|
|
744
|
-
} else {
|
|
745
|
-
const presetSource = envPreset === config.preset ? "environment variable" : "config file";
|
|
746
|
-
const availablePresets = config.presets ? Object.keys(config.presets).join(", ") : "none";
|
|
747
|
-
console.warn(
|
|
748
|
-
`[thoth-agents] Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`
|
|
749
|
-
);
|
|
750
|
-
}
|
|
1028
|
+
if (unsupportedKeyPaths.size > 0) {
|
|
1029
|
+
console.warn(
|
|
1030
|
+
`[thoth-agents] Config keys preset/presets are unsupported and ignored (${[...unsupportedKeyPaths].join(", ")}).`
|
|
1031
|
+
);
|
|
751
1032
|
}
|
|
752
1033
|
return config;
|
|
753
1034
|
}
|
|
754
|
-
function loadAgentPrompt(agentName
|
|
755
|
-
const
|
|
756
|
-
const promptsDir = path.join(getConfigDir(), PROMPTS_DIR_NAME);
|
|
757
|
-
const promptSearchDirs = presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
|
|
1035
|
+
function loadAgentPrompt(agentName) {
|
|
1036
|
+
const promptsDir = path2.join(getConfigDir(), PROMPTS_DIR_NAME);
|
|
758
1037
|
const result = {};
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
error instanceof Error ? error.message : String(error)
|
|
771
|
-
);
|
|
772
|
-
}
|
|
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;
|
|
773
1049
|
}
|
|
774
|
-
return void 0;
|
|
775
1050
|
};
|
|
776
|
-
result.prompt =
|
|
777
|
-
|
|
778
|
-
"Error reading prompt file"
|
|
779
|
-
);
|
|
780
|
-
result.appendPrompt = readFirstPrompt(
|
|
1051
|
+
result.prompt = readPrompt(`${agentName}.md`, "Error reading prompt file");
|
|
1052
|
+
result.appendPrompt = readPrompt(
|
|
781
1053
|
`${agentName}_append.md`,
|
|
782
1054
|
"Error reading append prompt file"
|
|
783
1055
|
);
|
|
@@ -788,7 +1060,7 @@ function loadAgentPrompt(agentName, preset) {
|
|
|
788
1060
|
var text = { type: "string" };
|
|
789
1061
|
var texts = { type: "array", items: text };
|
|
790
1062
|
var nullableText = { type: ["string", "null"] };
|
|
791
|
-
function
|
|
1063
|
+
function object2(properties) {
|
|
792
1064
|
return {
|
|
793
1065
|
type: "object",
|
|
794
1066
|
properties,
|
|
@@ -799,7 +1071,7 @@ function object(properties) {
|
|
|
799
1071
|
function array(items) {
|
|
800
1072
|
return { type: "array", items };
|
|
801
1073
|
}
|
|
802
|
-
var common =
|
|
1074
|
+
var common = object2({
|
|
803
1075
|
schemaVersion: { type: "number", const: 1 },
|
|
804
1076
|
role: {
|
|
805
1077
|
type: "string",
|
|
@@ -811,7 +1083,7 @@ var common = object({
|
|
|
811
1083
|
},
|
|
812
1084
|
conclusion: text,
|
|
813
1085
|
evidence: array(
|
|
814
|
-
|
|
1086
|
+
object2({
|
|
815
1087
|
id: { type: "string", minLength: 1 },
|
|
816
1088
|
kind: text,
|
|
817
1089
|
locator: text,
|
|
@@ -821,7 +1093,7 @@ var common = object({
|
|
|
821
1093
|
})
|
|
822
1094
|
),
|
|
823
1095
|
verification: array(
|
|
824
|
-
|
|
1096
|
+
object2({
|
|
825
1097
|
check: text,
|
|
826
1098
|
method: text,
|
|
827
1099
|
outcome: { type: "string", enum: ["pass", "fail", "not-run"] },
|
|
@@ -830,7 +1102,7 @@ var common = object({
|
|
|
830
1102
|
})
|
|
831
1103
|
),
|
|
832
1104
|
risks: array(
|
|
833
|
-
|
|
1105
|
+
object2({
|
|
834
1106
|
description: text,
|
|
835
1107
|
severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
836
1108
|
blocking: { type: "boolean" },
|
|
@@ -838,11 +1110,11 @@ var common = object({
|
|
|
838
1110
|
})
|
|
839
1111
|
),
|
|
840
1112
|
openQuestions: array(
|
|
841
|
-
|
|
1113
|
+
object2({ question: text, owner: text, options: texts, facts: texts })
|
|
842
1114
|
)
|
|
843
1115
|
});
|
|
844
1116
|
var review = {
|
|
845
|
-
...
|
|
1117
|
+
...object2({
|
|
846
1118
|
kind: {
|
|
847
1119
|
type: "string",
|
|
848
1120
|
enum: ["plan-review", "final-verification"]
|
|
@@ -858,7 +1130,7 @@ var SPECIALIST_RESULT_CONTRACT = {
|
|
|
858
1130
|
fence: "thoth-result",
|
|
859
1131
|
common,
|
|
860
1132
|
roles: {
|
|
861
|
-
oracle:
|
|
1133
|
+
oracle: object2({ nextAction: nullableText, review })
|
|
862
1134
|
},
|
|
863
1135
|
reviewKinds: {
|
|
864
1136
|
"plan-review": { verdicts: ["OKAY", "REJECT"] },
|
|
@@ -1381,25 +1653,30 @@ function piCapabilityDisclosure(capability) {
|
|
|
1381
1653
|
}
|
|
1382
1654
|
var OPENCODE_PROMPT_DIALECT = {
|
|
1383
1655
|
harness: "opencode",
|
|
1384
|
-
gateDeliveryInstruction: "
|
|
1656
|
+
gateDeliveryInstruction: "Foreground Oracle `subagent` gates get one `thoth-gate-verdict` line; background completions have none: use the explicit compact validator fallback.",
|
|
1385
1657
|
tools: {
|
|
1386
|
-
delegationTool: "
|
|
1387
|
-
backgroundDelegationTool: "
|
|
1388
|
-
|
|
1658
|
+
delegationTool: "subagent",
|
|
1659
|
+
backgroundDelegationTool: "subagent(background:true)",
|
|
1660
|
+
backgroundWaitInstruction: 'Native `<subagent sessionID=... state="completed">` completion wakes root. No status polling tool exists; never poll/sleep.',
|
|
1389
1661
|
userQuestionTool: "question",
|
|
1390
1662
|
progressTool: "todowrite",
|
|
1391
|
-
hostStatusSurface: "
|
|
1663
|
+
hostStatusSurface: "native subagent completion messages",
|
|
1392
1664
|
lifecycle: {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1665
|
+
rootInstructions: [
|
|
1666
|
+
"- New objectives, phases, work units, mutable surfaces or independent judgments require fresh specialist sessions via `subagent` (agent, description, prompt; omit sessionID). These are fresh-session boundaries, not permission for root execution; never pool completed agents.",
|
|
1667
|
+
"- Use sessionID only to continue/clarify the same assignment, including while running. Optional model; background:true for independent work. Default nesting depth is 1.",
|
|
1668
|
+
"- Every Oracle plan review, verification round, and PASS judgment uses a fresh Oracle instance. Existing ones only clarify their findings."
|
|
1669
|
+
].join("\n"),
|
|
1670
|
+
freshDelegation: "`subagent` with agent, description and prompt, omitting sessionID",
|
|
1671
|
+
sameAssignmentContinuation: "`subagent` with the prior sessionID, including while running",
|
|
1672
|
+
independentContext: "omitting sessionID creates an isolated child session",
|
|
1673
|
+
statusAction: "receive native completion automatically; never poll",
|
|
1674
|
+
terminalState: "a completed subagent result/message",
|
|
1675
|
+
nonterminalState: "a running placeholder",
|
|
1676
|
+
sameSessionProbe: "continue or clarify with subagent and the same sessionID; never ask for status",
|
|
1400
1677
|
enforcement: "runtime-supported"
|
|
1401
1678
|
},
|
|
1402
|
-
roleReference: (role) =>
|
|
1679
|
+
roleReference: (role) => `subagent agent:"${role}"`
|
|
1403
1680
|
},
|
|
1404
1681
|
capabilities: supportedCapabilityProfile(OPENCODE_CAPABILITIES),
|
|
1405
1682
|
dispatchLabel(method) {
|
|
@@ -1407,11 +1684,11 @@ var OPENCODE_PROMPT_DIALECT = {
|
|
|
1407
1684
|
case "root-coordinator":
|
|
1408
1685
|
return "root coordinator";
|
|
1409
1686
|
case "task":
|
|
1410
|
-
return "
|
|
1687
|
+
return "subagent";
|
|
1411
1688
|
}
|
|
1412
1689
|
},
|
|
1413
1690
|
renderRoleInvocation(role) {
|
|
1414
|
-
return
|
|
1691
|
+
return `subagent agent:"${role}"`;
|
|
1415
1692
|
}
|
|
1416
1693
|
};
|
|
1417
1694
|
var CODEX_PROMPT_DIALECT = {
|
|
@@ -2231,7 +2508,7 @@ Use \`${dialect.tools.userQuestionTool}\` only for a blocking material choice, d
|
|
|
2231
2508
|
}
|
|
2232
2509
|
function renderSubagentRules(section, dialect) {
|
|
2233
2510
|
const rules = [
|
|
2234
|
-
dialect.tools.progressTool ? `- Do not delegate further or call \`${dialect.tools.progressTool}\`; root owns progress.` : "- Do not delegate further; root owns progress.",
|
|
2511
|
+
dialect.harness === "opencode" ? "- Do not delegate further; no `todowrite` (root-owned)." : dialect.tools.progressTool ? `- Do not delegate further or call \`${dialect.tools.progressTool}\`; root owns progress.` : "- Do not delegate further; root owns progress.",
|
|
2235
2512
|
"- Use terminating checks; avoid watch processes and indefinite waits.",
|
|
2236
2513
|
"- Preserve operator-selected model/effort. Stop after the assigned outcome and checks; never expand scope to fill a timeout.",
|
|
2237
2514
|
"- After two consecutive attempts without new evidence or progress, return partial evidence and the smallest blocker; do not repeat searches or unchanged failing commands.",
|
|
@@ -2420,6 +2697,7 @@ function composeAgentPrompt({
|
|
|
2420
2697
|
}
|
|
2421
2698
|
|
|
2422
2699
|
export {
|
|
2700
|
+
PROFILE_ROLES,
|
|
2423
2701
|
SUBAGENT_NAMES,
|
|
2424
2702
|
ALL_AGENT_NAMES,
|
|
2425
2703
|
LEGACY_OPENAI_PRESET,
|
|
@@ -2428,19 +2706,30 @@ export {
|
|
|
2428
2706
|
DEFAULT_MODELS,
|
|
2429
2707
|
getDefaultOpenCodeModel,
|
|
2430
2708
|
getDefaultOpenCodeVariant,
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2709
|
+
resolveRoleProfile,
|
|
2710
|
+
mergeProfileModelChain,
|
|
2711
|
+
formatModelString,
|
|
2712
|
+
validateProfileName,
|
|
2713
|
+
ProfileConflictError,
|
|
2714
|
+
ModelProfileStore,
|
|
2715
|
+
getConfigDir,
|
|
2436
2716
|
getOpenCodeManagedModelStatePath,
|
|
2437
2717
|
getExistingLiteConfigPath,
|
|
2438
2718
|
getExistingConfigPath,
|
|
2439
2719
|
ensureConfigDir,
|
|
2720
|
+
MODEL_MAPPINGS,
|
|
2440
2721
|
generateLiteConfig,
|
|
2441
|
-
|
|
2722
|
+
openCodeProfileStore,
|
|
2723
|
+
activeOpenCodeProfile,
|
|
2724
|
+
effectiveOpenCodeProfile,
|
|
2725
|
+
withoutLegacyPresets,
|
|
2726
|
+
findPackageRoot,
|
|
2727
|
+
isValidPackageVersion,
|
|
2728
|
+
resolveExecutingPackageVersion,
|
|
2442
2729
|
parseConfig,
|
|
2443
2730
|
writeConfig,
|
|
2731
|
+
pluginEntries,
|
|
2732
|
+
isManagedOpenCodePlugin,
|
|
2444
2733
|
updateOpenCodeMainConfig,
|
|
2445
2734
|
writeLiteConfig,
|
|
2446
2735
|
detectCurrentConfig,
|