@botcord/daemon 0.2.77 → 0.2.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-discovery.d.ts +6 -0
- package/dist/agent-discovery.js +6 -0
- package/dist/attention-policy-fetcher.d.ts +14 -0
- package/dist/attention-policy-fetcher.js +59 -0
- package/dist/cloud-daemon.js +8 -0
- package/dist/cloud-gateway-runtime.d.ts +29 -0
- package/dist/cloud-gateway-runtime.js +122 -0
- package/dist/daemon-config-map.d.ts +6 -0
- package/dist/daemon-config-map.js +5 -4
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +32 -7
- package/dist/gateway/channels/botcord.js +29 -9
- package/dist/gateway/channels/login-session.d.ts +12 -0
- package/dist/gateway/channels/login-session.js +20 -2
- package/dist/gateway/channels/sanitize.d.ts +5 -18
- package/dist/gateway/channels/sanitize.js +5 -54
- package/dist/gateway/channels/text-split.d.ts +5 -11
- package/dist/gateway/channels/text-split.js +5 -31
- package/dist/gateway/dispatcher.d.ts +7 -1
- package/dist/gateway/dispatcher.js +88 -8
- package/dist/gateway/gateway.d.ts +16 -1
- package/dist/gateway/gateway.js +21 -0
- package/dist/gateway/policy-resolver.js +17 -9
- package/dist/gateway/runtimes/deepseek-tui.js +86 -19
- package/dist/gateway/types.d.ts +12 -57
- package/dist/gateway-control.js +18 -9
- package/dist/provision.d.ts +9 -3
- package/dist/provision.js +181 -9
- package/dist/room-recovery-context.d.ts +11 -0
- package/dist/room-recovery-context.js +97 -0
- package/dist/runtime-models.d.ts +17 -0
- package/dist/runtime-models.js +953 -0
- package/dist/runtime-route-options.d.ts +7 -0
- package/dist/runtime-route-options.js +45 -0
- package/package.json +2 -2
- package/src/__tests__/attention-policy-fetcher.test.ts +67 -0
- package/src/__tests__/cloud-gateway-runtime.test.ts +127 -0
- package/src/__tests__/daemon-config-map.test.ts +26 -1
- package/src/__tests__/gateway-control.test.ts +136 -0
- package/src/__tests__/policy-resolver.test.ts +20 -0
- package/src/__tests__/provision.test.ts +124 -0
- package/src/__tests__/runtime-discovery.test.ts +68 -9
- package/src/__tests__/runtime-models.test.ts +333 -0
- package/src/agent-discovery.ts +9 -0
- package/src/attention-policy-fetcher.ts +87 -0
- package/src/cloud-daemon.ts +8 -0
- package/src/cloud-gateway-runtime.ts +171 -0
- package/src/daemon-config-map.ts +17 -4
- package/src/daemon.ts +38 -9
- package/src/gateway/__tests__/botcord-channel.test.ts +97 -0
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +207 -1
- package/src/gateway/__tests__/dispatcher.test.ts +56 -0
- package/src/gateway/channels/botcord.ts +32 -8
- package/src/gateway/channels/login-session.ts +20 -2
- package/src/gateway/channels/sanitize.ts +8 -66
- package/src/gateway/channels/text-split.ts +5 -27
- package/src/gateway/dispatcher.ts +123 -27
- package/src/gateway/gateway.ts +29 -0
- package/src/gateway/policy-resolver.ts +20 -9
- package/src/gateway/runtimes/deepseek-tui.ts +86 -19
- package/src/gateway/types.ts +31 -59
- package/src/gateway-control.ts +21 -9
- package/src/provision.ts +202 -11
- package/src/room-recovery-context.ts +131 -0
- package/src/runtime-models.ts +972 -0
- package/src/runtime-route-options.ts +52 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const MODEL_LIST_TIMEOUT_MS = 5000;
|
|
6
|
+
const MODEL_LIST_MAX_BUFFER = 16 * 1024 * 1024;
|
|
7
|
+
const RUNTIME_CATALOG_CACHE_VERSION = 1;
|
|
8
|
+
const RUNTIME_CATALOG_CACHE_FRESH_MS = 10 * 60 * 1000;
|
|
9
|
+
const DEFAULT_RUNTIME_CATALOG_CACHE_DIR = path.join(homedir(), ".botcord", "daemon", "runtime-catalog-cache");
|
|
10
|
+
const CLAUDE_ALIAS_MODELS = [
|
|
11
|
+
{ id: "default", displayName: "Default", provider: "anthropic", source: "builtin" },
|
|
12
|
+
{ id: "best", displayName: "Best", provider: "anthropic", source: "builtin" },
|
|
13
|
+
{ id: "sonnet", displayName: "Sonnet", provider: "anthropic", source: "builtin" },
|
|
14
|
+
{ id: "opus", displayName: "Opus", provider: "anthropic", source: "builtin" },
|
|
15
|
+
{ id: "haiku", displayName: "Haiku", provider: "anthropic", source: "builtin" },
|
|
16
|
+
{ id: "sonnet[1m]", displayName: "Sonnet 1M", provider: "anthropic", source: "builtin" },
|
|
17
|
+
{ id: "opus[1m]", displayName: "Opus 1M", provider: "anthropic", source: "builtin" },
|
|
18
|
+
{ id: "opusplan", displayName: "Opus Plan", provider: "anthropic", source: "builtin" },
|
|
19
|
+
];
|
|
20
|
+
const CLAUDE_EFFORT_VALUES = ["low", "medium", "high", "xhigh", "max"];
|
|
21
|
+
const CLAUDE_PERMISSION_MODE_VALUES = [
|
|
22
|
+
"acceptEdits",
|
|
23
|
+
"auto",
|
|
24
|
+
"bypassPermissions",
|
|
25
|
+
"default",
|
|
26
|
+
"dontAsk",
|
|
27
|
+
"plan",
|
|
28
|
+
];
|
|
29
|
+
const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
|
|
30
|
+
const CODEX_APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"];
|
|
31
|
+
const DEEPSEEK_PROVIDER_VALUES = [
|
|
32
|
+
"deepseek",
|
|
33
|
+
"nvidia-nim",
|
|
34
|
+
"openai",
|
|
35
|
+
"openrouter",
|
|
36
|
+
"novita",
|
|
37
|
+
"fireworks",
|
|
38
|
+
"sglang",
|
|
39
|
+
"vllm",
|
|
40
|
+
"ollama",
|
|
41
|
+
];
|
|
42
|
+
const CODEX_FALLBACK_MODELS = [
|
|
43
|
+
{ id: "gpt-5.2", displayName: "GPT-5.2", provider: "openai", source: "builtin" },
|
|
44
|
+
{ id: "gpt-5.1", displayName: "GPT-5.1", provider: "openai", source: "builtin" },
|
|
45
|
+
{ id: "gpt-5", displayName: "GPT-5", provider: "openai", source: "builtin" },
|
|
46
|
+
{ id: "o4-mini", displayName: "o4-mini", provider: "openai", source: "builtin" },
|
|
47
|
+
];
|
|
48
|
+
const DEEPSEEK_FALLBACK_MODELS = [
|
|
49
|
+
{ id: "deepseek-v4-flash", displayName: "deepseek-v4-flash", provider: "deepseek", source: "builtin" },
|
|
50
|
+
];
|
|
51
|
+
const KIMI_FALLBACK_MODELS = [
|
|
52
|
+
{ id: "kimi-code/kimi-for-coding", displayName: "Kimi for Coding", provider: "managed:kimi-code", source: "builtin" },
|
|
53
|
+
{ id: "kimi-k2-5", displayName: "kimi-k2-5", provider: "kimi", source: "builtin" },
|
|
54
|
+
{ id: "kimi-k2-5-preview", displayName: "kimi-k2-5-preview", provider: "kimi", source: "builtin" },
|
|
55
|
+
{ id: "kimi-k2-0711", displayName: "kimi-k2-0711", provider: "kimi", source: "builtin" },
|
|
56
|
+
];
|
|
57
|
+
const backgroundRefreshes = new Set();
|
|
58
|
+
export function discoverRuntimeModelCatalog(entry) {
|
|
59
|
+
if (!entry.result.available)
|
|
60
|
+
return {};
|
|
61
|
+
const strategy = runtimeCatalogStrategy(entry);
|
|
62
|
+
if (!strategy)
|
|
63
|
+
return {};
|
|
64
|
+
return discoverRuntimeCatalogWithCache(strategy);
|
|
65
|
+
}
|
|
66
|
+
function runtimeCatalogStrategy(entry) {
|
|
67
|
+
switch (entry.id) {
|
|
68
|
+
case "claude-code":
|
|
69
|
+
return {
|
|
70
|
+
id: entry.id,
|
|
71
|
+
contextKey: runtimeCatalogContextKey(entry, {
|
|
72
|
+
settings: fileStatKey(path.join(homedir(), ".claude", "settings.json")),
|
|
73
|
+
env: pickEnv([
|
|
74
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
75
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
76
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
77
|
+
"ANTHROPIC_CUSTOM_MODEL_OPTION",
|
|
78
|
+
]),
|
|
79
|
+
}),
|
|
80
|
+
discoverFresh: discoverClaudeCatalog,
|
|
81
|
+
fallback: () => ({ models: CLAUDE_ALIAS_MODELS.slice(), parameters: discoverClaudeParameters() }),
|
|
82
|
+
};
|
|
83
|
+
case "codex":
|
|
84
|
+
return {
|
|
85
|
+
id: entry.id,
|
|
86
|
+
contextKey: runtimeCatalogContextKey(entry, {
|
|
87
|
+
codexHome: codexHomeDir(),
|
|
88
|
+
config: fileStatKey(path.join(codexHomeDir(), "config.toml")),
|
|
89
|
+
cliCache: fileStatKey(codexModelCachePath()),
|
|
90
|
+
env: pickEnv(["OPENAI_BASE_URL", "CODEX_HOME"]),
|
|
91
|
+
}),
|
|
92
|
+
discoverFresh: () => discoverCodexCatalog(entry.result.path),
|
|
93
|
+
fallback: () => ({ models: CODEX_FALLBACK_MODELS.slice(), parameters: discoverCodexParameters(null) }),
|
|
94
|
+
};
|
|
95
|
+
case "deepseek-tui":
|
|
96
|
+
return {
|
|
97
|
+
id: entry.id,
|
|
98
|
+
contextKey: runtimeCatalogContextKey(entry, {
|
|
99
|
+
config: fileStatKey(path.join(homedir(), ".deepseek", "config.toml")),
|
|
100
|
+
env: pickEnv(["BOTCORD_DEEPSEEK_TUI_BIN", "BOTCORD_DEEPSEEK_TUI_URL"]),
|
|
101
|
+
}),
|
|
102
|
+
discoverFresh: () => discoverDeepseekCatalog(entry.result.path),
|
|
103
|
+
fallback: () => ({
|
|
104
|
+
models: DEEPSEEK_FALLBACK_MODELS.slice(),
|
|
105
|
+
parameters: discoverDeepseekParameters(),
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
case "kimi-cli":
|
|
109
|
+
return {
|
|
110
|
+
id: entry.id,
|
|
111
|
+
contextKey: runtimeCatalogContextKey(entry, {
|
|
112
|
+
config: fileStatKey(path.join(homedir(), ".kimi", "config.toml")),
|
|
113
|
+
env: pickEnv(["BOTCORD_KIMI_CLI_BIN"]),
|
|
114
|
+
}),
|
|
115
|
+
discoverFresh: discoverKimiCatalog,
|
|
116
|
+
fallback: () => ({
|
|
117
|
+
models: KIMI_FALLBACK_MODELS.slice(),
|
|
118
|
+
parameters: [
|
|
119
|
+
{
|
|
120
|
+
id: "thinking",
|
|
121
|
+
displayName: "Thinking",
|
|
122
|
+
type: "boolean",
|
|
123
|
+
flag: "--thinking/--no-thinking",
|
|
124
|
+
source: "builtin",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
default:
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function discoverRuntimeCatalogWithCache(strategy) {
|
|
134
|
+
const cached = readRuntimeCatalogCache(strategy.id, strategy.contextKey);
|
|
135
|
+
if (cached && Date.now() - cached.updatedAt < RUNTIME_CATALOG_CACHE_FRESH_MS) {
|
|
136
|
+
scheduleRuntimeCatalogRefresh(strategy);
|
|
137
|
+
return cached.catalog;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const fresh = completeCatalogWithFallback(strategy.discoverFresh(), strategy);
|
|
141
|
+
if (hasCatalogData(fresh)) {
|
|
142
|
+
writeRuntimeCatalogCache(strategy.id, strategy.contextKey, fresh);
|
|
143
|
+
return fresh;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Fall through to stale cache or built-in fallback.
|
|
148
|
+
}
|
|
149
|
+
if (cached)
|
|
150
|
+
return cached.catalog;
|
|
151
|
+
try {
|
|
152
|
+
const fallback = strategy.fallback?.() ?? {};
|
|
153
|
+
return hasCatalogData(fallback) ? fallback : {};
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export function discoverRuntimeModels(entry) {
|
|
160
|
+
return discoverRuntimeModelCatalog(entry).models;
|
|
161
|
+
}
|
|
162
|
+
export function discoverRuntimeParameters(entry) {
|
|
163
|
+
return discoverRuntimeModelCatalog(entry).parameters;
|
|
164
|
+
}
|
|
165
|
+
function completeCatalogWithFallback(catalog, strategy) {
|
|
166
|
+
if (catalog.models?.length)
|
|
167
|
+
return catalog;
|
|
168
|
+
const fallback = strategy.fallback?.();
|
|
169
|
+
if (!fallback?.models?.length)
|
|
170
|
+
return catalog;
|
|
171
|
+
return {
|
|
172
|
+
...catalog,
|
|
173
|
+
models: fallback.models,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function discoverClaudeCatalog() {
|
|
177
|
+
return {
|
|
178
|
+
models: discoverClaudeModels(),
|
|
179
|
+
parameters: discoverClaudeParameters(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function discoverClaudeModels() {
|
|
183
|
+
const models = new Map();
|
|
184
|
+
const add = (model) => {
|
|
185
|
+
if (!model.id)
|
|
186
|
+
return;
|
|
187
|
+
const existing = models.get(model.id);
|
|
188
|
+
models.set(model.id, { ...existing, ...model });
|
|
189
|
+
};
|
|
190
|
+
for (const model of CLAUDE_ALIAS_MODELS)
|
|
191
|
+
add(model);
|
|
192
|
+
const settings = readJsonObject(path.join(homedir(), ".claude", "settings.json"));
|
|
193
|
+
const defaultModel = stringField(settings, "model");
|
|
194
|
+
if (defaultModel) {
|
|
195
|
+
add({
|
|
196
|
+
id: defaultModel,
|
|
197
|
+
displayName: defaultModel,
|
|
198
|
+
provider: "anthropic",
|
|
199
|
+
source: "config",
|
|
200
|
+
isDefault: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
for (const item of arrayField(settings, "availableModels")) {
|
|
204
|
+
if (typeof item === "string" && item) {
|
|
205
|
+
add({ id: item, displayName: item, provider: "anthropic", source: "config" });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const envName of [
|
|
209
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
210
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
211
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
212
|
+
"ANTHROPIC_CUSTOM_MODEL_OPTION",
|
|
213
|
+
]) {
|
|
214
|
+
const value = process.env[envName];
|
|
215
|
+
if (value) {
|
|
216
|
+
add({
|
|
217
|
+
id: value,
|
|
218
|
+
displayName: value,
|
|
219
|
+
provider: "anthropic",
|
|
220
|
+
source: "env",
|
|
221
|
+
metadata: { env: envName },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Array.from(models.values());
|
|
226
|
+
}
|
|
227
|
+
function discoverClaudeParameters() {
|
|
228
|
+
const settings = readJsonObject(path.join(homedir(), ".claude", "settings.json"));
|
|
229
|
+
const effort = stringField(settings, "effort");
|
|
230
|
+
const permissionMode = stringField(settings, "permissionMode") ?? stringField(settings, "permission-mode");
|
|
231
|
+
const out = [
|
|
232
|
+
{
|
|
233
|
+
id: "effort",
|
|
234
|
+
displayName: "Effort",
|
|
235
|
+
type: "enum",
|
|
236
|
+
flag: "--effort",
|
|
237
|
+
values: CLAUDE_EFFORT_VALUES,
|
|
238
|
+
source: "cli",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "permission_mode",
|
|
242
|
+
displayName: "Permission mode",
|
|
243
|
+
type: "enum",
|
|
244
|
+
flag: "--permission-mode",
|
|
245
|
+
values: CLAUDE_PERMISSION_MODE_VALUES,
|
|
246
|
+
source: "cli",
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
if (effort)
|
|
250
|
+
out[0] = { ...out[0], defaultValue: effort, source: "config" };
|
|
251
|
+
if (permissionMode)
|
|
252
|
+
out[1] = { ...out[1], defaultValue: permissionMode, source: "config" };
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
function discoverCodexCatalog(command) {
|
|
256
|
+
const raw = runCommand(codexCommand(command), ["debug", "models"]);
|
|
257
|
+
const fallbackRaw = raw ?? readCodexModelCacheRaw();
|
|
258
|
+
return {
|
|
259
|
+
models: fallbackRaw ? parseCodexModelCatalog(fallbackRaw) : undefined,
|
|
260
|
+
parameters: discoverCodexParameters(fallbackRaw),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export function discoverCodexModels(command) {
|
|
264
|
+
return discoverCodexCatalog(command).models;
|
|
265
|
+
}
|
|
266
|
+
export function parseCodexModelCatalog(raw) {
|
|
267
|
+
const parsed = JSON.parse(raw);
|
|
268
|
+
if (!Array.isArray(parsed.models))
|
|
269
|
+
return undefined;
|
|
270
|
+
const out = [];
|
|
271
|
+
for (const item of parsed.models) {
|
|
272
|
+
if (!item || typeof item !== "object")
|
|
273
|
+
continue;
|
|
274
|
+
const obj = item;
|
|
275
|
+
const id = stringField(obj, "slug");
|
|
276
|
+
if (!id)
|
|
277
|
+
continue;
|
|
278
|
+
if (stringField(obj, "visibility") === "hide")
|
|
279
|
+
continue;
|
|
280
|
+
const model = {
|
|
281
|
+
id,
|
|
282
|
+
provider: "openai",
|
|
283
|
+
source: "cli",
|
|
284
|
+
};
|
|
285
|
+
const displayName = stringField(obj, "display_name") ?? stringField(obj, "description");
|
|
286
|
+
if (displayName)
|
|
287
|
+
model.displayName = displayName;
|
|
288
|
+
const metadata = {};
|
|
289
|
+
const supportedInApi = obj.supported_in_api;
|
|
290
|
+
if (typeof supportedInApi === "boolean")
|
|
291
|
+
metadata.supportedInApi = supportedInApi;
|
|
292
|
+
const defaultReasoningLevel = stringField(obj, "default_reasoning_level");
|
|
293
|
+
if (defaultReasoningLevel)
|
|
294
|
+
metadata.defaultReasoningLevel = defaultReasoningLevel;
|
|
295
|
+
const supportedReasoningLevels = arrayField(obj, "supported_reasoning_levels")
|
|
296
|
+
.map((level) => level && typeof level === "object"
|
|
297
|
+
? stringField(level, "effort")
|
|
298
|
+
: undefined)
|
|
299
|
+
.filter((level) => !!level);
|
|
300
|
+
if (supportedReasoningLevels.length)
|
|
301
|
+
metadata.supportedReasoningLevels = supportedReasoningLevels;
|
|
302
|
+
if (Object.keys(metadata).length)
|
|
303
|
+
model.metadata = metadata;
|
|
304
|
+
if (supportedReasoningLevels.length) {
|
|
305
|
+
model.parameters = [
|
|
306
|
+
{
|
|
307
|
+
id: "reasoning_effort",
|
|
308
|
+
displayName: "Reasoning effort",
|
|
309
|
+
type: "enum",
|
|
310
|
+
flag: "-c model_reasoning_effort=<value>",
|
|
311
|
+
values: supportedReasoningLevels,
|
|
312
|
+
...(defaultReasoningLevel ? { defaultValue: defaultReasoningLevel } : {}),
|
|
313
|
+
source: "cli",
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
out.push(model);
|
|
318
|
+
}
|
|
319
|
+
return out.length ? out : undefined;
|
|
320
|
+
}
|
|
321
|
+
function discoverCodexParameters(rawCatalog) {
|
|
322
|
+
const config = readConfigScalars(path.join(homedir(), ".codex", "config.toml"));
|
|
323
|
+
const reasoningValues = new Set();
|
|
324
|
+
if (rawCatalog) {
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(rawCatalog);
|
|
327
|
+
if (Array.isArray(parsed.models)) {
|
|
328
|
+
for (const item of parsed.models) {
|
|
329
|
+
if (!item || typeof item !== "object")
|
|
330
|
+
continue;
|
|
331
|
+
for (const level of arrayField(item, "supported_reasoning_levels")) {
|
|
332
|
+
if (!level || typeof level !== "object")
|
|
333
|
+
continue;
|
|
334
|
+
const effort = stringField(level, "effort");
|
|
335
|
+
if (effort)
|
|
336
|
+
reasoningValues.add(effort);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// ignore malformed catalog; runtime-level defaults still come from config.
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return [
|
|
346
|
+
compactParameter({
|
|
347
|
+
id: "model",
|
|
348
|
+
displayName: "Default model",
|
|
349
|
+
type: "string",
|
|
350
|
+
flag: "-m, --model",
|
|
351
|
+
defaultValue: config.model,
|
|
352
|
+
source: config.model ? "config" : "cli",
|
|
353
|
+
}),
|
|
354
|
+
compactParameter({
|
|
355
|
+
id: "reasoning_effort",
|
|
356
|
+
displayName: "Reasoning effort",
|
|
357
|
+
type: reasoningValues.size > 0 ? "enum" : "string",
|
|
358
|
+
flag: "-c model_reasoning_effort=<value>",
|
|
359
|
+
values: reasoningValues.size > 0 ? Array.from(reasoningValues) : undefined,
|
|
360
|
+
defaultValue: config.model_reasoning_effort,
|
|
361
|
+
source: config.model_reasoning_effort ? "config" : "cli",
|
|
362
|
+
metadata: { scope: "per-model-values-may-differ" },
|
|
363
|
+
}),
|
|
364
|
+
compactParameter({
|
|
365
|
+
id: "approval_policy",
|
|
366
|
+
displayName: "Approval policy",
|
|
367
|
+
type: "enum",
|
|
368
|
+
flag: "-a, --ask-for-approval",
|
|
369
|
+
values: CODEX_APPROVAL_POLICIES,
|
|
370
|
+
defaultValue: config.approval_policy,
|
|
371
|
+
source: config.approval_policy ? "config" : "cli",
|
|
372
|
+
}),
|
|
373
|
+
compactParameter({
|
|
374
|
+
id: "sandbox_mode",
|
|
375
|
+
displayName: "Sandbox mode",
|
|
376
|
+
type: "enum",
|
|
377
|
+
flag: "-s, --sandbox",
|
|
378
|
+
values: CODEX_SANDBOX_MODES,
|
|
379
|
+
defaultValue: config.sandbox_mode,
|
|
380
|
+
source: config.sandbox_mode ? "config" : "cli",
|
|
381
|
+
}),
|
|
382
|
+
compactParameter({
|
|
383
|
+
id: "web_search",
|
|
384
|
+
displayName: "Web search",
|
|
385
|
+
type: "boolean",
|
|
386
|
+
flag: "--search",
|
|
387
|
+
defaultValue: parseTomlBool(config.web_search),
|
|
388
|
+
source: config.web_search ? "config" : "cli",
|
|
389
|
+
}),
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
function discoverDeepseekCatalog(command) {
|
|
393
|
+
return {
|
|
394
|
+
models: discoverDeepseekModels(command),
|
|
395
|
+
parameters: discoverDeepseekParameters(),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
export function discoverDeepseekModels(command) {
|
|
399
|
+
const raw = runCommand([command ?? "deepseek"], ["model", "list"]);
|
|
400
|
+
if (!raw)
|
|
401
|
+
return undefined;
|
|
402
|
+
return parseDeepseekModelList(raw);
|
|
403
|
+
}
|
|
404
|
+
export function parseDeepseekModelList(raw) {
|
|
405
|
+
const out = [];
|
|
406
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
407
|
+
const trimmed = line.trim();
|
|
408
|
+
if (!trimmed)
|
|
409
|
+
continue;
|
|
410
|
+
const match = trimmed.match(/^(.+?)\s+\(([^()]+)\)$/);
|
|
411
|
+
const id = match ? match[1].trim() : trimmed;
|
|
412
|
+
const provider = match ? match[2].trim() : "deepseek";
|
|
413
|
+
if (!id)
|
|
414
|
+
continue;
|
|
415
|
+
out.push({ id, displayName: id, provider, source: "cli" });
|
|
416
|
+
}
|
|
417
|
+
return out.length ? out : undefined;
|
|
418
|
+
}
|
|
419
|
+
function discoverDeepseekParameters() {
|
|
420
|
+
const config = readConfigScalars(path.join(homedir(), ".deepseek", "config.toml"));
|
|
421
|
+
return [
|
|
422
|
+
compactParameter({
|
|
423
|
+
id: "model",
|
|
424
|
+
displayName: "Default model",
|
|
425
|
+
type: "string",
|
|
426
|
+
flag: "--model",
|
|
427
|
+
defaultValue: config.default_text_model,
|
|
428
|
+
source: config.default_text_model ? "config" : "cli",
|
|
429
|
+
}),
|
|
430
|
+
compactParameter({
|
|
431
|
+
id: "provider",
|
|
432
|
+
displayName: "Provider",
|
|
433
|
+
type: "enum",
|
|
434
|
+
flag: "--provider",
|
|
435
|
+
values: DEEPSEEK_PROVIDER_VALUES,
|
|
436
|
+
defaultValue: config.provider,
|
|
437
|
+
source: config.provider ? "config" : "cli",
|
|
438
|
+
}),
|
|
439
|
+
compactParameter({
|
|
440
|
+
id: "reasoning_effort",
|
|
441
|
+
displayName: "Reasoning effort",
|
|
442
|
+
type: "string",
|
|
443
|
+
flag: "reasoning_effort",
|
|
444
|
+
defaultValue: config.reasoning_effort,
|
|
445
|
+
source: config.reasoning_effort ? "config" : "cli",
|
|
446
|
+
}),
|
|
447
|
+
compactParameter({
|
|
448
|
+
id: "approval_policy",
|
|
449
|
+
displayName: "Approval policy",
|
|
450
|
+
type: "string",
|
|
451
|
+
flag: "--approval-policy",
|
|
452
|
+
defaultValue: config.approval_policy,
|
|
453
|
+
source: config.approval_policy ? "config" : "cli",
|
|
454
|
+
}),
|
|
455
|
+
compactParameter({
|
|
456
|
+
id: "sandbox_mode",
|
|
457
|
+
displayName: "Sandbox mode",
|
|
458
|
+
type: "string",
|
|
459
|
+
flag: "--sandbox-mode",
|
|
460
|
+
defaultValue: config.sandbox_mode,
|
|
461
|
+
source: config.sandbox_mode ? "config" : "cli",
|
|
462
|
+
}),
|
|
463
|
+
];
|
|
464
|
+
}
|
|
465
|
+
function discoverKimiCatalog() {
|
|
466
|
+
const configPath = path.join(homedir(), ".kimi", "config.toml");
|
|
467
|
+
if (!existsSync(configPath))
|
|
468
|
+
return {};
|
|
469
|
+
const raw = readFileSync(configPath, "utf8");
|
|
470
|
+
return {
|
|
471
|
+
models: parseKimiConfigModels(raw),
|
|
472
|
+
parameters: parseKimiRuntimeParameters(raw),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
export function discoverKimiModels() {
|
|
476
|
+
return discoverKimiCatalog().models;
|
|
477
|
+
}
|
|
478
|
+
export function parseKimiConfigModels(raw) {
|
|
479
|
+
const defaultModel = matchScalar(raw, /^default_model\s*=\s*["']([^"']+)["']/m);
|
|
480
|
+
const out = [];
|
|
481
|
+
const sectionRe = /^\[models\."([^"]+)"\]\s*$/gm;
|
|
482
|
+
const matches = Array.from(raw.matchAll(sectionRe));
|
|
483
|
+
for (let i = 0; i < matches.length; i += 1) {
|
|
484
|
+
const match = matches[i];
|
|
485
|
+
const id = match[1];
|
|
486
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
487
|
+
const end = i + 1 < matches.length ? matches[i + 1].index ?? raw.length : raw.length;
|
|
488
|
+
const body = raw.slice(start, end);
|
|
489
|
+
const model = {
|
|
490
|
+
id,
|
|
491
|
+
source: "config",
|
|
492
|
+
isDefault: id === defaultModel,
|
|
493
|
+
};
|
|
494
|
+
const provider = matchScalar(body, /^\s*provider\s*=\s*["']([^"']+)["']/m);
|
|
495
|
+
if (provider)
|
|
496
|
+
model.provider = provider;
|
|
497
|
+
const displayName = matchScalar(body, /^\s*display_name\s*=\s*["']([^"']+)["']/m);
|
|
498
|
+
if (displayName)
|
|
499
|
+
model.displayName = displayName;
|
|
500
|
+
const contextLength = matchScalar(body, /^\s*max_context_size\s*=\s*(\d+)/m);
|
|
501
|
+
if (contextLength)
|
|
502
|
+
model.contextLength = Number(contextLength);
|
|
503
|
+
const capabilities = matchArray(body, /^\s*capabilities\s*=\s*\[([^\]]*)\]/m);
|
|
504
|
+
if (capabilities.length)
|
|
505
|
+
model.capabilities = capabilities;
|
|
506
|
+
if (capabilities.includes("thinking")) {
|
|
507
|
+
const defaultThinking = parseTomlBool(matchScalar(raw, /^default_thinking\s*=\s*(true|false)/m));
|
|
508
|
+
model.parameters = [
|
|
509
|
+
compactParameter({
|
|
510
|
+
id: "thinking",
|
|
511
|
+
displayName: "Thinking",
|
|
512
|
+
type: "boolean",
|
|
513
|
+
flag: "--thinking/--no-thinking",
|
|
514
|
+
defaultValue: defaultThinking,
|
|
515
|
+
source: defaultThinking === undefined ? "cli" : "config",
|
|
516
|
+
}),
|
|
517
|
+
];
|
|
518
|
+
}
|
|
519
|
+
const runtimeModel = matchScalar(body, /^\s*model\s*=\s*["']([^"']+)["']/m);
|
|
520
|
+
if (runtimeModel)
|
|
521
|
+
model.metadata = { model: runtimeModel };
|
|
522
|
+
out.push(model);
|
|
523
|
+
}
|
|
524
|
+
return out.length ? out : undefined;
|
|
525
|
+
}
|
|
526
|
+
export function parseKimiRuntimeParameters(raw) {
|
|
527
|
+
return [
|
|
528
|
+
compactParameter({
|
|
529
|
+
id: "model",
|
|
530
|
+
displayName: "Default model",
|
|
531
|
+
type: "string",
|
|
532
|
+
flag: "-m, --model",
|
|
533
|
+
defaultValue: matchScalar(raw, /^default_model\s*=\s*["']([^"']+)["']/m),
|
|
534
|
+
source: "config",
|
|
535
|
+
}),
|
|
536
|
+
compactParameter({
|
|
537
|
+
id: "thinking",
|
|
538
|
+
displayName: "Thinking",
|
|
539
|
+
type: "boolean",
|
|
540
|
+
flag: "--thinking/--no-thinking",
|
|
541
|
+
defaultValue: parseTomlBool(matchScalar(raw, /^default_thinking\s*=\s*(true|false)/m)),
|
|
542
|
+
source: "config",
|
|
543
|
+
}),
|
|
544
|
+
compactParameter({
|
|
545
|
+
id: "show_thinking_stream",
|
|
546
|
+
displayName: "Show thinking stream",
|
|
547
|
+
type: "boolean",
|
|
548
|
+
defaultValue: parseTomlBool(matchScalar(raw, /^show_thinking_stream\s*=\s*(true|false)/m)),
|
|
549
|
+
source: "config",
|
|
550
|
+
}),
|
|
551
|
+
compactParameter({
|
|
552
|
+
id: "yolo",
|
|
553
|
+
displayName: "Auto approve",
|
|
554
|
+
type: "boolean",
|
|
555
|
+
flag: "--yolo, --yes, -y",
|
|
556
|
+
defaultValue: parseTomlBool(matchScalar(raw, /^default_yolo\s*=\s*(true|false)/m)),
|
|
557
|
+
source: "config",
|
|
558
|
+
}),
|
|
559
|
+
compactParameter({
|
|
560
|
+
id: "plan_mode",
|
|
561
|
+
displayName: "Plan mode",
|
|
562
|
+
type: "boolean",
|
|
563
|
+
flag: "--plan",
|
|
564
|
+
defaultValue: parseTomlBool(matchScalar(raw, /^default_plan_mode\s*=\s*(true|false)/m)),
|
|
565
|
+
source: "config",
|
|
566
|
+
}),
|
|
567
|
+
compactParameter({
|
|
568
|
+
id: "max_steps_per_turn",
|
|
569
|
+
displayName: "Max steps per turn",
|
|
570
|
+
type: "integer",
|
|
571
|
+
flag: "--max-steps-per-turn",
|
|
572
|
+
defaultValue: parseTomlInt(matchScalar(raw, /^max_steps_per_turn\s*=\s*(\d+)/m)),
|
|
573
|
+
minimum: 1,
|
|
574
|
+
source: "config",
|
|
575
|
+
}),
|
|
576
|
+
compactParameter({
|
|
577
|
+
id: "max_retries_per_step",
|
|
578
|
+
displayName: "Max retries per step",
|
|
579
|
+
type: "integer",
|
|
580
|
+
flag: "--max-retries-per-step",
|
|
581
|
+
defaultValue: parseTomlInt(matchScalar(raw, /^max_retries_per_step\s*=\s*(\d+)/m)),
|
|
582
|
+
minimum: 1,
|
|
583
|
+
source: "config",
|
|
584
|
+
}),
|
|
585
|
+
compactParameter({
|
|
586
|
+
id: "max_ralph_iterations",
|
|
587
|
+
displayName: "Max Ralph iterations",
|
|
588
|
+
type: "integer",
|
|
589
|
+
flag: "--max-ralph-iterations",
|
|
590
|
+
defaultValue: parseTomlInt(matchScalar(raw, /^max_ralph_iterations\s*=\s*(-?\d+)/m)),
|
|
591
|
+
minimum: -1,
|
|
592
|
+
source: "config",
|
|
593
|
+
}),
|
|
594
|
+
compactParameter({
|
|
595
|
+
id: "reserved_context_size",
|
|
596
|
+
displayName: "Reserved context size",
|
|
597
|
+
type: "integer",
|
|
598
|
+
defaultValue: parseTomlInt(matchScalar(raw, /^reserved_context_size\s*=\s*(\d+)/m)),
|
|
599
|
+
minimum: 0,
|
|
600
|
+
source: "config",
|
|
601
|
+
}),
|
|
602
|
+
];
|
|
603
|
+
}
|
|
604
|
+
function runtimeCatalogContextKey(entry, extra) {
|
|
605
|
+
return JSON.stringify({
|
|
606
|
+
runtime: entry.id,
|
|
607
|
+
path: entry.result.path ?? null,
|
|
608
|
+
version: entry.result.version ?? null,
|
|
609
|
+
home: homedir(),
|
|
610
|
+
...extra,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
function runtimeCatalogCacheDir() {
|
|
614
|
+
const explicit = process.env.BOTCORD_RUNTIME_CATALOG_CACHE_DIR;
|
|
615
|
+
if (explicit && explicit.length > 0)
|
|
616
|
+
return expandLeadingTilde(explicit);
|
|
617
|
+
// Keep unit tests from writing to a developer's real ~/.botcord unless a
|
|
618
|
+
// test opts into a temp cache directory explicitly.
|
|
619
|
+
if (process.env.NODE_ENV === "test")
|
|
620
|
+
return null;
|
|
621
|
+
if (process.env.BOTCORD_RUNTIME_CATALOG_CACHE === "0")
|
|
622
|
+
return null;
|
|
623
|
+
return DEFAULT_RUNTIME_CATALOG_CACHE_DIR;
|
|
624
|
+
}
|
|
625
|
+
function runtimeCatalogCachePath(runtimeId) {
|
|
626
|
+
const dir = runtimeCatalogCacheDir();
|
|
627
|
+
if (!dir)
|
|
628
|
+
return null;
|
|
629
|
+
const safe = runtimeId.replace(/[^A-Za-z0-9_.-]+/g, "_");
|
|
630
|
+
return path.join(dir, `${safe}.json`);
|
|
631
|
+
}
|
|
632
|
+
function readRuntimeCatalogCache(runtimeId, contextKey) {
|
|
633
|
+
const file = runtimeCatalogCachePath(runtimeId);
|
|
634
|
+
if (!file)
|
|
635
|
+
return null;
|
|
636
|
+
try {
|
|
637
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
638
|
+
if (!parsed || typeof parsed !== "object")
|
|
639
|
+
return null;
|
|
640
|
+
if (parsed.version !== RUNTIME_CATALOG_CACHE_VERSION)
|
|
641
|
+
return null;
|
|
642
|
+
if (parsed.runtimeId !== runtimeId || parsed.contextKey !== contextKey)
|
|
643
|
+
return null;
|
|
644
|
+
if (typeof parsed.updatedAt !== "number" || parsed.updatedAt <= 0)
|
|
645
|
+
return null;
|
|
646
|
+
const catalog = normalizeCachedCatalog(parsed.catalog);
|
|
647
|
+
if (!catalog)
|
|
648
|
+
return null;
|
|
649
|
+
return { updatedAt: parsed.updatedAt, catalog };
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function writeRuntimeCatalogCache(runtimeId, contextKey, catalog) {
|
|
656
|
+
if (!hasCatalogData(catalog))
|
|
657
|
+
return;
|
|
658
|
+
const file = runtimeCatalogCachePath(runtimeId);
|
|
659
|
+
if (!file)
|
|
660
|
+
return;
|
|
661
|
+
try {
|
|
662
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
663
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
664
|
+
const payload = {
|
|
665
|
+
version: RUNTIME_CATALOG_CACHE_VERSION,
|
|
666
|
+
runtimeId,
|
|
667
|
+
contextKey,
|
|
668
|
+
updatedAt: Date.now(),
|
|
669
|
+
catalog,
|
|
670
|
+
};
|
|
671
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
672
|
+
renameSync(tmp, file);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
// Cache writes must never affect runtime discovery.
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function scheduleRuntimeCatalogRefresh(strategy) {
|
|
679
|
+
if (process.env.NODE_ENV === "test")
|
|
680
|
+
return;
|
|
681
|
+
if (process.env.BOTCORD_RUNTIME_CATALOG_BACKGROUND_REFRESH === "0")
|
|
682
|
+
return;
|
|
683
|
+
const cacheKey = `${strategy.id}:${strategy.contextKey}`;
|
|
684
|
+
if (backgroundRefreshes.has(cacheKey))
|
|
685
|
+
return;
|
|
686
|
+
backgroundRefreshes.add(cacheKey);
|
|
687
|
+
const timer = setTimeout(() => {
|
|
688
|
+
try {
|
|
689
|
+
const fresh = completeCatalogWithFallback(strategy.discoverFresh(), strategy);
|
|
690
|
+
if (hasCatalogData(fresh)) {
|
|
691
|
+
writeRuntimeCatalogCache(strategy.id, strategy.contextKey, fresh);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
// Keep serving the previous cache entry.
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
backgroundRefreshes.delete(cacheKey);
|
|
699
|
+
}
|
|
700
|
+
}, 0);
|
|
701
|
+
timer.unref?.();
|
|
702
|
+
}
|
|
703
|
+
function normalizeCachedCatalog(raw) {
|
|
704
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
705
|
+
return null;
|
|
706
|
+
const obj = raw;
|
|
707
|
+
const models = normalizeCachedModels(obj.models);
|
|
708
|
+
const parameters = normalizeCachedParameters(obj.parameters);
|
|
709
|
+
const out = {};
|
|
710
|
+
if (models?.length)
|
|
711
|
+
out.models = models;
|
|
712
|
+
if (parameters?.length)
|
|
713
|
+
out.parameters = parameters;
|
|
714
|
+
return hasCatalogData(out) ? out : null;
|
|
715
|
+
}
|
|
716
|
+
function normalizeCachedModels(raw) {
|
|
717
|
+
if (!Array.isArray(raw))
|
|
718
|
+
return undefined;
|
|
719
|
+
const models = raw
|
|
720
|
+
.map((item) => {
|
|
721
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
722
|
+
return null;
|
|
723
|
+
const obj = item;
|
|
724
|
+
const id = stringField(obj, "id");
|
|
725
|
+
if (!id)
|
|
726
|
+
return null;
|
|
727
|
+
const model = { id };
|
|
728
|
+
const displayName = stringField(obj, "displayName");
|
|
729
|
+
if (displayName)
|
|
730
|
+
model.displayName = displayName;
|
|
731
|
+
const provider = stringField(obj, "provider");
|
|
732
|
+
if (provider)
|
|
733
|
+
model.provider = provider;
|
|
734
|
+
const source = stringField(obj, "source");
|
|
735
|
+
if (isRuntimeCatalogSource(source))
|
|
736
|
+
model.source = source;
|
|
737
|
+
if (obj.isDefault === true)
|
|
738
|
+
model.isDefault = true;
|
|
739
|
+
const capabilities = arrayField(obj, "capabilities").filter((cap) => typeof cap === "string" && cap.length > 0);
|
|
740
|
+
if (capabilities.length)
|
|
741
|
+
model.capabilities = capabilities;
|
|
742
|
+
if (typeof obj.contextLength === "number")
|
|
743
|
+
model.contextLength = obj.contextLength;
|
|
744
|
+
if (obj.metadata && typeof obj.metadata === "object" && !Array.isArray(obj.metadata)) {
|
|
745
|
+
model.metadata = obj.metadata;
|
|
746
|
+
}
|
|
747
|
+
const parameters = normalizeCachedParameters(obj.parameters);
|
|
748
|
+
if (parameters?.length)
|
|
749
|
+
model.parameters = parameters;
|
|
750
|
+
return model;
|
|
751
|
+
})
|
|
752
|
+
.filter((model) => !!model);
|
|
753
|
+
return models.length ? models : undefined;
|
|
754
|
+
}
|
|
755
|
+
function normalizeCachedParameters(raw) {
|
|
756
|
+
if (!Array.isArray(raw))
|
|
757
|
+
return undefined;
|
|
758
|
+
const parameters = raw
|
|
759
|
+
.map((item) => {
|
|
760
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
761
|
+
return null;
|
|
762
|
+
const obj = item;
|
|
763
|
+
const id = stringField(obj, "id");
|
|
764
|
+
const type = stringField(obj, "type");
|
|
765
|
+
if (!id || !isRuntimeParameterType(type))
|
|
766
|
+
return null;
|
|
767
|
+
const param = { id, type };
|
|
768
|
+
const displayName = stringField(obj, "displayName");
|
|
769
|
+
if (displayName)
|
|
770
|
+
param.displayName = displayName;
|
|
771
|
+
const flag = stringField(obj, "flag");
|
|
772
|
+
if (flag)
|
|
773
|
+
param.flag = flag;
|
|
774
|
+
const values = arrayField(obj, "values").filter(isRuntimeParameterValue);
|
|
775
|
+
if (values.length)
|
|
776
|
+
param.values = values;
|
|
777
|
+
if (isRuntimeParameterValue(obj.defaultValue))
|
|
778
|
+
param.defaultValue = obj.defaultValue;
|
|
779
|
+
if (typeof obj.minimum === "number")
|
|
780
|
+
param.minimum = obj.minimum;
|
|
781
|
+
if (typeof obj.maximum === "number")
|
|
782
|
+
param.maximum = obj.maximum;
|
|
783
|
+
const source = stringField(obj, "source");
|
|
784
|
+
if (isRuntimeCatalogSource(source))
|
|
785
|
+
param.source = source;
|
|
786
|
+
if (obj.metadata && typeof obj.metadata === "object" && !Array.isArray(obj.metadata)) {
|
|
787
|
+
param.metadata = obj.metadata;
|
|
788
|
+
}
|
|
789
|
+
return param;
|
|
790
|
+
})
|
|
791
|
+
.filter((param) => !!param);
|
|
792
|
+
return parameters.length ? parameters : undefined;
|
|
793
|
+
}
|
|
794
|
+
function isRuntimeCatalogSource(value) {
|
|
795
|
+
return (value === "builtin" ||
|
|
796
|
+
value === "config" ||
|
|
797
|
+
value === "cli" ||
|
|
798
|
+
value === "api" ||
|
|
799
|
+
value === "gateway" ||
|
|
800
|
+
value === "env");
|
|
801
|
+
}
|
|
802
|
+
function isRuntimeParameterType(value) {
|
|
803
|
+
return value === "enum" || value === "boolean" || value === "integer" || value === "string";
|
|
804
|
+
}
|
|
805
|
+
function isRuntimeParameterValue(value) {
|
|
806
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
807
|
+
}
|
|
808
|
+
function hasCatalogData(catalog) {
|
|
809
|
+
return !!(catalog.models?.length || catalog.parameters?.length);
|
|
810
|
+
}
|
|
811
|
+
function fileStatKey(file) {
|
|
812
|
+
try {
|
|
813
|
+
const st = statSync(file);
|
|
814
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function pickEnv(names) {
|
|
821
|
+
const out = {};
|
|
822
|
+
for (const name of names) {
|
|
823
|
+
const value = process.env[name];
|
|
824
|
+
if (value)
|
|
825
|
+
out[name] = value;
|
|
826
|
+
}
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
function codexHomeDir() {
|
|
830
|
+
return process.env.CODEX_HOME ? expandLeadingTilde(process.env.CODEX_HOME) : path.join(homedir(), ".codex");
|
|
831
|
+
}
|
|
832
|
+
function codexModelCachePath() {
|
|
833
|
+
return path.join(codexHomeDir(), "models_cache.json");
|
|
834
|
+
}
|
|
835
|
+
function readCodexModelCacheRaw() {
|
|
836
|
+
try {
|
|
837
|
+
return readFileSync(codexModelCachePath(), "utf8");
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function expandLeadingTilde(value) {
|
|
844
|
+
if (value === "~")
|
|
845
|
+
return homedir();
|
|
846
|
+
if (value.startsWith("~/"))
|
|
847
|
+
return path.join(homedir(), value.slice(2));
|
|
848
|
+
return value;
|
|
849
|
+
}
|
|
850
|
+
function codexCommand(command) {
|
|
851
|
+
if (command?.endsWith(".js"))
|
|
852
|
+
return [process.execPath, command];
|
|
853
|
+
return [command ?? "codex"];
|
|
854
|
+
}
|
|
855
|
+
function runCommand(command, args) {
|
|
856
|
+
try {
|
|
857
|
+
return execFileSync(command[0], [...command.slice(1), ...args], {
|
|
858
|
+
encoding: "utf8",
|
|
859
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
860
|
+
timeout: MODEL_LIST_TIMEOUT_MS,
|
|
861
|
+
maxBuffer: MODEL_LIST_MAX_BUFFER,
|
|
862
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function readJsonObject(file) {
|
|
870
|
+
try {
|
|
871
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
872
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
873
|
+
? parsed
|
|
874
|
+
: null;
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function readConfigScalars(file) {
|
|
881
|
+
try {
|
|
882
|
+
const raw = readFileSync(file, "utf8");
|
|
883
|
+
const out = {};
|
|
884
|
+
for (const match of raw.matchAll(/^([A-Za-z0-9_.-]+)\s*=\s*(.+?)\s*$/gm)) {
|
|
885
|
+
const key = match[1];
|
|
886
|
+
const rawValue = match[2]?.trim();
|
|
887
|
+
if (!key || !rawValue)
|
|
888
|
+
continue;
|
|
889
|
+
out[key] = rawValue.replace(/^["']|["']$/g, "");
|
|
890
|
+
}
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return {};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function compactParameter(param) {
|
|
898
|
+
const out = {
|
|
899
|
+
id: param.id,
|
|
900
|
+
type: param.type,
|
|
901
|
+
};
|
|
902
|
+
if (param.displayName)
|
|
903
|
+
out.displayName = param.displayName;
|
|
904
|
+
if (param.flag)
|
|
905
|
+
out.flag = param.flag;
|
|
906
|
+
if (param.values?.length)
|
|
907
|
+
out.values = param.values;
|
|
908
|
+
if (param.defaultValue !== undefined)
|
|
909
|
+
out.defaultValue = param.defaultValue;
|
|
910
|
+
if (param.minimum !== undefined)
|
|
911
|
+
out.minimum = param.minimum;
|
|
912
|
+
if (param.maximum !== undefined)
|
|
913
|
+
out.maximum = param.maximum;
|
|
914
|
+
if (param.source)
|
|
915
|
+
out.source = param.source;
|
|
916
|
+
if (param.metadata && Object.keys(param.metadata).length > 0)
|
|
917
|
+
out.metadata = param.metadata;
|
|
918
|
+
return out;
|
|
919
|
+
}
|
|
920
|
+
function parseTomlBool(value) {
|
|
921
|
+
if (value === "true")
|
|
922
|
+
return true;
|
|
923
|
+
if (value === "false")
|
|
924
|
+
return false;
|
|
925
|
+
return undefined;
|
|
926
|
+
}
|
|
927
|
+
function parseTomlInt(value) {
|
|
928
|
+
if (value === undefined)
|
|
929
|
+
return undefined;
|
|
930
|
+
const n = Number(value);
|
|
931
|
+
return Number.isInteger(n) ? n : undefined;
|
|
932
|
+
}
|
|
933
|
+
function stringField(obj, key) {
|
|
934
|
+
const value = obj?.[key];
|
|
935
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
936
|
+
}
|
|
937
|
+
function arrayField(obj, key) {
|
|
938
|
+
const value = obj?.[key];
|
|
939
|
+
return Array.isArray(value) ? value : [];
|
|
940
|
+
}
|
|
941
|
+
function matchScalar(raw, re) {
|
|
942
|
+
const match = raw.match(re);
|
|
943
|
+
return match?.[1]?.trim() || undefined;
|
|
944
|
+
}
|
|
945
|
+
function matchArray(raw, re) {
|
|
946
|
+
const match = raw.match(re);
|
|
947
|
+
if (!match?.[1])
|
|
948
|
+
return [];
|
|
949
|
+
return match[1]
|
|
950
|
+
.split(",")
|
|
951
|
+
.map((part) => part.trim().replace(/^["']|["']$/g, ""))
|
|
952
|
+
.filter(Boolean);
|
|
953
|
+
}
|