claude-token-saver 3.44.0 → 3.55.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/LICENSE +202 -21
- package/NOTICE +35 -0
- package/README.ko.md +150 -48
- package/README.md +205 -192
- package/bin/cli.js +108 -39
- package/docs/CODEX.ko.md +184 -0
- package/docs/CODEX.md +497 -0
- package/examples/statusline-command.ps1 +11 -5
- package/examples/statusline-command.sh +9 -4
- package/package.json +20 -6
- package/presets/delegation/bounds.md +29 -0
- package/presets/model-rules.json +50 -9
- package/presets/ratchet-rules.json +4 -1
- package/src/agent.js +33 -0
- package/src/caps-cache.js +6 -2
- package/src/changelog.js +70 -0
- package/src/cli-name.js +25 -0
- package/src/codex-brief.js +89 -0
- package/src/codex-budget.js +73 -0
- package/src/codex-cache-policy.js +203 -0
- package/src/codex-cache.js +46 -0
- package/src/codex-delegation.js +154 -0
- package/src/codex-doc2md-ledger.js +45 -0
- package/src/codex-doc2md.js +99 -0
- package/src/codex-doctor.js +53 -0
- package/src/codex-harness.js +200 -0
- package/src/codex-hooks.js +138 -0
- package/src/codex-installer.js +79 -0
- package/src/codex-ledger.js +143 -0
- package/src/codex-panel-launcher.js +48 -0
- package/src/codex-panel-runner.js +74 -0
- package/src/codex-panel-session.js +35 -0
- package/src/codex-panel-setup.js +98 -0
- package/src/codex-panel-shell.js +51 -0
- package/src/codex-panel-state.js +40 -0
- package/src/codex-panel.js +448 -0
- package/src/codex-parser.js +397 -0
- package/src/codex-route-scan.js +171 -0
- package/src/commands/brief.js +24 -1
- package/src/commands/codex-delegate.js +59 -0
- package/src/commands/codex.js +579 -0
- package/src/commands/delegate.js +133 -0
- package/src/commands/harness.js +16 -3
- package/src/commands/install.js +109 -12
- package/src/commands/korean.js +2 -2
- package/src/commands/mode.js +22 -2
- package/src/commands/profile-map.js +164 -0
- package/src/commands/route-scan.js +74 -7
- package/src/commands/seed.js +16 -12
- package/src/commands/uninstall.js +28 -4
- package/src/commands/upgrade.js +13 -0
- package/src/compact-window.js +7 -1
- package/src/config.js +22 -3
- package/src/cost.js +34 -0
- package/src/delegation-guard.js +278 -0
- package/src/doc2md-ledger.cjs +3 -3
- package/src/doc2md.cjs +72 -28
- package/src/effort-palette.js +145 -0
- package/src/formatters/statusline.js +352 -123
- package/src/formatters/table.js +5 -4
- package/src/gateway-auth.js +86 -0
- package/src/glyphs.js +103 -0
- package/src/handoff.js +27 -7
- package/src/harness-analyzer.cjs +7 -2
- package/src/harness-templates.js +33 -1
- package/src/harness.js +51 -17
- package/src/hook-manager.js +42 -14
- package/src/hook.cjs +34 -2
- package/src/installer.js +300 -16
- package/src/litellm-budget.js +38 -67
- package/src/litellm-models.js +264 -0
- package/src/model-alias.js +170 -16
- package/src/model-rules.js +53 -17
- package/src/parser.js +89 -1
- package/src/preset-ratchet.js +133 -0
- package/src/route-inject.js +161 -0
- package/src/route-scan.js +68 -8
- package/src/seed-rules.js +117 -55
- package/src/session-paths.js +242 -0
- package/src/statusline-mode.js +106 -0
- package/src/stdin-payload.js +40 -0
- package/src/ultracode.js +202 -0
- package/src/update-check.js +217 -3
- package/src/window-labels.js +19 -8
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const BEGIN = '# >>> sprag codex panel >>>';
|
|
6
|
+
const END = '# <<< sprag codex panel <<<';
|
|
7
|
+
|
|
8
|
+
export const PANEL_SHELL_BLOCK = `${BEGIN}
|
|
9
|
+
function codex() {
|
|
10
|
+
local sprag_root="$PWD" sprag_open=1 sprag_next=0 sprag_arg
|
|
11
|
+
case "\${1-}" in
|
|
12
|
+
exec|e|review|login|logout|mcp|plugin|app-server|remote-control|completion|update|doctor|sandbox|debug|apply|queue|archive|delete|migrate-rollouts|unarchive|cloud|exec-server|features|help|agents|app) sprag_open=0 ;;
|
|
13
|
+
esac
|
|
14
|
+
for sprag_arg in "$@"; do
|
|
15
|
+
if (( sprag_next )); then
|
|
16
|
+
sprag_root="$sprag_arg"
|
|
17
|
+
sprag_next=0
|
|
18
|
+
continue
|
|
19
|
+
fi
|
|
20
|
+
case "$sprag_arg" in
|
|
21
|
+
-C|--cd) sprag_next=1 ;;
|
|
22
|
+
--cd=*) sprag_root="\${sprag_arg#--cd=}" ;;
|
|
23
|
+
--help|-h|--version|-V) sprag_open=0 ;;
|
|
24
|
+
esac
|
|
25
|
+
done
|
|
26
|
+
if [[ -t 0 && -t 1 && "\${SPRAG_CODEX_PANEL-}" != 1 ]] && (( sprag_open )); then
|
|
27
|
+
command sprag panel run --agent codex -- "$@"
|
|
28
|
+
return $?
|
|
29
|
+
fi
|
|
30
|
+
command codex "$@"
|
|
31
|
+
}
|
|
32
|
+
${END}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export function installPanelShell({ file = join(process.env.ZDOTDIR || homedir(), '.zshrc'), remove = false } = {}) {
|
|
36
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
37
|
+
const start = text.indexOf(BEGIN);
|
|
38
|
+
const end = text.indexOf(END);
|
|
39
|
+
if ((start < 0) !== (end < 0) || (start >= 0 && end < start)) throw new Error('Malformed Sprag shell block; repair it before installation.');
|
|
40
|
+
if (start < 0 && !remove && /(?:function\s+codex\b|\bcodex\s*\(\)|\balias\s+codex=)/m.test(text)) {
|
|
41
|
+
throw new Error('An existing codex alias/function is present; keeping it unchanged.');
|
|
42
|
+
}
|
|
43
|
+
const next = start >= 0 ? text.slice(0, start) + (remove ? '' : PANEL_SHELL_BLOCK.trimEnd()) + text.slice(end + END.length)
|
|
44
|
+
: remove ? text : text + (text.endsWith('\n') || !text ? '' : '\n') + PANEL_SHELL_BLOCK;
|
|
45
|
+
if (next !== text) {
|
|
46
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
47
|
+
if (text) writeFileSync(`${file}.bak-${Date.now()}`, text);
|
|
48
|
+
writeFileSync(file, next);
|
|
49
|
+
}
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { userDataDir } from './paths.js';
|
|
5
|
+
import { codexUserDir } from './agent.js';
|
|
6
|
+
|
|
7
|
+
export function panelStatePath(root, kind, dir = userDataDir(), sessionId) {
|
|
8
|
+
const key = createHash('sha256').update(codexUserDir() + '\0' + resolve(root) + (sessionId ? `\0${sessionId}` : '')).digest('hex');
|
|
9
|
+
return join(dir, `codex-panel-${key}-${kind}.json`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function recordPanelState(root, kind, value, dir = userDataDir()) {
|
|
13
|
+
try {
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
const file = panelStatePath(root, kind, dir);
|
|
16
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
17
|
+
const text = JSON.stringify({ ...value, at: new Date().toISOString() });
|
|
18
|
+
writeFileSync(tmp, text);
|
|
19
|
+
renameSync(tmp, file);
|
|
20
|
+
if (value.sessionId) {
|
|
21
|
+
const sessionFile = panelStatePath(root, kind, dir, value.sessionId);
|
|
22
|
+
const sessionTmp = `${sessionFile}.${process.pid}.tmp`;
|
|
23
|
+
writeFileSync(sessionTmp, text);
|
|
24
|
+
renameSync(sessionTmp, sessionFile);
|
|
25
|
+
}
|
|
26
|
+
} catch { /* Diagnostics must not stop a session or panel. */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readPanelState(root, kind, dir = userDataDir(), sessionId) {
|
|
30
|
+
try { return JSON.parse(readFileSync(panelStatePath(root, kind, dir, sessionId), 'utf8')); }
|
|
31
|
+
catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function panelHealth(root, { now = Date.now(), dir, sessionId } = {}) {
|
|
35
|
+
const hook = readPanelState(root, 'hook', dir, sessionId);
|
|
36
|
+
const frame = readPanelState(root, 'frame', dir, sessionId);
|
|
37
|
+
const age = frame ? now - Date.parse(frame.at) : Infinity;
|
|
38
|
+
const live = frame?.status === 'rendered' && age >= 0 && age < 15000;
|
|
39
|
+
return { hook, frame, live };
|
|
40
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { resolve, relative, isAbsolute } from 'node:path';
|
|
2
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { stat } from 'node:fs/promises';
|
|
4
|
+
import { readPanelBinding } from './codex-panel-session.js';
|
|
5
|
+
import { discoverCodexSessionFiles, parseCodexSessionFile } from './codex-parser.js';
|
|
6
|
+
import { codexHarnessStatus, codexRatchetCandidates } from './codex-harness.js';
|
|
7
|
+
import { harnessListRules, findProjectRoot } from './harness.js';
|
|
8
|
+
import { loadConfig, statuslineDefaults } from './config.js';
|
|
9
|
+
import { glyphsFor } from './glyphs.js';
|
|
10
|
+
import { gaugeBar, formatMoney } from './formatters/statusline.js';
|
|
11
|
+
import { codexCacheTimer } from './codex-cache.js';
|
|
12
|
+
import { readCodexCachePolicy, readCodexPrices } from './codex-cache-policy.js';
|
|
13
|
+
import { codexIssues } from './codex-brief.js';
|
|
14
|
+
import { labelForKey } from './window-labels.js';
|
|
15
|
+
import { resolveLabelMode } from './statusline-mode.js';
|
|
16
|
+
import { formatResetIn } from './format-time.js';
|
|
17
|
+
import { codexDocumentTotals } from './codex-doc2md-ledger.js';
|
|
18
|
+
import { codexHookStatus } from './codex-installer.js';
|
|
19
|
+
import { koreanStyleEnabled } from './korean-style.js';
|
|
20
|
+
import { codexRoutingSavedTotals } from './codex-ledger.js';
|
|
21
|
+
import { readCodexRouteScan, openCodexCandidates } from './codex-route-scan.js';
|
|
22
|
+
import { codexBudgetProvider } from './codex-budget.js';
|
|
23
|
+
|
|
24
|
+
export const CODEX_SEGMENT_ORDER = [
|
|
25
|
+
'cap-warn', 'usage', 'ctx', 'ttl', 'model', 'effort',
|
|
26
|
+
'hit', 'saved', 'doc2md', 'harness', 'korean', 'version',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Only cache full-session parses. A moving time cutoff would make an unchanged
|
|
30
|
+
// file's cached totals stale. The panel reports session totals, not window totals.
|
|
31
|
+
export function createPanelReader({ root = process.cwd(), home, sessionId: pinnedId, sessionFile, days = 7, readBudget = () => null,
|
|
32
|
+
readCachePolicy = (session) => readCodexCachePolicy(session, { home }), hasFlag = () => false } = {}) {
|
|
33
|
+
if (pinnedId && sessionFile) throw new Error('Choose --session or --session-file, not both.');
|
|
34
|
+
const cache = new Map();
|
|
35
|
+
const ratchetCache = new Map();
|
|
36
|
+
const ratchetFor = (session) => {
|
|
37
|
+
if (!session?.filePath) return [];
|
|
38
|
+
try {
|
|
39
|
+
const s = statSync(session.filePath);
|
|
40
|
+
const key = `${s.mtimeMs}:${s.size}`;
|
|
41
|
+
// Candidates expire by age, so a quiet file still needs a periodic re-read.
|
|
42
|
+
const hit = ratchetCache.get(session.filePath);
|
|
43
|
+
if (hit?.key === key && Date.now() - hit.at < 60000) return hit.value;
|
|
44
|
+
const value = codexRatchetCandidates(session.filePath);
|
|
45
|
+
ratchetCache.clear();
|
|
46
|
+
ratchetCache.set(session.filePath, { key, at: Date.now(), value });
|
|
47
|
+
return value;
|
|
48
|
+
} catch { return []; }
|
|
49
|
+
};
|
|
50
|
+
return async function readPanel() {
|
|
51
|
+
const binding = sessionFile ? readPanelBinding(sessionFile) : null;
|
|
52
|
+
const sessionId = pinnedId || binding?.sessionId;
|
|
53
|
+
const selection = sessionFile ? 'bound' : pinnedId ? 'pinned' : 'project';
|
|
54
|
+
let files;
|
|
55
|
+
if (binding?.transcriptPath) {
|
|
56
|
+
try {
|
|
57
|
+
const s = await stat(binding.transcriptPath);
|
|
58
|
+
if (s.isFile()) files = [{ path: binding.transcriptPath, mtime: s.mtimeMs, size: s.size }];
|
|
59
|
+
} catch { /* A resumed or archived session may have moved. Search by ID. */ }
|
|
60
|
+
}
|
|
61
|
+
// A missing binding must never fall back to another session in the project.
|
|
62
|
+
files ??= sessionFile && !sessionId ? [] : await discoverCodexSessionFiles({ home, days: sessionId ? Infinity : days });
|
|
63
|
+
const existing = new Set(files.map((f) => f.path));
|
|
64
|
+
for (const path of cache.keys()) if (!existing.has(path)) cache.delete(path);
|
|
65
|
+
let selected = null;
|
|
66
|
+
let matches = 0;
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const f of files.slice().reverse()) {
|
|
69
|
+
let entry = cache.get(f.path);
|
|
70
|
+
if (!entry || entry.mtime !== f.mtime || entry.size !== f.size) {
|
|
71
|
+
try {
|
|
72
|
+
entry = { ...f, session: await parseCodexSessionFile(f.path) };
|
|
73
|
+
cache.set(f.path, entry);
|
|
74
|
+
} catch { continue; }
|
|
75
|
+
}
|
|
76
|
+
const s = entry.session;
|
|
77
|
+
if ((!sessionId || sessionFile) && s.isSubagent) continue;
|
|
78
|
+
if (sessionId ? s.sessionId !== sessionId : !insideProject(root, s.projectDir)) continue;
|
|
79
|
+
const key = s.sessionId || s.filePath;
|
|
80
|
+
if (seen.has(key)) continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
matches++;
|
|
83
|
+
// Activity timestamps survive archive moves that update filesystem mtime.
|
|
84
|
+
if (!selected || (s.lastActivity?.getTime() || 0) > (selected.lastActivity?.getTime() || 0)) selected = s;
|
|
85
|
+
}
|
|
86
|
+
const project = selected?.projectDir ? findProjectRoot(selected.projectDir, { agent: 'codex' }) : root;
|
|
87
|
+
const cfg = loadConfig();
|
|
88
|
+
const harness = {};
|
|
89
|
+
const rules = {};
|
|
90
|
+
let hooks;
|
|
91
|
+
try { hooks = codexHookStatus(); } catch { hooks = null; }
|
|
92
|
+
for (const scope of ['project', 'global']) {
|
|
93
|
+
try { harness[scope] = codexHarnessStatus({ root: project, scope }); }
|
|
94
|
+
catch (e) { harness[scope] = { error: e.message }; }
|
|
95
|
+
try { rules[scope] = harnessListRules({ root: project, scope, agent: 'codex' }).rules.length; }
|
|
96
|
+
catch { rules[scope] = null; }
|
|
97
|
+
}
|
|
98
|
+
let route = [];
|
|
99
|
+
try { route = openCodexCandidates(readCodexRouteScan(), { root: project }); } catch { /* No scan yet. */ }
|
|
100
|
+
let routingSaved = null;
|
|
101
|
+
try { routingSaved = { ...codexRoutingSavedTotals(), pricesAvailable: !!readCodexPrices({ provider: codexBudgetProvider(home ? { home } : {}), ...(home ? { home } : {}) }) }; }
|
|
102
|
+
catch { /* Unreadable ledger reads as n/a. */ }
|
|
103
|
+
return { session: selected, matches, root, sessionId, selection, bindingPending: !!sessionFile && !sessionId, days, harness, rules,
|
|
104
|
+
ratchet: ratchetFor(selected), route, routingSaved,
|
|
105
|
+
labelMode: resolveLabelMode({ cfg: statuslineDefaults(), hasFlag }).mode,
|
|
106
|
+
color: statuslineDefaults().color,
|
|
107
|
+
timer: !hasFlag('--no-timer') && (hasFlag('--timer') || statuslineDefaults().timer),
|
|
108
|
+
cacheTtl: cfg?.codex?.cacheTtl ?? 'auto',
|
|
109
|
+
cachePolicy: selected ? await readCachePolicy(selected) : null,
|
|
110
|
+
hooks,
|
|
111
|
+
documents: codexDocumentTotals({ home }),
|
|
112
|
+
budget: readBudget(),
|
|
113
|
+
korean: koreanStyleEnabled(cfg),
|
|
114
|
+
lint: cfg?.koreanStyle?.lint || 'block',
|
|
115
|
+
cohesion: cfg?.cohesion?.enabled === true,
|
|
116
|
+
brief: cfg?.codex?.brief !== false,
|
|
117
|
+
delegate: cfg?.codex?.delegate === true,
|
|
118
|
+
doc2md: cfg?.codex?.doc2md !== false && process.env.CTS_NO_DOC2MD !== '1',
|
|
119
|
+
updatedAt: new Date() };
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function insideProject(root, cwd) {
|
|
124
|
+
if (!cwd) return false;
|
|
125
|
+
const canonical = (path) => {
|
|
126
|
+
try { return realpathSync(path); } catch { return resolve(path); }
|
|
127
|
+
};
|
|
128
|
+
const rel = relative(canonical(root), canonical(cwd));
|
|
129
|
+
return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith('../') && !rel.startsWith('..\\'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Transcript strings must never supply terminal control sequences. Restrict
|
|
133
|
+
// dynamic labels to printable ASCII so column counts remain exact on any font.
|
|
134
|
+
export function terminalText(value) {
|
|
135
|
+
return String(value ?? '').replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g, '')
|
|
136
|
+
.replace(/[^\x20-\x7e]/g, '?');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const number = (n) => Number.isFinite(n) ? n.toLocaleString('en-US') : 'n/a';
|
|
140
|
+
const short = (n) => !Number.isFinite(n) ? 'n/a' : n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
141
|
+
const pct = (n) => Number.isFinite(n) ? `${n.toFixed(1)}%` : 'n/a';
|
|
142
|
+
const gauge = (n) => {
|
|
143
|
+
if (!Number.isFinite(n)) return '[------------]';
|
|
144
|
+
const ticks = n <= 0 ? 0 : n >= 100 ? 12 : Math.max(1, Math.min(11, Math.round(n * 12 / 100)));
|
|
145
|
+
return `[${'#'.repeat(ticks)}${'.'.repeat(12 - ticks)}]`;
|
|
146
|
+
};
|
|
147
|
+
function age(date, now) {
|
|
148
|
+
if (!date) return 'unknown';
|
|
149
|
+
const seconds = Math.max(0, Math.floor((now - new Date(date)) / 1000));
|
|
150
|
+
if (!Number.isFinite(seconds)) return 'unknown';
|
|
151
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
152
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
153
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
154
|
+
}
|
|
155
|
+
function windowLabel(minutes, fallback) {
|
|
156
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return fallback;
|
|
157
|
+
if (minutes % 1440 === 0) return `${minutes / 1440}D`;
|
|
158
|
+
if (minutes % 60 === 0) return `${minutes / 60}H`;
|
|
159
|
+
return `${minutes}m`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function effortLabel(value) {
|
|
163
|
+
return value === 'none' ? 'Reasoning off' : value ? terminalText(value) : 'Effort unknown';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const signedMoney = (usd) => usd < 0 ? `-${formatMoney(-usd)}` : formatMoney(usd);
|
|
167
|
+
|
|
168
|
+
/** Measured routing savings, or the reason they are unknown. Never a fabricated zero. */
|
|
169
|
+
function routingChip(totals, chip) {
|
|
170
|
+
if (!totals?.runs) {
|
|
171
|
+
return chip(`Routing saved n/a (${totals?.pricesAvailable ? 'no attributed runs' : 'prices unavailable'})`);
|
|
172
|
+
}
|
|
173
|
+
if (!totals.priced) return chip(`Routing saved n/a (${totals.runs} runs unpriced)`);
|
|
174
|
+
const unpriced = totals.runs - totals.priced;
|
|
175
|
+
return chip(`Routing saved ${signedMoney(totals.week)} (7d) / ${signedMoney(totals.total)} total${unpriced ? ` +${unpriced} unpriced` : ''}`,
|
|
176
|
+
totals.total < 0 ? 33 : 32);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sessionLabel(data) {
|
|
180
|
+
const id = data.sessionId || data.session?.sessionId;
|
|
181
|
+
const label = data.selection === 'bound' ? 'session' : data.sessionId ? 'pinned' : 'latest project';
|
|
182
|
+
return `${label}${id ? ` ...${terminalText(id).slice(-8)}` : ''}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Labels from logs are ASCII-sanitized; only our known glyphs need wide cells.
|
|
186
|
+
const graphemes = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
187
|
+
export function panelCellWidth(text) {
|
|
188
|
+
return [...graphemes.segment(text.replace(/\x1b\[[0-9;]*m/g, ''))].reduce((n, { segment }) =>
|
|
189
|
+
n + (/^[\p{Mark}\u200d]+$/u.test(segment) ? 0 : segment.includes('\ufe0f') || segment.codePointAt(0) >= 0x1f000 ? 2 : 1), 0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function panelGroups(data, { now, version = '', full = false, mode = full ? 'text' : data.labelMode || 'icon' }) {
|
|
193
|
+
const g = glyphsFor(mode);
|
|
194
|
+
const icon = (key) => mode === 'text' ? '' : `${g[key]} `;
|
|
195
|
+
const s = data.session;
|
|
196
|
+
const input = s ? s.totals.input + s.totals.cacheRead : null;
|
|
197
|
+
const context = s?.contextWindow && s.lastContextTokens != null ? s.lastContextTokens / s.contextWindow * 100 : null;
|
|
198
|
+
const h = data.harness?.project?.configured ? data.harness.project : data.harness?.global;
|
|
199
|
+
const registry = new Map();
|
|
200
|
+
const register = (name, group, chips) => registry.set(name, { group, chips: chips.filter(Boolean) });
|
|
201
|
+
const chip = (text, tone = 90) => ({ text, tone });
|
|
202
|
+
const level = (n) => n >= 90 ? 31 : n >= 70 ? 33 : 32;
|
|
203
|
+
const bar = (n) => full ? gauge(n) : gaugeBar(n, mode === 'text' ? 'narrow' : mode);
|
|
204
|
+
const warnings = [];
|
|
205
|
+
if (data.error) warnings.push(chip(`ERROR: ${terminalText(data.error)}`, 31));
|
|
206
|
+
for (const issue of codexIssues(s, { now })) {
|
|
207
|
+
const key = issue.key.slice(6), win = s.rateLimits?.[key];
|
|
208
|
+
const text = issue.key === 'context' ? `WARNING: Ctx ${pct(context)}`
|
|
209
|
+
: `WARNING: ${windowLabel(win.window_minutes, key)} limit ${pct(win.used_percent)}`;
|
|
210
|
+
warnings.push(chip(text, issue.level === 'critical' ? 31 : 33));
|
|
211
|
+
}
|
|
212
|
+
register('ctx', 'priority', context !== null ? [chip(full
|
|
213
|
+
? `Ctx ${bar(context)} ${pct(context)} ${short(s.lastContextTokens)} / ${short(s.contextWindow)}`
|
|
214
|
+
: `${icon('ctx')}Ctx ${short(s.contextWindow)} ${bar(context)} ${pct(context)}`, level(context))] : []);
|
|
215
|
+
const timer = data.timer === false ? null : codexCacheTimer(s, { now, ttl: data.cacheTtl, policy: data.cachePolicy });
|
|
216
|
+
register('ttl', 'priority', s && timer ? [chip(`${icon('ttl')}${timer.text}`, timer.tone)] : []);
|
|
217
|
+
register('model', 'priority', [chip(s ? `${full ? 'Model ' : icon('model')}${terminalText(s.model)}`
|
|
218
|
+
: `SPRAG / Waiting for session ${data.bindingPending ? 'binding' : 'log'}`, 35)]);
|
|
219
|
+
register('effort', 'priority', s ? [chip(`${icon('effort')}${effortLabel(s.effort)}`, 36), chip(sessionLabel(data)),
|
|
220
|
+
s.status && chip(terminalText(s.status), s.status === 'working' ? 36 : 90)] : []);
|
|
221
|
+
const limits = [];
|
|
222
|
+
const budget = data.budget;
|
|
223
|
+
if (Number.isFinite(budget?.maxBudget) && budget.maxBudget > 0 && Number.isFinite(budget.spend)) {
|
|
224
|
+
const used = Math.max(0, budget.spend / budget.maxBudget * 100);
|
|
225
|
+
const label = labelForKey('litellm_budget');
|
|
226
|
+
const prefix = mode === 'text' ? '' : `${mode === 'narrow' ? label.narrowIcon : label.icon} `;
|
|
227
|
+
const stale = !budget.checkedAt || now - budget.checkedAt > 600000;
|
|
228
|
+
const reset = Number.isFinite(budget.budgetResetAt) ? budget.budgetResetAt : null;
|
|
229
|
+
if (used >= 90 && !stale && (reset === null || reset > now)) warnings.push(chip(`WARNING: Budget ${Math.round(used)}%`, used >= 100 ? 31 : 33));
|
|
230
|
+
limits.push(chip(`${prefix}${full ? 'Budget' : 'budget'} ${bar(used)} ${Math.round(used)}% ${formatMoney(budget.spend)}/${formatMoney(budget.maxBudget)}` +
|
|
231
|
+
(reset === null ? '' : reset <= now ? ' / awaiting reset update' : ` ${icon('reset')}${formatResetIn(reset / 1000, new Date(now))}`) +
|
|
232
|
+
(budget.source === 'user' ? ' (user limit)' : '') + (stale ? ' STALE' : ''), stale ? 33 : level(used)));
|
|
233
|
+
}
|
|
234
|
+
for (const key of ['primary', 'secondary']) {
|
|
235
|
+
const win = s?.rateLimits?.[key];
|
|
236
|
+
if (!Number.isFinite(win?.used_percent)) continue;
|
|
237
|
+
const reset = Number.isFinite(win.resets_at) ? win.resets_at * 1000 : null;
|
|
238
|
+
const elapsed = reset !== null && reset <= now;
|
|
239
|
+
const stale = !s.rateLimitsAt || now - new Date(s.rateLimitsAt) > 300000;
|
|
240
|
+
limits.push(chip(`${windowLabel(win.window_minutes, key)} ${bar(win.used_percent)} ${pct(win.used_percent)} used` +
|
|
241
|
+
(reset === null ? '' : elapsed ? ' / awaiting new usage' : ` ${icon('reset')}${formatResetIn(win.resets_at, new Date(now))}`) +
|
|
242
|
+
(stale ? ' STALE' : ''), elapsed || stale ? 33 : level(win.used_percent)));
|
|
243
|
+
}
|
|
244
|
+
if (limits.length && s?.rateLimitsAt) limits.push(chip(`snapshot ${age(s.rateLimitsAt, now)}`));
|
|
245
|
+
register('cap-warn', 'priority', warnings);
|
|
246
|
+
register('usage', 'priority', limits);
|
|
247
|
+
register('hit', 'totals', input > 0 ? [chip(`${icon('hit')}Cache hit ${pct(s.totals.cacheRead / input * 100)} (session)`,
|
|
248
|
+
s.totals.cacheRead / input >= 0.85 ? 32 : s.totals.cacheRead / input >= 0.7 ? 33 : 31)] : []);
|
|
249
|
+
register('saved', 'totals', s ? [
|
|
250
|
+
chip(`${icon('hit')}Cache reused ${short(s.totals.cacheRead)} tokens`, 32),
|
|
251
|
+
chip(`In ${short(input)}`), chip(`Out ${short(s.totals.output)}`),
|
|
252
|
+
chip(`Reasoning ${short(s.reasoningOutputTokens)} (in output)`),
|
|
253
|
+
] : []);
|
|
254
|
+
const docs = data.documents;
|
|
255
|
+
register('doc2md', 'documents', docs?.scope === 'codex-total' && docs.docs > 0 ? [
|
|
256
|
+
chip(`${icon('doc')}Doc2md ${number(docs.docs)} docs (Codex total)`, 32),
|
|
257
|
+
...(docs.byExt || []).map((row) => chip(`${terminalText(row.ext)} ${number(row.docs)}x`)),
|
|
258
|
+
] : []);
|
|
259
|
+
register('harness', 'preferences', [
|
|
260
|
+
chip(`${icon('harness')}Harness ${h?.error ? 'ERROR' : h ? `${h.configured}/${h.total}` : 'n/a'}`, h?.configured === 5 ? 32 : 33),
|
|
261
|
+
chip(`Ratchet ${data.rules?.global ?? '?'}/${data.rules?.project ?? '?'}`),
|
|
262
|
+
// A repeated failure outranks a delegation candidate, as in the Claude statusline.
|
|
263
|
+
data.ratchet?.length ? chip(`ratchet? #${data.ratchet[0].id} x${data.ratchet[0].count}`, 33) : null,
|
|
264
|
+
data.route?.length ? chip(`route? R${data.route[0].id} ${terminalText(data.route[0].category)}${data.route.length > 1 ? ` +${data.route.length - 1}` : ''}`, 33) : null,
|
|
265
|
+
]);
|
|
266
|
+
register('korean', 'preferences', [
|
|
267
|
+
chip(`${icon('korean')}Korean ${data.korean ? `on/${data.lint}` : 'off'}`),
|
|
268
|
+
...(data.cohesion ? [chip(`Cohesion ${data.korean ? 'via Korean' : 'on'}`)] : []),
|
|
269
|
+
chip(`${icon('doc')}Doc2md ${data.doc2md ? 'on' : 'off'} (prefs)`),
|
|
270
|
+
...(data.hooks ? [chip(`Hooks ${data.hooks.filter((hook) => hook.registered).length}/${data.hooks.length} registered`)] : []),
|
|
271
|
+
...(data.brief !== undefined ? [chip(`Brief ${data.brief ? 'on' : 'off'}`)] : []),
|
|
272
|
+
...(data.delegate !== undefined ? [chip(`Delegate ${data.delegate ? 'on' : 'off'} (model routing)`)] : []),
|
|
273
|
+
routingChip(data.routingSaved, chip),
|
|
274
|
+
]);
|
|
275
|
+
register('version', 'preferences', [
|
|
276
|
+
chip(`SPRAG${version ? ` v${terminalText(version)}` : ''} / ${s ? age(s.lastActivity, now) : 'waiting'}`,
|
|
277
|
+
s && (!s.lastActivity || now - new Date(s.lastActivity) > 300000) ? 33 : 90),
|
|
278
|
+
]);
|
|
279
|
+
// Keep session measurements together, before lifetime document counts.
|
|
280
|
+
const groups = [];
|
|
281
|
+
let lastGroup;
|
|
282
|
+
for (const name of CODEX_SEGMENT_ORDER) {
|
|
283
|
+
const entry = registry.get(name);
|
|
284
|
+
if (!entry?.chips.length) continue;
|
|
285
|
+
if (lastGroup !== entry.group) groups.push([]);
|
|
286
|
+
groups.at(-1).push(...entry.chips);
|
|
287
|
+
lastGroup = entry.group;
|
|
288
|
+
}
|
|
289
|
+
return groups;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function wrapPanelGroups(groups, { width, color }) {
|
|
293
|
+
const lines = [];
|
|
294
|
+
const paint = ({ text, tone }) => color ? `\x1b[${tone}m${text}\x1b[0m` : text;
|
|
295
|
+
for (const group of groups) {
|
|
296
|
+
let line = '', used = 0;
|
|
297
|
+
for (const segment of group) {
|
|
298
|
+
const size = panelCellWidth(segment.text);
|
|
299
|
+
if (used && used + 3 + size > width) { lines.push(line); line = ''; used = 0; }
|
|
300
|
+
if (size > width) {
|
|
301
|
+
let part = '';
|
|
302
|
+
for (const { segment: ch } of graphemes.segment(segment.text)) {
|
|
303
|
+
if (panelCellWidth(part + ch) > width) { lines.push(paint({ ...segment, text: part })); part = ''; }
|
|
304
|
+
if (panelCellWidth(ch) <= width) part += ch;
|
|
305
|
+
}
|
|
306
|
+
line = paint({ ...segment, text: part }); used = panelCellWidth(part);
|
|
307
|
+
} else {
|
|
308
|
+
line += (used ? paint({ text: ' | ', tone: 90 }) : '') + paint(segment);
|
|
309
|
+
used += (used ? 3 : 0) + size;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (line) lines.push(line);
|
|
313
|
+
}
|
|
314
|
+
return lines;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function compactPanel(data, { width, height, color, now, version = '' }) {
|
|
318
|
+
const lines = wrapPanelGroups(panelGroups(data, { now, version }), { width, color });
|
|
319
|
+
const visible = lines.slice(0, height);
|
|
320
|
+
// Make hidden rows explicit rather than silently dropping trailing metrics.
|
|
321
|
+
if (lines.length > height && height > 1) {
|
|
322
|
+
const text = `+${lines.length - height + 1} more rows`.slice(0, width);
|
|
323
|
+
visible[height - 1] = color ? `\x1b[90m${text}\x1b[0m` : text;
|
|
324
|
+
}
|
|
325
|
+
return visible.join('\n');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Plain ASCII layout first; ANSI color is applied after width/height clipping. */
|
|
329
|
+
export function formatCodexPanel(data, { columns = 100, rows = 30, color = true, now = Date.now(), version = '', compact = false } = {}) {
|
|
330
|
+
const width = Math.max(1, Math.floor(columns) || 80);
|
|
331
|
+
const height = Math.max(1, Math.floor(rows) || 24);
|
|
332
|
+
const lines = [];
|
|
333
|
+
const add = (text = '', tone = '') => {
|
|
334
|
+
const clean = terminalText(text);
|
|
335
|
+
// Wrap at spaces; hard-wrap long IDs so even a narrow split stays bounded.
|
|
336
|
+
if (!clean) { lines.push({ text: '', tone }); return; }
|
|
337
|
+
let rest = clean;
|
|
338
|
+
while (rest.length > width) {
|
|
339
|
+
const space = rest.lastIndexOf(' ', width);
|
|
340
|
+
const at = space > width / 2 ? space : width;
|
|
341
|
+
lines.push({ text: rest.slice(0, at), tone });
|
|
342
|
+
rest = rest.slice(at).trimStart();
|
|
343
|
+
}
|
|
344
|
+
lines.push({ text: rest, tone });
|
|
345
|
+
};
|
|
346
|
+
add(`SPRAG / CODEX${version ? ` v${version}` : ''}`, 'cyan');
|
|
347
|
+
const s = data.session;
|
|
348
|
+
if (compact) {
|
|
349
|
+
return compactPanel(data, { width, height, color, now, version });
|
|
350
|
+
}
|
|
351
|
+
const last = s?.lastActivity ? new Date(s.lastActivity).getTime() : null;
|
|
352
|
+
const stale = !last || now - last > 5 * 60000;
|
|
353
|
+
const groups = panelGroups(data, { now, version, full: true });
|
|
354
|
+
for (const text of wrapPanelGroups(groups, { width, color })) lines.push({ text });
|
|
355
|
+
add(s ? `LOG ${stale ? 'STALE' : 'RECENT'} | updated ${age(s.lastActivity, now)} | ${sessionLabel(data)}`
|
|
356
|
+
: data.bindingPending ? 'WAITING / session binding not received' : 'WAITING / no matching session log', stale ? 'yellow' : 'green');
|
|
357
|
+
if (s?.branch) add(`Branch ${s.branch} (recorded)`);
|
|
358
|
+
if (data.hooks) add('Hook trust: check Codex /hooks', 'dim');
|
|
359
|
+
add('Cost n/a (session) | Cache expiry not reported', 'dim');
|
|
360
|
+
add();
|
|
361
|
+
add(`Project: ${s?.projectDir || data.root}`);
|
|
362
|
+
add(`Session: ${s?.sessionId || data.sessionId || 'none'}`);
|
|
363
|
+
if (s) {
|
|
364
|
+
add(`Session totals: ${number(s.requestCount)} usage updates`);
|
|
365
|
+
add(`Input ${number(s.totals.input + s.totals.cacheRead)} / cached ${number(s.totals.cacheRead)}`);
|
|
366
|
+
add(`Output ${number(s.totals.output)} / reasoning ${number(s.reasoningOutputTokens)} (included)`);
|
|
367
|
+
add(`Peak input ${number(s.maxContextPerRequest)} | matching sessions ${data.matches}`);
|
|
368
|
+
} else add(`Discovery: last ${data.days} days. Waiting for Codex rollout JSONL usage.`, 'dim');
|
|
369
|
+
const footer = `Refreshed ${new Date(data.updatedAt || now).toISOString().slice(11, 19)} UTC | r refresh | q quit`;
|
|
370
|
+
// Keep the footer visible, including when the terminal is resized mid-frame.
|
|
371
|
+
const visible = lines.slice(0, Math.max(0, height - 1));
|
|
372
|
+
visible.push({ text: footer.slice(0, width), tone: 'dim' });
|
|
373
|
+
const tones = { cyan: 36, green: 32, yellow: 33, red: 31, dim: 90 };
|
|
374
|
+
return visible.map(({ text, tone }) => color && (tones[tone] || typeof tone === 'number') ? `\x1b[${tones[tone] || tone}m${text}\x1b[0m` : text).join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function runCodexPanel({ read, interval = 2000, once = false, color = true, version = '', compact = false, input = process.stdin, output = process.stdout, fitRows = () => {}, onFrame = () => {}, onClose = () => {} } = {}) {
|
|
378
|
+
if (!Number.isFinite(interval) || interval < 500 || interval > 60000) throw new Error('--interval must be between 0.5 and 60 seconds');
|
|
379
|
+
if (!once && (!input.isTTY || !output.isTTY)) throw new Error('Live panel requires a terminal. Use --once for piped output.');
|
|
380
|
+
let stopped = false;
|
|
381
|
+
let timer;
|
|
382
|
+
let lastData = { root: process.cwd() };
|
|
383
|
+
let previousLines = [];
|
|
384
|
+
let previousSize = '';
|
|
385
|
+
let wake;
|
|
386
|
+
const wait = () => new Promise((done) => {
|
|
387
|
+
wake = () => { clearTimeout(timer); wake = null; done(); };
|
|
388
|
+
timer = setTimeout(wake, interval);
|
|
389
|
+
});
|
|
390
|
+
const stop = () => { stopped = true; wake?.(); };
|
|
391
|
+
const key = (buf) => {
|
|
392
|
+
if (/[q\x03\x04]/i.test(String(buf))) stop();
|
|
393
|
+
else if (/r/i.test(String(buf))) wake?.();
|
|
394
|
+
};
|
|
395
|
+
const wasRaw = !!input.isRaw;
|
|
396
|
+
const paused = input.isPaused();
|
|
397
|
+
const draw = () => {
|
|
398
|
+
const columns = output.columns || 100, rows = output.rows || 30;
|
|
399
|
+
const text = formatCodexPanel(lastData, { columns: columns - (once ? 0 : 1), rows: rows - (once ? 0 : 1), color: color && lastData.color !== false, version, compact });
|
|
400
|
+
if (once) { output.write(text + '\n'); return; }
|
|
401
|
+
const lines = text.split('\n');
|
|
402
|
+
const size = `${columns}x${rows}`;
|
|
403
|
+
const resized = size !== previousSize;
|
|
404
|
+
// A full erase and newline repaint moves the terminal's IME anchor through
|
|
405
|
+
// the whole split. Address only changed rows, without scrolling or reflow.
|
|
406
|
+
let frame = resized ? '\x1b[H\x1b[2J' : '';
|
|
407
|
+
const count = resized ? lines.length : Math.max(lines.length, previousLines.length);
|
|
408
|
+
for (let i = 0; i < count; i++) {
|
|
409
|
+
if (resized || lines[i] !== previousLines[i]) frame += `\x1b[${i + 1};1H${lines[i] || ''}\x1b[K`;
|
|
410
|
+
}
|
|
411
|
+
if (frame) output.write(frame);
|
|
412
|
+
previousLines = lines;
|
|
413
|
+
previousSize = size;
|
|
414
|
+
onFrame(lastData);
|
|
415
|
+
};
|
|
416
|
+
try {
|
|
417
|
+
if (!once) {
|
|
418
|
+
output.write('\x1b[?1049h\x1b[?25l');
|
|
419
|
+
input.setRawMode(true);
|
|
420
|
+
input.resume();
|
|
421
|
+
input.on('data', key);
|
|
422
|
+
output.on('resize', draw);
|
|
423
|
+
process.on('SIGINT', stop);
|
|
424
|
+
process.on('SIGTERM', stop);
|
|
425
|
+
process.on('SIGHUP', stop);
|
|
426
|
+
}
|
|
427
|
+
do {
|
|
428
|
+
try { lastData = await read(); }
|
|
429
|
+
catch (e) { lastData = { ...lastData, error: e.message }; }
|
|
430
|
+
if (stopped) break;
|
|
431
|
+
if (compact && !once) fitRows(formatCodexPanel(lastData, { columns: (output.columns || 100) - 1, rows: 10000, color: false, compact: true }).split('\n').length);
|
|
432
|
+
draw();
|
|
433
|
+
if (once) break;
|
|
434
|
+
await wait();
|
|
435
|
+
} while (!stopped);
|
|
436
|
+
} finally {
|
|
437
|
+
clearTimeout(timer);
|
|
438
|
+
if (!once) {
|
|
439
|
+
input.off('data', key);
|
|
440
|
+
output.off('resize', draw);
|
|
441
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.off(signal, stop);
|
|
442
|
+
input.setRawMode(wasRaw);
|
|
443
|
+
if (paused || input.listenerCount('data') === 0) input.pause();
|
|
444
|
+
output.write('\x1b[0m\x1b[?25h\x1b[?1049l');
|
|
445
|
+
onClose();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|