sprag-cli 3.67.0 → 3.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.ko.md +29 -62
  2. package/README.md +26 -61
  3. package/bin/cli.js +122 -61
  4. package/docs/CODEX.ko.md +32 -0
  5. package/docs/CODEX.md +34 -0
  6. package/package.json +11 -4
  7. package/src/{codex-brief.js → codex/brief.js} +7 -7
  8. package/src/{codex-budget.js → codex/budget.js} +2 -2
  9. package/src/{codex-cache-policy.js → codex/cache-policy.js} +9 -9
  10. package/src/{codex-delegation-diagnostics.js → codex/delegation-diagnostics.js} +2 -2
  11. package/src/{codex-delegation.js → codex/delegation.js} +11 -11
  12. package/src/{codex-doc2md-ledger.js → codex/doc2md-ledger.js} +5 -5
  13. package/src/{codex-doc2md.js → codex/doc2md.js} +4 -4
  14. package/src/{codex-doctor.js → codex/doctor.js} +4 -4
  15. package/src/{codex-harness.js → codex/harness.js} +5 -5
  16. package/src/{codex-hooks.js → codex/hooks.js} +20 -20
  17. package/src/{codex-installer.js → codex/installer.js} +2 -2
  18. package/src/{codex-ledger.js → codex/ledger.js} +6 -6
  19. package/src/{codex-panel-launcher.js → codex/panel-launcher.js} +2 -2
  20. package/src/{codex-panel-runner.js → codex/panel-runner.js} +2 -2
  21. package/src/{codex-panel-session.js → codex/panel-session.js} +1 -1
  22. package/src/{codex-panel-setup.js → codex/panel-setup.js} +1 -1
  23. package/src/{codex-panel-state.js → codex/panel-state.js} +3 -3
  24. package/src/{codex-panel.js → codex/panel.js} +43 -34
  25. package/src/{codex-parser.js → codex/parser.js} +5 -9
  26. package/src/{codex-reply-language.js → codex/reply-language.js} +2 -2
  27. package/src/{codex-route-scan.js → codex/route-scan.js} +10 -10
  28. package/src/{codex-rule-health.js → codex/rule-health.js} +4 -4
  29. package/src/commands/codex-delegate.js +6 -6
  30. package/src/commands/codex.js +44 -44
  31. package/src/commands/install.js +10 -7
  32. package/src/commands/korean.js +1 -1
  33. package/src/commands/litellm-budget.js +56 -0
  34. package/src/commands/saved.js +60 -5
  35. package/src/commands/uninstall.js +3 -3
  36. package/src/config.js +4 -0
  37. package/src/formatters/statusline.js +713 -504
  38. package/src/glyphs.js +2 -0
  39. package/src/installer.js +77 -1
  40. package/src/model-alias.js +45 -60
  41. package/src/model-share.js +136 -0
  42. package/src/parser.js +4 -13
  43. package/src/session-records.js +2 -8
  44. package/src/statusline-base.js +9 -3
  45. package/src/stdin-payload.js +1 -1
  46. package/src/transcript-lines.js +40 -0
  47. /package/src/{codex-cache.js → codex/cache.js} +0 -0
  48. /package/src/{codex-panel-shell.js → codex/panel-shell.js} +0 -0
@@ -1,17 +1,17 @@
1
1
  import { readFileSync, mkdirSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { userDataDir } from './paths.js';
5
- import { writeViaTmp } from './state-file.js';
6
- import { loadConfig } from './config.js';
7
- import { findProjectRoot } from './harness.js';
8
- import { discoverCodexSessionFiles, parseCodexTurns } from './codex-parser.js';
4
+ import { userDataDir } from '../paths.js';
5
+ import { writeViaTmp } from '../state-file.js';
6
+ import { loadConfig } from '../config.js';
7
+ import { findProjectRoot } from '../harness.js';
8
+ import { discoverCodexSessionFiles, parseCodexTurns } from './parser.js';
9
9
  import { categorize, isSkippable, tierOf, calibrateThresholds, MIN_RECURRENCE,
10
- RESCAN_MIN_INTERVAL_MS, RESCAN_BIG_DELTA_BYTES, RESCAN_MAX_AGE_MS } from './route-scan.js';
11
- import { codexPromptCategory, loadCodexModelRules, sharedCodexModelRules, codexSharedTargets } from './codex-delegation.js';
12
- import { codexRunCost, codexPriceBook, isDirectOpenAI, readCodexPrices } from './codex-cache-policy.js';
13
- import { OPENAI_PRICES_CHECKED_AT } from './openai-prices.js';
14
- import { refreshCodexLedger } from './codex-ledger.js';
10
+ RESCAN_MIN_INTERVAL_MS, RESCAN_BIG_DELTA_BYTES, RESCAN_MAX_AGE_MS } from '../route-scan.js';
11
+ import { codexPromptCategory, loadCodexModelRules, sharedCodexModelRules, codexSharedTargets } from './delegation.js';
12
+ import { codexRunCost, codexPriceBook, isDirectOpenAI, readCodexPrices } from './cache-policy.js';
13
+ import { OPENAI_PRICES_CHECKED_AT } from '../openai-prices.js';
14
+ import { refreshCodexLedger } from './ledger.js';
15
15
 
