sprag-cli 3.66.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 (59) hide show
  1. package/README.ko.md +29 -62
  2. package/README.md +26 -61
  3. package/bin/cli.js +130 -62
  4. package/docs/CODEX.ko.md +32 -0
  5. package/docs/CODEX.md +34 -0
  6. package/package.json +11 -4
  7. package/presets/ratchet-rules.json +7 -0
  8. package/src/agents.js +33 -11
  9. package/src/{codex-brief.js → codex/brief.js} +7 -7
  10. package/src/{codex-budget.js → codex/budget.js} +2 -2
  11. package/src/{codex-cache-policy.js → codex/cache-policy.js} +9 -9
  12. package/src/{codex-delegation-diagnostics.js → codex/delegation-diagnostics.js} +2 -2
  13. package/src/{codex-delegation.js → codex/delegation.js} +11 -11
  14. package/src/{codex-doc2md-ledger.js → codex/doc2md-ledger.js} +5 -5
  15. package/src/{codex-doc2md.js → codex/doc2md.js} +4 -4
  16. package/src/{codex-doctor.js → codex/doctor.js} +4 -4
  17. package/src/{codex-harness.js → codex/harness.js} +5 -5
  18. package/src/{codex-hooks.js → codex/hooks.js} +20 -20
  19. package/src/{codex-installer.js → codex/installer.js} +2 -2
  20. package/src/{codex-ledger.js → codex/ledger.js} +6 -6
  21. package/src/{codex-panel-launcher.js → codex/panel-launcher.js} +2 -2
  22. package/src/{codex-panel-runner.js → codex/panel-runner.js} +2 -2
  23. package/src/{codex-panel-session.js → codex/panel-session.js} +1 -1
  24. package/src/{codex-panel-setup.js → codex/panel-setup.js} +1 -1
  25. package/src/{codex-panel-state.js → codex/panel-state.js} +3 -3
  26. package/src/{codex-panel.js → codex/panel.js} +43 -34
  27. package/src/{codex-parser.js → codex/parser.js} +5 -9
  28. package/src/{codex-reply-language.js → codex/reply-language.js} +2 -2
  29. package/src/{codex-route-scan.js → codex/route-scan.js} +10 -10
  30. package/src/{codex-rule-health.js → codex/rule-health.js} +4 -4
  31. package/src/commands/codex-delegate.js +6 -6
  32. package/src/commands/codex.js +44 -44
  33. package/src/commands/cowork.js +107 -0
  34. package/src/commands/delegate.js +43 -0
  35. package/src/commands/install.js +21 -7
  36. package/src/commands/korean.js +1 -1
  37. package/src/commands/litellm-budget.js +56 -0
  38. package/src/commands/route-scan.js +6 -0
  39. package/src/commands/saved.js +60 -5
  40. package/src/commands/uninstall.js +7 -3
  41. package/src/config.js +4 -0
  42. package/src/cowork-sync.js +361 -0
  43. package/src/delegation-agents.js +133 -0
  44. package/src/formatters/statusline.js +713 -504
  45. package/src/glyphs.js +2 -0
  46. package/src/harness.js +1 -1
  47. package/src/implementation-delegation.js +5 -2
  48. package/src/installer.js +77 -1
  49. package/src/model-alias.js +45 -60
  50. package/src/model-rules.js +12 -5
  51. package/src/model-share.js +136 -0
  52. package/src/parser.js +4 -13
  53. package/src/route-inject.js +8 -3
  54. package/src/session-records.js +2 -8
  55. package/src/statusline-base.js +9 -3
  56. package/src/stdin-payload.js +1 -1
  57. package/src/transcript-lines.js +40 -0
  58. /package/src/{codex-cache.js → codex/cache.js} +0 -0
  59. /package/src/{codex-panel-shell.js → codex/panel-shell.js} +0 -0
