sprag-cli 3.53.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/README.ko.md +54 -3
- package/README.md +63 -4
- package/bin/cli.js +30 -7
- package/docs/CODEX.ko.md +184 -0
- package/docs/CODEX.md +497 -0
- package/package.json +15 -4
- package/presets/ratchet-rules.json +4 -1
- package/src/agent.js +33 -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/codex-delegate.js +59 -0
- package/src/commands/codex.js +579 -0
- package/src/commands/harness.js +13 -2
- package/src/commands/install.js +106 -10
- package/src/commands/route-scan.js +4 -0
- package/src/commands/seed.js +16 -12
- package/src/commands/uninstall.js +23 -0
- package/src/commands/upgrade.js +13 -0
- package/src/doc2md.cjs +72 -28
- package/src/handoff.js +27 -7
- package/src/harness-templates.js +33 -1
- package/src/harness.js +51 -17
- package/src/preset-ratchet.js +133 -0
- package/src/seed-rules.js +88 -39
- package/src/update-check.js +37 -2
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { createArgs } from '../cli-args.js';
|
|
2
|
+
import { configureCodexHooks } from '../codex-installer.js';
|
|
3
|
+
import { initCodexHarness, uninitCodexHarness, codexHarnessStatus } from '../codex-harness.js';
|
|
4
|
+
import { findProjectRoot, harnessPromote, harnessListRules, harnessRmRule, harnessPrune } from '../harness.js';
|
|
5
|
+
import { loadConfig, saveConfig } from '../config.js';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const HELP = `Sprag / Codex
|
|
9
|
+
|
|
10
|
+
Usage: sprag <command> --agent codex [options]
|
|
11
|
+
--statusline Compact, one-shot terminal status (not the native footer)
|
|
12
|
+
panel Live companion panel; --once for piped output
|
|
13
|
+
panel run -- <codex args> Codex above the panel in tmux
|
|
14
|
+
Local, session-bound execution (--no-daemon)
|
|
15
|
+
--keep-native-statusline Keep Codex's duplicate status row (before --)
|
|
16
|
+
panel auto on|off|status Optional macOS Terminal auto-start
|
|
17
|
+
panel shell install|remove Optional macOS zsh integration
|
|
18
|
+
panel doctor Panel hook and rendering diagnostics
|
|
19
|
+
doctor [--format json] Config, hooks, harness, logs, and capabilities
|
|
20
|
+
capabilities [--format json] Support matrix, including native/unsupported areas
|
|
21
|
+
brief [on|off|status] Current warnings or prompt-briefing preference
|
|
22
|
+
last / history Codex warning history; --days, --session, --project
|
|
23
|
+
handoff [--cwd path] Write Git state and Codex usage to HANDOFF-*.md
|
|
24
|
+
delegate on|off|status Native model routing and SubagentStart guidance
|
|
25
|
+
delegate model <model> Default target for spawns without an explicit model
|
|
26
|
+
delegate rules Codex-only category rules; add/rm with explicit scope
|
|
27
|
+
route-scan [--refresh] Discover recurring simple turns in local logs
|
|
28
|
+
route-scan dismiss R<N> Dismiss a candidate without registering a rule
|
|
29
|
+
route-scan savings Attributed routing estimates; --refresh or --format json
|
|
30
|
+
seed [accept|skip|reset] Bundled Codex ratchet presets; acceptance needs a scope
|
|
31
|
+
cache [auto|off|<duration>] Display timer policy (e.g. 30m); never changes retention
|
|
32
|
+
cache status --refresh Resolve the session's gateway deployment and cache policy
|
|
33
|
+
harness init|uninit|check|promote|list|rm|prune
|
|
34
|
+
Rules in Codex AGENTS.md and ratchet files
|
|
35
|
+
korean / cohesion / doc2md Writing guidance and document conversion
|
|
36
|
+
mode Shared label/color preferences
|
|
37
|
+
install [--no-panel] Register hooks and global harness; trust via /hooks
|
|
38
|
+
uninstall Remove global integration; keep rules and shared state
|
|
39
|
+
upgrade --print Print Codex-only upgrade commands
|
|
40
|
+
|
|
41
|
+
Report: sprag --agent codex [--days N | --hours N] [--format table|json|csv]
|
|
42
|
+
--project TEXT Report/history project substring filter
|
|
43
|
+
--session ID Report/history exact session filter
|
|
44
|
+
Panel/statusline:
|
|
45
|
+
--project PATH --session ID --days N --text --narrow --no-color
|
|
46
|
+
--columns N Statusline wrapping width (default terminal or 100)
|
|
47
|
+
--single-line Unwrapped statusline output for terminal integrations
|
|
48
|
+
--timer / --no-timer Override the shared countdown preference
|
|
49
|
+
|
|
50
|
+
Codex tui.status_line accepts native item identifiers, not shell commands.
|
|
51
|
+
Cache clocks use resolved provider policies, not measured expiry times.
|
|
52
|
+
Routing estimates need matching LiteLLM prices and attributed child usage.
|
|
53
|
+
Unknown prices and session billing remain unavailable; Claude totals are not imported.
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
function scopeOf(args, dflt) {
|
|
57
|
+
const { getArg, hasFlag } = createArgs(args);
|
|
58
|
+
const scope = getArg('--scope') || (hasFlag('--global') ? 'global' : hasFlag('--project') ? 'project' : dflt);
|
|
59
|
+
if (scope && !['global', 'project'].includes(scope)) throw new Error('--scope expects global or project');
|
|
60
|
+
if (hasFlag('--global') && hasFlag('--project')) throw new Error('Choose one scope: --global or --project');
|
|
61
|
+
return scope;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function run({ args, version }) {
|
|
65
|
+
const { getArg, hasFlag } = createArgs(args);
|
|
66
|
+
const numArg = (name, { dflt, min, max } = {}) => {
|
|
67
|
+
const raw = getArg(name);
|
|
68
|
+
if (raw === undefined && !hasFlag(name)) return dflt;
|
|
69
|
+
const value = Number(raw);
|
|
70
|
+
if (!raw || !Number.isFinite(value)) throw new Error(`${name} expects a number`);
|
|
71
|
+
if (min !== undefined && value < min) throw new Error(`${name} must be >= ${min}`);
|
|
72
|
+
if (max !== undefined && value > max) throw new Error(`${name} must be <= ${max}`);
|
|
73
|
+
return value;
|
|
74
|
+
};
|
|
75
|
+
const cmd = args[0];
|
|
76
|
+
if (hasFlag('--help') || hasFlag('-h') || cmd === 'help') { console.log(HELP); return; }
|
|
77
|
+
const separator = args.indexOf('--');
|
|
78
|
+
const ownArgs = separator < 0 ? args : args.slice(0, separator);
|
|
79
|
+
const own = createArgs(ownArgs);
|
|
80
|
+
for (const flag of ['--project', '--session', '--session-file', '--cwd', '--format', '-f', '--columns', '--interval', '--hours', '--days', '-d', '-p']) {
|
|
81
|
+
// Harness, rule, and seed --project selects scope and intentionally takes no value.
|
|
82
|
+
if ((['harness', 'seed'].includes(cmd) || (cmd === 'delegate' && args[1] === 'rules')) && flag === '--project') continue;
|
|
83
|
+
if (!ownArgs.some((arg) => arg === flag || arg.startsWith(`${flag}=`))) continue;
|
|
84
|
+
const value = own.getArg(flag);
|
|
85
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
86
|
+
if (['--columns', '--interval', '--hours', '--days', '-d'].includes(flag) && (!Number.isFinite(Number(value)) || Number(value) < 0)) {
|
|
87
|
+
throw new Error(`${flag} expects a non-negative number`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const root = findProjectRoot(process.cwd(), { agent: 'codex' });
|
|
91
|
+
const selectedRoot = !['harness', 'delegate', 'seed'].includes(cmd) && getArg('--project') ? resolve(getArg('--project')) : root;
|
|
92
|
+
const format = getArg('--format') || getArg('-f') || 'table';
|
|
93
|
+
if (cmd === 'cache') {
|
|
94
|
+
const { parseCodexCacheTtl } = await import('../codex-cache.js');
|
|
95
|
+
const mode = args[1] || 'status';
|
|
96
|
+
const cfg = loadConfig();
|
|
97
|
+
if (mode !== 'status') {
|
|
98
|
+
const ttl = parseCodexCacheTtl(mode);
|
|
99
|
+
cfg.codex = { ...cfg.codex, cacheTtl: ttl };
|
|
100
|
+
saveConfig(cfg);
|
|
101
|
+
}
|
|
102
|
+
const ttl = cfg.codex?.cacheTtl ?? 'auto';
|
|
103
|
+
console.log(`Codex cache timer: ${typeof ttl === 'number' ? `${ttl / 60}m (configured estimate)` : ttl}`);
|
|
104
|
+
const { createPanelReader } = await import('../codex-panel.js');
|
|
105
|
+
const { refreshCodexCachePolicy } = await import('../codex-cache-policy.js');
|
|
106
|
+
const { codexCacheTimer } = await import('../codex-cache.js');
|
|
107
|
+
const data = await createPanelReader({ root: selectedRoot, sessionId: getArg('--session'),
|
|
108
|
+
...(hasFlag('--refresh') ? { readCachePolicy: refreshCodexCachePolicy } : {}) })();
|
|
109
|
+
if (data.session) {
|
|
110
|
+
const { terminalText } = await import('../codex-panel.js');
|
|
111
|
+
console.log(`Session: ${terminalText(data.session.sessionId)} | ${terminalText(data.session.provider)} / ${terminalText(data.session.model)}`);
|
|
112
|
+
if (data.cachePolicy) {
|
|
113
|
+
const p = data.cachePolicy;
|
|
114
|
+
console.log(`Policy: ${p.backend} / ${terminalText(p.model)} / ${p.seconds / 60}m minimum retention after a cache write or reuse`);
|
|
115
|
+
console.log(`Source: ${p.reference}`);
|
|
116
|
+
} else console.log('Policy: unresolved. Run sprag cache status --refresh --agent codex to query the configured gateway.');
|
|
117
|
+
console.log(codexCacheTimer(data.session, { ttl, policy: data.cachePolicy })?.text || 'Cache timer off');
|
|
118
|
+
} else console.log('No matching Codex session log.');
|
|
119
|
+
console.log('The clock starts at recorded cache usage. The window ending does not prove eviction; a cache hit also requires a matching prefix. Provider retention is unchanged.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (cmd === 'capabilities' || cmd === 'doctor') {
|
|
123
|
+
if (!['table', 'json'].includes(format)) throw new Error('Diagnostics format must be table or json.');
|
|
124
|
+
const { codexDoctor, CODEX_CAPABILITIES } = await import('../codex-doctor.js');
|
|
125
|
+
const report = cmd === 'capabilities' ? { agent: 'codex', capabilities: CODEX_CAPABILITIES }
|
|
126
|
+
: await codexDoctor({ root: selectedRoot, sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }) });
|
|
127
|
+
if (format === 'json') { console.log(JSON.stringify(report, null, 2)); return; }
|
|
128
|
+
const { terminalText } = await import('../codex-panel.js');
|
|
129
|
+
if (cmd === 'doctor') {
|
|
130
|
+
console.log(`Codex home: ${terminalText(report.home)}`);
|
|
131
|
+
console.log(`Config: ${report.config.error || (report.config.present ? 'readable' : 'absent (defaults)')}`);
|
|
132
|
+
console.log(`Hooks: ${report.hooks.error || `${report.hooks.events.filter((hook) => hook.registered).length}/${report.hooks.events.length} registered; trust unknown`}`);
|
|
133
|
+
for (const hook of report.hooks.events) console.log(` ${hook.event}: ${hook.registered ? 'registered' : 'missing/outdated; run sprag install --agent codex'}`);
|
|
134
|
+
if (report.config.hooksDisabled) console.log('WARNING: user config disables hooks. Review features.hooks in Codex config.');
|
|
135
|
+
for (const [scope, harness] of Object.entries(report.harness)) console.log(`Harness ${scope}: ${harness.error ? terminalText(harness.error) : `${harness.configured}/${harness.total}`}`);
|
|
136
|
+
console.log(`Session: ${report.session ? `${terminalText(report.session.id)} (${terminalText(report.session.model)})` : 'no matching rollout log'}`);
|
|
137
|
+
console.log(`Panel: ${report.panel.live ? 'recent rendered frame observed' : 'no recent rendered frame'}`);
|
|
138
|
+
console.log(`Preferences: ${JSON.stringify(report.preferences)}`);
|
|
139
|
+
console.log('Review hook trust in Codex /hooks. User config does not include profile/project/admin/runtime overrides.');
|
|
140
|
+
}
|
|
141
|
+
for (const feature of report.capabilities) console.log(`${feature.feature}: ${feature.support} | ${[feature.command, feature.note].filter(Boolean).join(' | ')}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (cmd === 'delegate') return (await import('./codex-delegate.js')).run({ args, getArg, hasFlag, root });
|
|
145
|
+
if (cmd === 'seed') return (await import('./seed.js')).run({ args, hasFlag, agent: 'codex', root });
|
|
146
|
+
if (cmd === 'route-scan') {
|
|
147
|
+
const rs = await import('../codex-route-scan.js');
|
|
148
|
+
const days = numArg('--days', { dflt: 14, min: 1 });
|
|
149
|
+
if (args[1] === 'savings') {
|
|
150
|
+
const { refreshCodexLedger, loadCodexLedger, codexRoutingSavedTotals } = await import('../codex-ledger.js');
|
|
151
|
+
if (hasFlag('--refresh')) await refreshCodexLedger();
|
|
152
|
+
const events = Object.entries(loadCodexLedger().events).map(([id, e]) => ({ id, ...e })).sort((a, b) => b.ts - a.ts);
|
|
153
|
+
const totals = codexRoutingSavedTotals();
|
|
154
|
+
if (format === 'json') { console.log(JSON.stringify({ agent: 'codex', totals, events }, null, 2)); return; }
|
|
155
|
+
const money = (usd) => Number.isFinite(usd) ? `${usd < 0 ? '-' : ''}$${Math.abs(usd).toFixed(4)}` : 'n/a (unpriced)';
|
|
156
|
+
console.log(`Codex routing saved: ${totals.priced ? `${money(totals.week)} (7d) / ${money(totals.total)} total` : 'n/a'} | ${totals.runs} attributed run(s), ${totals.runs - totals.priced} unpriced`);
|
|
157
|
+
for (const e of events.slice(0, 20)) {
|
|
158
|
+
console.log(`${new Date(e.ts).toISOString().slice(0, 16)} ${e.source}${e.category ? `/${e.category}` : ''} ${e.from} -> ${e.to} ${money(e.usd)}${e.complete ? '' : ' (incomplete)'}`);
|
|
159
|
+
}
|
|
160
|
+
console.log('Counterfactual: the child run\'s own tokens priced at the parent model. Only Sprag-routed spawns with a recorded route id count.');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (args[1] === 'dismiss') {
|
|
164
|
+
const cand = rs.findCodexCandidate(args[2]);
|
|
165
|
+
if (!cand) throw new Error(`No open Codex route candidate ${args[2] || ''}. List them with sprag route-scan --agent codex.`);
|
|
166
|
+
rs.resolveCodexCandidate(cand);
|
|
167
|
+
console.log(`Dismissed R${cand.id}: ${cand.category} (${cand.from}) in ${cand.projectRoot}. It will not be offered again.`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (args[1] && !args[1].startsWith('-')) throw new Error('Usage: sprag route-scan [--refresh] [--days N] | dismiss R<N> | savings --agent codex');
|
|
171
|
+
let cache = rs.readCodexRouteScan();
|
|
172
|
+
if (hasFlag('--refresh') || !cache) {
|
|
173
|
+
// Prices live in the gateway snapshot; refresh it when stale so candidates can name a target.
|
|
174
|
+
const { readCodexPrices, refreshCodexPrices } = await import('../codex-cache-policy.js');
|
|
175
|
+
if (!readCodexPrices()) { try { await refreshCodexPrices(); } catch { /* Candidates still list; targets need --model. */ } }
|
|
176
|
+
cache = await rs.runCodexRouteScan({ days });
|
|
177
|
+
}
|
|
178
|
+
if (hasFlag('--quiet')) return;
|
|
179
|
+
if (format === 'json') { console.log(JSON.stringify(cache, null, 2)); return; }
|
|
180
|
+
const open = rs.openCodexCandidates(cache);
|
|
181
|
+
console.log(`Codex route-scan: ${cache.totalEpisodes} turns in ${cache.days} days, scanned ${cache.scannedAt}${cache.priceCheckedAt ? '' : ' | prices unavailable'}`);
|
|
182
|
+
if (!open.length) console.log('No recurring delegable pattern. Needs 3+ similar simple turns per project and parent model.');
|
|
183
|
+
for (const c of open) {
|
|
184
|
+
const target = c.suggestedModel ? `-> ${c.suggestedModel}${Number.isFinite(c.estSavedUsd) ? ` (~$${c.estSavedUsd.toFixed(4)} over ${c.count} turns)` : ''}`
|
|
185
|
+
: `-> choose --model${c.alternatives.length ? ` (priced cheaper: ${c.alternatives.join(', ')})` : ''}`;
|
|
186
|
+
console.log(`R${c.id} ${c.category} x${c.count} [T2 ${c.tiers.T2} / T1 ${c.tiers.T1}] ${c.provider || 'unknown provider'} / ${c.from} ${target}`);
|
|
187
|
+
console.log(` ${c.projectRoot}`);
|
|
188
|
+
console.log(` e.g. ${JSON.stringify(c.example)}`);
|
|
189
|
+
}
|
|
190
|
+
if (open.length) console.log('Approve (ask the user for scope first): sprag delegate rules add R<N> --global|--project --agent codex\nDecline: sprag route-scan dismiss R<N> --agent codex');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (cmd === 'brief') {
|
|
194
|
+
if (args.some((arg) => arg.startsWith('--hook'))) throw new Error('Use codex-hook for Codex hook payloads.');
|
|
195
|
+
const sub = args[1]?.startsWith('-') ? undefined : args[1];
|
|
196
|
+
if (sub && !['on', 'off', 'status'].includes(sub)) throw new Error(`Usage: sprag ${cmd} [on|off|status] --agent codex`);
|
|
197
|
+
if (['on', 'off'].includes(sub)) {
|
|
198
|
+
if (sub === 'on') configureCodexHooks();
|
|
199
|
+
const cfg = loadConfig();
|
|
200
|
+
cfg.codex = { ...cfg.codex, [cmd]: sub === 'on' };
|
|
201
|
+
saveConfig(cfg);
|
|
202
|
+
}
|
|
203
|
+
if (sub) {
|
|
204
|
+
const value = loadConfig()?.codex?.[cmd];
|
|
205
|
+
console.log(`Codex ${cmd}: ${value !== false ? 'on' : 'off'} (preference)`);
|
|
206
|
+
console.log('Run sprag install --agent codex and review/trust hooks in Codex /hooks.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const { createPanelReader, terminalText } = await import('../codex-panel.js');
|
|
210
|
+
const { codexIssues } = await import('../codex-brief.js');
|
|
211
|
+
const data = await createPanelReader({ root: selectedRoot, sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }) })();
|
|
212
|
+
const issues = codexIssues(data.session);
|
|
213
|
+
if (!issues.length) console.log('No fresh Codex context or rate-limit warnings. Missing/stale logs do not prove healthy usage.');
|
|
214
|
+
for (const issue of issues) {
|
|
215
|
+
console.log(terminalText(issue.message));
|
|
216
|
+
console.log(terminalText(issue.advice));
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (cmd === 'history' || cmd === 'last') {
|
|
221
|
+
if (!['table', 'json'].includes(format)) throw new Error('History format must be table or json.');
|
|
222
|
+
const { readCodexHistory } = await import('../codex-brief.js');
|
|
223
|
+
const { terminalText } = await import('../codex-panel.js');
|
|
224
|
+
let events = readCodexHistory({ days: numArg('--days', { dflt: cmd === 'last' ? 1 : 7, min: 0 }),
|
|
225
|
+
sessionId: getArg('--session'), project: getArg('--project') || getArg('-p') });
|
|
226
|
+
if (cmd === 'last') events = events.slice(-1);
|
|
227
|
+
if (format === 'json') { console.log(JSON.stringify({ agent: 'codex', events }, null, 2)); return; }
|
|
228
|
+
if (!events.length) console.log('No Codex warning history in this window. Records are written by the trusted prompt hook.');
|
|
229
|
+
else if (hasFlag('--list')) console.log([...new Set(events.map((event) => event.at.slice(0, 10)))].reverse().join('\n'));
|
|
230
|
+
else for (const event of events) {
|
|
231
|
+
console.log(terminalText(`${event.at} [${event.sessionId}] ${event.message}`));
|
|
232
|
+
console.log(terminalText(event.advice));
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (cmd === 'handoff') {
|
|
237
|
+
const { createPanelReader } = await import('../codex-panel.js');
|
|
238
|
+
const { writeHandoff } = await import('../handoff.js');
|
|
239
|
+
const cwd = getArg('--cwd') || process.cwd();
|
|
240
|
+
const data = await createPanelReader({ root: findProjectRoot(resolve(cwd), { agent: 'codex' }), sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }) })();
|
|
241
|
+
const result = writeHandoff({ cwd, agent: 'codex', session: data.session });
|
|
242
|
+
console.log(`Handoff written: ${result.path}`);
|
|
243
|
+
console.log('Fill in the remaining-work sections, then ask the next Codex session to read this file.');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const statusline = hasFlag('--statusline') || format === 'statusline';
|
|
247
|
+
if (statusline && (!cmd || cmd.startsWith('-'))) {
|
|
248
|
+
const { createPanelReader, formatCodexPanel } = await import('../codex-panel.js');
|
|
249
|
+
const data = await createPanelReader({ root: selectedRoot, sessionId: getArg('--session'), days: numArg('--days', { dflt: 7, min: 0 }), hasFlag })();
|
|
250
|
+
const columns = numArg('--columns', { dflt: process.stdout.columns || 100, min: 1, max: 10000 });
|
|
251
|
+
const text = formatCodexPanel(data, { compact: true, columns: hasFlag('--single-line') ? 100000 : columns,
|
|
252
|
+
rows: 10000, version, color: !hasFlag('--no-color') && !process.env.NO_COLOR && !!process.stdout.isTTY && data.color });
|
|
253
|
+
console.log(hasFlag('--single-line') ? text.split('\n').join(' | ') : text);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (cmd === 'panel') {
|
|
257
|
+
if (args[1] === 'run') {
|
|
258
|
+
const separator = args.indexOf('--');
|
|
259
|
+
const forwarded = separator < 0 ? [] : args.slice(separator + 1);
|
|
260
|
+
const { runCodexWithPanel } = await import('../codex-panel-runner.js');
|
|
261
|
+
let project = process.cwd();
|
|
262
|
+
for (let i = 0; i < forwarded.length; i++) {
|
|
263
|
+
if (forwarded[i] === '--') break;
|
|
264
|
+
if (['-C', '--cd'].includes(forwarded[i]) && forwarded[i + 1]) project = forwarded[++i];
|
|
265
|
+
else if (forwarded[i].startsWith('--cd=')) project = forwarded[i].slice(5);
|
|
266
|
+
}
|
|
267
|
+
process.exitCode = runCodexWithPanel(forwarded, { root: project, keepNativeStatusline: own.hasFlag('--keep-native-statusline') });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (args[1] === 'shell') {
|
|
271
|
+
if (process.platform !== 'darwin') throw new Error('Immediate panel launch currently supports macOS zsh.');
|
|
272
|
+
if (!['install', 'remove'].includes(args[2])) throw new Error('Usage: sprag panel shell install|remove --agent codex');
|
|
273
|
+
const { installPanelShell } = await import('../codex-panel-shell.js');
|
|
274
|
+
const file = installPanelShell({ remove: args[2] === 'remove' });
|
|
275
|
+
// install adds the integration by default, so a removal must be
|
|
276
|
+
// remembered or the next upgrade puts it back.
|
|
277
|
+
const cfg = loadConfig();
|
|
278
|
+
cfg.codex = { ...cfg.codex, panelShell: args[2] === 'install' };
|
|
279
|
+
saveConfig(cfg);
|
|
280
|
+
console.log(`Shell integration ${args[2]}: ${file}. Open a new terminal to apply it.`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const { recordPanelState, panelHealth } = await import('../codex-panel-state.js');
|
|
284
|
+
const panelRoot = getArg('--project') ? resolve(getArg('--project')) : root;
|
|
285
|
+
if (args[1] === 'doctor') {
|
|
286
|
+
const { codexPanelAutoEnabled } = await import('../codex-installer.js');
|
|
287
|
+
const health = panelHealth(panelRoot, { sessionId: getArg('--session') });
|
|
288
|
+
console.log(`Panel hook registered: ${codexPanelAutoEnabled() ? 'yes' : 'no'}`);
|
|
289
|
+
console.log(`Project: ${panelRoot}`);
|
|
290
|
+
console.log(`Last hook execution: ${health.hook ? `${health.hook.status} at ${health.hook.at}` : 'not observed'}`);
|
|
291
|
+
if (health.hook?.error) console.log(`Launch error: ${health.hook.error}`);
|
|
292
|
+
console.log(`Panel rendering: ${health.live ? 'verified (recent frame)' : 'not verified (no recent frame)'}`);
|
|
293
|
+
if (health.frame) console.log(`Last frame: ${health.frame.at} (${health.frame.status})`);
|
|
294
|
+
console.log('Trust is managed by Codex /hooks; registration alone does not prove approval or execution.');
|
|
295
|
+
if (!health.live) console.log('Open now: sprag panel --open --agent codex. Check /hooks, macOS Automation permission, and the hook PATH.');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (args[1] === 'auto') {
|
|
299
|
+
const { codexPanelAutoEnabled } = await import('../codex-installer.js');
|
|
300
|
+
const mode = args[2] || 'status';
|
|
301
|
+
if (!['on', 'off', 'status'].includes(mode)) throw new Error('Usage: sprag panel auto on|off|status --agent codex');
|
|
302
|
+
if (mode === 'on' && process.platform !== 'darwin') throw new Error('Automatic panel windows currently require macOS Terminal.');
|
|
303
|
+
if (mode !== 'status') {
|
|
304
|
+
configureCodexHooks({ panelAuto: mode === 'on' });
|
|
305
|
+
const cfg = loadConfig();
|
|
306
|
+
cfg.codex = { ...cfg.codex, panelAuto: mode === 'on' };
|
|
307
|
+
saveConfig(cfg);
|
|
308
|
+
}
|
|
309
|
+
console.log(`Codex panel auto-start: ${codexPanelAutoEnabled() ? 'on' : 'off'}`);
|
|
310
|
+
if (mode === 'on') console.log('Review and trust the new panel-start hook in Codex /hooks, then restart or resume Codex.');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (hasFlag('--open')) {
|
|
314
|
+
const { openCodexPanel } = await import('../codex-panel-launcher.js');
|
|
315
|
+
console.log(`Codex panel: ${openCodexPanel(getArg('--project') || root, { sessionId: getArg('--session') })}`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const { createPanelReader, runCodexPanel } = await import('../codex-panel.js');
|
|
319
|
+
const { createCodexCachePolicyReader, refreshCodexCachePolicy } = await import('../codex-cache-policy.js');
|
|
320
|
+
const { createCodexBudgetReader, codexBudgetProvider, fetchCodexBudget } = await import('../codex-budget.js');
|
|
321
|
+
const readBudget = hasFlag('--once') ? (() => {
|
|
322
|
+
let snapshot = null;
|
|
323
|
+
return { read: () => snapshot, set: (value) => { snapshot = value; } };
|
|
324
|
+
})() : { read: createCodexBudgetReader() };
|
|
325
|
+
if (hasFlag('--once')) {
|
|
326
|
+
const provider = codexBudgetProvider();
|
|
327
|
+
if (provider) { try { readBudget.set(await fetchCodexBudget(provider)); } catch { /* No verified budget. */ } }
|
|
328
|
+
}
|
|
329
|
+
const { createInlinePanelResizer } = await import('../codex-panel-runner.js');
|
|
330
|
+
const rawInterval = getArg('--interval');
|
|
331
|
+
const interval = rawInterval === undefined ? 2 : Number(rawInterval);
|
|
332
|
+
if (!Number.isFinite(interval) || interval < 0.5 || interval > 60) throw new Error('--interval must be between 0.5 and 60 seconds');
|
|
333
|
+
for (const flag of ['--session', '--project', '--interval']) {
|
|
334
|
+
if (hasFlag(flag) && (!getArg(flag) || getArg(flag).startsWith('--'))) throw new Error(`${flag} requires a value`);
|
|
335
|
+
}
|
|
336
|
+
let renderedSessionId;
|
|
337
|
+
return runCodexPanel({
|
|
338
|
+
read: createPanelReader({ root: getArg('--project') ? resolve(getArg('--project')) : root,
|
|
339
|
+
sessionId: getArg('--session'), sessionFile: getArg('--session-file'), days: numArg('--days', { dflt: 7, min: 0 }), readBudget: readBudget.read,
|
|
340
|
+
readCachePolicy: hasFlag('--once') ? async (session) => {
|
|
341
|
+
const { readCodexCachePolicy } = await import('../codex-cache-policy.js');
|
|
342
|
+
try { return readCodexCachePolicy(session) || await refreshCodexCachePolicy(session); }
|
|
343
|
+
catch { return null; }
|
|
344
|
+
} : createCodexCachePolicyReader(), hasFlag }),
|
|
345
|
+
interval: interval * 1000, once: hasFlag('--once'),
|
|
346
|
+
color: !hasFlag('--no-color') && !process.env.NO_COLOR && !!process.stdout.isTTY, version, compact: hasFlag('--compact'),
|
|
347
|
+
fitRows: createInlinePanelResizer(),
|
|
348
|
+
onFrame: (data) => {
|
|
349
|
+
renderedSessionId = data.sessionId || data.session?.sessionId;
|
|
350
|
+
recordPanelState(panelRoot, 'frame', { status: 'rendered', pid: process.pid, sessionId: renderedSessionId });
|
|
351
|
+
},
|
|
352
|
+
onClose: () => recordPanelState(panelRoot, 'frame', { status: 'closed', pid: process.pid, sessionId: renderedSessionId }),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (cmd === 'codex-hook') {
|
|
356
|
+
const { readStdinJson } = await import('../stdin-payload.js');
|
|
357
|
+
const { codexHookOutput } = await import('../codex-hooks.js');
|
|
358
|
+
try {
|
|
359
|
+
const payload = readStdinJson();
|
|
360
|
+
const { bindPanelSession } = await import('../codex-panel-session.js');
|
|
361
|
+
bindPanelSession(getArg('--event'), payload);
|
|
362
|
+
if (getArg('--event') === 'panel-start') {
|
|
363
|
+
if (process.env.SPRAG_CODEX_PANEL === '1') return;
|
|
364
|
+
if (!payload?.cwd || typeof payload.session_id !== 'string' || !payload.session_id.trim() || !['startup', 'resume'].includes(payload.source)) return;
|
|
365
|
+
const { openCodexPanel } = await import('../codex-panel-launcher.js');
|
|
366
|
+
const { recordPanelState } = await import('../codex-panel-state.js');
|
|
367
|
+
const panelRoot = findProjectRoot(payload.cwd, { agent: 'codex' });
|
|
368
|
+
recordPanelState(panelRoot, 'hook', { status: 'started', sessionId: payload.session_id });
|
|
369
|
+
try {
|
|
370
|
+
const status = openCodexPanel(panelRoot, { sessionId: payload.session_id });
|
|
371
|
+
recordPanelState(panelRoot, 'hook', { status, sessionId: payload.session_id });
|
|
372
|
+
} catch (e) {
|
|
373
|
+
recordPanelState(panelRoot, 'hook', { status: 'failed', error: String(e.message).slice(0, 1000), sessionId: payload.session_id });
|
|
374
|
+
console.log(JSON.stringify({ systemMessage: 'Sprag panel could not open. Run sprag panel --agent codex in a separate terminal; macOS may require Terminal automation permission.' }));
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const out = await codexHookOutput(getArg('--event'), payload);
|
|
379
|
+
if (out) console.log(JSON.stringify(out));
|
|
380
|
+
} catch { /* Hook failures must not break the agent loop. */ }
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (cmd === 'install') {
|
|
384
|
+
const cfg = loadConfig();
|
|
385
|
+
const { setupCodexPanel, describePanelSetup } = await import('../codex-panel-setup.js');
|
|
386
|
+
const panel = setupCodexPanel({ config: cfg || {}, noPanel: hasFlag('--no-panel') });
|
|
387
|
+
const panelAuto = panel.panelAuto;
|
|
388
|
+
const hook = configureCodexHooks({ panelAuto });
|
|
389
|
+
console.log(`Codex hooks: ${hook.file} (${hook.action})`);
|
|
390
|
+
for (const line of describePanelSetup(panel)) console.log(`Codex ${line}`);
|
|
391
|
+
if (process.env.CTS_NO_HARNESS !== '1') {
|
|
392
|
+
const h = initCodexHarness({ root, scope: 'global' });
|
|
393
|
+
console.log(`Codex harness: ${h.file}`);
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const { preparePresetRatchet } = await import('../preset-ratchet.js');
|
|
397
|
+
const p = preparePresetRatchet({ agent: 'codex' });
|
|
398
|
+
console.log(`Codex preset rules: ${p.file} (${p.rules} registered${p.moved ? `, ${p.moved} moved from ratchet.md` : ''})`);
|
|
399
|
+
} catch { /* Presets are optional; `seed` still registers them. */ }
|
|
400
|
+
console.log('Open /hooks in Codex and review/trust the Sprag hooks before they can run.');
|
|
401
|
+
console.log(`Panel auto-start: ${panelAuto ? 'registered with the other hooks' : 'off'}. Hook registration is complete; runtime activation is not verified.`);
|
|
402
|
+
console.log('After approval, start or resume Codex and run: sprag panel doctor --agent codex');
|
|
403
|
+
if (panel.mode === 'window') console.log('Some Codex versions defer SessionStart until the first prompt. For immediate opening on typing codex: sprag panel shell install --agent codex (optional zsh integration).');
|
|
404
|
+
console.log('Existing config.toml, Claude Code settings, and ratchet rules are preserved.');
|
|
405
|
+
console.log('Korean guidance is opt-in: sprag korean on --agent codex');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (cmd === 'uninstall') {
|
|
409
|
+
if (hasFlag('--purge')) throw new Error('Codex uninstall keeps shared Sprag state; --purge is not supported.');
|
|
410
|
+
const hook = configureCodexHooks({ remove: true });
|
|
411
|
+
const h = uninitCodexHarness({ root, scope: 'global' });
|
|
412
|
+
console.log(`Codex hooks: ${hook.file} (${hook.action})`);
|
|
413
|
+
console.log(`Codex harness: ${h.file} (${h.removed ? 'removed' : 'absent'})`);
|
|
414
|
+
console.log('Ratchet rules, project harnesses, backups, and shared Sprag state kept.');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (cmd === 'harness') {
|
|
418
|
+
const sub = args[1] || 'check';
|
|
419
|
+
const scope = scopeOf(args, sub === 'promote' ? null : 'project');
|
|
420
|
+
const opts = { root, scope, agent: 'codex' };
|
|
421
|
+
if (sub === 'init') {
|
|
422
|
+
console.log(JSON.stringify(initCodexHarness(opts), null, 2));
|
|
423
|
+
} else if (sub === 'uninit' || sub === 'remove') {
|
|
424
|
+
if (hasFlag('--purge-ratchet')) throw new Error('Codex uninit keeps ratchet rules; use harness rm or prune.');
|
|
425
|
+
console.log(JSON.stringify(uninitCodexHarness(opts), null, 2));
|
|
426
|
+
} else if (sub === 'check') {
|
|
427
|
+
let s = codexHarnessStatus(opts);
|
|
428
|
+
if (!scopeOf(args, null) && !s.configured) s = codexHarnessStatus({ root, scope: 'global' });
|
|
429
|
+
console.log(`Harness ${s.configured}/${s.total}: ${s.file}`);
|
|
430
|
+
for (const name of s.missing) console.log(`Missing: ${name}`);
|
|
431
|
+
} else if (sub === 'promote') {
|
|
432
|
+
if (!scope) throw new Error('Ask the user for scope, then pass --global or --project.');
|
|
433
|
+
const words = [];
|
|
434
|
+
for (let i = 2; i < args.length; i++) {
|
|
435
|
+
if (args[i] === '--scope' || args[i] === '--session') { i++; continue; }
|
|
436
|
+
if (args[i] === '--global' || args[i] === '--project' || args[i].startsWith('--scope=') || args[i].startsWith('--session=')) continue;
|
|
437
|
+
words.push(args[i]);
|
|
438
|
+
}
|
|
439
|
+
const rule = words.join(' ').trim();
|
|
440
|
+
if (!rule) throw new Error('Provide rule text or a ratchet candidate number (see ratchet? in sprag --statusline --agent codex).');
|
|
441
|
+
if (/^R\d+$/i.test(rule)) throw new Error(`Approve route candidates with: sprag delegate rules add ${rule.toUpperCase()} --global|--project --agent codex`);
|
|
442
|
+
if (/^\d+$/.test(rule)) {
|
|
443
|
+
const { createPanelReader } = await import('../codex-panel.js');
|
|
444
|
+
const { codexCandidateRule } = await import('../codex-harness.js');
|
|
445
|
+
const { userLanguage } = await import('../config.js');
|
|
446
|
+
const data = await createPanelReader({ root, sessionId: getArg('--session') })();
|
|
447
|
+
const cand = (data.ratchet || []).find((c) => c.id === Number(rule));
|
|
448
|
+
if (!cand) throw new Error(`No ratchet candidate #${rule} in ${data.session ? `session ${data.session.sessionId}` : 'this project'}. Candidates last 30 minutes.`);
|
|
449
|
+
const text = codexCandidateRule(cand, userLanguage());
|
|
450
|
+
console.log(`Candidate #${cand.id} from session ${data.session.sessionId}: ${cand.pattern}`);
|
|
451
|
+
console.log(`Appended to ${harnessPromote(text, opts).path}`);
|
|
452
|
+
console.log('Replace the TODO with the cause and prevention: sprag harness list --agent codex');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
console.log(`Appended to ${harnessPromote(rule, opts).path}`);
|
|
456
|
+
} else if (sub === 'list' || sub === 'ls') {
|
|
457
|
+
const r = harnessListRules(opts);
|
|
458
|
+
console.log(`Codex ratchet [${scope}]: ${r.path}`);
|
|
459
|
+
for (const rule of r.rules) console.log(`#${rule.index} ${rule.text}`);
|
|
460
|
+
if (scope === 'global') {
|
|
461
|
+
const { acceptedGlobalPresets, presetRatchetPath } = await import('../preset-ratchet.js');
|
|
462
|
+
const presets = acceptedGlobalPresets({ agent: 'codex' });
|
|
463
|
+
if (presets.length) {
|
|
464
|
+
console.log(`Codex preset rules [global]: ${presetRatchetPath({ agent: 'codex' })}`);
|
|
465
|
+
for (const p of presets) console.log(`${p.id} ${p.text}`);
|
|
466
|
+
console.log('Remove a preset with: sprag seed skip <id> --agent codex');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} else if (sub === 'rm') {
|
|
470
|
+
const raw = args[2];
|
|
471
|
+
// Locate the index even when scope flags precede it.
|
|
472
|
+
const index = args.slice(2).find((s) => /^\d+$/.test(s));
|
|
473
|
+
if (!index) throw new Error(`Expected rule index, got ${raw || 'nothing'}`);
|
|
474
|
+
const r = harnessRmRule(Number(index), opts);
|
|
475
|
+
if (!r.ok) throw new Error(r.error);
|
|
476
|
+
console.log(`Removed #${index}; backup: ${r.backup}`);
|
|
477
|
+
} else if (sub === 'prune') {
|
|
478
|
+
const r = harnessPrune({ ...opts, tag: getArg('--tag'), olderThanMonths: numArg('--older-than', { min: 1 }), dryRun: hasFlag('--dry-run') });
|
|
479
|
+
if (!r.ok) throw new Error(r.error);
|
|
480
|
+
console.log(JSON.stringify(r, null, 2));
|
|
481
|
+
} else throw new Error(`Codex harness supports init, uninit, check, promote, list, rm, prune (got ${sub}).`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (cmd === 'korean' && ['on', 'off'].includes(args[1])) {
|
|
485
|
+
if (args.some((arg) => arg.startsWith('--hook'))) throw new Error('Use codex-hook for Codex hook payloads.');
|
|
486
|
+
const { setKoreanStyleEnabled } = await import('../korean-style.js');
|
|
487
|
+
if (args[1] === 'on') configureCodexHooks();
|
|
488
|
+
setKoreanStyleEnabled(args[1] === 'on');
|
|
489
|
+
console.log(`Korean guidance: ${args[1]} (shared Sprag preference). Review/trust the Sprag hooks in Codex /hooks.`);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (cmd === 'doc2md' && ['on', 'off'].includes(args[1])) {
|
|
493
|
+
if (args.some((arg) => arg.startsWith('--hook'))) throw new Error('Use codex-hook for Codex hook payloads.');
|
|
494
|
+
if (args[1] === 'on') configureCodexHooks();
|
|
495
|
+
const cfg = loadConfig();
|
|
496
|
+
cfg.codex = { ...cfg.codex, doc2md: args[1] === 'on' };
|
|
497
|
+
saveConfig(cfg);
|
|
498
|
+
console.log(`Codex document conversion: ${args[1]}. Codex hooks must be installed and trusted.`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (cmd === 'doc2md' && (!args[1] || args[1] === 'status')) {
|
|
502
|
+
const { createRequire } = await import('node:module');
|
|
503
|
+
const doc2md = createRequire(import.meta.url)('../doc2md.cjs');
|
|
504
|
+
const { codexHookStatus } = await import('../codex-installer.js');
|
|
505
|
+
console.log(`Codex document conversion: ${loadConfig()?.codex?.doc2md === false || process.env.CTS_NO_DOC2MD === '1' ? 'off' : 'on'}`);
|
|
506
|
+
console.log(`Formats: ${doc2md.TARGET_EXTENSIONS.join(' ')}`);
|
|
507
|
+
console.log(`Converter: ${doc2md.findInterpreter() || 'missing; run sprag doc2md install-converter --agent codex'}`);
|
|
508
|
+
console.log(`Cache: ${doc2md.cacheDir()}`);
|
|
509
|
+
try {
|
|
510
|
+
for (const hook of codexHookStatus().filter((h) => ['PreToolUse', 'UserPromptSubmit'].includes(h.event))) console.log(`${hook.event}: ${hook.registered ? 'registered' : 'missing/outdated'}`);
|
|
511
|
+
} catch {
|
|
512
|
+
console.log('Hooks: cannot read Codex hooks.json; file left unchanged. Run sprag doctor --agent codex for diagnostics.');
|
|
513
|
+
}
|
|
514
|
+
console.log('Prompt conversion, supported reads, and document/cache write guards require trusted hooks. Review in Codex /hooks.');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (cmd === 'doc2md' && args[1] && !args[1].startsWith('-') && args[1] !== 'install-converter') {
|
|
518
|
+
const { convertCodexDocument } = await import('../codex-doc2md.js');
|
|
519
|
+
const result = convertCodexDocument(resolve(args[1]));
|
|
520
|
+
if (!result.ok) {
|
|
521
|
+
console.error(`Doc2md failed: ${result.reason}`);
|
|
522
|
+
if (result.reason === 'no-markitdown') console.error('Run sprag doc2md install-converter --agent codex, then retry.');
|
|
523
|
+
process.exitCode = 1;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
console.log(`${result.cached ? 'Cached' : 'Converted'}: ${result.cacheFile}`);
|
|
527
|
+
console.log(`Source ${result.meta.size} bytes; Markdown ${result.meta.markdownBytes} bytes.`);
|
|
528
|
+
if (result.meta.clipped || result.meta.truncated) console.log('Text extraction is incomplete; inspect the source for omitted content.');
|
|
529
|
+
if (result.meta.note) console.log(result.meta.note);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (cmd === 'upgrade') {
|
|
533
|
+
const command = 'npm i -g sprag-cli --ignore-scripts';
|
|
534
|
+
if (!hasFlag('--print')) throw new Error(`For a Codex-only upgrade, run ${command}, then sprag install --agent codex.`);
|
|
535
|
+
console.log(command);
|
|
536
|
+
console.log('sprag install --agent codex');
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const shared = new Set(['korean', 'cohesion', 'doc2md', 'mode', 'feedback', 'update-check']);
|
|
540
|
+
if (shared.has(cmd)) {
|
|
541
|
+
if (args.some((a) => a.startsWith('--hook'))) throw new Error('Use codex-hook for Codex hook payloads.');
|
|
542
|
+
if (cmd === 'cohesion' && args[1] === 'on') configureCodexHooks();
|
|
543
|
+
const { userDataDir } = await import('../paths.js');
|
|
544
|
+
const result = await (await import(`./${cmd}.js`)).run({ args, getArg, hasFlag, numArg, version, dataDir: userDataDir() });
|
|
545
|
+
if (cmd === 'cohesion' && args[1] === 'on') console.log('Review/trust the Sprag hooks in Codex /hooks.');
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
if (cmd && !cmd.startsWith('-')) throw new Error(`${cmd} is not supported for Codex yet. Use --agent claude for Claude Code integration.`);
|
|
549
|
+
if (hasFlag('--install-hook') || hasFlag('--uninstall-hook') || hasFlag('--hook-run')) {
|
|
550
|
+
throw new Error('Claude command statusline and legacy hooks are not supported by Codex. Use sprag --agent codex for a report.');
|
|
551
|
+
}
|
|
552
|
+
const days = numArg('--hours', { min: 0 }) !== undefined ? numArg('--hours', { min: 0 }) / 24
|
|
553
|
+
: (numArg('--days', { min: 0 }) ?? numArg('-d', { min: 0 }) ?? 30);
|
|
554
|
+
if (!['table', 'json', 'csv'].includes(format)) throw new Error('Codex report format must be table, json, or csv.');
|
|
555
|
+
const { parseAllCodexSessions } = await import('../codex-parser.js');
|
|
556
|
+
const { summary, dailyTrend } = await import('../stats.js');
|
|
557
|
+
const sessions = (await parseAllCodexSessions({ days, projectFilter: getArg('--project') || getArg('-p') }))
|
|
558
|
+
.filter((session) => !getArg('--session') || session.sessionId === getArg('--session'));
|
|
559
|
+
const sum = summary(sessions);
|
|
560
|
+
const report = {
|
|
561
|
+
agent: 'codex', options: { days }, summary: sum, usageUpdates: sum.apiCalls, trend: dailyTrend(sessions),
|
|
562
|
+
reasoningOutputTokens: sessions.reduce((n, s) => n + s.reasoningOutputTokens, 0),
|
|
563
|
+
cost: null, ttl: null,
|
|
564
|
+
usageNote: 'Local rollout estimates; reasoning is included in output. Cost and cache TTL are not inferred. Usage updates are not an exact API request count.',
|
|
565
|
+
sessions,
|
|
566
|
+
};
|
|
567
|
+
if (format === 'json') console.log(JSON.stringify(report, null, 2));
|
|
568
|
+
else if (format === 'csv') {
|
|
569
|
+
console.log('agent,sessions,usage_updates,input_tokens,cached_input_tokens,output_tokens,reasoning_output_tokens');
|
|
570
|
+
console.log(['codex', sum.sessions, sum.apiCalls, sum.totalInput, sum.cacheRead, sum.output, report.reasoningOutputTokens].join(','));
|
|
571
|
+
} else {
|
|
572
|
+
console.log(`Sprag / Codex / ${days} days`);
|
|
573
|
+
console.log(`Sessions: ${sum.sessions} | Usage updates: ${sum.apiCalls}`);
|
|
574
|
+
console.log(`Input: ${sum.totalInput} (${sum.cacheRead} cached, ${(sum.hitRate * 100).toFixed(1)}% hit rate)`);
|
|
575
|
+
console.log(`Output: ${sum.output} (${report.reasoningOutputTokens} reasoning, included)`);
|
|
576
|
+
console.log('Cost: unavailable | Cache TTL: unavailable');
|
|
577
|
+
if (!sessions.length) console.log('No Codex usage records found. Check CODEX_HOME, session logging, or --days 90.');
|
|
578
|
+
}
|
|
579
|
+
}
|
package/src/commands/harness.js
CHANGED
|
@@ -337,7 +337,7 @@ export async function run({ args, hasFlag }) {
|
|
|
337
337
|
// rules, and a project ratchet inherits global anyway). Strictly opt-in:
|
|
338
338
|
// install/init never auto-injects rules.
|
|
339
339
|
const scope = parseHarnessScope(args.slice(2), 'global');
|
|
340
|
-
const r = harnessPull({ scope });
|
|
340
|
+
const r = await harnessPull({ scope });
|
|
341
341
|
console.log(`Curated preset rules → ${r.path} [${r.scope}]`);
|
|
342
342
|
if (r.added.length) {
|
|
343
343
|
console.log(`✅ ${r.added.length}/${r.presets} rule(s) registered:`);
|
|
@@ -366,7 +366,18 @@ export async function run({ args, hasFlag }) {
|
|
|
366
366
|
console.log('');
|
|
367
367
|
};
|
|
368
368
|
if (wantProject) print('project');
|
|
369
|
-
if (wantGlobal)
|
|
369
|
+
if (wantGlobal) {
|
|
370
|
+
print('global');
|
|
371
|
+
// Presets live in their own tool-owned file and are removed by id,
|
|
372
|
+
// so they are listed apart from the numbered rules `rm <N>` acts on.
|
|
373
|
+
const { acceptedGlobalPresets, presetRatchetPath } = await import('../preset-ratchet.js');
|
|
374
|
+
const presets = acceptedGlobalPresets();
|
|
375
|
+
if (presets.length) {
|
|
376
|
+
console.log(`📦 Preset rules [global] — ${presetRatchetPath()}\n`);
|
|
377
|
+
for (const p of presets) console.log(` ${p.id} ${p.text}`);
|
|
378
|
+
console.log('\nRemove a preset with: sprag seed skip <id>\n');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
370
381
|
console.log('Remove with: sprag harness rm [--global|--project] <N>');
|
|
371
382
|
console.log('Archive in bulk: sprag harness prune [--global] [--tag <t>] [--older-than <months>] [--dry-run]');
|
|
372
383
|
return;
|