16
16
  /*
17
17
  * Codex route-scan: recurring easy turns become delegation-rule candidates.
@@ -1,8 +1,8 @@
1
1
  import { resolve } from 'node:path';
2
- import { loadCodexLedger } from './codex-ledger.js';
3
- import { isFailedRun } from './route-scan.js';
4
- import { wilsonLowerBound, HEALTH_ERR_RATE, HEALTH_MIN_SAMPLE_DELEGATED } from './model-rules.js';
5
- import { loadSharedModelRules, sharedModelRulesForProject, realProjectRoot } from './shared-model-rules.js';
2
+ import { loadCodexLedger } from './ledger.js';
3
+ import { isFailedRun } from '../route-scan.js';
4
+ import { wilsonLowerBound, HEALTH_ERR_RATE, HEALTH_MIN_SAMPLE_DELEGATED } from '../model-rules.js';
5
+ import { loadSharedModelRules, sharedModelRulesForProject, realProjectRoot } from '../shared-model-rules.js';
6
6
 
7
7
  /**
8
8
  * Measured outcome of one Codex rule, judged as Claude's rule health judges
@@ -1,15 +1,15 @@
1
1
  import { loadConfig, saveConfig } from '../config.js';
2
- import { configureCodexHooks } from '../codex-installer.js';
2
+ import { configureCodexHooks } from '../codex/installer.js';
3
3
  import { validateCodexTarget, loadCodexModelRules, addCodexModelRule, removeCodexModelRule,
4
4
  validateCodexSharedTarget, codexSharedTargets, codexDelegateMinContext, DEFAULT_DELEGATE_MIN_CONTEXT,
5
- autoCodexSharedTargets, codexDelegateEnabled, codexImplementationHint } from '../codex-delegation.js';
6
- import { findCodexCandidate, resolveCodexCandidate } from '../codex-route-scan.js';
7
- import { codexRoutingSavedTotals, loadCodexLedger } from '../codex-ledger.js';
8
- import { codexRuleHealth, codexRulesInReview, codexSharedRuleHealth } from '../codex-rule-health.js';
5
+ autoCodexSharedTargets, codexDelegateEnabled, codexImplementationHint } from '../codex/delegation.js';
6
+ import { findCodexCandidate, resolveCodexCandidate } from '../codex/route-scan.js';
7
+ import { codexRoutingSavedTotals, loadCodexLedger } from '../codex/ledger.js';
8
+ import { codexRuleHealth, codexRulesInReview, codexSharedRuleHealth } from '../codex/rule-health.js';
9
9
  import { signedUsd } from '../money.js';
10
10
  import { codexProviderName, codexModelName } from '../agent.js';
11
11
  import { loadSharedModelRules, sharedModelRulesForProject, SHARED_RULE_TIERS } from '../shared-model-rules.js';
12
- import { codexDelegationDiagnostics } from '../codex-delegation-diagnostics.js';
12
+ import { codexDelegationDiagnostics } from '../codex/delegation-diagnostics.js';
13
13
 
14
14
  // Starts from the working directory, as the hooks do, so a project policy
15
15
  // stored under either agent's project root is counted.
@@ -1,6 +1,6 @@
1
1
  import { createArgs } from '../cli-args.js';
2
- import { configureCodexHooks } from '../codex-installer.js';
3
- import { initCodexHarness, uninitCodexHarness, codexHarnessStatus, syncCodexKoreanBlock } from '../codex-harness.js';
2
+ import { configureCodexHooks } from '../codex/installer.js';
3
+ import { initCodexHarness, uninitCodexHarness, codexHarnessStatus, syncCodexKoreanBlock } from '../codex/harness.js';
4
4
  import { findProjectRoot, harnessPromote, harnessListRules, harnessRmRule, harnessPrune } from '../harness.js';
5
5
  import { loadConfig, saveConfig } from '../config.js';
6
6
  import { resolve } from 'node:path';
@@ -103,7 +103,7 @@ export async function run({ args, version }) {
103
103
  const selectedRoot = !['harness', 'delegate', 'seed'].includes(cmd) && getArg('--project') ? resolve(getArg('--project')) : root;
104
104
  const format = getArg('--format') || getArg('-f') || 'table';
105
105
  if (cmd === 'cache') {
106
- const { parseCodexCacheTtl } = await import('../codex-cache.js');
106
+ const { parseCodexCacheTtl } = await import('../codex/cache.js');
107
107
  const mode = args[1] || 'status';
108
108
  const cfg = loadConfig();
109
109
  if (mode !== 'status') {
@@ -113,13 +113,13 @@ export async function run({ args, version }) {
113
113
  }
114
114
  const ttl = cfg.codex?.cacheTtl ?? 'auto';
115
115
  console.log(`Codex cache timer: ${typeof ttl === 'number' ? `${ttl / 60}m (configured estimate)` : ttl}`);
116
- const { createPanelReader } = await import('../codex-panel.js');
117
- const { refreshCodexCachePolicy } = await import('../codex-cache-policy.js');
118
- const { codexCacheTimer } = await import('../codex-cache.js');
116
+ const { createPanelReader } = await import('../codex/panel.js');
117
+ const { refreshCodexCachePolicy } = await import('../codex/cache-policy.js');
118
+ const { codexCacheTimer } = await import('../codex/cache.js');
119
119
  const data = await createPanelReader({ root: selectedRoot, sessionId: getArg('--session'),
120
120
  ...(hasFlag('--refresh') ? { readCachePolicy: refreshCodexCachePolicy } : {}) })();
121
121
  if (data.session) {
122
- const { terminalText } = await import('../codex-panel.js');
122
+ const { terminalText } = await import('../codex/panel.js');
123
123
  console.log(`Session: ${terminalText(data.session.sessionId)} | ${terminalText(data.session.provider)} / ${terminalText(data.session.model)}`);
124
124
  if (data.cachePolicy) {
125
125
  const p = data.cachePolicy;
@@ -133,11 +133,11 @@ export async function run({ args, version }) {
133
133
  }
134
134
  if (cmd === 'capabilities' || cmd === 'doctor') {
135
135
  if (!['table', 'json'].includes(format)) throw new Error('Diagnostics format must be table or json.');
136
- const { codexDoctor, CODEX_CAPABILITIES } = await import('../codex-doctor.js');
136
+ const { codexDoctor, CODEX_CAPABILITIES } = await import('../codex/doctor.js');
137
137
  const report = cmd === 'capabilities' ? { agent: 'codex', capabilities: CODEX_CAPABILITIES }
138
138
  : await codexDoctor({ root: selectedRoot, sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }) });
139
139
  if (format === 'json') { console.log(JSON.stringify(report, null, 2)); return; }
140
- const { terminalText } = await import('../codex-panel.js');
140
+ const { terminalText } = await import('../codex/panel.js');
141
141
  if (cmd === 'doctor') {
142
142
  console.log(`Codex home: ${terminalText(report.home)}`);
143
143
  console.log(`Config: ${report.config.error || (report.config.present ? 'readable' : 'absent (defaults)')}`);
@@ -161,10 +161,10 @@ export async function run({ args, version }) {
161
161
  if (cmd === 'saved') return (await import('./saved.js')).run({ args, hasFlag, agent: 'codex' });
162
162
  if (cmd === 'seed') return (await import('./seed.js')).run({ args, hasFlag, agent: 'codex', root });
163
163
  if (cmd === 'route-scan') {
164
- const rs = await import('../codex-route-scan.js');
164
+ const rs = await import('../codex/route-scan.js');
165
165
  const days = numArg('--days', { dflt: 14, min: 1 });
166
166
  if (args[1] === 'savings') {
167
- const { refreshCodexLedger, loadCodexLedger, codexRoutingSavedTotals } = await import('../codex-ledger.js');
167
+ const { refreshCodexLedger, loadCodexLedger, codexRoutingSavedTotals } = await import('../codex/ledger.js');
168
168
  if (hasFlag('--refresh')) await refreshCodexLedger();
169
169
  // The prompt hook runs this detached to price finished delegations.
170
170
  if (hasFlag('--quiet')) return;
@@ -190,7 +190,7 @@ export async function run({ args, version }) {
190
190
  let cache = rs.readCodexRouteScan();
191
191
  if (hasFlag('--refresh') || !cache) {
192
192
  // Prices live in the gateway snapshot; refresh it when stale so candidates can name a target.
193
- const { readCodexPrices, refreshCodexPrices } = await import('../codex-cache-policy.js');
193
+ const { readCodexPrices, refreshCodexPrices } = await import('../codex/cache-policy.js');
194
194
  if (!readCodexPrices()) { try { await refreshCodexPrices(); } catch (e) { debug('codex:prices', e); } }
195
195
  cache = await rs.runCodexRouteScan({ days });
196
196
  }
@@ -225,8 +225,8 @@ export async function run({ args, version }) {
225
225
  console.log('Run sprag install --agent codex and review/trust hooks in Codex /hooks.');
226
226
  return;
227
227
  }
228
- const { createPanelReader, terminalText } = await import('../codex-panel.js');
229
- const { codexIssues } = await import('../codex-brief.js');
228
+ const { createPanelReader, terminalText } = await import('../codex/panel.js');
229
+ const { codexIssues } = await import('../codex/brief.js');
230
230
  const data = await createPanelReader({ root: selectedRoot, sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }) })();
231
231
  const issues = codexIssues(data.session);
232
232
  if (!issues.length) console.log('No fresh Codex context or rate-limit warnings. Missing/stale logs do not prove healthy usage.');
@@ -238,8 +238,8 @@ export async function run({ args, version }) {
238
238
  }
239
239
  if (cmd === 'history' || cmd === 'last') {
240
240
  if (!['table', 'json'].includes(format)) throw new Error('History format must be table or json.');
241
- const { readCodexHistory } = await import('../codex-brief.js');
242
- const { terminalText } = await import('../codex-panel.js');
241
+ const { readCodexHistory } = await import('../codex/brief.js');
242
+ const { terminalText } = await import('../codex/panel.js');
243
243
  let events = readCodexHistory({ days: numArg('--days', { dflt: cmd === 'last' ? 1 : 7, min: 0 }),
244
244
  sessionId: getArg('--session'), project: getArg('--project') || getArg('-p') });
245
245
  if (cmd === 'last') events = events.slice(-1);
@@ -253,13 +253,13 @@ export async function run({ args, version }) {
253
253
  return;
254
254
  }
255
255
  if (cmd === 'handoff') {
256
- const { createPanelReader } = await import('../codex-panel.js');
256
+ const { createPanelReader } = await import('../codex/panel.js');
257
257
  const { writeHandoff } = await import('../handoff.js');
258
258
  const cwd = getArg('--cwd') || process.cwd();
259
259
  const data = await createPanelReader({ root: findProjectRoot(resolve(cwd), { agent: 'codex' }), sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }) })();
260
260
  const result = writeHandoff({ cwd, agent: 'codex', session: data.session });
261
261
  try {
262
- const { recordCodexHistoryEvent } = await import('../codex-brief.js');
262
+ const { recordCodexHistoryEvent } = await import('../codex/brief.js');
263
263
  recordCodexHistoryEvent({ sessionId: data.session?.sessionId, project: cwd, key: 'handoff',
264
264
  message: `Handoff written: ${result.path}` });
265
265
  } catch (e) { debug('codex:handoff-record', e); }
@@ -269,7 +269,7 @@ export async function run({ args, version }) {
269
269
  }
270
270
  const statusline = hasFlag('--statusline') || format === 'statusline';
271
271
  if (statusline && (!cmd || cmd.startsWith('-'))) {
272
- const { createPanelReader, formatCodexPanel } = await import('../codex-panel.js');
272
+ const { createPanelReader, formatCodexPanel } = await import('../codex/panel.js');
273
273
  const data = await createPanelReader({ root: selectedRoot, sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }), hasFlag, version })();
274
274
  const columns = numArg('--columns', { dflt: process.stdout.columns || 100, min: 1, max: 10000 });
275
275
  const text = formatCodexPanel(data, { compact: true, columns: hasFlag('--single-line') ? 100000 : columns,
@@ -281,7 +281,7 @@ export async function run({ args, version }) {
281
281
  if (args[1] === 'run') {
282
282
  const separator = args.indexOf('--');
283
283
  const forwarded = separator < 0 ? [] : args.slice(separator + 1);
284
- const { runCodexWithPanel } = await import('../codex-panel-runner.js');
284
+ const { runCodexWithPanel } = await import('../codex/panel-runner.js');
285
285
  let project = process.cwd();
286
286
  for (let i = 0; i < forwarded.length; i++) {
287
287
  if (forwarded[i] === '--') break;
@@ -294,7 +294,7 @@ export async function run({ args, version }) {
294
294
  if (args[1] === 'shell') {
295
295
  if (process.platform !== 'darwin') throw new Error('Immediate panel launch currently supports macOS zsh.');
296
296
  if (!['install', 'remove'].includes(args[2])) throw new Error('Usage: sprag panel shell install|remove --agent codex');
297
- const { installPanelShell } = await import('../codex-panel-shell.js');
297
+ const { installPanelShell } = await import('../codex/panel-shell.js');
298
298
  const file = installPanelShell({ remove: args[2] === 'remove' });
299
299
  // install adds the integration by default, so a removal must be
300
300
  // remembered or the next upgrade puts it back.
@@ -304,10 +304,10 @@ export async function run({ args, version }) {
304
304
  console.log(`Shell integration ${args[2]}: ${file}. Open a new terminal to apply it.`);
305
305
  return;
306
306
  }
307
- const { recordPanelState, panelHealth } = await import('../codex-panel-state.js');
307
+ const { recordPanelState, panelHealth } = await import('../codex/panel-state.js');
308
308
  const panelRoot = getArg('--project') ? resolve(getArg('--project')) : root;
309
309
  if (args[1] === 'doctor') {
310
- const { codexPanelAutoEnabled } = await import('../codex-installer.js');
310
+ const { codexPanelAutoEnabled } = await import('../codex/installer.js');
311
311
  const health = panelHealth(panelRoot, { sessionId: getArg('--session') });
312
312
  console.log(`Panel hook registered: ${codexPanelAutoEnabled() ? 'yes' : 'no'}`);
313
313
  console.log(`Project: ${panelRoot}`);
@@ -320,7 +320,7 @@ export async function run({ args, version }) {
320
320
  return;
321
321
  }
322
322
  if (args[1] === 'auto') {
323
- const { codexPanelAutoEnabled } = await import('../codex-installer.js');
323
+ const { codexPanelAutoEnabled } = await import('../codex/installer.js');
324
324
  const mode = args[2] || 'status';
325
325
  if (!['on', 'off', 'status'].includes(mode)) throw new Error('Usage: sprag panel auto on|off|status --agent codex');
326
326
  if (mode === 'on' && process.platform !== 'darwin') throw new Error('Automatic panel windows currently require macOS Terminal.');
@@ -335,13 +335,13 @@ export async function run({ args, version }) {
335
335
  return;
336
336
  }
337
337
  if (hasFlag('--open')) {
338
- const { openCodexPanel } = await import('../codex-panel-launcher.js');
338
+ const { openCodexPanel } = await import('../codex/panel-launcher.js');
339
339
  console.log(`Codex panel: ${openCodexPanel(getArg('--project') || root, { sessionId: getArg('--session') })}`);
340
340
  return;
341
341
  }
342
- const { createPanelReader, runCodexPanel } = await import('../codex-panel.js');
343
- const { createCodexCachePolicyReader, refreshCodexCachePolicy } = await import('../codex-cache-policy.js');
344
- const { createCodexBudgetReader, codexBudgetProvider, fetchCodexBudget } = await import('../codex-budget.js');
342
+ const { createPanelReader, runCodexPanel } = await import('../codex/panel.js');
343
+ const { createCodexCachePolicyReader, refreshCodexCachePolicy } = await import('../codex/cache-policy.js');
344
+ const { createCodexBudgetReader, codexBudgetProvider, fetchCodexBudget } = await import('../codex/budget.js');
345
345
  const readBudget = hasFlag('--once') ? (() => {
346
346
  let snapshot = null;
347
347
  return { read: () => snapshot, set: (value) => { snapshot = value; } };
@@ -350,7 +350,7 @@ export async function run({ args, version }) {
350
350
  const provider = codexBudgetProvider();
351
351
  if (provider) { try { readBudget.set(await fetchCodexBudget(provider)); } catch (e) { debug('codex:budget', e); } }
352
352
  }
353
- const { createInlinePanelResizer } = await import('../codex-panel-runner.js');
353
+ const { createInlinePanelResizer } = await import('../codex/panel-runner.js');
354
354
  const rawInterval = getArg('--interval');
355
355
  const interval = rawInterval === undefined ? 2 : Number(rawInterval);
356
356
  if (!Number.isFinite(interval) || interval < 0.5 || interval > 60) throw new Error('--interval must be between 0.5 and 60 seconds');
@@ -362,7 +362,7 @@ export async function run({ args, version }) {
362
362
  read: createPanelReader({ root: getArg('--project') ? resolve(getArg('--project')) : root,
363
363
  sessionId: getArg('--session'), sessionFile: getArg('--session-file'), days: numArg('--days', { dflt: 7, min: 0 }), readBudget: readBudget.read,
364
364
  version, refreshUpdate: !hasFlag('--once'), readCachePolicy: hasFlag('--once') ? async (session) => {
365
- const { readCodexCachePolicy } = await import('../codex-cache-policy.js');
365
+ const { readCodexCachePolicy } = await import('../codex/cache-policy.js');
366
366
  try { return readCodexCachePolicy(session) || await refreshCodexCachePolicy(session); }
367
367
  catch (e) { debug('codex:cache-policy', e); return null; }
368
368
  } : createCodexCachePolicyReader(), hasFlag }),
@@ -378,16 +378,16 @@ export async function run({ args, version }) {
378
378
  }
379
379
  if (cmd === 'codex-hook') {
380
380
  const { readStdinJson } = await import('../stdin-payload.js');
381
- const { codexHookOutput } = await import('../codex-hooks.js');
381
+ const { codexHookOutput } = await import('../codex/hooks.js');
382
382
  try {
383
383
  const payload = readStdinJson();
384
- const { bindPanelSession } = await import('../codex-panel-session.js');
384
+ const { bindPanelSession } = await import('../codex/panel-session.js');
385
385
  bindPanelSession(getArg('--event'), payload);
386
386
  if (getArg('--event') === 'panel-start') {
387
387
  if (process.env.SPRAG_CODEX_PANEL === '1') return;
388
388
  if (!payload?.cwd || typeof payload.session_id !== 'string' || !payload.session_id.trim() || !['startup', 'resume'].includes(payload.source)) return;
389
- const { openCodexPanel } = await import('../codex-panel-launcher.js');
390
- const { recordPanelState } = await import('../codex-panel-state.js');
389
+ const { openCodexPanel } = await import('../codex/panel-launcher.js');
390
+ const { recordPanelState } = await import('../codex/panel-state.js');
391
391
  const panelRoot = findProjectRoot(payload.cwd, { agent: 'codex' });
392
392
  recordPanelState(panelRoot, 'hook', { status: 'started', sessionId: payload.session_id });
393
393
  try {
@@ -406,7 +406,7 @@ export async function run({ args, version }) {
406
406
  }
407
407
  if (cmd === 'install') {
408
408
  const cfg = loadConfig();
409
- const { setupCodexPanel, describePanelSetup } = await import('../codex-panel-setup.js');
409
+ const { setupCodexPanel, describePanelSetup } = await import('../codex/panel-setup.js');
410
410
  const panel = setupCodexPanel({ config: cfg || {}, noPanel: hasFlag('--no-panel') });
411
411
  const panelAuto = panel.panelAuto;
412
412
  const hook = configureCodexHooks({ panelAuto });
@@ -432,7 +432,7 @@ export async function run({ args, version }) {
432
432
  console.log('Existing config.toml, Claude Code settings, and ratchet rules are preserved.');
433
433
  try {
434
434
  const { codexModelName, codexProviderName } = await import('../agent.js');
435
- const { autoCodexSharedTargets } = await import('../codex-delegation.js');
435
+ const { autoCodexSharedTargets } = await import('../codex/delegation.js');
436
436
  const parent = codexModelName(), provider = codexProviderName();
437
437
  const auto = parent ? autoCodexSharedTargets({ model: parent, provider }) : { targets: [], reason: 'Codex config names no default model' };
438
438
  console.log(auto.targets.length
@@ -447,7 +447,7 @@ export async function run({ args, version }) {
447
447
  if (hasFlag('--purge')) throw new Error('Codex uninstall keeps shared Sprag state; --purge is not supported.');
448
448
  const hook = configureCodexHooks({ remove: true });
449
449
  const h = uninitCodexHarness({ root, scope: 'global' });
450
- const { installPanelShell } = await import('../codex-panel-shell.js');
450
+ const { installPanelShell } = await import('../codex/panel-shell.js');
451
451
  const shellFile = installPanelShell({ remove: true });
452
452
  console.log(`Codex hooks: ${hook.file} (${hook.action})`);
453
453
  console.log(`Codex harness: ${h.file} (${h.removed ? 'removed' : 'absent'})`);
@@ -481,8 +481,8 @@ export async function run({ args, version }) {
481
481
  if (!rule) throw new Error('Provide rule text or a ratchet candidate number (see ratchet? in sprag --statusline --agent codex).');
482
482
  if (/^R\d+$/i.test(rule)) throw new Error(`Approve route candidates with: sprag delegate rules add ${rule.toUpperCase()} --global|--project --agent codex`);
483
483
  if (/^\d+$/.test(rule)) {
484
- const { createPanelReader } = await import('../codex-panel.js');
485
- const { codexCandidateRule } = await import('../codex-harness.js');
484
+ const { createPanelReader } = await import('../codex/panel.js');
485
+ const { codexCandidateRule } = await import('../codex/harness.js');
486
486
  const { userLanguage } = await import('../config.js');
487
487
  const data = await createPanelReader({ root, sessionId: getArg('--session') })();
488
488
  const cand = (data.ratchet || []).find((c) => c.id === Number(rule));
@@ -543,7 +543,7 @@ export async function run({ args, version }) {
543
543
  if (cmd === 'doc2md' && (!args[1] || args[1] === 'status')) {
544
544
  const { createRequire } = await import('node:module');
545
545
  const doc2md = createRequire(import.meta.url)('../doc2md.cjs');
546
- const { codexHookStatus } = await import('../codex-installer.js');
546
+ const { codexHookStatus } = await import('../codex/installer.js');
547
547
  console.log(`Codex document conversion: ${loadConfig()?.codex?.doc2md === false || process.env.CTS_NO_DOC2MD === '1' ? 'off' : 'on'}`);
548
548
  console.log(`Formats: ${doc2md.TARGET_EXTENSIONS.join(' ')}`);
549
549
  console.log(`Converter: ${doc2md.findInterpreter() || 'missing; run sprag doc2md install-converter --agent codex'}`);
@@ -558,7 +558,7 @@ export async function run({ args, version }) {
558
558
  return;
559
559
  }
560
560
  if (cmd === 'doc2md' && args[1] === 'savings') {
561
- const { codexDocumentTotals } = await import('../codex-doc2md-ledger.js');
561
+ const { codexDocumentTotals } = await import('../codex/doc2md-ledger.js');
562
562
  const totals = codexDocumentTotals();
563
563
  if (format === 'json') console.log(JSON.stringify({ agent: 'codex', ...totals }, null, 2));
564
564
  else {
@@ -569,11 +569,11 @@ export async function run({ args, version }) {
569
569
  return;
570
570
  }
571
571
  if (cmd === 'doc2md' && args[1] && !args[1].startsWith('-') && args[1] !== 'install-converter') {
572
- const { convertCodexDocument } = await import('../codex-doc2md.js');
572
+ const { convertCodexDocument } = await import('../codex/doc2md.js');
573
573
  const sessionId = getArg('--session') || process.env.CODEX_THREAD_ID;
574
574
  let identity = {};
575
575
  if (sessionId) {
576
- const { createPanelReader } = await import('../codex-panel.js');
576
+ const { createPanelReader } = await import('../codex/panel.js');
577
577
  const { session } = await createPanelReader({ root, sessionId })();
578
578
  if (session) identity = { model: session.model, provider: session.provider };
579
579
  }
@@ -617,7 +617,7 @@ export async function run({ args, version }) {
617
617
  const days = numArg('--hours', { min: 0 }) !== undefined ? numArg('--hours', { min: 0 }) / 24
618
618
  : (numArg('--days', { min: 0 }) ?? numArg('-d', { min: 0 }) ?? 30);
619
619
  if (!['table', 'json', 'csv'].includes(format)) throw new Error('Codex report format must be table, json, or csv.');
620
- const { parseAllCodexSessions } = await import('../codex-parser.js');
620
+ const { parseAllCodexSessions } = await import('../codex/parser.js');
621
621
  const { summary, dailyTrend } = await import('../stats.js');
622
622
  const sessions = (await parseAllCodexSessions({ days, projectFilter: getArg('--project') || getArg('-p') }))
623
623
  .filter((session) => !getArg('--session') || session.sessionId === getArg('--session'));
@@ -102,6 +102,9 @@ export async function run({ hasFlag }) {
102
102
  console.log(lang === 'ko'
103
103
  ? ` statusline: 기존 statusline이 이미 설정되어 있습니다: ${s.existingCommand}`
104
104
  : ` statusline: an existing statusline is already configured: ${s.existingCommand}`);
105
+ // A wrapper around our own CLI is worth naming here too: the non-tty line
106
+ // below prints the same sentence, so both paths say what the entry is.
107
+ if (s.wrapper) console.log(` ${s.reason}`);
105
108
  const pick = await choose(lang === 'ko'
106
109
  ? ' statusline을 어떻게 표시할까요?'
107
110
  : ' How should the statusline be shown?', [
@@ -212,8 +215,8 @@ export async function run({ hasFlag }) {
212
215
  console.log(lang === 'ko' ? ` harness: 작성 ${p}` : ` harness: wrote ${p}`);
213
216
  }
214
217
  console.log(lang === 'ko'
215
- ? ' harness: 5원칙을 ~/.claude/CLAUDE.md 에 설정했습니다 — statusline에 🅷 5/5 가 표시됩니다.'
216
- : ' harness: 5 principles installed in ~/.claude/CLAUDE.md — the statusline now shows 🅷 5/5.');
218
+ ? ' harness: 5원칙을 ~/.claude/CLAUDE.md 에 설정했습니다. 점수가 5/5에 못 미치거나 경고가 있을 때 statusline에 🅷 가 표시됩니다.'
219
+ : ' harness: 5 principles installed in ~/.claude/CLAUDE.md. The statusline shows 🅷 when the score drops below 5/5 or a warning is due.');
217
220
  console.log(lang === 'ko'
218
221
  ? ' 되돌리려면: sprag harness uninit --global'
219
222
  : ' to undo: sprag harness uninit --global');
@@ -436,15 +439,15 @@ export async function run({ hasFlag }) {
436
439
  : ' codex: Codex is installed here. Set up its hooks and harness too?', { defaultValue: true });
437
440
  }
438
441
  if (proceed) {
439
- const { configureCodexHooks } = await import('../codex-installer.js');
440
- const { initCodexHarness } = await import('../codex-harness.js');
442
+ const { configureCodexHooks } = await import('../codex/installer.js');
443
+ const { initCodexHarness } = await import('../codex/harness.js');
441
444
  const { loadConfig } = await import('../config.js');
442
- const { setupCodexPanel, describePanelSetup } = await import('../codex-panel-setup.js');
445
+ const { setupCodexPanel, describePanelSetup } = await import('../codex/panel-setup.js');
443
446
  const config = loadConfig() || {};
444
447
  // A manual install asks before touching the system package manager.
445
448
  let env = process.env;
446
449
  if (interactive && process.platform === 'darwin') {
447
- const { tmuxAvailable } = await import('../codex-panel-setup.js');
450
+ const { tmuxAvailable } = await import('../codex/panel-setup.js');
448
451
  if (!tmuxAvailable() && !await confirm(lang === 'ko'
449
452
  ? ' codex: tmux 가 없습니다. Homebrew 로 설치해 같은 창에 패널을 붙일까요?'
450
453
  : ' codex: tmux is missing. Install it with Homebrew so the panel shares the Codex window?', { defaultValue: true })) {
@@ -466,7 +469,7 @@ export async function run({ hasFlag }) {
466
469
  const h = initCodexHarness({ scope: 'global' });
467
470
  console.log(` codex: harness ${h.file}`);
468
471
  try {
469
- const { syncCodexKoreanBlock } = await import('../codex-harness.js');
472
+ const { syncCodexKoreanBlock } = await import('../codex/harness.js');
470
473
  const k = syncCodexKoreanBlock({ cfg: config });
471
474
  console.log(` codex: korean-style ${k.action} (${k.file})`);
472
475
  } catch (e) { debug('install:codex-korean', e); }
@@ -224,7 +224,7 @@ export async function run({ args, hasFlag }) {
224
224
  const enabled = sub === 'on';
225
225
  ks.setKoreanStyleEnabled(enabled);
226
226
  // Codex reads the guide from its global AGENTS.md; keep that block in step.
227
- try { (await import('../codex-harness.js')).syncCodexKoreanBlock({ cfg: loadConfig() }); } catch (e) { debug('korean:codex-sync', e); }
227
+ try { (await import('../codex/harness.js')).syncCodexKoreanBlock({ cfg: loadConfig() }); } catch (e) { debug('korean:codex-sync', e); }
228
228
  const {
229
229
  installKoreanLintHook, removeKoreanLintHook,
230
230
  installReplyLanguageHook, removeReplyLanguageHook,
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Subcommand: litellm-budget · LiteLLM 게이트웨이 키의 max_budget/spend 조회.
3
+ * sprag litellm-budget # 캐시된 예산을 게이지·금액으로 출력
4
+ * sprag litellm-budget --json # 캐시 원본 JSON 출력
5
+ * sprag litellm-budget --refresh # 지금 프록시에 물어봄 (detached 자식이 사용)
6
+ */
7
+
8
+ import { debug } from '../debug.js';
9
+
10
+ export async function run({ hasFlag }) {
11
+ const { gatewayBase, readBudgetState, refreshBudgetState, formatBudgetReport, resolveKey } =
12
+ await import('../litellm-budget.js');
13
+ const quiet = hasFlag('--quiet');
14
+ if (hasFlag('--refresh')) {
15
+ try {
16
+ // apiKeyHelper 가 토큰 수명(TTL)을 소유합니다. 헬퍼를 한 번만 불러 그
17
+ // 키를 예산 갱신과 게이트웨이 모델맵 갱신 양쪽에 재사용합니다. 갱신
18
+ // 경로마다 resolveKey() 를 다시 부르면 사본을 만드는 셈이 되고, 헬퍼
19
+ // 호출 횟수도 그만큼 늘어납니다.
20
+ const key = resolveKey();
21
+ let next = null;
22
+ if (key) {
23
+ next = await refreshBudgetState(process.env, fetch, key);
24
+ const { maybeRefreshGatewayModelMap } = await import('../litellm-models.js');
25
+ await maybeRefreshGatewayModelMap({ base: gatewayBase(), key });
26
+ }
27
+ if (!quiet) console.log(JSON.stringify(next, null, 2));
28
+ } catch (e) {
29
+ debug('litellm-budget:refresh', e);
30
+ if (!quiet) console.error(`litellm-budget refresh failed: ${e.message}`);
31
+ process.exitCode = 1;
32
+ }
33
+ return;
34
+ }
35
+ // 읽기 경로는 캐시만 보므로 키를 요구하지 않습니다 (apiKeyHelper 환경 배려).
36
+ const base = gatewayBase();
37
+ if (!base) {
38
+ console.log('게이트웨이가 감지되지 않았습니다 (ANTHROPIC_BASE_URL 필요).');
39
+ return;
40
+ }
41
+ const state = readBudgetState();
42
+ if (hasFlag('--json')) {
43
+ console.log(JSON.stringify(state, null, 2));
44
+ return;
45
+ }
46
+ if (state.base !== base || state.maxBudget === undefined) {
47
+ console.log('예산 캐시가 비어 있습니다. `sprag litellm-budget --refresh` 로 먼저 조회하십시오.');
48
+ return;
49
+ }
50
+ // Same resolver the statusline uses: this report draws a gauge too, and a
51
+ // glyph the terminal's font lacks garbles here exactly as it does there.
52
+ const { resolveLabelMode } = await import('../statusline-mode.js');
53
+ const { statuslineDefaults } = await import('../config.js');
54
+ const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg: statuslineDefaults() });
55
+ for (const line of formatBudgetReport(state, new Date(), labelMode)) console.log(line);
56
+ }
@@ -26,18 +26,19 @@ const SCOPE_SETS = {
26
26
  async function currentTotals(scope, dir) {
27
27
  if (scope === 'routing') {
28
28
  const { delegationSavedTotals } = await import('../savings-ledger.js');
29
- return { usd: delegationSavedTotals(Date.now(), dir).total };
29
+ const t = delegationSavedTotals(Date.now(), dir);
30
+ return { usd: t.total, spent: t.spent, spentMissing: t.spentMissing };
30
31
  }
31
32
  if (scope === 'doc2md') {
32
33
  const { doc2mdSavedTotals } = await import('../doc2md-ledger.cjs');
33
34
  return { usd: doc2mdSavedTotals(dir).total };
34
35
  }
35
36
  if (scope === 'codex-routing') {
36
- const { codexRoutingSavedTotals } = await import('../codex-ledger.js');
37
+ const { codexRoutingSavedTotals } = await import('../codex/ledger.js');
37
38
  const totals = codexRoutingSavedTotals({ dir });
38
39
  return { usd: totals.priced ? totals.total : null, unpriced: totals.runs - totals.priced };
39
40
  }
40
- const { codexDocumentTotals } = await import('../codex-doc2md-ledger.js');
41
+ const { codexDocumentTotals } = await import('../codex/doc2md-ledger.js');
41
42
  const totals = codexDocumentTotals({ dir });
42
43
  return { docs: totals.docs, usd: totals.savings.priced ? totals.savings.total : null, unpriced: totals.savings.unpriced };
43
44
  }
@@ -67,6 +68,54 @@ async function refreshRuleSavings(ko, rescan) {
67
68
  }
68
69
  }
