sprag-cli 3.67.0 → 3.69.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 +33 -66
- package/README.md +41 -74
- package/bin/cli.js +150 -62
- package/docs/CODEX.ko.md +32 -0
- package/docs/CODEX.md +34 -0
- package/package.json +15 -4
- 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/install.js +10 -7
- package/src/commands/korean.js +1 -1
- package/src/commands/litellm-budget.js +56 -0
- package/src/commands/saved.js +60 -5
- package/src/commands/uninstall.js +3 -3
- package/src/config.js +4 -0
- package/src/delegation-agents.js +13 -0
- package/src/first-run-note.js +16 -0
- package/src/formatters/statusline.js +725 -504
- package/src/formatters/table.js +33 -2
- package/src/glyphs.js +2 -0
- package/src/installer.js +77 -1
- package/src/model-alias.js +45 -60
- package/src/model-rules.js +25 -4
- package/src/model-share.js +136 -0
- package/src/parser.js +4 -13
- package/src/session-records.js +2 -8
- package/src/star-note.js +62 -0
- package/src/statusline-base.js +9 -3
- package/src/stdin-payload.js +1 -1
- package/src/transcript-lines.js +40 -0
- package/src/changelog.js +0 -70
- /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,
|
|
@@ -58,6 +59,7 @@ import { chipForIssues } from '../src/advice.js';
|
|
|
58
59
|
import { debug } from '../src/debug.js';
|
|
59
60
|
import { createArgs } from '../src/cli-args.js';
|
|
60
61
|
import { readUpdateChip } from '../src/update-notice.js';
|
|
62
|
+
import { readStarChip } from '../src/star-note.js';
|
|
61
63
|
import { selectAgent } from '../src/agent.js';
|
|
62
64
|
|
|
63
65
|
const selection = (() => {
|
|
@@ -87,12 +89,78 @@ function tableColor() {
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
// Subcommands this build knows how to run. Used only by the guard below.
|
|
92
|
+
/**
|
|
93
|
+
* `--segments a,b,c` as a list, or null for the default order. One parser for
|
|
94
|
+
* the demo, the populated line and the no-session line, so `--segments harness`
|
|
95
|
+
* means the same chip set on each of them.
|
|
96
|
+
*/
|
|
97
|
+
function segmentsFromArgs() {
|
|
98
|
+
const raw = getArg('--segments');
|
|
99
|
+
return raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
const KNOWN_SUBCOMMANDS = new Set([
|
|
91
103
|
'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean', 'cohesion',
|
|
92
104
|
'doc2md', 'delegate', 'harness', 'route-scan', 'compact-window', 'update-check', 'upgrade',
|
|
93
105
|
'seed', 'litellm-budget', 'profile-map', 'feedback', 'saved', 'cowork',
|
|
94
106
|
]);
|
|
95
107
|
|
|
108
|
+
// Flags each entry path reads, collected from the hasFlag/getArg/numArg calls in
|
|
109
|
+
// this file, src/statusline-mode.js, src/hook.cjs and the install/uninstall
|
|
110
|
+
// commands. `value` flags take an argument (`--days 3` or `--days=3`); `bool`
|
|
111
|
+
// flags do not. --agent is not listed: selectAgent() removes it before `args`
|
|
112
|
+
// exists. Only the default path (report, statusline, hook management) and
|
|
113
|
+
// install/uninstall are checked; every other subcommand parses its own arguments.
|
|
114
|
+
// Keep these in step with the code that reads the flag: a flag read but not
|
|
115
|
+
// listed is rejected, and a flag listed but never read is silently ignored again.
|
|
116
|
+
const FLAG_SPECS = {
|
|
117
|
+
report: {
|
|
118
|
+
value: ['--format', '-f', '--project', '-p', '--days', '-d', '--hours', '--demo',
|
|
119
|
+
'--demo-cycle-sec', '--segments', '--exclude-session', '--threshold'],
|
|
120
|
+
bool: ['--statusline', '--verbose', '--no-verbose', '--compact', '--no-color', '--icon',
|
|
121
|
+
'--no-icon', '--text', '--narrow', '--no-timer', '--single-line', '--demo-label',
|
|
122
|
+
'--version', '-v', '--help', '-h', '--hook', '--hook-run', '--install-hook', '--uninstall-hook'],
|
|
123
|
+
},
|
|
124
|
+
install: { value: [], bool: ['--force', '--manual', '--yes', '--no-input', '--help', '-h'] },
|
|
125
|
+
// --yes and --no-input are the explicit spelling of "do not ask", and
|
|
126
|
+
// uninstall never asks, so they are accepted as no-ops.
|
|
127
|
+
uninstall: { value: [], bool: ['--purge', '--yes', '--no-input', '--help', '-h'] },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Which spec applies to this invocation, or null for a subcommand that parses its own arguments. */
|
|
131
|
+
function flagSpecFor(first) {
|
|
132
|
+
if (!first || first.startsWith('-')) return FLAG_SPECS.report;
|
|
133
|
+
return FLAG_SPECS[first] || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The flags in `argv` that `spec` does not list. A value flag swallows the next
|
|
138
|
+
* token the way getArg() does, so `--exclude-session --x` reads `--x` as a path,
|
|
139
|
+
* not as a flag, and both `--days 3` and `--days=3` are understood. A bool flag
|
|
140
|
+
* written `--no-color=1` is reported too: hasFlag() compares whole tokens, so
|
|
141
|
+
* that form was never honoured. Everything after a bare `--` is not a flag.
|
|
142
|
+
*
|
|
143
|
+
* @returns {string[]} the offending tokens as typed
|
|
144
|
+
*/
|
|
145
|
+
function findUnknownFlags(argv, spec) {
|
|
146
|
+
const value = new Set(spec.value);
|
|
147
|
+
const bool = new Set(spec.bool);
|
|
148
|
+
const unknown = [];
|
|
149
|
+
for (let i = 0; i < argv.length; i++) {
|
|
150
|
+
const tok = argv[i];
|
|
151
|
+
if (tok === '--') break;
|
|
152
|
+
if (tok.length < 2 || !tok.startsWith('-')) continue;
|
|
153
|
+
const eq = tok.indexOf('=');
|
|
154
|
+
const name = eq === -1 ? tok : tok.slice(0, eq);
|
|
155
|
+
if (value.has(name)) {
|
|
156
|
+
if (eq === -1) i++;
|
|
157
|
+
} else if (!bool.has(name) || eq !== -1) {
|
|
158
|
+
unknown.push(tok);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return unknown;
|
|
162
|
+
}
|
|
163
|
+
|
|
96
164
|
const USAGE = `sprag — Claude Code and Codex agent harness
|
|
97
165
|
|
|
98
166
|
Usage:
|
|
@@ -171,6 +239,34 @@ async function main() {
|
|
|
171
239
|
return;
|
|
172
240
|
}
|
|
173
241
|
|
|
242
|
+
// Refuse what this build cannot honour, before anything is scanned or written.
|
|
243
|
+
// An unknown first word used to fall into the 30-day report, and an unknown
|
|
244
|
+
// flag was dropped without a word: `sprag install --dry-run` ran the real
|
|
245
|
+
// install and rewrote settings.json. Exit 2 is the usage-error code.
|
|
246
|
+
if (args[0] && !args[0].startsWith('-') && !KNOWN_SUBCOMMANDS.has(args[0])) {
|
|
247
|
+
console.error(`Unknown command: ${args[0]}`);
|
|
248
|
+
console.error('Run `sprag --help` for the list of commands.');
|
|
249
|
+
process.exitCode = 2;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const flagSpec = flagSpecFor(args[0]);
|
|
253
|
+
const unknownFlags = flagSpec ? findUnknownFlags(args, flagSpec) : [];
|
|
254
|
+
if (unknownFlags.length > 0) {
|
|
255
|
+
// The statusline and the PostToolUse hook run from a fixed command in
|
|
256
|
+
// settings.json, and a typo or a flag from a newer release there must not
|
|
257
|
+
// blank the line or fail the hook. Warn and carry on; every other caller is
|
|
258
|
+
// a person at a prompt who is better told.
|
|
259
|
+
const lenient = hasFlag('--statusline') || getArg('--format') === 'statusline' || hasFlag('--hook-run');
|
|
260
|
+
if (lenient) {
|
|
261
|
+
for (const f of unknownFlags) console.error(`Warning: Unknown option: ${f} (ignored)`);
|
|
262
|
+
} else {
|
|
263
|
+
for (const f of unknownFlags) console.error(`Unknown option: ${f}`);
|
|
264
|
+
console.error('Run `sprag --help` for usage.');
|
|
265
|
+
process.exitCode = 2;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
174
270
|
// Subcommand: last — print the most recent warning + how to handle it.
|
|
175
271
|
// Designed for the auto-trigger skill so the user immediately sees
|
|
176
272
|
// "what just fired and how to fix it" without having to read the whole
|
|
@@ -346,56 +442,9 @@ async function main() {
|
|
|
346
442
|
}
|
|
347
443
|
|
|
348
444
|
// Subcommand: litellm-budget · LiteLLM 게이트웨이 키의 max_budget/spend 조회.
|
|
349
|
-
// sprag litellm-budget
|
|
350
|
-
// sprag litellm-budget --json # 캐시 원본 JSON 출력
|
|
351
|
-
// sprag litellm-budget --refresh # 지금 프록시에 물어봄 (detached 자식이 사용)
|
|
445
|
+
// sprag litellm-budget [--json | --refresh]
|
|
352
446
|
if (args[0] === 'litellm-budget') {
|
|
353
|
-
|
|
354
|
-
await import('../src/litellm-budget.js');
|
|
355
|
-
const quiet = hasFlag('--quiet');
|
|
356
|
-
if (hasFlag('--refresh')) {
|
|
357
|
-
try {
|
|
358
|
-
// apiKeyHelper 가 토큰 수명(TTL)을 소유합니다. 헬퍼를 한 번만 불러 그
|
|
359
|
-
// 키를 예산 갱신과 게이트웨이 모델맵 갱신 양쪽에 재사용합니다. 갱신
|
|
360
|
-
// 경로마다 resolveKey() 를 다시 부르면 사본을 만드는 셈이 되고, 헬퍼
|
|
361
|
-
// 호출 횟수도 그만큼 늘어납니다.
|
|
362
|
-
const key = resolveKey();
|
|
363
|
-
let next = null;
|
|
364
|
-
if (key) {
|
|
365
|
-
next = await refreshBudgetState(process.env, fetch, key);
|
|
366
|
-
const { maybeRefreshGatewayModelMap } = await import('../src/litellm-models.js');
|
|
367
|
-
await maybeRefreshGatewayModelMap({ base: gatewayBase(), key });
|
|
368
|
-
}
|
|
369
|
-
if (!quiet) console.log(JSON.stringify(next, null, 2));
|
|
370
|
-
} catch (e) {
|
|
371
|
-
debug('litellm-budget:refresh', e);
|
|
372
|
-
if (!quiet) console.error(`litellm-budget refresh failed: ${e.message}`);
|
|
373
|
-
process.exitCode = 1;
|
|
374
|
-
}
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
// 읽기 경로는 캐시만 보므로 키를 요구하지 않습니다 (apiKeyHelper 환경 배려).
|
|
378
|
-
const base = gatewayBase();
|
|
379
|
-
if (!base) {
|
|
380
|
-
console.log('게이트웨이가 감지되지 않았습니다 (ANTHROPIC_BASE_URL 필요).');
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
const state = readBudgetState();
|
|
384
|
-
if (hasFlag('--json')) {
|
|
385
|
-
console.log(JSON.stringify(state, null, 2));
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (state.base !== base || state.maxBudget === undefined) {
|
|
389
|
-
console.log('예산 캐시가 비어 있습니다. `sprag litellm-budget --refresh` 로 먼저 조회하십시오.');
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
// Same resolver the statusline uses: this report draws a gauge too, and a
|
|
393
|
-
// glyph the terminal's font lacks garbles here exactly as it does there.
|
|
394
|
-
const { resolveLabelMode } = await import('../src/statusline-mode.js');
|
|
395
|
-
const { statuslineDefaults } = await import('../src/config.js');
|
|
396
|
-
const { mode: labelMode } = resolveLabelMode({ hasFlag, cfg: statuslineDefaults() });
|
|
397
|
-
for (const line of formatBudgetReport(state, new Date(), labelMode)) console.log(line);
|
|
398
|
-
return;
|
|
447
|
+
return (await import('../src/commands/litellm-budget.js')).run({ hasFlag });
|
|
399
448
|
}
|
|
400
449
|
|
|
401
450
|
// Subcommand: profile-map · LiteLLM 게이트웨이의 프로파일 ID → 모델 별칭
|
|
@@ -484,10 +533,7 @@ async function main() {
|
|
|
484
533
|
: (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
|
|
485
534
|
const showTimer = hasFlag('--no-timer') ? false : cfg.timer;
|
|
486
535
|
const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
|
|
487
|
-
const
|
|
488
|
-
const segments = segmentsArg
|
|
489
|
-
? segmentsArg.split(',').map((s) => s.trim()).filter(Boolean)
|
|
490
|
-
: null;
|
|
536
|
+
const segments = segmentsFromArgs();
|
|
491
537
|
const out = formatReport(data, {
|
|
492
538
|
color: colorOk,
|
|
493
539
|
verbose: isVerbose,
|
|
@@ -591,8 +637,8 @@ async function main() {
|
|
|
591
637
|
const { withBaseStatusline } = await import('../src/statusline-base.js');
|
|
592
638
|
console.log(withBaseStatusline(formatNoSession(
|
|
593
639
|
{ caps, model, effort, windowLabel, version: PKG_VERSION, update: readUpdateChip(PKG_VERSION) },
|
|
594
|
-
{ color: colorOk, mode: labelMode },
|
|
595
|
-
)));
|
|
640
|
+
{ color: colorOk, mode: labelMode, segments: segmentsFromArgs() },
|
|
641
|
+
), { input: readStdinRaw() }));
|
|
596
642
|
return;
|
|
597
643
|
}
|
|
598
644
|
// JSON mode stays JSON. A caller that asked for machine-readable output
|
|
@@ -768,12 +814,13 @@ async function main() {
|
|
|
768
814
|
// 이미 파싱한 세션을 재사용하고, 사용자가 창을 줄여 둔 경우(mode 1d 등)에만
|
|
769
815
|
// 월초까지 다시 파싱한다. 파싱 결과는 세션 캐시가 받아 주므로 싸다.
|
|
770
816
|
let monthSpendInfo = null;
|
|
817
|
+
let monthSessions = null;
|
|
771
818
|
if (isStatusline) {
|
|
772
819
|
try {
|
|
773
820
|
const { monthSpend, monthStartMs } = await import('../src/month-spend.js');
|
|
774
821
|
const since = monthStartMs();
|
|
775
822
|
const daysNeeded = (Date.now() - since) / 86400000;
|
|
776
|
-
|
|
823
|
+
monthSessions = days >= daysNeeded
|
|
777
824
|
? sessions
|
|
778
825
|
: await parseAllSessions({ days: Math.max(1, Math.ceil(daysNeeded)), projectFilter, sinceMs: since });
|
|
779
826
|
// 월 경계에 걸친 세션은 월초 이후 요청만 남긴다. 그러지 않으면 1일 자정을
|
|
@@ -784,6 +831,22 @@ async function main() {
|
|
|
784
831
|
}
|
|
785
832
|
}
|
|
786
833
|
|
|
834
|
+
// 모델별 사용 비중: `sprag saved` 와 같은 기간(라우팅 초기화 기록, 없으면 월초)의
|
|
835
|
+
// 비용 기준 분포. 이미 파싱한 월간 세션을 재사용한다. 실패하면 칩만 빠진다.
|
|
836
|
+
let modelShare = null;
|
|
837
|
+
if (isStatusline) {
|
|
838
|
+
try {
|
|
839
|
+
const { statuslineDefaults } = await import('../src/config.js');
|
|
840
|
+
if (statuslineDefaults().modelShare) {
|
|
841
|
+
const { loadModelShare } = await import('../src/model-share.js');
|
|
842
|
+
const { userDataDir } = await import('../src/paths.js');
|
|
843
|
+
modelShare = await loadModelShare({ dir: userDataDir(), env: process.env, projectFilter, monthSessions: monthSessions || undefined });
|
|
844
|
+
}
|
|
845
|
+
} catch (e) {
|
|
846
|
+
debug('model-share', e);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
787
850
|
// Rolling week/month/lifetime totals from the delegation ledger — the
|
|
788
851
|
// statusline's headline line. Falls back to null (chip hidden) on any error.
|
|
789
852
|
// A lookup, never a scan: the statusline re-renders every few seconds and a
|
|
@@ -878,8 +941,14 @@ async function main() {
|
|
|
878
941
|
options: { days, windowHours, windowLabel, version: PKG_VERSION },
|
|
879
942
|
// Cached-only; the background refresh it may trigger lands on a later render.
|
|
880
943
|
update: format === 'statusline' ? readUpdateChip(PKG_VERSION) : null,
|
|
944
|
+
// The totals are already in hand above; passing them spares a second read of
|
|
945
|
+
// both ledgers on every render.
|
|
946
|
+
star: format === 'statusline'
|
|
947
|
+
? readStarChip({ savedUsd: delegationSaved + (Number(doc2mdTotals?.total) || 0) })
|
|
948
|
+
: null,
|
|
881
949
|
lastActivity,
|
|
882
950
|
monthSpend: monthSpendInfo,
|
|
951
|
+
modelShare,
|
|
883
952
|
spikeReport,
|
|
884
953
|
contextWindow,
|
|
885
954
|
ctxLive,
|
|
@@ -922,10 +991,7 @@ async function main() {
|
|
|
922
991
|
const colorOk =
|
|
923
992
|
!hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
|
|
924
993
|
|
|
925
|
-
const
|
|
926
|
-
const segments = segmentsArg
|
|
927
|
-
? segmentsArg.split(',').map((s) => s.trim()).filter(Boolean)
|
|
928
|
-
: null;
|
|
994
|
+
const segments = segmentsFromArgs();
|
|
929
995
|
output = formatReport(data, {
|
|
930
996
|
color: colorOk,
|
|
931
997
|
verbose: isVerbose,
|
|
@@ -938,13 +1004,35 @@ async function main() {
|
|
|
938
1004
|
singleLine: hasFlag('--single-line'),
|
|
939
1005
|
});
|
|
940
1006
|
} else {
|
|
1007
|
+
// The first screen is the pitch: what share of recent work a cheaper model
|
|
1008
|
+
// could take. The scan is local and writes only sprag's own cache file;
|
|
1009
|
+
// --project skips it because the scan covers all projects, not one.
|
|
1010
|
+
let routing = null;
|
|
1011
|
+
if (!projectFilter && process.env.CTS_NO_ROUTE_SCAN !== '1') {
|
|
1012
|
+
try {
|
|
1013
|
+
const rs = await import('../src/route-scan.js');
|
|
1014
|
+
let cache = rs.readRouteScan();
|
|
1015
|
+
if (!cache) {
|
|
1016
|
+
process.stderr.write('Scanning for delegation candidates (first run)...\n');
|
|
1017
|
+
cache = await rs.runRouteScan({ days: 14 });
|
|
1018
|
+
}
|
|
1019
|
+
routing = {
|
|
1020
|
+
days: cache.days,
|
|
1021
|
+
total: cache.totalEpisodes,
|
|
1022
|
+
easy: cache.easyEpisodes,
|
|
1023
|
+
scannedAt: cache.scannedAt,
|
|
1024
|
+
candidates: rs.openCandidates(cache).slice(0, 3),
|
|
1025
|
+
};
|
|
1026
|
+
} catch (e) { debug('report:routing', e); }
|
|
1027
|
+
}
|
|
941
1028
|
const { formatReport } = await import('../src/formatters/table.js');
|
|
942
|
-
output = formatReport(data, { color: tableColor() });
|
|
1029
|
+
output = formatReport({ ...data, routing }, { color: tableColor() });
|
|
943
1030
|
}
|
|
944
1031
|
|
|
945
1032
|
if (format === 'statusline') {
|
|
946
1033
|
const { withBaseStatusline } = await import('../src/statusline-base.js');
|
|
947
|
-
|
|
1034
|
+
// The payload was read above; the base gets the same text, not a second read.
|
|
1035
|
+
output = withBaseStatusline(output, { input: readStdinRaw() });
|
|
948
1036
|
}
|
|
949
1037
|
console.log(output);
|
|
950
1038
|
}
|
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.69.0",
|
|
4
|
+
"description": "Model-delegation router for Claude Code and Codex: lookups, checks and test runs go to cheaper models, documents go in as compact Markdown, and a live statusline shows 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",
|
|
@@ -49,10 +49,21 @@
|
|
|
49
49
|
"claude-code-statusline",
|
|
50
50
|
"statusline",
|
|
51
51
|
"model-routing",
|
|
52
|
+
"model-router",
|
|
53
|
+
"model-delegation",
|
|
54
|
+
"llm-router",
|
|
55
|
+
"claude-code-router",
|
|
52
56
|
"subagent",
|
|
53
57
|
"prompt-caching",
|
|
54
58
|
"haiku",
|
|
55
|
-
"cli"
|
|
59
|
+
"cli",
|
|
60
|
+
"token",
|
|
61
|
+
"tokens",
|
|
62
|
+
"cost",
|
|
63
|
+
"llm-cost",
|
|
64
|
+
"cost-optimization",
|
|
65
|
+
"openai-codex",
|
|
66
|
+
"pptx-to-markdown"
|
|
56
67
|
],
|
|
57
68
|
"license": "Apache-2.0",
|
|
58
69
|
"repository": {
|
|
@@ -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. */
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parse } from 'smol-toml';
|
|
4
|
-
import { codexUserDir } from '
|
|
5
|
-
import { codexHookStatus } from './
|
|
6
|
-
import { createPanelReader } from './
|
|
7
|
-
import { panelHealth } from './
|
|
4
|
+
import { codexUserDir } from '../agent.js';
|
|
5
|
+
import { codexHookStatus } from './installer.js';
|
|
6
|
+
import { createPanelReader } from './panel.js';
|
|
7
|
+
import { panelHealth } from './panel-state.js';
|
|
8
8
|
|
|
9
9
|
export const CODEX_CAPABILITIES = [
|
|
10
10
|
{ feature: 'statusline', support: 'supported', command: 'sprag --statusline --agent codex', note: 'Local terminal output; native footer accepts only Codex item identifiers.' },
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { codexUserDir } from '
|
|
5
|
-
import { loadConfig } from '
|
|
6
|
-
import { koreanStyleEnabled, koreanStyleInjection } from '
|
|
7
|
-
import { codexToolEvents, codexLegacyToolEvent, readCodexTailLines, forkedCopyFilter } from './
|
|
8
|
-
import { implementationDelegationRule } from '
|
|
4
|
+
import { codexUserDir } from '../agent.js';
|
|
5
|
+
import { loadConfig } from '../config.js';
|
|
6
|
+
import { koreanStyleEnabled, koreanStyleInjection } from '../korean-style.js';
|
|
7
|
+
import { codexToolEvents, codexLegacyToolEvent, readCodexTailLines, forkedCopyFilter } from './parser.js';
|
|
8
|
+
import { implementationDelegationRule } from '../implementation-delegation.js';
|
|
9
9
|
|
|
10
10
|
export const CODEX_BEGIN = '<!-- sprag:codex:harness:begin -->';
|
|
11
11
|
export const CODEX_END = '<!-- sprag:codex:harness:end -->';
|