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.
- package/README.ko.md +29 -62
- package/README.md +26 -61
- package/bin/cli.js +130 -62
- package/docs/CODEX.ko.md +32 -0
- package/docs/CODEX.md +34 -0
- package/package.json +11 -4
- package/presets/ratchet-rules.json +7 -0
- package/src/agents.js +33 -11
- package/src/{codex-brief.js → codex/brief.js} +7 -7
- package/src/{codex-budget.js → codex/budget.js} +2 -2
- package/src/{codex-cache-policy.js → codex/cache-policy.js} +9 -9
- package/src/{codex-delegation-diagnostics.js → codex/delegation-diagnostics.js} +2 -2
- package/src/{codex-delegation.js → codex/delegation.js} +11 -11
- package/src/{codex-doc2md-ledger.js → codex/doc2md-ledger.js} +5 -5
- package/src/{codex-doc2md.js → codex/doc2md.js} +4 -4
- package/src/{codex-doctor.js → codex/doctor.js} +4 -4
- package/src/{codex-harness.js → codex/harness.js} +5 -5
- package/src/{codex-hooks.js → codex/hooks.js} +20 -20
- package/src/{codex-installer.js → codex/installer.js} +2 -2
- package/src/{codex-ledger.js → codex/ledger.js} +6 -6
- package/src/{codex-panel-launcher.js → codex/panel-launcher.js} +2 -2
- package/src/{codex-panel-runner.js → codex/panel-runner.js} +2 -2
- package/src/{codex-panel-session.js → codex/panel-session.js} +1 -1
- package/src/{codex-panel-setup.js → codex/panel-setup.js} +1 -1
- package/src/{codex-panel-state.js → codex/panel-state.js} +3 -3
- package/src/{codex-panel.js → codex/panel.js} +43 -34
- package/src/{codex-parser.js → codex/parser.js} +5 -9
- package/src/{codex-reply-language.js → codex/reply-language.js} +2 -2
- package/src/{codex-route-scan.js → codex/route-scan.js} +10 -10
- package/src/{codex-rule-health.js → codex/rule-health.js} +4 -4
- package/src/commands/codex-delegate.js +6 -6
- package/src/commands/codex.js +44 -44
- package/src/commands/cowork.js +107 -0
- package/src/commands/delegate.js +43 -0
- package/src/commands/install.js +21 -7
- package/src/commands/korean.js +1 -1
- package/src/commands/litellm-budget.js +56 -0
- package/src/commands/route-scan.js +6 -0
- package/src/commands/saved.js +60 -5
- package/src/commands/uninstall.js +7 -3
- package/src/config.js +4 -0
- package/src/cowork-sync.js +361 -0
- package/src/delegation-agents.js +133 -0
- package/src/formatters/statusline.js +713 -504
- package/src/glyphs.js +2 -0
- package/src/harness.js +1 -1
- package/src/implementation-delegation.js +5 -2
- package/src/installer.js +77 -1
- package/src/model-alias.js +45 -60
- package/src/model-rules.js +12 -5
- package/src/model-share.js +136 -0
- package/src/parser.js +4 -13
- package/src/route-inject.js +8 -3
- package/src/session-records.js +2 -8
- package/src/statusline-base.js +9 -3
- package/src/stdin-payload.js +1 -1
- package/src/transcript-lines.js +40 -0
- /package/src/{codex-cache.js → codex/cache.js} +0 -0
- /package/src/{codex-panel-shell.js → codex/panel-shell.js} +0 -0
package/bin/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ import { dirname, isAbsolute, join } from 'node:path';
|
|
|
32
32
|
|
|
33
33
|
import {
|
|
34
34
|
readStdinJson,
|
|
35
|
+
readStdinRaw,
|
|
35
36
|
extractCaps,
|
|
36
37
|
extractContextUsage,
|
|
37
38
|
extractEffort,
|
|
@@ -87,12 +88,78 @@ function tableColor() {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
// Subcommands this build knows how to run. Used only by the guard below.
|
|
91
|
+
/**
|
|
92
|
+
* `--segments a,b,c` as a list, or null for the default order. One parser for
|
|
93
|
+
* the demo, the populated line and the no-session line, so `--segments harness`
|
|
94
|
+
* means the same chip set on each of them.
|
|
95
|
+
*/
|
|
96
|
+
function segmentsFromArgs() {
|
|
97
|
+
const raw = getArg('--segments');
|
|
98
|
+
return raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
const KNOWN_SUBCOMMANDS = new Set([
|
|
91
102
|
'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean', 'cohesion',
|
|
92
103
|
'doc2md', 'delegate', 'harness', 'route-scan', 'compact-window', 'update-check', 'upgrade',
|
|
93
|
-
'seed', 'litellm-budget', 'profile-map', 'feedback', 'saved',
|
|
104
|
+
'seed', 'litellm-budget', 'profile-map', 'feedback', 'saved', 'cowork',
|
|
94
105
|
]);
|
|
95
106
|
|
|
107
|
+
// Flags each entry path reads, collected from the hasFlag/getArg/numArg calls in
|
|
108
|
+
// this file, src/statusline-mode.js, src/hook.cjs and the install/uninstall
|
|
109
|
+
// commands. `value` flags take an argument (`--days 3` or `--days=3`); `bool`
|
|
110
|
+
// flags do not. --agent is not listed: selectAgent() removes it before `args`
|
|
111
|
+
// exists. Only the default path (report, statusline, hook management) and
|
|
112
|
+
// install/uninstall are checked; every other subcommand parses its own arguments.
|
|
113
|
+
// Keep these in step with the code that reads the flag: a flag read but not
|
|
114
|
+
// listed is rejected, and a flag listed but never read is silently ignored again.
|
|
115
|
+
const FLAG_SPECS = {
|
|
116
|
+
report: {
|
|
117
|
+
value: ['--format', '-f', '--project', '-p', '--days', '-d', '--hours', '--demo',
|
|
118
|
+
'--demo-cycle-sec', '--segments', '--exclude-session', '--threshold'],
|
|
119
|
+
bool: ['--statusline', '--verbose', '--no-verbose', '--compact', '--no-color', '--icon',
|
|
120
|
+
'--no-icon', '--text', '--narrow', '--no-timer', '--single-line', '--demo-label',
|
|
121
|
+
'--version', '-v', '--help', '-h', '--hook', '--hook-run', '--install-hook', '--uninstall-hook'],
|
|
122
|
+
},
|
|
123
|
+
install: { value: [], bool: ['--force', '--manual', '--yes', '--no-input', '--help', '-h'] },
|
|
124
|
+
// --yes and --no-input are the explicit spelling of "do not ask", and
|
|
125
|
+
// uninstall never asks, so they are accepted as no-ops.
|
|
126
|
+
uninstall: { value: [], bool: ['--purge', '--yes', '--no-input', '--help', '-h'] },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Which spec applies to this invocation, or null for a subcommand that parses its own arguments. */
|
|
130
|
+
function flagSpecFor(first) {
|
|
131
|
+
if (!first || first.startsWith('-')) return FLAG_SPECS.report;
|
|
132
|
+
return FLAG_SPECS[first] || null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The flags in `argv` that `spec` does not list. A value flag swallows the next
|
|
137
|
+
* token the way getArg() does, so `--exclude-session --x` reads `--x` as a path,
|
|
138
|
+
* not as a flag, and both `--days 3` and `--days=3` are understood. A bool flag
|
|
139
|
+
* written `--no-color=1` is reported too: hasFlag() compares whole tokens, so
|
|
140
|
+
* that form was never honoured. Everything after a bare `--` is not a flag.
|
|
141
|
+
*
|
|
142
|
+
* @returns {string[]} the offending tokens as typed
|
|
143
|
+
*/
|
|
144
|
+
function findUnknownFlags(argv, spec) {
|
|
145
|
+
const value = new Set(spec.value);
|
|
146
|
+
const bool = new Set(spec.bool);
|
|
147
|
+
const unknown = [];
|
|
148
|
+
for (let i = 0; i < argv.length; i++) {
|
|
149
|
+
const tok = argv[i];
|
|
150
|
+
if (tok === '--') break;
|
|
151
|
+
if (tok.length < 2 || !tok.startsWith('-')) continue;
|
|
152
|
+
const eq = tok.indexOf('=');
|
|
153
|
+
const name = eq === -1 ? tok : tok.slice(0, eq);
|
|
154
|
+
if (value.has(name)) {
|
|
155
|
+
if (eq === -1) i++;
|
|
156
|
+
} else if (!bool.has(name) || eq !== -1) {
|
|
157
|
+
unknown.push(tok);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return unknown;
|
|
161
|
+
}
|
|
162
|
+
|
|
96
163
|
const USAGE = `sprag — Claude Code and Codex agent harness
|
|
97
164
|
|
|
98
165
|
Usage:
|
|
@@ -116,6 +183,7 @@ Usage:
|
|
|
116
183
|
sprag uninstall --agent codex remove the global Codex integration
|
|
117
184
|
--agent claude|codex (default: claude; accepted before or after subcommands)
|
|
118
185
|
sprag route-scan detect recurring easy work → delegation candidates
|
|
186
|
+
sprag delegate agents [on|off] tool-restricted delegation agents (no MCP tools)
|
|
119
187
|
sprag profile-map show/refresh the gateway model map (LiteLLM /model/info)
|
|
120
188
|
sprag install set up everything (presets, and Codex when present) without asking
|
|
121
189
|
sprag install --manual confirm each optional step
|
|
@@ -127,6 +195,7 @@ Usage:
|
|
|
127
195
|
sprag handoff write a session handoff file
|
|
128
196
|
sprag mode [keywords] statusline preferences (icon/text, en/ko, window, ttl)
|
|
129
197
|
sprag seed register the bundled starter rules, one answer at a time
|
|
198
|
+
sprag cowork [sync|on|off|exclude <dir>|include <dir>] copy the ratchet rules into Cowork tab folders
|
|
130
199
|
sprag korean on|off|status Korean writing guidance + write-time check
|
|
131
200
|
sprag cohesion on|off|status English sentence-connection guidance
|
|
132
201
|
sprag doc2md on|off|<file> convert documents to Markdown before the model reads them
|
|
@@ -169,6 +238,34 @@ async function main() {
|
|
|
169
238
|
return;
|
|
170
239
|
}
|
|
171
240
|
|
|
241
|
+
// Refuse what this build cannot honour, before anything is scanned or written.
|
|
242
|
+
// An unknown first word used to fall into the 30-day report, and an unknown
|
|
243
|
+
// flag was dropped without a word: `sprag install --dry-run` ran the real
|
|
244
|
+
// install and rewrote settings.json. Exit 2 is the usage-error code.
|
|
245
|
+
if (args[0] && !args[0].startsWith('-') && !KNOWN_SUBCOMMANDS.has(args[0])) {
|
|
246
|
+
console.error(`Unknown command: ${args[0]}`);
|
|
247
|
+
console.error('Run `sprag --help` for the list of commands.');
|
|
248
|
+
process.exitCode = 2;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const flagSpec = flagSpecFor(args[0]);
|
|
252
|
+
const unknownFlags = flagSpec ? findUnknownFlags(args, flagSpec) : [];
|
|
253
|
+
if (unknownFlags.length > 0) {
|
|
254
|
+
// The statusline and the PostToolUse hook run from a fixed command in
|
|
255
|
+
// settings.json, and a typo or a flag from a newer release there must not
|
|
256
|
+
// blank the line or fail the hook. Warn and carry on; every other caller is
|
|
257
|
+
// a person at a prompt who is better told.
|
|
258
|
+
const lenient = hasFlag('--statusline') || getArg('--format') === 'statusline' || hasFlag('--hook-run');
|
|
259
|
+
if (lenient) {
|
|
260
|
+
for (const f of unknownFlags) console.error(`Warning: Unknown option: ${f} (ignored)`);
|
|
261
|
+
} else {
|
|
262
|
+
for (const f of unknownFlags) console.error(`Unknown option: ${f}`);
|
|
263
|
+
console.error('Run `sprag --help` for usage.');
|
|
264
|
+
process.exitCode = 2;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
172
269
|
// Subcommand: last — print the most recent warning + how to handle it.
|
|
173
270
|
// Designed for the auto-trigger skill so the user immediately sees
|
|
174
271
|
// "what just fired and how to fix it" without having to read the whole
|
|
@@ -290,6 +387,11 @@ async function main() {
|
|
|
290
387
|
return (await import('../src/commands/seed.js')).run({ args, hasFlag });
|
|
291
388
|
}
|
|
292
389
|
|
|
390
|
+
// Subcommand: cowork — ratchet copies for the Claude app's Cowork tab.
|
|
391
|
+
if (args[0] === 'cowork') {
|
|
392
|
+
return (await import('../src/commands/cowork.js')).run({ args, hasFlag });
|
|
393
|
+
}
|
|
394
|
+
|
|
293
395
|
// Subcommand: saved — stopwatch reset for the lifetime "saved" counters.
|
|
294
396
|
// sprag saved | saved reset [routing|doc2md|all] | saved undo
|
|
295
397
|
if (args[0] === 'saved') {
|
|
@@ -339,56 +441,9 @@ async function main() {
|
|
|
339
441
|
}
|
|
340
442
|
|
|
341
443
|
// Subcommand: litellm-budget · LiteLLM 게이트웨이 키의 max_budget/spend 조회.
|
|
342
|
-
// sprag litellm-budget
|
|
343
|
-
// sprag litellm-budget --json # 캐시 원본 JSON 출력
|
|
344
|
-
// sprag litellm-budget --refresh # 지금 프록시에 물어봄 (detached 자식이 사용)
|
|
444
|
+
// sprag litellm-budget [--json | --refresh]
|
|
345
445
|
if (args[0] === 'litellm-budget') {
|
|
346
|
-
|
|
347
|
-
await import('../src/litellm-budget.js');
|
|
348
|
-
const quiet = hasFlag('--quiet');
|
|
349
|
-
if (hasFlag('--refresh')) {
|
|
350
|
-
try {
|
|
351
|
-
// apiKeyHelper 가 토큰 수명(TTL)을 소유합니다. 헬퍼를 한 번만 불러 그
|
|
352
|
-
// 키를 예산 갱신과 게이트웨이 모델맵 갱신 양쪽에 재사용합니다. 갱신
|
|
353
|
-
// 경로마다 resolveKey() 를 다시 부르면 사본을 만드는 셈이 되고, 헬퍼
|
|
354
|
-
// 호출 횟수도 그만큼 늘어납니다.
|
|
355
|
-
const key = resolveKey();
|
|
356
|
-
let next = null;
|
|
357
|
-
if (key) {
|
|
358
|
-
next = await refreshBudgetState(process.env, fetch, key);
|
|
359
|
-
const { maybeRefreshGatewayModelMap } = await import('../src/litellm-models.js');
|
|
360
|
-
await maybeRefreshGatewayModelMap({ base: gatewayBase(), key });
|
|
361
|
-
}
|
|
362
|
-
if (!quiet) console.log(JSON.stringify(next, null, 2));
|
|
363
|
-
} catch (e) {
|
|
364
|
-
debug('litellm-budget:refresh', e);
|
|
365
|
-
if (!quiet) console.error(`litellm-budget refresh failed: ${e.message}`);
|
|
366
|
-
process.exitCode = 1;
|
|
367
|
-
}
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
// 읽기 경로는 캐시만 보므로 키를 요구하지 않습니다 (apiKeyHelper 환경 배려).
|
|
371
|
-
const base = gatewayBase();
|
|
372
|
-
if (!base) {
|
|
373
|
-
console.log('게이트웨이가 감지되지 않았습니다 (ANTHROPIC_BASE_URL 필요).');
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const state = readBudgetState();
|
|
377
|
-
if (hasFlag('--json')) {
|
|
378
|
-
console.log(JSON.stringify(state, null, 2));
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (state.base !== base || state.maxBudget === undefined) {
|
|
382
|
-
console.log('예산 캐시가 비어 있습니다. `sprag litellm-budget --refresh` 로 먼저 조회하십시오.');
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
// Same resolver the statusline uses: this report draws a gauge too, and a
|
|
386
|
-
// glyph the terminal's font lacks garbles here exactly as it does there.
|
|
387
|
-
const { resolveLabelMode } = await import('../src/statusline-mode.js');
|
|
388
|
-
const { statuslineDefaults } = await import('../src/config.js');
|
|
389
|
-
const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg: statuslineDefaults() });
|
|
390
|
-
for (const line of formatBudgetReport(state, new Date(), labelMode)) console.log(line);
|
|
391
|
-
return;
|
|
446
|
+
return (await import('../src/commands/litellm-budget.js')).run({ hasFlag });
|
|
392
447
|
}
|
|
393
448
|
|
|
394
449
|
// Subcommand: profile-map · LiteLLM 게이트웨이의 프로파일 ID → 모델 별칭
|
|
@@ -477,10 +532,7 @@ async function main() {
|
|
|
477
532
|
: (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
|
|
478
533
|
const showTimer = hasFlag('--no-timer') ? false : cfg.timer;
|
|
479
534
|
const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
|
|
480
|
-
const
|
|
481
|
-
const segments = segmentsArg
|
|
482
|
-
? segmentsArg.split(',').map((s) => s.trim()).filter(Boolean)
|
|
483
|
-
: null;
|
|
535
|
+
const segments = segmentsFromArgs();
|
|
484
536
|
const out = formatReport(data, {
|
|
485
537
|
color: colorOk,
|
|
486
538
|
verbose: isVerbose,
|
|
@@ -584,8 +636,8 @@ async function main() {
|
|
|
584
636
|
const { withBaseStatusline } = await import('../src/statusline-base.js');
|
|
585
637
|
console.log(withBaseStatusline(formatNoSession(
|
|
586
638
|
{ caps, model, effort, windowLabel, version: PKG_VERSION, update: readUpdateChip(PKG_VERSION) },
|
|
587
|
-
{ color: colorOk, mode: labelMode },
|
|
588
|
-
)));
|
|
639
|
+
{ color: colorOk, mode: labelMode, segments: segmentsFromArgs() },
|
|
640
|
+
), { input: readStdinRaw() }));
|
|
589
641
|
return;
|
|
590
642
|
}
|
|
591
643
|
// JSON mode stays JSON. A caller that asked for machine-readable output
|
|
@@ -761,12 +813,13 @@ async function main() {
|
|
|
761
813
|
// 이미 파싱한 세션을 재사용하고, 사용자가 창을 줄여 둔 경우(mode 1d 등)에만
|
|
762
814
|
// 월초까지 다시 파싱한다. 파싱 결과는 세션 캐시가 받아 주므로 싸다.
|
|
763
815
|
let monthSpendInfo = null;
|
|
816
|
+
let monthSessions = null;
|
|
764
817
|
if (isStatusline) {
|
|
765
818
|
try {
|
|
766
819
|
const { monthSpend, monthStartMs } = await import('../src/month-spend.js');
|
|
767
820
|
const since = monthStartMs();
|
|
768
821
|
const daysNeeded = (Date.now() - since) / 86400000;
|
|
769
|
-
|
|
822
|
+
monthSessions = days >= daysNeeded
|
|
770
823
|
? sessions
|
|
771
824
|
: await parseAllSessions({ days: Math.max(1, Math.ceil(daysNeeded)), projectFilter, sinceMs: since });
|
|
772
825
|
// 월 경계에 걸친 세션은 월초 이후 요청만 남긴다. 그러지 않으면 1일 자정을
|
|
@@ -777,6 +830,22 @@ async function main() {
|
|
|
777
830
|
}
|
|
778
831
|
}
|
|
779
832
|
|
|
833
|
+
// 모델별 사용 비중: `sprag saved` 와 같은 기간(라우팅 초기화 기록, 없으면 월초)의
|
|
834
|
+
// 비용 기준 분포. 이미 파싱한 월간 세션을 재사용한다. 실패하면 칩만 빠진다.
|
|
835
|
+
let modelShare = null;
|
|
836
|
+
if (isStatusline) {
|
|
837
|
+
try {
|
|
838
|
+
const { statuslineDefaults } = await import('../src/config.js');
|
|
839
|
+
if (statuslineDefaults().modelShare) {
|
|
840
|
+
const { loadModelShare } = await import('../src/model-share.js');
|
|
841
|
+
const { userDataDir } = await import('../src/paths.js');
|
|
842
|
+
modelShare = await loadModelShare({ dir: userDataDir(), env: process.env, projectFilter, monthSessions: monthSessions || undefined });
|
|
843
|
+
}
|
|
844
|
+
} catch (e) {
|
|
845
|
+
debug('model-share', e);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
780
849
|
// Rolling week/month/lifetime totals from the delegation ledger — the
|
|
781
850
|
// statusline's headline line. Falls back to null (chip hidden) on any error.
|
|
782
851
|
// A lookup, never a scan: the statusline re-renders every few seconds and a
|
|
@@ -873,6 +942,7 @@ async function main() {
|
|
|
873
942
|
update: format === 'statusline' ? readUpdateChip(PKG_VERSION) : null,
|
|
874
943
|
lastActivity,
|
|
875
944
|
monthSpend: monthSpendInfo,
|
|
945
|
+
modelShare,
|
|
876
946
|
spikeReport,
|
|
877
947
|
contextWindow,
|
|
878
948
|
ctxLive,
|
|
@@ -915,10 +985,7 @@ async function main() {
|
|
|
915
985
|
const colorOk =
|
|
916
986
|
!hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
|
|
917
987
|
|
|
918
|
-
const
|
|
919
|
-
const segments = segmentsArg
|
|
920
|
-
? segmentsArg.split(',').map((s) => s.trim()).filter(Boolean)
|
|
921
|
-
: null;
|
|
988
|
+
const segments = segmentsFromArgs();
|
|
922
989
|
output = formatReport(data, {
|
|
923
990
|
color: colorOk,
|
|
924
991
|
verbose: isVerbose,
|
|
@@ -937,7 +1004,8 @@ async function main() {
|
|
|
937
1004
|
|
|
938
1005
|
if (format === 'statusline') {
|
|
939
1006
|
const { withBaseStatusline } = await import('../src/statusline-base.js');
|
|
940
|
-
|
|
1007
|
+
// The payload was read above; the base gets the same text, not a second read.
|
|
1008
|
+
output = withBaseStatusline(output, { input: readStdinRaw() });
|
|
941
1009
|
}
|
|
942
1010
|
console.log(output);
|
|
943
1011
|
}
|
package/docs/CODEX.ko.md
CHANGED
|
@@ -38,6 +38,38 @@ macOS zsh 환경에서는 설치가 tmux 설치 여부를 먼저 확인합니다
|
|
|
38
38
|
`sprag panel auto off --agent codex`로 끈 설정은 재설치해도 유지합니다.
|
|
39
39
|
패널 실행 여부는 `sprag panel doctor --agent codex`로 확인하십시오.
|
|
40
40
|
|
|
41
|
+
## 명령 한눈에 보기
|
|
42
|
+
|
|
43
|
+
명령의 기본 대상은 Claude Code입니다. `--agent codex`를 붙이면 Codex를 고릅니다.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm i -g sprag-cli --ignore-scripts # Claude Code를 건드리지 않는 Codex 전용 설치
|
|
47
|
+
sprag install --agent codex
|
|
48
|
+
sprag --agent codex --days 7
|
|
49
|
+
sprag panel --agent codex # 별도 터미널에서 실시간 패널 실행
|
|
50
|
+
sprag --statusline --agent codex --text
|
|
51
|
+
sprag doctor --agent codex # 문제가 생겼을 때만
|
|
52
|
+
sprag route-scan --agent codex
|
|
53
|
+
sprag seed --agent codex
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
설치 후 Codex의 `/hooks`에서 새 훅을 검토하고 신뢰해야 실행됩니다. Codex가 요구하는
|
|
57
|
+
절차입니다. Codex 기본 상태줄은 셸 명령을 실행하지 않으므로, Sprag는 터미널 상태줄과 패널을
|
|
58
|
+
따로 제공합니다. 기능별 지원 여부는 `sprag capabilities --agent codex`로 확인하십시오.
|
|
59
|
+
|
|
60
|
+
## Claude Code와 Codex 비교
|
|
61
|
+
|
|
62
|
+
| 기능 | Claude Code | Codex |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| 세션 지침 | `CLAUDE.md`와 훅 | `AGENTS.md`와 기본 훅 |
|
|
65
|
+
| 실시간 상태 | 기본 상태줄에 명령 등록 | 별도 패널 또는 tmux 구성 |
|
|
66
|
+
| 규칙 후보 | Claude 기록과 모델 등급 프리셋 | Codex 기록과 호환 래칫 프리셋 |
|
|
67
|
+
| 모델 위임 | Haiku·Sonnet 등급 규칙 | 공급자 모델 ID와 범위별 규칙 |
|
|
68
|
+
| 위임 원장 | Claude 실행별 예상 차액 | LiteLLM 단가와 하위 실행별 예상 차액 |
|
|
69
|
+
|
|
70
|
+
두 연동을 함께 설치할 수 있습니다. `--agent codex`를 붙이지 않으면 기존 Claude 동작을 유지합니다.
|
|
71
|
+
README의 금액 수치는 Claude Code 세션 기록에서 나온 값이며 Codex 벤치마크가 아닙니다.
|
|
72
|
+
|
|
41
73
|
## 지원 기능
|
|
42
74
|
|
|
43
75
|
| 기능 | Codex 동작 |
|
package/docs/CODEX.md
CHANGED
|
@@ -72,6 +72,40 @@ fails, the preference stays unchanged. Review new or changed definitions in
|
|
|
72
72
|
`/hooks`; registration does not grant trust. Turning a feature off keeps the
|
|
73
73
|
shared hooks needed by other features.
|
|
74
74
|
|
|
75
|
+
## Command Quick Reference
|
|
76
|
+
|
|
77
|
+
Commands default to Claude Code. Add `--agent codex` to select Codex.
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npm i -g sprag-cli --ignore-scripts # Codex-only install, leaves Claude Code alone
|
|
81
|
+
sprag install --agent codex
|
|
82
|
+
sprag --agent codex --days 7
|
|
83
|
+
sprag panel --agent codex # live companion in a separate terminal
|
|
84
|
+
sprag --statusline --agent codex --text
|
|
85
|
+
sprag doctor --agent codex # troubleshooting only
|
|
86
|
+
sprag route-scan --agent codex
|
|
87
|
+
sprag seed --agent codex
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Review and trust the new hooks in Codex's `/hooks` menu; Codex requires that
|
|
91
|
+
step. The native Codex footer does not accept shell commands, so Sprag provides
|
|
92
|
+
a terminal statusline and a panel instead. Run `sprag capabilities --agent codex`
|
|
93
|
+
for the support matrix.
|
|
94
|
+
|
|
95
|
+
## Claude Code And Codex Compared
|
|
96
|
+
|
|
97
|
+
| Workflow | Claude Code | Codex |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| Session instructions | `CLAUDE.md` and hooks | `AGENTS.md` and native hooks |
|
|
100
|
+
| Live telemetry | Native statusline command | Companion panel or tmux layout |
|
|
101
|
+
| Rule candidates | Claude transcripts and tier presets | Codex rollouts and compatible ratchet presets |
|
|
102
|
+
| Model routing | Haiku/Sonnet tier rules | Explicit provider model IDs and scoped rules |
|
|
103
|
+
| Routing ledger | Claude run estimates | Attributed child estimates with LiteLLM prices |
|
|
104
|
+
|
|
105
|
+
Both integrations coexist. Commands without `--agent codex` retain Claude behavior.
|
|
106
|
+
The dollar figures in the README come from Claude Code sessions, not from a Codex
|
|
107
|
+
benchmark.
|
|
108
|
+
|
|
75
109
|
## Supported Workflows
|
|
76
110
|
|
|
77
111
|
| Feature | Codex behavior |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.68.0",
|
|
4
|
+
"description": "Cut Claude Code and Codex token costs: cheaper-model routing, compact Markdown for documents, and a live statusline of spend and savings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sprag": "bin/cli.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"NOTICE"
|
|
35
35
|
],
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
37
|
+
"node": ">=20"
|
|
38
38
|
},
|
|
39
39
|
"keywords": [
|
|
40
40
|
"claude-code",
|
|
@@ -52,7 +52,14 @@
|
|
|
52
52
|
"subagent",
|
|
53
53
|
"prompt-caching",
|
|
54
54
|
"haiku",
|
|
55
|
-
"cli"
|
|
55
|
+
"cli",
|
|
56
|
+
"token",
|
|
57
|
+
"tokens",
|
|
58
|
+
"cost",
|
|
59
|
+
"llm-cost",
|
|
60
|
+
"cost-optimization",
|
|
61
|
+
"openai-codex",
|
|
62
|
+
"pptx-to-markdown"
|
|
56
63
|
],
|
|
57
64
|
"license": "Apache-2.0",
|
|
58
65
|
"repository": {
|
|
@@ -40,6 +40,13 @@
|
|
|
40
40
|
{
|
|
41
41
|
"ko": "지침이나 룰이 지켜지지 않아 같은 품질 문제가 재발하면 문구를 다시 쓰지 말 것 → 그 조항이 기계로 판정 가능한지부터 따지고(문자열 비교나 정규식으로 결정되면 판정 가능), 가능하면 쓰기 시점 훅이나 테스트 같은 차단 지점을 추가한다. 판단이 필요한 잔여 부분만 지침에 남기고 그 경계를 지침에 적는다. 실제 코퍼스에 돌려 오탐 건수를 센 뒤 좁히고, 회귀 테스트를 같은 커밋에 넣는다",
|
|
42
42
|
"en": "When a guideline keeps being violated and the same quality defect recurs, do NOT reword the instruction → first ask whether the clause is machine-decidable (a string comparison or regex settles it), and if so add an enforcement point such as a write-time hook or a test. Leave only the judgement remainder in the prose and state that boundary there. Measure the false-positive count on a real corpus, narrow until it is acceptable, and land a regression test in the same commit"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"ko": "여러 파일에 걸친 구현을 맡으면 → 설계와 스펙 확정은 메인 모델이 하고 코드 작성은 서브에이전트에 위임한다(Claude는 `model: sonnet`, Codex는 명시적 매핑을 우선해 해석한 T1을 쓰며 haiku나 T2에는 구현을 맡기지 않는다). 위임 프롬프트에는 설계, 수정할 파일, 제약, 완료 기준, 테스트 명령, 예산, 권장 모델과 그 이유를 적어 다른 문맥 없이 실행할 수 있게 한다. 이미 읽은 파일의 약 50줄 이하 변경이나 의존 단계가 하나뿐인 짧은 구현, 그리고 위임 도구나 하위 모델을 쓸 수 없는 경우는 메인 모델이 직접 처리한다. 위임한 구현은 끝날 때까지 기다리고 같은 구현을 중복 실행하지 않으며, 돌아온 diff와 테스트 결과를 메인 모델이 직접 검증한 뒤에 완료로 보고한다",
|
|
46
|
+
"en": "When an implementation spans several files → the main model settles the design and specification, then delegates the code writing to a subagent (Claude: `model: sonnet`; Codex: its resolved T1, explicit mapping first; never haiku or T2 for implementation). Write the delegation prompt so it runs without other context: design, files to change, constraints, completion criteria, test commands, budget, and the recommended model with a one-line reason. Keep changes of roughly 50 lines or less in files already read, a single short dependency chain, or cases where the subagent tools or the lower model are unavailable on the main model. Wait for the delegated work instead of duplicating it, and verify the returned diff and the actual test results before reporting completion",
|
|
47
|
+
"agents": [
|
|
48
|
+
"claude"
|
|
49
|
+
]
|
|
43
50
|
}
|
|
44
51
|
]
|
|
45
52
|
}
|
package/src/agents.js
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* part that actually saves tokens and it is universal, so `model: haiku` is the
|
|
9
9
|
* default phrasing — the agent name is only added when the file is really
|
|
10
10
|
* there, which keeps the tool-restricted preset in play for users who have it.
|
|
11
|
+
*
|
|
12
|
+
* A rule that names a tier alone (`sonnet`, `haiku`) resolves to that tier's
|
|
13
|
+
* delegation-only agent (`sonnet-worker`, `haiku-explore`, installed by
|
|
14
|
+
* delegation-agents.js) when its file exists. A phrase that names an agent
|
|
15
|
+
* always carries a general-purpose fallback, because Cowork reads no agents
|
|
16
|
+
* directory and a copied rule would otherwise name an agent that is not there.
|
|
11
17
|
*/
|
|
12
18
|
|
|
13
19
|
import { existsSync } from 'node:fs';
|
|
@@ -20,9 +26,9 @@ const AGENT_MODEL = {
|
|
|
20
26
|
'haiku-runner': 'haiku',
|
|
21
27
|
'haiku-translate': 'haiku',
|
|
22
28
|
'sonnet-worker': 'sonnet',
|
|
23
|
-
// A T1 rule names its target by tier alone (`agent: 'sonnet'`)
|
|
24
|
-
//
|
|
25
|
-
//
|
|
29
|
+
// A T1 rule names its target by tier alone (`agent: 'sonnet'`). Without these
|
|
30
|
+
// the phrase fell through to the haiku default, and a sonnet rule told the
|
|
31
|
+
// session to delegate to haiku. resolveAgentName maps them to an agent file.
|
|
26
32
|
sonnet: 'sonnet',
|
|
27
33
|
haiku: 'haiku',
|
|
28
34
|
};
|
|
@@ -43,9 +49,23 @@ export function agentExists(name, root = process.cwd()) {
|
|
|
43
49
|
existsSync(join(claudeUserDir(), 'agents', `${name}.md`));
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
/** Agent a bare tier name stands for when that agent's file exists. */
|
|
53
|
+
const TIER_AGENT = { sonnet: 'sonnet-worker', haiku: 'haiku-explore' };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The agent a rule target really names on this machine, or null when none is
|
|
57
|
+
* installed. A name wins when its own file exists; a bare tier name falls back
|
|
58
|
+
* to its delegation-only agent.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveAgentName(name, root = process.cwd()) {
|
|
61
|
+
if (agentExists(name, root)) return name;
|
|
62
|
+
const alt = TIER_AGENT[name];
|
|
63
|
+
return alt && agentExists(alt, root) ? alt : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
/**
|
|
47
67
|
* Korean phrase for a delegation target, e.g.
|
|
48
|
-
* agent present → `haiku-explore(model 인자
|
|
68
|
+
* agent present → `haiku-explore(model 인자 생략. …general-purpose에 model: haiku)`
|
|
49
69
|
* agent absent → `model: haiku`
|
|
50
70
|
*
|
|
51
71
|
* A named agent carries its model in its own definition, and the Agent tool's
|
|
@@ -56,18 +76,20 @@ export function agentExists(name, root = process.cwd()) {
|
|
|
56
76
|
* now says to leave the argument out.
|
|
57
77
|
*/
|
|
58
78
|
export function agentPhrase(name, { model, root } = {}) {
|
|
59
|
-
const
|
|
60
|
-
|
|
79
|
+
const resolved = resolveAgentName(name, root);
|
|
80
|
+
if (!resolved) return `model: ${model || AGENT_MODEL[name] || 'haiku'}`;
|
|
81
|
+
const tier = AGENT_MODEL[resolved] || model || 'haiku';
|
|
82
|
+
return `${resolved}(model 인자 생략. 이 에이전트가 없거나 MCP 도구가 필요하면 general-purpose에 model: ${tier})`;
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
/**
|
|
64
86
|
* English phrase, including the article, e.g.
|
|
65
|
-
* agent present → `the haiku-explore subagent (omit the model argument)`
|
|
87
|
+
* agent present → `the haiku-explore subagent (omit the model argument; …)`
|
|
66
88
|
* agent absent → `a model: haiku subagent`
|
|
67
89
|
*/
|
|
68
90
|
export function agentPhraseEn(name, { model, root } = {}) {
|
|
69
|
-
const
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
const resolved = resolveAgentName(name, root);
|
|
92
|
+
if (!resolved) return `a model: ${model || AGENT_MODEL[name] || 'haiku'} subagent`;
|
|
93
|
+
const tier = AGENT_MODEL[resolved] || model || 'haiku';
|
|
94
|
+
return `the ${resolved} subagent (omit the model argument; if it is unavailable or MCP tools are needed, use general-purpose with model: ${tier})`;
|
|
73
95
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
-
import { codexUserDir } from '
|
|
5
|
-
import { userDataDir } from '
|
|
6
|
-
import { writeViaTmp } from '
|
|
7
|
-
import { readCodexSnapshot } from './
|
|
8
|
-
import { loadCodexModelRules } from './
|
|
9
|
-
import { loadCodexLedger } from './
|
|
10
|
-
import { codexRulesInReview, codexSharedRuleHealth } from './
|
|
4
|
+
import { codexUserDir } from '../agent.js';
|
|
5
|
+
import { userDataDir } from '../paths.js';
|
|
6
|
+
import { writeViaTmp } from '../state-file.js';
|
|
7
|
+
import { readCodexSnapshot } from './parser.js';
|
|
8
|
+
import { loadCodexModelRules } from './delegation.js';
|
|
9
|
+
import { loadCodexLedger } from './ledger.js';
|
|
10
|
+
import { codexRulesInReview, codexSharedRuleHealth } from './rule-health.js';
|
|
11
11
|
|
|
12
12
|
const RESET_TOLERANCE_S = 300;
|
|
13
13
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
@@ -3,8 +3,8 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { parse } from 'smol-toml';
|
|
6
|
-
import { codexUserDir } from '
|
|
7
|
-
import { pickBudgetSource } from '
|
|
6
|
+
import { codexUserDir } from '../agent.js';
|
|
7
|
+
import { pickBudgetSource } from '../litellm-budget.js';
|
|
8
8
|
|
|
9
9
|
const run = promisify(execFile);
|
|
10
10
|
|
|
@@ -2,15 +2,15 @@ import { readFileSync, mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { parse } from 'smol-toml';
|
|
5
|
-
import { codexUserDir } from '
|
|
6
|
-
import { userDataDir } from '
|
|
7
|
-
import { writeViaTmp } from '
|
|
8
|
-
import { codexBudgetProvider, codexProviderKey } from './
|
|
9
|
-
import { codexCachePolicy } from './
|
|
10
|
-
import { PRICE_MAX_AGE_MS, gatewayPrices } from '
|
|
11
|
-
import { OPENAI_LIST_PRICES, OPENAI_PRICES_CHECKED_AT } from '
|
|
12
|
-
|
|
13
|
-
export { PRICE_MAX_AGE_MS, rate, deploymentPrice, gatewayPrices, codexRunCost } from '
|
|
5
|
+
import { codexUserDir } from '../agent.js';
|
|
6
|
+
import { userDataDir } from '../paths.js';
|
|
7
|
+
import { writeViaTmp } from '../state-file.js';
|
|
8
|
+
import { codexBudgetProvider, codexProviderKey } from './budget.js';
|
|
9
|
+
import { codexCachePolicy } from './cache.js';
|
|
10
|
+
import { PRICE_MAX_AGE_MS, gatewayPrices } from '../gateway-prices.js';
|
|
11
|
+
import { OPENAI_LIST_PRICES, OPENAI_PRICES_CHECKED_AT } from '../openai-prices.js';
|
|
12
|
+
|
|
13
|
+
export { PRICE_MAX_AGE_MS, rate, deploymentPrice, gatewayPrices, codexRunCost } from '../gateway-prices.js';
|
|
14
14
|
|
|
15
15
|
export const CACHE_POLICY_MAX_AGE_MS = 300000;
|
|
16
16
|
const OPENAI_POLICY = 'https://developers.openai.com/api/docs/guides/prompt-caching#cache-lifetime';
|
|
@@ -2,8 +2,8 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { parse } from 'smol-toml';
|
|
5
|
-
import { codexUserDir, codexModelName } from '
|
|
6
|
-
import { codexDelegationStatus } from './
|
|
5
|
+
import { codexUserDir, codexModelName } from '../agent.js';
|
|
6
|
+
import { codexDelegationStatus } from './ledger.js';
|
|
7
7
|
|
|
8
8
|
/** Local introspection only. A CLI feature flag cannot prove a model received a tool. */
|
|
9
9
|
export function codexDelegationDiagnostics({ home = codexUserDir(), model = codexModelName({ home }), sessionId, dir,
|
|
@@ -2,17 +2,17 @@ import { readFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { parse } from 'smol-toml';
|
|
5
|
-
import { userDataDir } from '
|
|
6
|
-
import { writeViaTmp } from '
|
|
7
|
-
import { codexUserDir, codexProviderName } from '
|
|
8
|
-
import { categorize, ESCALATE_RE, EDIT_RE } from '
|
|
9
|
-
import { looksPasted } from '
|
|
10
|
-
import { newRouteId, recordCodexDelegation } from './
|
|
11
|
-
import { readCodexTailLines } from './
|
|
12
|
-
import { loadConfig } from '
|
|
13
|
-
import { loadSharedModelRules, sharedModelRulesForProject, sharedRuleBounds, SHARED_RULE_TIERS } from '
|
|
14
|
-
import { codexPriceBook, readCodexPrices } from './
|
|
15
|
-
import { codexRunCost } from '
|
|
5
|
+
import { userDataDir } from '../paths.js';
|
|
6
|
+
import { writeViaTmp } from '../state-file.js';
|
|
7
|
+
import { codexUserDir, codexProviderName } from '../agent.js';
|
|
8
|
+
import { categorize, ESCALATE_RE, EDIT_RE } from '../route-scan.js';
|
|
9
|
+
import { looksPasted } from '../route-inject.js';
|
|
10
|
+
import { newRouteId, recordCodexDelegation } from './ledger.js';
|
|
11
|
+
import { readCodexTailLines } from './parser.js';
|
|
12
|
+
import { loadConfig } from '../config.js';
|
|
13
|
+
import { loadSharedModelRules, sharedModelRulesForProject, sharedRuleBounds, SHARED_RULE_TIERS } from '../shared-model-rules.js';
|
|
14
|
+
import { codexPriceBook, readCodexPrices } from './cache-policy.js';
|
|
15
|
+
import { codexRunCost } from '../gateway-prices.js';
|
|
16
16
|
|
|
17
17
|
export const CODEX_ROUTE_CATEGORIES = ['paste', 'translate', 'explore', 'read', 'check', 'run'];
|
|
18
18
|
const MARKER = '<!-- sprag:codex:delegation -->';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdirSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
3
|
import { extname, join, resolve } from 'node:path';
|
|
4
|
-
import { codexUserDir } from '
|
|
5
|
-
import { userDataDir } from '
|
|
6
|
-
import { writeViaTmp } from '
|
|
7
|
-
import { readResetMarks, isBeforeReset } from '
|
|
8
|
-
import { codexPriceBook, readCodexPrices } from './
|
|
4
|
+
import { codexUserDir } from '../agent.js';
|
|
5
|
+
import { userDataDir } from '../paths.js';
|
|
6
|
+
import { writeViaTmp } from '../state-file.js';
|
|
7
|
+
import { readResetMarks, isBeforeReset } from '../saved-reset.cjs';
|
|
8
|
+
import { codexPriceBook, readCodexPrices } from './cache-policy.js';
|
|
9
9
|
|
|
10
10
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
11
11
|
const ledgerDir = ({ home = codexUserDir(), dir = userDataDir() } = {}) => join(dir, 'codex-doc2md', hash(resolve(home)));
|
|
@@ -2,12 +2,12 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { realpathSync, readFileSync, writeFileSync, renameSync, rmSync, openSync, readSync, closeSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, resolve } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { recordCodexDocument } from './
|
|
6
|
-
import { readCodexSnapshot } from './
|
|
5
|
+
import { recordCodexDocument } from './doc2md-ledger.js';
|
|
6
|
+
import { readCodexSnapshot } from './parser.js';
|
|
7
7
|
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
|
-
const doc2md = require('
|
|
10
|
-
const lint = require('
|
|
9
|
+
const doc2md = require('../doc2md.cjs');
|
|
10
|
+
const lint = require('../korean-lint.cjs');
|
|
11
11
|
const shellTools = new Set(['Bash', 'exec_command', 'shell_command', 'shell']);
|
|
12
12
|
|
|
13
13
|
/** Hook identity, never the user's default model or another project's session. */
|