69
70
 
71
+ const FAMILY_NAMES = { opus: 'Opus', sonnet: 'Sonnet', haiku: 'Haiku', fable: 'Fable', mythos: 'Mythos' };
72
+
73
+ /**
74
+ * The model-share table for the routing counter's period. Best-effort like the
75
+ * statusline: a failure to read transcripts must not break the totals above it.
76
+ */
77
+ async function printModelShare({ ko, dir, money }) {
78
+ const { loadModelShare, roundShares } = await import('../model-share.js');
79
+ let share;
80
+ try {
81
+ share = await loadModelShare({ dir, env: process.env });
82
+ } catch (e) {
83
+ debug('saved:model-share', e);
84
+ return;
85
+ }
86
+ let title = ko ? '모델별 사용 비중 (같은 기간, 비용 기준)' : 'Model share (same period, by cost)';
87
+ if (share.sinceKind === 'month') title += ko ? ' · 초기화 기록이 없어 이번 달 기준' : ' · no reset on record, this month';
88
+ else if (share.sinceKind === 'capped') title += ko ? ' · 최근 90일' : ' · last 90 days';
89
+ console.log('');
90
+ console.log(title);
91
+ if (!(share.totalUsd > 0)) {
92
+ console.log(ko ? '이 기간에 집계할 사용 기록이 없습니다.' : 'No usage to count in this period.');
93
+ } else {
94
+ const names = share.families.map((f) => FAMILY_NAMES[f.family] || f.family);
95
+ const nameW = Math.max(...names.map((n) => n.length));
96
+ const pcts = share.families.map((f) => f.pct.toFixed(1));
97
+ const pctW = Math.max(...pcts.map((p) => p.length));
98
+ share.families.forEach((f, i) => {
99
+ const filled = roundShares([f.usd, share.totalUsd - f.usd], 20)[0];
100
+ const bar = '█'.repeat(filled) + '░'.repeat(20 - filled);
101
+ console.log(` ${names[i].padEnd(nameW)} ${bar} ${pcts[i].padStart(pctW)}% ${money(f.usd)}`);
102
+ });
103
+ if (share.subagent.usd > 0) {
104
+ const subs = share.subagent.families;
105
+ const subPct = Math.round(share.subagent.pct);
106
+ // A share that rounds to 0 still spent money; "0%" would read as unused.
107
+ const pctText = (p) => (p > 0 && Math.round(p) === 0 ? '<1' : String(Math.round(p)));
108
+ const detail = subs
109
+ .map((f) => `${f.family} ${pctText(f.pct)}%`)
110
+ .join(', ');
111
+ console.log(` ${ko ? '서브에이전트 몫' : 'Subagents'}: ${subPct}% (${detail})`);
112
+ }
113
+ }
114
+ if (share.unpriced > 0) {
115
+ console.log(ko ? `단가가 없어 제외한 부분 ${share.unpriced}건` : `${share.unpriced} part(s) left out: no price`);
116
+ }
117
+ }
118
+
70
119
  export async function run({ args, hasFlag, agent = 'claude', dir: dirArg, rescan }) {
71
120
  const { userLanguage } = await import('../config.js');
72
121
  const { userDataDir } = await import('../paths.js');
@@ -79,8 +128,13 @@ export async function run({ args, hasFlag, agent = 'claude', dir: dirArg, rescan
79
128
  const money = (v) => (v < 0 ? `-$${(-v).toFixed(2)}` : `$${v.toFixed(2)}`);
80
129
  const fmtTotal = (t) => {
81
130
  const value = Number.isFinite(t.usd) ? money(t.usd) : 'n/a';
82
- return t.docs !== undefined ? `${ko ? `${t.docs}건` : `${t.docs} doc(s)`}, ${value} (${ko ? 'XML 대비 추정' : 'XML estimate'}; ${t.unpriced} unpriced)`
83
- : `${value}${t.unpriced ? ` +${t.unpriced} unpriced` : ''}`;
131
+ if (t.docs !== undefined) return `${ko ? `${t.docs}건` : `${t.docs} doc(s)`}, ${value} (${ko ? 'XML 대비 추정' : 'XML estimate'}; ${t.unpriced} unpriced)`;
132
+ // What the delegated runs cost on the models they ran on. The statusline
133
+ // no longer draws it beside the saving, so this is where it is read.
134
+ const spent = Number(t.spent) > 0
135
+ ? ` · ${ko ? '위임 실행 비용' : 'spent on delegated runs'} ${money(t.spent)}${t.spentMissing > 0 ? ` +${t.spentMissing} unpriced` : ''}`
136
+ : '';
137
+ return `${value}${t.unpriced ? ` +${t.unpriced} unpriced` : ''}${spent}`;
84
138
  };
85
139
  const fmtTime = (ms) => new Date(ms).toLocaleString(ko ? 'ko-KR' : 'en-US');
86
140
  const label = (scope) => ({
@@ -101,6 +155,7 @@ export async function run({ args, hasFlag, agent = 'claude', dir: dirArg, rescan
101
155
  : (ko ? `${fmtTime(marks[s])} 이후` : `since ${fmtTime(marks[s])}`);
102
156
  console.log(`${label(s)}: ${fmtTotal(t)} (${since})`);
103
157
  }
158
+ if (agent !== 'codex') await printModelShare({ ko, dir, money });
104
159
  return;
105
160
  }
106
161
 
@@ -43,8 +43,8 @@ export async function run({ hasFlag, args = [] }) {
43
43
  // undo that as well; otherwise Codex keeps calling a command that may no
44
44
  // longer exist. Only our own entries go, exactly as `uninstall --agent codex`.
45
45
  try {
46
- const { codexHookStatus, configureCodexHooks } = await import('../codex-installer.js');
47
- const { codexHarnessStatus, uninitCodexHarness } = await import('../codex-harness.js');
46
+ const { codexHookStatus, configureCodexHooks } = await import('../codex/installer.js');
47
+ const { codexHarnessStatus, uninitCodexHarness } = await import('../codex/harness.js');
48
48
  const hooked = codexHookStatus().some((h) => h.registered);
49
49
  let harness = false;
50
50
  try { harness = codexHarnessStatus({ scope: 'global' }).configured > 0; } catch { /* no AGENTS.md */ }
@@ -56,7 +56,7 @@ export async function run({ hasFlag, args = [] }) {
56
56
  : ` Codex: hooks ${hook.action} (${hook.file}), harness ${h.removed ? 'removed' : 'absent'}`);
57
57
  if (process.platform === 'darwin') {
58
58
  try {
59
- const { installPanelShell } = await import('../codex-panel-shell.js');
59
+ const { installPanelShell } = await import('../codex/panel-shell.js');
60
60
  installPanelShell({ remove: true });
61
61
  } catch (e) { /* a malformed or foreign block stays for the user to repair */ }
62
62
  }
package/src/config.js CHANGED
@@ -56,6 +56,9 @@ const KEYWORDS = {
56
56
  'no-timer': { key: 'timer', value: false },
57
57
  color: { key: 'color', value: true },
58
58
  'no-color': { key: 'color', value: false },
59
+ // Model-share bar and letters beside the routing chip; on unless switched off.
60
+ share: { key: 'modelShare', value: true },
61
+ 'no-share': { key: 'modelShare', value: false },
59
62
  };
60
63
 
61
64
  // Output language for `last`/`history`/advice. Stored at top level
@@ -195,6 +198,7 @@ export function statuslineDefaults() {
195
198
  verbose: s.verbose !== false,
196
199
  timer: s.timer !== false,
197
200
  color: s.color !== false,
201
+ modelShare: s.modelShare !== false,
198
202
  windowHours,
199
203
  windowLabel: formatWindow(windowHours),
200
204
  // 'auto' | '5m' | '1h'. Auto lets the measured split decide and falls back