codeclaw 0.2.8 → 0.2.9
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/bot-telegram.js +47 -23
- package/dist/bot.js +9 -3
- package/dist/code-agent.js +295 -25
- package/package.json +1 -1
package/dist/bot-telegram.js
CHANGED
|
@@ -14,27 +14,33 @@ import { splitText } from './channel-base.js';
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Context window sizes (max input tokens per model family)
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
|
-
function getContextWindowSize(model) {
|
|
18
|
-
if (!model)
|
|
19
|
-
return null;
|
|
20
|
-
const m = model.toLowerCase();
|
|
21
|
-
// Claude models: 200k context
|
|
22
|
-
if (m.includes('claude'))
|
|
23
|
-
return 200_000;
|
|
24
|
-
// GPT-4.1 / GPT-5 / o3/o4 family: 200k (1M for o3, but default to 200k)
|
|
25
|
-
if (m.startsWith('gpt-') || m.startsWith('o3') || m.startsWith('o4'))
|
|
26
|
-
return 200_000;
|
|
27
|
-
// Gemini: 1M context
|
|
28
|
-
if (m.includes('gemini'))
|
|
29
|
-
return 1_000_000;
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
17
|
// ---------------------------------------------------------------------------
|
|
33
18
|
// Telegram HTML formatting
|
|
34
19
|
// ---------------------------------------------------------------------------
|
|
35
20
|
function escapeHtml(t) {
|
|
36
21
|
return t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
37
22
|
}
|
|
23
|
+
function claudeModelAlias(modelId) {
|
|
24
|
+
const value = String(modelId || '').trim().toLowerCase();
|
|
25
|
+
if (!value)
|
|
26
|
+
return null;
|
|
27
|
+
if (value === 'opus' || value.startsWith('claude-opus-'))
|
|
28
|
+
return 'opus';
|
|
29
|
+
if (value === 'sonnet' || value.startsWith('claude-sonnet-'))
|
|
30
|
+
return 'sonnet';
|
|
31
|
+
if (value === 'haiku' || value.startsWith('claude-haiku-'))
|
|
32
|
+
return 'haiku';
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function modelMatchesSelection(agent, selection, currentModel) {
|
|
36
|
+
if (selection === currentModel)
|
|
37
|
+
return true;
|
|
38
|
+
if (agent !== 'claude')
|
|
39
|
+
return false;
|
|
40
|
+
const a = claudeModelAlias(selection);
|
|
41
|
+
const b = claudeModelAlias(currentModel);
|
|
42
|
+
return !!a && a === b;
|
|
43
|
+
}
|
|
38
44
|
function mdToTgHtml(text) {
|
|
39
45
|
const result = [];
|
|
40
46
|
const lines = text.split('\n');
|
|
@@ -478,13 +484,24 @@ export class TelegramBot extends Bot {
|
|
|
478
484
|
const cs = this.chat(ctx.chatId);
|
|
479
485
|
const res = this.fetchModels(cs.agent);
|
|
480
486
|
const currentModel = this.modelForAgent(cs.agent);
|
|
481
|
-
const lines = [`<b>Models for ${escapeHtml(cs.agent)}</b
|
|
487
|
+
const lines = [`<b>Models for ${escapeHtml(cs.agent)}</b>`];
|
|
488
|
+
if (res.sources.length)
|
|
489
|
+
lines.push(`<i>Source: ${escapeHtml(res.sources.join(', '))}</i>`);
|
|
490
|
+
if (res.note)
|
|
491
|
+
lines.push(`<i>${escapeHtml(res.note)}</i>`);
|
|
492
|
+
lines.push('');
|
|
482
493
|
const rows = [];
|
|
494
|
+
if (!res.models.length) {
|
|
495
|
+
lines.push('<i>No discoverable models found.</i>');
|
|
496
|
+
}
|
|
483
497
|
for (const m of res.models) {
|
|
484
|
-
const isCurrent =
|
|
498
|
+
const isCurrent = modelMatchesSelection(cs.agent, m.id, currentModel);
|
|
485
499
|
const status = isCurrent ? '\u25CF' : '\u25CB';
|
|
486
500
|
const display = m.alias ? `${m.alias} (${m.id})` : m.id;
|
|
487
|
-
|
|
501
|
+
const currentSuffix = isCurrent
|
|
502
|
+
? (m.id === currentModel ? ' \u2190 current' : ` \u2190 current (${escapeHtml(currentModel)})`)
|
|
503
|
+
: '';
|
|
504
|
+
lines.push(`${status} <code>${escapeHtml(display)}</code>${currentSuffix}`);
|
|
488
505
|
const label = isCurrent ? `\u25CF ${m.alias || m.id}` : (m.alias || m.id);
|
|
489
506
|
rows.push([{ text: label, callback_data: `mod:${m.id}` }]);
|
|
490
507
|
}
|
|
@@ -513,7 +530,8 @@ export class TelegramBot extends Bot {
|
|
|
513
530
|
this.log(`restart: spawning \`${bin} ${allArgs.join(' ')}\``);
|
|
514
531
|
// Collect all known chat IDs so the new process can send startup notices
|
|
515
532
|
const knownIds = new Set(this.allowedChatIds);
|
|
516
|
-
|
|
533
|
+
const knownChats = this.channel.knownChats instanceof Set ? this.channel.knownChats : new Set();
|
|
534
|
+
for (const cid of knownChats)
|
|
517
535
|
knownIds.add(cid);
|
|
518
536
|
const child = spawn(bin, allArgs, {
|
|
519
537
|
stdio: 'inherit',
|
|
@@ -634,10 +652,12 @@ export class TelegramBot extends Bot {
|
|
|
634
652
|
tp.push(`cached: ${fmtTokens(result.cachedInputTokens)}`);
|
|
635
653
|
if (result.outputTokens != null)
|
|
636
654
|
tp.push(`out: ${fmtTokens(result.outputTokens)}`);
|
|
637
|
-
// Context window usage percentage
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
655
|
+
// Context window usage percentage from CLI-reported contextWindow
|
|
656
|
+
// For Codex use cumulative input (= full session context); for Claude use inputTokens
|
|
657
|
+
const ctxMax = result.contextWindow;
|
|
658
|
+
const ctxInput = result.codexCumulative?.input ?? result.inputTokens;
|
|
659
|
+
if (ctxMax && ctxInput != null) {
|
|
660
|
+
const pct = (ctxInput / ctxMax * 100).toFixed(1);
|
|
641
661
|
tp.push(`ctx: ${pct}%`);
|
|
642
662
|
}
|
|
643
663
|
tokenBlock = `\n<blockquote expandable>${tp.join(' ')}</blockquote>`;
|
|
@@ -778,11 +798,13 @@ export class TelegramBot extends Bot {
|
|
|
778
798
|
const cs = this.chat(ctx.chatId);
|
|
779
799
|
if (sessionId === 'new') {
|
|
780
800
|
cs.sessionId = null;
|
|
801
|
+
cs.codexCumulative = undefined;
|
|
781
802
|
await ctx.answerCallback('New session');
|
|
782
803
|
await ctx.editReply(ctx.messageId, 'Session reset. Send a message to start.', {});
|
|
783
804
|
}
|
|
784
805
|
else {
|
|
785
806
|
cs.sessionId = sessionId;
|
|
807
|
+
cs.codexCumulative = undefined;
|
|
786
808
|
await ctx.answerCallback(`Session: ${sessionId.slice(0, 12)}`);
|
|
787
809
|
await ctx.editReply(ctx.messageId, `Switched to session: <code>${escapeHtml(sessionId.slice(0, 16))}</code>`, { parseMode: 'HTML' });
|
|
788
810
|
}
|
|
@@ -797,6 +819,7 @@ export class TelegramBot extends Bot {
|
|
|
797
819
|
}
|
|
798
820
|
cs.agent = agent;
|
|
799
821
|
cs.sessionId = null;
|
|
822
|
+
cs.codexCumulative = undefined;
|
|
800
823
|
this.log(`agent switched to ${agent} chat=${ctx.chatId}`);
|
|
801
824
|
await ctx.answerCallback(`Switched to ${agent}`);
|
|
802
825
|
await ctx.editReply(ctx.messageId, `<b>Switched to ${escapeHtml(agent)}</b>\n\nSession has been reset. Previous conversation history will not carry over.\nSend a message to start a new conversation.`, { parseMode: 'HTML' });
|
|
@@ -812,6 +835,7 @@ export class TelegramBot extends Bot {
|
|
|
812
835
|
}
|
|
813
836
|
this.setModelForAgent(cs.agent, modelId);
|
|
814
837
|
cs.sessionId = null;
|
|
838
|
+
cs.codexCumulative = undefined;
|
|
815
839
|
this.log(`model switched to ${modelId} for ${cs.agent} chat=${ctx.chatId}`);
|
|
816
840
|
await ctx.answerCallback(`Switched to ${modelId}`);
|
|
817
841
|
await ctx.editReply(ctx.messageId, `<b>Model switched to <code>${escapeHtml(modelId)}</code></b>\n\nAgent: ${escapeHtml(cs.agent)}\nSession has been reset. Send a message to start a new conversation.`, { parseMode: 'HTML' });
|
package/dist/bot.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { execSync, spawn } from 'node:child_process';
|
|
10
10
|
import { doStream, getSessions, getUsage, listAgents, listModels, } from './code-agent.js';
|
|
11
|
-
export const VERSION = '0.2.
|
|
11
|
+
export const VERSION = '0.2.9';
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Helpers
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
@@ -187,7 +187,7 @@ export class Bot {
|
|
|
187
187
|
return listAgents();
|
|
188
188
|
}
|
|
189
189
|
fetchModels(agent) {
|
|
190
|
-
return listModels(agent);
|
|
190
|
+
return listModels(agent, { workdir: this.workdir, currentModel: this.modelForAgent(agent) });
|
|
191
191
|
}
|
|
192
192
|
setModelForAgent(agent, modelId) {
|
|
193
193
|
if (agent === 'codex')
|
|
@@ -232,8 +232,10 @@ export class Bot {
|
|
|
232
232
|
switchWorkdir(newPath) {
|
|
233
233
|
const old = this.workdir;
|
|
234
234
|
this.workdir = newPath;
|
|
235
|
-
for (const [, cs] of this.chats)
|
|
235
|
+
for (const [, cs] of this.chats) {
|
|
236
236
|
cs.sessionId = null;
|
|
237
|
+
cs.codexCumulative = undefined;
|
|
238
|
+
}
|
|
237
239
|
this.log(`switch workdir: ${old} -> ${newPath}`);
|
|
238
240
|
return old;
|
|
239
241
|
}
|
|
@@ -252,6 +254,7 @@ export class Bot {
|
|
|
252
254
|
attachments: attachments.length ? attachments : undefined,
|
|
253
255
|
codexModel: this.codexModel, codexFullAccess: this.codexFullAccess,
|
|
254
256
|
codexExtraArgs: this.codexExtraArgs.length ? this.codexExtraArgs : undefined,
|
|
257
|
+
codexPrevCumulative: cs.codexCumulative,
|
|
255
258
|
claudeModel: this.claudeModel, claudePermissionMode: this.claudePermissionMode,
|
|
256
259
|
claudeExtraArgs: this.claudeExtraArgs.length ? this.claudeExtraArgs : undefined,
|
|
257
260
|
};
|
|
@@ -263,6 +266,9 @@ export class Bot {
|
|
|
263
266
|
this.stats.totalOutputTokens += result.outputTokens;
|
|
264
267
|
if (result.cachedInputTokens)
|
|
265
268
|
this.stats.totalCachedTokens += result.cachedInputTokens;
|
|
269
|
+
// Store cumulative Codex totals for next invocation delta
|
|
270
|
+
if (result.codexCumulative)
|
|
271
|
+
cs.codexCumulative = result.codexCumulative;
|
|
266
272
|
// Only update sessionId if it hasn't been changed externally (e.g. user switched session during run)
|
|
267
273
|
if (result.sessionId && cs.sessionId === snapshotSessionId)
|
|
268
274
|
cs.sessionId = result.sessionId;
|
package/dist/code-agent.js
CHANGED
|
@@ -16,6 +16,8 @@ async function run(cmd, opts, parseLine) {
|
|
|
16
16
|
sessionId: opts.sessionId, text: '', thinking: '', msgs: [], thinkParts: [],
|
|
17
17
|
model: opts.model, thinkingEffort: opts.thinkingEffort, errors: null,
|
|
18
18
|
inputTokens: null, outputTokens: null, cachedInputTokens: null,
|
|
19
|
+
contextWindow: null,
|
|
20
|
+
codexCumulative: null,
|
|
19
21
|
stopReason: null,
|
|
20
22
|
};
|
|
21
23
|
const shellCmd = cmd.map(Q).join(' ');
|
|
@@ -89,6 +91,8 @@ async function run(cmd, opts, parseLine) {
|
|
|
89
91
|
thinking: s.thinking.trim() || null,
|
|
90
92
|
elapsedS: (Date.now() - start) / 1000,
|
|
91
93
|
inputTokens: s.inputTokens, outputTokens: s.outputTokens, cachedInputTokens: s.cachedInputTokens,
|
|
94
|
+
contextWindow: s.contextWindow,
|
|
95
|
+
codexCumulative: s.codexCumulative,
|
|
92
96
|
error,
|
|
93
97
|
stopReason: s.stopReason,
|
|
94
98
|
incomplete,
|
|
@@ -116,7 +120,7 @@ function codexCmd(o) {
|
|
|
116
120
|
args.push('-');
|
|
117
121
|
return args;
|
|
118
122
|
}
|
|
119
|
-
function codexParse(ev, s) {
|
|
123
|
+
function codexParse(ev, s, opts) {
|
|
120
124
|
const t = ev.type || '';
|
|
121
125
|
if (t === 'thread.started') {
|
|
122
126
|
s.sessionId = ev.thread_id ?? s.sessionId;
|
|
@@ -136,15 +140,26 @@ function codexParse(ev, s) {
|
|
|
136
140
|
if (t === 'turn.completed') {
|
|
137
141
|
const u = ev.usage;
|
|
138
142
|
if (u) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
// Codex reports cumulative session totals in turn.completed.
|
|
144
|
+
// Store raw cumulative and compute per-invocation delta.
|
|
145
|
+
const cumInput = u.input_tokens ?? 0;
|
|
146
|
+
const cumOutput = u.output_tokens ?? 0;
|
|
147
|
+
const cumCached = u.cached_input_tokens ?? 0;
|
|
148
|
+
s.codexCumulative = { input: cumInput, output: cumOutput, cached: cumCached };
|
|
149
|
+
const prev = opts.codexPrevCumulative;
|
|
150
|
+
s.inputTokens = prev ? Math.max(0, cumInput - prev.input) : cumInput;
|
|
151
|
+
s.outputTokens = prev ? Math.max(0, cumOutput - prev.output) : cumOutput;
|
|
152
|
+
s.cachedInputTokens = prev ? Math.max(0, cumCached - prev.cached) : cumCached;
|
|
142
153
|
}
|
|
143
154
|
s.model = ev.model ?? s.model;
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
|
-
export function doCodexStream(opts) {
|
|
147
|
-
|
|
157
|
+
export async function doCodexStream(opts) {
|
|
158
|
+
const result = await run(codexCmd(opts), opts, (ev, s) => codexParse(ev, s, opts));
|
|
159
|
+
// Codex doesn't report context_window in stream events; read from models_cache.json
|
|
160
|
+
if (!result.contextWindow)
|
|
161
|
+
result.contextWindow = readCodexContextWindow(result.model);
|
|
162
|
+
return result;
|
|
148
163
|
}
|
|
149
164
|
// --- claude ---
|
|
150
165
|
const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
|
|
@@ -260,6 +275,16 @@ function claudeParse(ev, s) {
|
|
|
260
275
|
s.cachedInputTokens = (u.cache_read_input_tokens ?? u.cached_input_tokens) ?? s.cachedInputTokens;
|
|
261
276
|
s.outputTokens = u.output_tokens ?? s.outputTokens;
|
|
262
277
|
}
|
|
278
|
+
// Extract contextWindow from modelUsage (Claude CLI reports this in result event)
|
|
279
|
+
const mu = ev.modelUsage;
|
|
280
|
+
if (mu && typeof mu === 'object') {
|
|
281
|
+
for (const info of Object.values(mu)) {
|
|
282
|
+
if (info?.contextWindow > 0) {
|
|
283
|
+
s.contextWindow = info.contextWindow;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
263
288
|
}
|
|
264
289
|
}
|
|
265
290
|
export async function doClaudeStream(opts) {
|
|
@@ -361,7 +386,7 @@ function readLastLine(filePath) {
|
|
|
361
386
|
}
|
|
362
387
|
}
|
|
363
388
|
function parseCodexSession(filePath) {
|
|
364
|
-
const lines = readLines(filePath,
|
|
389
|
+
const lines = readLines(filePath, 40);
|
|
365
390
|
const line = lines[0];
|
|
366
391
|
if (!line)
|
|
367
392
|
return null;
|
|
@@ -370,24 +395,32 @@ function parseCodexSession(filePath) {
|
|
|
370
395
|
if (ev.type !== 'session_meta')
|
|
371
396
|
return null;
|
|
372
397
|
const p = ev.payload || {};
|
|
398
|
+
let model = typeof p.model === 'string' ? p.model : null;
|
|
373
399
|
let title = null;
|
|
374
400
|
for (const raw of lines.slice(1)) {
|
|
375
401
|
if (!raw || raw[0] !== '{')
|
|
376
402
|
continue;
|
|
377
403
|
try {
|
|
378
404
|
const item = JSON.parse(raw);
|
|
405
|
+
if (!model && item.type === 'turn_context') {
|
|
406
|
+
const payload = item.payload || {};
|
|
407
|
+
model =
|
|
408
|
+
(typeof payload.model === 'string' ? payload.model : null)
|
|
409
|
+
|| (typeof payload?.collaboration_mode?.settings?.model === 'string' ? payload.collaboration_mode.settings.model : null)
|
|
410
|
+
|| model;
|
|
411
|
+
}
|
|
379
412
|
if (item.type === 'response_item' && item.payload?.role === 'user' && item.payload?.type === 'message') {
|
|
380
413
|
const content = item.payload.content;
|
|
381
414
|
if (Array.isArray(content)) {
|
|
382
415
|
const textBlock = content.find((b) => b?.type === 'input_text' && b.text && !/^[<#]/.test(b.text));
|
|
383
|
-
if (textBlock)
|
|
416
|
+
if (textBlock)
|
|
384
417
|
title = textBlock.text.slice(0, 120);
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
418
|
}
|
|
388
419
|
}
|
|
389
420
|
}
|
|
390
421
|
catch { /* skip */ }
|
|
422
|
+
if (model && title)
|
|
423
|
+
break;
|
|
391
424
|
}
|
|
392
425
|
// Codex writes task_complete as the last event when done
|
|
393
426
|
let running = false;
|
|
@@ -403,7 +436,7 @@ function parseCodexSession(filePath) {
|
|
|
403
436
|
sessionId: p.id ?? path.basename(filePath, '.jsonl'),
|
|
404
437
|
agent: 'codex',
|
|
405
438
|
workdir: p.cwd ?? null,
|
|
406
|
-
model:
|
|
439
|
+
model: model ?? null,
|
|
407
440
|
createdAt: p.timestamp ?? null,
|
|
408
441
|
title,
|
|
409
442
|
running,
|
|
@@ -528,20 +561,257 @@ export function listAgents() {
|
|
|
528
561
|
],
|
|
529
562
|
};
|
|
530
563
|
}
|
|
531
|
-
|
|
532
|
-
{
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
564
|
+
function shellOutput(cmd) {
|
|
565
|
+
try {
|
|
566
|
+
return execSync(cmd, { encoding: 'utf-8', timeout: 3000 }).trim() || null;
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function pushUnique(items, value) {
|
|
573
|
+
if (!items.includes(value))
|
|
574
|
+
items.push(value);
|
|
575
|
+
}
|
|
576
|
+
function pushModel(models, seen, id, alias) {
|
|
577
|
+
const cleanId = id.trim();
|
|
578
|
+
if (!cleanId || seen.has(cleanId))
|
|
579
|
+
return;
|
|
580
|
+
seen.add(cleanId);
|
|
581
|
+
models.push({ id: cleanId, alias: alias?.trim() || null });
|
|
582
|
+
}
|
|
583
|
+
function claudeModelAlias(modelId) {
|
|
584
|
+
const value = String(modelId || '').trim().toLowerCase();
|
|
585
|
+
if (!value)
|
|
586
|
+
return null;
|
|
587
|
+
if (value === 'opus' || value.startsWith('claude-opus-'))
|
|
588
|
+
return 'opus';
|
|
589
|
+
if (value === 'sonnet' || value.startsWith('claude-sonnet-'))
|
|
590
|
+
return 'sonnet';
|
|
591
|
+
if (value === 'haiku' || value.startsWith('claude-haiku-'))
|
|
592
|
+
return 'haiku';
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
function isClaudeModelToken(token) {
|
|
596
|
+
return token === 'opus' || token === 'sonnet' || token === 'haiku' || token.startsWith('claude-');
|
|
597
|
+
}
|
|
598
|
+
function addClaudeModel(models, seen, rawModel) {
|
|
599
|
+
const clean = String(rawModel || '').trim();
|
|
600
|
+
if (!clean)
|
|
601
|
+
return false;
|
|
602
|
+
const alias = claudeModelAlias(clean);
|
|
603
|
+
if (!alias && !clean.toLowerCase().startsWith('claude-'))
|
|
604
|
+
return false;
|
|
605
|
+
if (clean === alias) {
|
|
606
|
+
if (models.some((m) => m.alias === alias))
|
|
607
|
+
return false;
|
|
608
|
+
pushModel(models, seen, clean, null);
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
if (alias) {
|
|
612
|
+
const aliasIndex = models.findIndex((m) => m.id === alias && !m.alias);
|
|
613
|
+
if (aliasIndex >= 0) {
|
|
614
|
+
models.splice(aliasIndex, 1);
|
|
615
|
+
seen.delete(alias);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
pushModel(models, seen, clean, alias);
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
function isCodexModelToken(token) {
|
|
622
|
+
return /^(?:o\d(?:-[a-z0-9.-]+)?|gpt-[a-z0-9.-]+|codex-mini(?:-[a-z0-9.-]+)?)$/i.test(token);
|
|
623
|
+
}
|
|
624
|
+
function addCodexModel(models, seen, rawModel) {
|
|
625
|
+
const clean = String(rawModel || '').trim();
|
|
626
|
+
if (!clean || !isCodexModelToken(clean))
|
|
627
|
+
return false;
|
|
628
|
+
pushModel(models, seen, clean, null);
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
function readCodexConfigModels(home) {
|
|
632
|
+
const configPath = path.join(home, '.codex', 'config.toml');
|
|
633
|
+
if (!fs.existsSync(configPath))
|
|
634
|
+
return [];
|
|
635
|
+
try {
|
|
636
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
637
|
+
const found = [];
|
|
638
|
+
const defaultModel = raw.match(/^\s*model\s*=\s*"([^"]+)"/m)?.[1];
|
|
639
|
+
if (defaultModel)
|
|
640
|
+
pushUnique(found, defaultModel);
|
|
641
|
+
const migrationsSection = raw.match(/\[notice\.model_migrations\]\n([\s\S]*?)(?:\n\[|$)/)?.[1] || '';
|
|
642
|
+
for (const match of migrationsSection.matchAll(/"[^"]+"\s*=\s*"([^"]+)"/g)) {
|
|
643
|
+
if (match[1])
|
|
644
|
+
pushUnique(found, match[1]);
|
|
645
|
+
}
|
|
646
|
+
return found.filter(isCodexModelToken);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function readCodexCachedModels(home) {
|
|
653
|
+
const cachePath = path.join(home, '.codex', 'models_cache.json');
|
|
654
|
+
if (!fs.existsSync(cachePath))
|
|
655
|
+
return [];
|
|
656
|
+
try {
|
|
657
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
658
|
+
const models = Array.isArray(raw?.models) ? raw.models : [];
|
|
659
|
+
return models
|
|
660
|
+
.filter((m) => m?.visibility === 'list')
|
|
661
|
+
.sort((a, b) => (Number(a?.priority) || 0) - (Number(b?.priority) || 0))
|
|
662
|
+
.map((m) => String(m?.slug || '').trim())
|
|
663
|
+
.filter(isCodexModelToken);
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/** Look up context_window for a Codex model from ~/.codex/models_cache.json */
|
|
670
|
+
function readCodexContextWindow(model) {
|
|
671
|
+
if (!model)
|
|
672
|
+
return null;
|
|
673
|
+
const home = process.env.HOME || '';
|
|
674
|
+
const cachePath = path.join(home, '.codex', 'models_cache.json');
|
|
675
|
+
if (!fs.existsSync(cachePath))
|
|
676
|
+
return null;
|
|
677
|
+
try {
|
|
678
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
679
|
+
const models = Array.isArray(raw?.models) ? raw.models : [];
|
|
680
|
+
const entry = models.find((m) => m?.slug === model);
|
|
681
|
+
const cw = Number(entry?.context_window);
|
|
682
|
+
return cw > 0 ? cw : null;
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function readClaudeStateModels(home, workdir) {
|
|
689
|
+
const statePath = path.join(home, '.claude.json');
|
|
690
|
+
if (!fs.existsSync(statePath))
|
|
691
|
+
return [];
|
|
692
|
+
try {
|
|
693
|
+
const raw = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
694
|
+
const projects = raw?.projects;
|
|
695
|
+
if (!projects || typeof projects !== 'object')
|
|
696
|
+
return [];
|
|
697
|
+
const found = [];
|
|
698
|
+
const addFromProject = (projectState) => {
|
|
699
|
+
const usage = projectState?.lastModelUsage;
|
|
700
|
+
if (!usage || typeof usage !== 'object')
|
|
701
|
+
return;
|
|
702
|
+
for (const modelId of Object.keys(usage)) {
|
|
703
|
+
if (isClaudeModelToken(modelId))
|
|
704
|
+
pushUnique(found, modelId);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
if (workdir && typeof projects[workdir] === 'object') {
|
|
708
|
+
addFromProject(projects[workdir]);
|
|
709
|
+
}
|
|
710
|
+
for (const [projectPath, projectState] of Object.entries(projects)) {
|
|
711
|
+
if (projectPath === workdir)
|
|
712
|
+
continue;
|
|
713
|
+
addFromProject(projectState);
|
|
714
|
+
}
|
|
715
|
+
return found;
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
return [];
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function discoverClaudeModels(opts) {
|
|
722
|
+
const models = [];
|
|
723
|
+
const seen = new Set();
|
|
724
|
+
const sources = [];
|
|
725
|
+
const home = process.env.HOME || '';
|
|
726
|
+
if (opts.currentModel?.trim()) {
|
|
727
|
+
addClaudeModel(models, seen, opts.currentModel);
|
|
728
|
+
pushUnique(sources, 'current config');
|
|
729
|
+
}
|
|
730
|
+
let foundStateModel = false;
|
|
731
|
+
for (const modelId of readClaudeStateModels(home, opts.workdir)) {
|
|
732
|
+
foundStateModel = addClaudeModel(models, seen, modelId) || foundStateModel;
|
|
733
|
+
}
|
|
734
|
+
if (foundStateModel)
|
|
735
|
+
pushUnique(sources, '~/.claude.json');
|
|
736
|
+
const help = shellOutput('claude --help 2>/dev/null');
|
|
737
|
+
if (help) {
|
|
738
|
+
let foundHelpModel = false;
|
|
739
|
+
for (const match of help.matchAll(/\b(?:opus|sonnet|haiku|claude-(?:opus|sonnet|haiku)-[a-z0-9-]+)\b/gi)) {
|
|
740
|
+
const token = match[0].trim();
|
|
741
|
+
foundHelpModel = addClaudeModel(models, seen, token) || foundHelpModel;
|
|
742
|
+
}
|
|
743
|
+
if (foundHelpModel)
|
|
744
|
+
pushUnique(sources, 'claude --help');
|
|
745
|
+
}
|
|
746
|
+
if (opts.workdir) {
|
|
747
|
+
const sessions = getClaudeSessions({ agent: 'claude', workdir: opts.workdir, limit: 20 });
|
|
748
|
+
let foundSessionModel = false;
|
|
749
|
+
for (const session of sessions.sessions) {
|
|
750
|
+
if (!session.model)
|
|
751
|
+
continue;
|
|
752
|
+
foundSessionModel = addClaudeModel(models, seen, session.model) || foundSessionModel;
|
|
753
|
+
}
|
|
754
|
+
if (foundSessionModel)
|
|
755
|
+
pushUnique(sources, 'recent sessions');
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
agent: 'claude',
|
|
759
|
+
models,
|
|
760
|
+
sources,
|
|
761
|
+
note: 'Claude CLI does not expose a machine-readable model list; entries are discovered from current config, ~/.claude.json, CLI help, and local session state.',
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function discoverCodexModels(opts) {
|
|
765
|
+
const models = [];
|
|
766
|
+
const seen = new Set();
|
|
767
|
+
const sources = [];
|
|
768
|
+
const home = process.env.HOME || '';
|
|
769
|
+
let foundCacheModel = false;
|
|
770
|
+
for (const modelId of readCodexCachedModels(home)) {
|
|
771
|
+
foundCacheModel = addCodexModel(models, seen, modelId) || foundCacheModel;
|
|
772
|
+
}
|
|
773
|
+
if (foundCacheModel)
|
|
774
|
+
pushUnique(sources, '~/.codex/models_cache.json');
|
|
775
|
+
const help = shellOutput('codex --help 2>/dev/null');
|
|
776
|
+
if (help) {
|
|
777
|
+
let foundHelpModel = false;
|
|
778
|
+
for (const match of help.matchAll(/model="([^"]+)"/g)) {
|
|
779
|
+
addCodexModel(models, seen, match[1]);
|
|
780
|
+
foundHelpModel = true;
|
|
781
|
+
}
|
|
782
|
+
if (foundHelpModel)
|
|
783
|
+
pushUnique(sources, 'codex --help');
|
|
784
|
+
}
|
|
785
|
+
if (opts.currentModel?.trim()) {
|
|
786
|
+
addCodexModel(models, seen, opts.currentModel);
|
|
787
|
+
pushUnique(sources, 'current config');
|
|
788
|
+
}
|
|
789
|
+
let foundConfigModel = false;
|
|
790
|
+
for (const modelId of readCodexConfigModels(home)) {
|
|
791
|
+
foundConfigModel = addCodexModel(models, seen, modelId) || foundConfigModel;
|
|
792
|
+
}
|
|
793
|
+
if (foundConfigModel)
|
|
794
|
+
pushUnique(sources, '~/.codex/config.toml');
|
|
795
|
+
if (opts.workdir) {
|
|
796
|
+
const sessions = getCodexSessions({ agent: 'codex', workdir: opts.workdir, limit: 20 });
|
|
797
|
+
let foundSessionModel = false;
|
|
798
|
+
for (const session of sessions.sessions) {
|
|
799
|
+
if (!session.model)
|
|
800
|
+
continue;
|
|
801
|
+
foundSessionModel = addCodexModel(models, seen, session.model) || foundSessionModel;
|
|
802
|
+
}
|
|
803
|
+
if (foundSessionModel)
|
|
804
|
+
pushUnique(sources, 'recent sessions');
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
agent: 'codex',
|
|
808
|
+
models,
|
|
809
|
+
sources,
|
|
810
|
+
note: 'Codex CLI does not expose a model-list subcommand; entries are discovered from the local Codex model cache and other local state.',
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
export function listModels(agent, opts = {}) {
|
|
814
|
+
return agent === 'codex' ? discoverCodexModels(opts) : discoverClaudeModels(opts);
|
|
545
815
|
}
|
|
546
816
|
function toIsoFromEpochSeconds(value) {
|
|
547
817
|
const n = Number(value);
|