@@ -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'));
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Subcommand: cowork : keep the Claude app's Cowork tab on the same ratchet rules.
3
+ *
4
+ * sprag cowork status
5
+ * sprag cowork sync discover connected folders and write the copies now
6
+ * sprag cowork on | off turn the sync on (and sync) or off (and remove the copies)
7
+ * sprag cowork exclude <dir> never write into <dir> (removes its copy)
8
+ * sprag cowork include <dir> undo exclude (and sync)
9
+ */
10
+
11
+ import { resolve } from 'node:path';
12
+ import { realpathSync } from 'node:fs';
13
+
14
+ const REASONS = {
15
+ ko: {
16
+ 'not-absolute': '절대 경로가 아님', missing: '폴더가 없음', 'not-a-directory': '디렉터리가 아님',
17
+ 'home-or-ancestor': '홈 디렉터리 또는 그 상위 경로', 'inside-claude-dir': '~/.claude 안',
18
+ 'inside-library': '~/Library 안', 'inside-sprag-data': 'sprag 데이터 폴더 안', excluded: '제외 목록에 있음',
19
+ 'not-owned': '같은 이름의 사용자 파일이 있어 덮어쓰지 않음', 'no-source': '복사할 원본 래칫 파일이 없음',
20
+ },
21
+ en: {
22
+ 'not-absolute': 'not an absolute path', missing: 'folder does not exist', 'not-a-directory': 'not a directory',
23
+ 'home-or-ancestor': 'the home directory or one of its parents', 'inside-claude-dir': 'inside ~/.claude',
24
+ 'inside-library': 'inside ~/Library', 'inside-sprag-data': 'inside the sprag data folder', excluded: 'on the exclude list',
25
+ 'not-owned': 'a user file of the same name exists, not overwritten', 'no-source': 'no source ratchet file to copy',
26
+ },
27
+ };
28
+
29
+ const norm = (p) => {
30
+ const abs = resolve(p);
31
+ try { return realpathSync(abs); } catch { return abs; }
32
+ };
33
+
34
+ export async function run({ args }) {
35
+ const cw = await import('../cowork-sync.js');
36
+ const { userLanguage } = await import('../config.js');
37
+ const ko = userLanguage() === 'ko';
38
+ const why = (r) => (REASONS[ko ? 'ko' : 'en'][r] || r);
39
+ const sub = args[1];
40
+
41
+ const summarize = (r) => {
42
+ console.log(ko
43
+ ? `Cowork 래칫 동기화: 새로 씀 ${r.written.length}, 변경 없음 ${r.unchanged.length}, 건너뜀 ${r.skipped.length}, 제거 ${r.removed.length}`
44
+ : `Cowork ratchet sync: written ${r.written.length}, unchanged ${r.unchanged.length}, skipped ${r.skipped.length}, removed ${r.removed.length}`);
45
+ for (const d of r.written) console.log(` + ${d}`);
46
+ for (const d of r.removed) console.log(` - ${d}`);
47
+ for (const s of r.skipped) console.log(` ! ${s.dir} (${why(s.reason)})`);
48
+ };
49
+
50
+ if (sub === 'sync') return summarize(cw.syncCoworkRatchets({ discover: true }));
51
+
52
+ if (sub === 'off') {
53
+ cw.saveCoworkConfig({ sync: false });
54
+ console.log(ko ? 'Cowork 래칫 동기화를 껐습니다. 만들어 둔 사본을 지웁니다.' : 'Cowork ratchet sync is off. Removing the copies it made.');
55
+ return summarize(cw.removeCoworkRatchets());
56
+ }
57
+
58
+ if (sub === 'on') {
59
+ cw.saveCoworkConfig({ sync: true });
60
+ console.log(ko ? 'Cowork 래칫 동기화를 켰습니다.' : 'Cowork ratchet sync is on.');
61
+ return summarize(cw.syncCoworkRatchets({ discover: true }));
62
+ }
63
+
64
+ if (sub === 'exclude' || sub === 'include') {
65
+ const target = args.slice(2).find((a) => !a.startsWith('-'));
66
+ if (!target) {
67
+ console.error(ko ? '폴더 경로를 지정하십시오.' : 'Pass a folder path.');
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ const dir = norm(target);
72
+ const list = cw.coworkConfig().exclude;
73
+ if (sub === 'exclude') {
74
+ if (!list.includes(dir)) cw.saveCoworkConfig({ exclude: [...list, dir] });
75
+ const removed = cw.removeCoworkRatchetIn(dir);
76
+ console.log(ko
77
+ ? `제외 목록에 추가했습니다: ${dir}${removed ? ' (사본을 지웠습니다)' : ''}`
78
+ : `Added to the exclude list: ${dir}${removed ? ' (copy removed)' : ''}`);
79
+ return;
80
+ }
81
+ cw.saveCoworkConfig({ exclude: list.filter((x) => x !== dir && norm(x) !== dir) });
82
+ console.log(ko ? `제외 목록에서 뺐습니다: ${dir}` : `Removed from the exclude list: ${dir}`);
83
+ return summarize(cw.syncCoworkRatchets({ discover: true }));
84
+ }
85
+
86
+ if (sub !== undefined) {
87
+ console.error(ko ? `알 수 없는 하위 명령입니다: ${sub}` : `Unknown subcommand: ${sub}`);
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+
92
+ const st = cw.coworkStatus();
93
+ console.log(ko ? `Cowork 래칫 동기화: ${st.enabled ? '켜짐' : '꺼짐'}` : `Cowork ratchet sync: ${st.enabled ? 'on' : 'off'}`);
94
+ console.log(ko ? `마지막 동기화: ${st.lastSync || '없음'}` : `Last sync: ${st.lastSync || 'never'}`);
95
+ if (st.folders.length === 0) {
96
+ console.log(ko ? '발견한 Cowork 연결 폴더가 없습니다.' : 'No Cowork folders discovered.');
97
+ return;
98
+ }
99
+ console.log(ko ? '연결 폴더:' : 'Connected folders:');
100
+ for (const f of st.folders) {
101
+ const state = f.hasCopy ? (ko ? '사본 있음' : 'copy present')
102
+ : f.foreign ? why('not-owned')
103
+ : f.eligible ? (ko ? '사본 없음' : 'no copy yet')
104
+ : (ko ? `제외: ${why(f.reason)}` : `skipped: ${why(f.reason)}`);
105
+ console.log(` ${f.dir} [${state}]`);
106
+ }
107
+ }
@@ -59,6 +59,13 @@ export async function run({ args, hasFlag }) {
59
59
  const { userLanguage } = await import('../config.js');
60
60
  const lang = userLanguage();
61
61
 
62
+ // `delegate agents` manages the tool-restricted subagent files. It is split
63
+ // off by the second word so it never mixes with the on/off PreToolUse guard.
64
+ if (sub === 'agents') {
65
+ await agentsCommand(args[2], lang);
66
+ return;
67
+ }
68
+
62
69
  if (sub === 'on') {
63
70
  // Install first, and only record the feature as on once that worked.
64
71
  // Saving the flag first left `delegate: on, hook not registered` behind
@@ -131,3 +138,39 @@ export async function run({ args, hasFlag }) {
131
138
  console.log('Enable with: sprag delegate on');
132
139
  }
133
140
  }
141
+
142
+ async function agentsCommand(action, lang) {
143
+ const ko = lang === 'ko';
144
+ const da = await import('../delegation-agents.js');
145
+ const report = (r) => {
146
+ for (const n of r.written) console.log(ko ? `✓ ${n} 에이전트를 설치했습니다.` : `✓ installed agent ${n}`);
147
+ for (const n of r.unchanged) console.log(ko ? ` ${n} 에이전트는 이미 최신입니다.` : ` agent ${n} is already current`);
148
+ for (const k of r.skipped) {
149
+ console.log(ko
150
+ ? `✗ ${k.name} 에이전트는 건너뛰었습니다 (${k.reason === 'not-owned' ? '직접 만든 같은 이름의 파일이 있습니다' : k.reason}).`
151
+ : `✗ skipped agent ${k.name} (${k.reason})`);
152
+ }
153
+ for (const n of r.removed) console.log(ko ? `✓ ${n} 에이전트를 제거했습니다.` : `✓ removed agent ${n}`);
154
+ };
155
+ const rerender = async () => {
156
+ try { (await import('../model-rules.js')).syncAllFiles(); } catch { /* the next session start re-renders */ }
157
+ };
158
+
159
+ if (action === 'on' || action === 'off') {
160
+ const { loadConfig, saveConfig } = await import('../config.js');
161
+ saveConfig({ ...loadConfig(), delegationAgents: action === 'on' });
162
+ const r = action === 'on' ? da.syncDelegationAgents() : da.removeDelegationAgents();
163
+ await rerender();
164
+ report(r);
165
+ console.log(action === 'on'
166
+ ? (ko ? '위임 전용 에이전트를 켰습니다. 래칫 문구가 에이전트 이름으로 다시 만들어졌습니다.' : 'Delegation agents are on. The ratchet wording was re-rendered with the agent names.')
167
+ : (ko ? '위임 전용 에이전트를 껐습니다. 래칫 문구가 model 지정 방식으로 다시 만들어졌습니다.' : 'Delegation agents are off. The ratchet wording was re-rendered with plain model tiers.'));
168
+ return;
169
+ }
170
+
171
+ const st = da.delegationAgentStatus();
172
+ console.log(ko ? `delegate agents: ${st.enabled ? 'on' : 'off'}` : `delegate agents: ${st.enabled ? 'on' : 'off'}`);
173
+ const label = { 'installed-by-sprag': ko ? 'sprag 가 설치함' : 'installed by sprag', 'user-file': ko ? '직접 만든 파일' : 'user file', absent: ko ? '없음' : 'absent' };
174
+ for (const a of st.agents) console.log(` ${a.name}: ${a.state} (${label[a.state]})`);
175
+ console.log(ko ? '켜기 또는 끄기: sprag delegate agents on|off' : 'Toggle with: sprag delegate agents on|off');
176
+ }
@@ -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');
@@ -368,6 +371,17 @@ export async function run({ hasFlag }) {
368
371
  // they have not read. All the install does is say what is waiting.
369
372
  // Presets now live in ratchet-preset.md. Moving the ones older versions
370
373
  // appended to ratchet.md comes first, so they are not offered again below.
374
+ try {
375
+ const d = (await import('../delegation-agents.js')).syncDelegationAgents();
376
+ if (d.written.length) {
377
+ console.log(lang === 'ko'
378
+ ? ` agents: 위임 전용 에이전트 ${d.written.length}개를 ~/.claude/agents 에 설치했습니다 (MCP 도구 없음).`
379
+ : ` agents: installed ${d.written.length} delegation-only agent(s) in ~/.claude/agents (no MCP tools).`);
380
+ }
381
+ } catch (e) {
382
+ debug('install:delegation-agents', e);
383
+ }
384
+
371
385
  try {
372
386
  const { preparePresetRatchet } = await import('../preset-ratchet.js');
373
387
  const p = preparePresetRatchet();
@@ -425,15 +439,15 @@ export async function run({ hasFlag }) {
425
439
  : ' codex: Codex is installed here. Set up its hooks and harness too?', { defaultValue: true });
426
440
  }
427
441
  if (proceed) {
428
- const { configureCodexHooks } = await import('../codex-installer.js');
429
- const { initCodexHarness } = await import('../codex-harness.js');
442
+ const { configureCodexHooks } = await import('../codex/installer.js');
443
+ const { initCodexHarness } = await import('../codex/harness.js');
430
444
  const { loadConfig } = await import('../config.js');
431
- const { setupCodexPanel, describePanelSetup } = await import('../codex-panel-setup.js');
445
+ const { setupCodexPanel, describePanelSetup } = await import('../codex/panel-setup.js');
432
446
  const config = loadConfig() || {};
433
447
  // A manual install asks before touching the system package manager.
434
448
  let env = process.env;
435
449
  if (interactive && process.platform === 'darwin') {
436
- const { tmuxAvailable } = await import('../codex-panel-setup.js');
450
+ const { tmuxAvailable } = await import('../codex/panel-setup.js');
437
451
  if (!tmuxAvailable() && !await confirm(lang === 'ko'
438
452
  ? ' codex: tmux 가 없습니다. Homebrew 로 설치해 같은 창에 패널을 붙일까요?'
439
453
  : ' codex: tmux is missing. Install it with Homebrew so the panel shares the Codex window?', { defaultValue: true })) {
@@ -455,7 +469,7 @@ export async function run({ hasFlag }) {
455
469
  const h = initCodexHarness({ scope: 'global' });
456
470
  console.log(` codex: harness ${h.file}`);
457
471
  try {
458
- const { syncCodexKoreanBlock } = await import('../codex-harness.js');
472
+ const { syncCodexKoreanBlock } = await import('../codex/harness.js');
459
473
  const k = syncCodexKoreanBlock({ cfg: config });
460
474
  console.log(` codex: korean-style ${k.action} (${k.file})`);
461
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
+ }
@@ -296,11 +296,15 @@ export async function run({ args, hasFlag, numArg }) {
296
296
  // would keep reading the old wording indefinitely. The call writes only
297
297
  // when the rendering differs, so the common case is a read and a string
298
298
  // compare.
299
+ // Agent files come first so the ratchet-model.md rendered next names them.
300
+ try { (await import('../delegation-agents.js')).syncDelegationAgents(); } catch (e) { debug('route-scan:delegation-agents', e); }
299
301
  try { mrHook.syncAllFiles(); } catch (e) { debug('route-scan:sync-ratchet', e); }
300
302
  // Same reason for the preset ratchet: an upgrade that rewords or drops a
301
303
  // bundled rule reaches ratchet-preset.md here, without a reinstall. It
302
304
  // also moves presets older versions appended to ratchet.md.
303
305
  try { (await import('../preset-ratchet.js')).preparePresetRatchet(); } catch (e) { debug('route-scan:sync-preset', e); }
306
+ // Cowork cannot follow the ~/.claude imports, so it gets a pasted copy.
307
+ try { (await import('../cowork-sync.js')).syncCoworkRatchets({ discover: true }); } catch (e) { debug('route-scan:cowork-sync', e); }
304
308
  // Registered rules whose delegated-category error rate crossed the
305
309
  // health threshold since promotion — the user approved these, so a
306
310
  // status change must be briefed, not just written into the md file.
@@ -466,6 +470,8 @@ export async function run({ args, hasFlag, numArg }) {
466
470
  let cache = rs.readRouteScan();
467
471
  if (hasFlag('--refresh') || (cache && cache.days !== days) || await rs.shouldRescan(cache, { days })) {
468
472
  cache = await rs.runRouteScan({ days });
473
+ // model-rules were rewritten by the scan; carry them to the Cowork copies.
474
+ try { (await import('../cowork-sync.js')).syncCoworkRatchets({ discover: true }); } catch (e) { debug('route-scan:cowork-sync', e); }
469
475
  }
470
476
  if (hasFlag('--quiet')) return;
471
477
  if (hasFlag('--json')) {