sprag-cli 3.67.0 → 3.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +29 -62
- package/README.md +26 -61
- package/bin/cli.js +122 -61
- package/docs/CODEX.ko.md +32 -0
- package/docs/CODEX.md +34 -0
- package/package.json +11 -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/formatters/statusline.js +713 -504
- package/src/glyphs.js +2 -0
- package/src/installer.js +77 -1
- package/src/model-alias.js +45 -60
- package/src/model-share.js +136 -0
- package/src/parser.js +4 -13
- 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/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": {
|
|
@@ -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 -->';
|
|
@@ -3,25 +3,25 @@ import { spawn } from 'node:child_process';
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { mkdirSync, openSync, readFileSync, readSync, closeSync, statSync } from 'node:fs';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
|
-
import { loadConfig, userLanguage } from '
|
|
7
|
-
import { koreanStyleInjection, koreanStyleEnabled } from '
|
|
8
|
-
import { cohesionInjection } from '
|
|
9
|
-
import { codexHarnessPaths, syncCodexKoreanBlock } from './
|
|
10
|
-
import { writeViaTmp } from '
|
|
11
|
-
import { findProjectRoot } from '
|
|
12
|
-
import { runCodexBrief } from './
|
|
13
|
-
import { codexDocumentTool, codexDocumentNote, convertCodexDocument, codexDocumentContext } from './
|
|
14
|
-
import { codexDelegationTool, codexRouteHint, codexDelegateEnabled } from './
|
|
15
|
-
import { bindCodexSubagent, closeStaleCodexRoutes, unsettledCodexRoutes } from './
|
|
16
|
-
import { seedOfferBlock } from '
|
|
17
|
-
import { readCodexRouteScan, openCodexCandidates, shouldRescanCodex } from './
|
|
18
|
-
import { userDataDir } from '
|
|
19
|
-
import { debug } from '
|
|
20
|
-
import { updateOffer } from '
|
|
6
|
+
import { loadConfig, userLanguage } from '../config.js';
|
|
7
|
+
import { koreanStyleInjection, koreanStyleEnabled } from '../korean-style.js';
|
|
8
|
+
import { cohesionInjection } from '../cohesion.js';
|
|
9
|
+
import { codexHarnessPaths, syncCodexKoreanBlock } from './harness.js';
|
|
10
|
+
import { writeViaTmp } from '../state-file.js';
|
|
11
|
+
import { findProjectRoot } from '../harness.js';
|
|
12
|
+
import { runCodexBrief } from './brief.js';
|
|
13
|
+
import { codexDocumentTool, codexDocumentNote, convertCodexDocument, codexDocumentContext } from './doc2md.js';
|
|
14
|
+
import { codexDelegationTool, codexRouteHint, codexDelegateEnabled } from './delegation.js';
|
|
15
|
+
import { bindCodexSubagent, closeStaleCodexRoutes, unsettledCodexRoutes } from './ledger.js';
|
|
16
|
+
import { seedOfferBlock } from '../seed-rules.js';
|
|
17
|
+
import { readCodexRouteScan, openCodexCandidates, shouldRescanCodex } from './route-scan.js';
|
|
18
|
+
import { userDataDir } from '../paths.js';
|
|
19
|
+
import { debug } from '../debug.js';
|
|
20
|
+
import { updateOffer } from '../update-notice.js';
|
|
21
21
|
|
|
22
22
|
const require = createRequire(import.meta.url);
|
|
23
|
-
const lint = require('
|
|
24
|
-
const doc2md = require('
|
|
23
|
+
const lint = require('../korean-lint.cjs');
|
|
24
|
+
const doc2md = require('../doc2md.cjs');
|
|
25
25
|
|
|
26
26
|
function context(event, text) {
|
|
27
27
|
return text ? { hookSpecificOutput: { hookEventName: event, additionalContext: text } } : null;
|
|
@@ -167,7 +167,7 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
167
167
|
if (!payload || typeof payload !== 'object') return null;
|
|
168
168
|
if (event === 'stop') {
|
|
169
169
|
if (payload.stop_hook_active || !koreanStyleEnabled(cfg) || cfg?.koreanReplyCheck === false) return null;
|
|
170
|
-
const { checkCodexReplyLanguage } = await import('./
|
|
170
|
+
const { checkCodexReplyLanguage } = await import('./reply-language.js');
|
|
171
171
|
if (!checkCodexReplyLanguage(payload)) return null;
|
|
172
172
|
return { decision: 'block', reason: '사용자는 한국어로 질문했는데 마지막 답변이 영어로 작성됐습니다. 같은 내용을 한국어로 다시 답하십시오. 코드, 명령어, 오류 원문, 커밋 메시지는 그대로 유지하십시오.' };
|
|
173
173
|
}
|
|
@@ -180,9 +180,9 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
|
|
|
180
180
|
const parts = [];
|
|
181
181
|
// Keep the preset file current with this version's wording before it is read.
|
|
182
182
|
if (event === 'session-start') {
|
|
183
|
-
try { (await import('
|
|
183
|
+
try { (await import('../preset-ratchet.js')).preparePresetRatchet({ agent: 'codex' }); } catch (e) { debug('codex:preset', e); }
|
|
184
184
|
}
|
|
185
|
-
const { presetRatchetPath } = await import('
|
|
185
|
+
const { presetRatchetPath } = await import('../preset-ratchet.js');
|
|
186
186
|
// Lower priority survives the budget first. Rules and style move to a file
|
|
187
187
|
// when they do not fit; offers and notices are simply left out.
|
|
188
188
|
for (const [scope, ratchet] of [['global', codexHarnessPaths(root, 'global').ratchet],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
|
-
import { codexUserDir } from '
|
|
4
|
-
import { writeViaTmp } from '
|
|
3
|
+
import { codexUserDir } from '../agent.js';
|
|
4
|
+
import { writeViaTmp } from '../state-file.js';
|
|
5
5
|
|
|
6
6
|
export const CODEX_HANDLERS = {
|
|
7
7
|
SessionStart: { matcher: 'startup|resume|clear|compact', command: 'sprag codex-hook --agent codex --event session-start', timeout: 15 },
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
4
|
-
import { userDataDir } from '
|
|
5
|
-
import { writeViaTmp } from '
|
|
6
|
-
import { roundUsd } from '
|
|
7
|
-
import { readResetMarks, isBeforeReset } from '
|
|
8
|
-
import { discoverCodexSessionFiles, parseCodexTurns } from './
|
|
9
|
-
import { codexRunCost, codexPriceBook, readCodexPrices } from './
|
|
4
|
+
import { userDataDir } from '../paths.js';
|
|
5
|
+
import { writeViaTmp } from '../state-file.js';
|
|
6
|
+
import { roundUsd } from '../savings-ledger.js';
|
|
7
|
+
import { readResetMarks, isBeforeReset } from '../saved-reset.cjs';
|
|
8
|
+
import { discoverCodexSessionFiles, parseCodexTurns } from './parser.js';
|
|
9
|
+
import { codexRunCost, codexPriceBook, readCodexPrices } from './cache-policy.js';
|
|
10
10
|
|
|
11
11
|
/*
|
|
12
12
|
* Codex routing savings, kept apart from Claude's delegation ledger.
|
|
@@ -2,11 +2,11 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
-
import { codexUserDir } from '
|
|
5
|
+
import { codexUserDir } from '../agent.js';
|
|
6
6
|
|
|
7
7
|
const quote = (value) => `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
8
8
|
|
|
9
|
-
export function panelLaunchCommand(root, { node = process.execPath, cli = fileURLToPath(new URL('
|
|
9
|
+
export function panelLaunchCommand(root, { node = process.execPath, cli = fileURLToPath(new URL('../../bin/cli.js', import.meta.url)), home = codexUserDir(), sessionId, sessionFile } = {}) {
|
|
10
10
|
return ['env', `CODEX_HOME=${home}`, node, cli, 'panel', '--agent', 'codex', '--project', resolve(root),
|
|
11
11
|
...(sessionId ? ['--session', sessionId] : []), ...(sessionFile ? ['--session-file', sessionFile] : [])]
|
|
12
12
|
.map(quote).join(' ');
|
|
@@ -3,8 +3,8 @@ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { tmpdir, homedir } from 'node:os';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import { panelLaunchCommand } from './
|
|
7
|
-
import { createPanelBinding } from './
|
|
6
|
+
import { panelLaunchCommand } from './panel-launcher.js';
|
|
7
|
+
import { createPanelBinding } from './panel-session.js';
|
|
8
8
|
|
|
9
9
|
const quote = (s) => `'${String(s).replace(/'/g, `'"'"'`)}'`;
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute } from 'node:path';
|
|
3
|
-
import { writeViaTmp } from '
|
|
3
|
+
import { writeViaTmp } from '../state-file.js';
|
|
4
4
|
|
|
5
5
|
export function createPanelBinding(file) {
|
|
6
6
|
writeFileSync(file, JSON.stringify({ version: 1, sessionId: null, transcriptPath: null }), { mode: 0o600 });
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { execFileSync } from 'node:child_process';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
13
|
import { basename, dirname, join } from 'node:path';
|
|
14
|
-
import { installPanelShell } from './
|
|
14
|
+
import { installPanelShell } from './panel-shell.js';
|
|
15
15
|
|
|
16
16
|
/** Whether a tmux binary answers `-V`. */
|
|
17
17
|
export function tmuxAvailable({ exec = execFileSync } = {}) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFileSync, mkdirSync, readdirSync, lstatSync, unlinkSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
-
import { userDataDir } from '
|
|
5
|
-
import { codexUserDir } from '
|
|
6
|
-
import { writeViaTmp, runHousekeeping, sweepStaleTmp } from '
|
|
4
|
+
import { userDataDir } from '../paths.js';
|
|
5
|
+
import { codexUserDir } from '../agent.js';
|
|
6
|
+
import { writeViaTmp, runHousekeeping, sweepStaleTmp } from '../state-file.js';
|
|
7
7
|
|
|
8
8
|
// Every panel session leaves a `-frame` and a `-hook` file behind, keyed by a
|
|
9
9
|
// hash that is not reused once the session is gone. They are diagnostics for a
|
|
@@ -1,33 +1,39 @@
|
|
|
1
1
|
import { resolve, relative, isAbsolute, basename } from 'node:path';
|
|
2
2
|
import { realpathSync, statSync } from 'node:fs';
|
|
3
3
|
import { stat } from 'node:fs/promises';
|
|
4
|
-
import { readPanelBinding } from './
|
|
5
|
-
import { discoverCodexSessionFiles, parseCodexSessionFile } from './
|
|
6
|
-
import { codexHarnessStatus, codexRatchetCandidates } from './
|
|
7
|
-
import { harnessListRules, findProjectRoot } from '
|
|
8
|
-
import { loadConfig, statuslineDefaults } from '
|
|
9
|
-
import { glyphsFor } from '
|
|
10
|
-
import { gaugeBar, formatMoney } from '
|
|
11
|
-
import { codexCacheTimer } from './
|
|
12
|
-
import { readCodexCachePolicy, readCodexPrices, isDirectOpenAI } from './
|
|
13
|
-
import { codexIssues } from './
|
|
14
|
-
import { labelForKey } from '
|
|
15
|
-
import { resolveLabelMode } from '
|
|
16
|
-
import { formatResetIn } from '
|
|
17
|
-
import { codexDocumentTotals } from './
|
|
18
|
-
import { codexHookStatus } from './
|
|
19
|
-
import { koreanStyleEnabled } from '
|
|
20
|
-
import { codexRoutingSavedTotals, loadCodexLedger, codexDelegationStatus } from './
|
|
21
|
-
import { loadCodexModelRules, codexDelegateEnabled } from './
|
|
22
|
-
import { codexRulesInReview, codexSharedRuleHealth } from './
|
|
23
|
-
import { readCodexRouteScan, openCodexCandidates } from './
|
|
24
|
-
import { codexBudgetProvider } from './
|
|
25
|
-
import { readUpdateChip } from '
|
|
4
|
+
import { readPanelBinding } from './panel-session.js';
|
|
5
|
+
import { discoverCodexSessionFiles, parseCodexSessionFile } from './parser.js';
|
|
6
|
+
import { codexHarnessStatus, codexRatchetCandidates } from './harness.js';
|
|
7
|
+
import { harnessListRules, findProjectRoot } from '../harness.js';
|
|
8
|
+
import { loadConfig, statuslineDefaults } from '../config.js';
|
|
9
|
+
import { glyphsFor } from '../glyphs.js';
|
|
10
|
+
import { gaugeBar, formatMoney, DEFAULT_SEGMENT_ORDER } from '../formatters/statusline.js';
|
|
11
|
+
import { codexCacheTimer } from './cache.js';
|
|
12
|
+
import { readCodexCachePolicy, readCodexPrices, isDirectOpenAI } from './cache-policy.js';
|
|
13
|
+
import { codexIssues } from './brief.js';
|
|
14
|
+
import { labelForKey } from '../window-labels.js';
|
|
15
|
+
import { resolveLabelMode } from '../statusline-mode.js';
|
|
16
|
+
import { formatResetIn } from '../format-time.js';
|
|
17
|
+
import { codexDocumentTotals } from './doc2md-ledger.js';
|
|
18
|
+
import { codexHookStatus } from './installer.js';
|
|
19
|
+
import { koreanStyleEnabled } from '../korean-style.js';
|
|
20
|
+
import { codexRoutingSavedTotals, loadCodexLedger, codexDelegationStatus } from './ledger.js';
|
|
21
|
+
import { loadCodexModelRules, codexDelegateEnabled } from './delegation.js';
|
|
22
|
+
import { codexRulesInReview, codexSharedRuleHealth } from './rule-health.js';
|
|
23
|
+
import { readCodexRouteScan, openCodexCandidates } from './route-scan.js';
|
|
24
|
+
import { codexBudgetProvider } from './budget.js';
|
|
25
|
+
import { readUpdateChip } from '../update-notice.js';
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
'cap-warn', '
|
|
29
|
-
'
|
|
30
|
-
];
|
|
27
|
+
const CODEX_SUPPORTED_SEGMENTS = new Set([
|
|
28
|
+
'cap-warn', 'spike', 'usage', 'ctx', 'ttl', 'model', 'effort',
|
|
29
|
+
'delegated', 'doc2md', 'hit', 'saved', 'harness', 'korean', 'version',
|
|
30
|
+
]);
|
|
31
|
+
// The Claude statusline order, restricted to what Codex draws. Codex keeps its
|
|
32
|
+
// own upgrade chip right after the cap warning; the Claude line has no such
|
|
33
|
+
// segment and moves `version` forward instead.
|
|
34
|
+
export const CODEX_SEGMENT_ORDER = DEFAULT_SEGMENT_ORDER
|
|
35
|
+
.filter((name) => CODEX_SUPPORTED_SEGMENTS.has(name))
|
|
36
|
+
.flatMap((name) => (name === 'cap-warn' ? [name, 'update'] : [name]));
|
|
31
37
|
|
|
32
38
|
// Only cache full-session parses. A moving time cutoff would make an unchanged
|
|
33
39
|
// file's cached totals stale. The panel reports session totals, not window totals.
|
|
@@ -233,12 +239,13 @@ function panelGroups(data, { now, version = '', full = false, mode = full ? 'tex
|
|
|
233
239
|
const level = (n) => n >= 90 ? 31 : n >= 70 ? 33 : 32;
|
|
234
240
|
const bar = (n) => full ? gauge(n) : gaugeBar(n, mode === 'text' ? 'narrow' : mode);
|
|
235
241
|
const warnings = [];
|
|
242
|
+
const contextWarnings = [];
|
|
236
243
|
if (data.error) warnings.push(chip(`ERROR: ${terminalText(data.error)}`, 31));
|
|
237
244
|
for (const issue of codexIssues(s, { now })) {
|
|
238
245
|
const key = issue.key.slice(6), win = s.rateLimits?.[key];
|
|
239
246
|
const text = issue.key === 'context' ? `WARNING: Ctx ${pct(context)}`
|
|
240
247
|
: `WARNING: ${windowLabel(win.window_minutes, key)} limit ${pct(win.used_percent)}`;
|
|
241
|
-
warnings.push(chip(text, issue.level === 'critical' ? 31 : 33));
|
|
248
|
+
(issue.key === 'context' ? contextWarnings : warnings).push(chip(text, issue.level === 'critical' ? 31 : 33));
|
|
242
249
|
}
|
|
243
250
|
register('ctx', 'priority', context !== null ? [chip(full
|
|
244
251
|
? `Ctx ${bar(context)} ${pct(context)} ${short(s.lastContextTokens)} / ${short(s.contextWindow)}`
|
|
@@ -274,6 +281,7 @@ function panelGroups(data, { now, version = '', full = false, mode = full ? 'tex
|
|
|
274
281
|
}
|
|
275
282
|
if (limits.length && s?.rateLimitsAt) limits.push(chip(`snapshot ${age(s.rateLimitsAt, now)}`));
|
|
276
283
|
register('cap-warn', 'priority', warnings);
|
|
284
|
+
register('spike', 'priority', contextWarnings);
|
|
277
285
|
register('usage', 'priority', limits);
|
|
278
286
|
register('hit', 'totals', input > 0 ? [chip(`${icon('hit')}Cache hit ${pct(s.totals.cacheRead / input * 100)} (session)`,
|
|
279
287
|
s.totals.cacheRead / input >= 0.85 ? 32 : s.totals.cacheRead / input >= 0.7 ? 33 : 31)] : []);
|
|
@@ -283,8 +291,14 @@ function panelGroups(data, { now, version = '', full = false, mode = full ? 'tex
|
|
|
283
291
|
chip(`Reasoning ${short(s.reasoningOutputTokens)} (in output)`),
|
|
284
292
|
] : []);
|
|
285
293
|
const docs = data.documents;
|
|
294
|
+
// Lifetime figures, in the Claude statusline's order: routing, then documents.
|
|
295
|
+
const delegation = data.delegation;
|
|
296
|
+
register('delegated', 'lifetime', [routingChip(data.routingSaved, chip),
|
|
297
|
+
delegation?.offers ? chip(`Delegation ${delegation.settled} completed / ${delegation.bound} bound / ${delegation.pending} pending / ${delegation.unclaimed} unclaimed (session)`,
|
|
298
|
+
delegation.unclaimed && !delegation.settled && !delegation.bound ? 33 : 90) : null,
|
|
299
|
+
]);
|
|
286
300
|
const docSavings = docs?.scope === 'codex-total' ? docs.savings : null;
|
|
287
|
-
register('doc2md', '
|
|
301
|
+
register('doc2md', 'lifetime', [
|
|
288
302
|
chip(`Doc2md saved ${docSavings?.priced ? `${signedMoney(docSavings.total)} est. (XML vs MD)`
|
|
289
303
|
: `n/a (${docs?.docs ? `${docs.docs} unpriced docs` : 'no documents'})`}`, docSavings?.priced ? (docSavings.total < 0 ? 33 : 32) : 90),
|
|
290
304
|
...(docSavings?.priced && docSavings.unpriced ? [chip(`${docSavings.unpriced} docs unpriced`)] : []),
|
|
@@ -292,11 +306,6 @@ function panelGroups(data, { now, version = '', full = false, mode = full ? 'tex
|
|
|
292
306
|
...(docs.byExt || []).map((row) => chip(`${terminalText(row.ext)} ${number(row.docs)}x`)),
|
|
293
307
|
] : []),
|
|
294
308
|
]);
|
|
295
|
-
const delegation = data.delegation;
|
|
296
|
-
register('routing', 'savings', [routingChip(data.routingSaved, chip),
|
|
297
|
-
delegation?.offers ? chip(`Delegation ${delegation.settled} completed / ${delegation.bound} bound / ${delegation.pending} pending / ${delegation.unclaimed} unclaimed (session)`,
|
|
298
|
-
delegation.unclaimed && !delegation.settled && !delegation.bound ? 33 : 90) : null,
|
|
299
|
-
]);
|
|
300
309
|
register('harness', 'preferences', [
|
|
301
310
|
chip(`${icon('harness')}Harness ${h?.error ? 'ERROR' : h ? `${h.configured}/${h.total}` : 'n/a'}`, h?.configured === 5 ? 32 : 33),
|
|
302
311
|
chip(`Ratchet ${data.rules?.global ?? '?'}/${data.rules?.project ?? '?'}`),
|
|
@@ -322,7 +331,7 @@ function panelGroups(data, { now, version = '', full = false, mode = full ? 'tex
|
|
|
322
331
|
chip(`SPRAG${version ? ` v${terminalText(version)}` : ''} / ${s ? age(s.lastActivity, now) : 'waiting'}`,
|
|
323
332
|
s && (!s.lastActivity || now - new Date(s.lastActivity) > 300000) ? 33 : 90),
|
|
324
333
|
]);
|
|
325
|
-
//
|
|
334
|
+
// Adjacent entries with the same timeframe share a wrapping group.
|
|
326
335
|
const groups = [];
|
|
327
336
|
let lastGroup;
|
|
328
337
|
for (const name of CODEX_SEGMENT_ORDER) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { openSync, readSync, closeSync, fstatSync } from 'node:fs';
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
3
|
+
import { transcriptLines } from '../transcript-lines.js';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
|
-
import { codexUserDir } from '
|
|
5
|
+
import { codexUserDir } from '../agent.js';
|
|
6
6
|
|
|
7
7
|
const count = (n) => Number.isSafeInteger(n) && n >= 0 ? n : 0;
|
|
8
8
|
const emptyTotals = () => ({ input: 0, cacheRead: 0, cacheCreation: 0, ephemeral5m: 0, ephemeral1h: 0, output: 0 });
|
|
@@ -212,9 +212,7 @@ export async function parseCodexTurns(filePath, { cutoffMs = -Infinity } = {}) {
|
|
|
212
212
|
let previousUsage = null;
|
|
213
213
|
let current = null;
|
|
214
214
|
let context = { model: null, effort: null, cwd: null };
|
|
215
|
-
const
|
|
216
|
-
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
217
|
-
for await (const line of lines) {
|
|
215
|
+
for await (const line of transcriptLines(filePath)) {
|
|
218
216
|
let e;
|
|
219
217
|
try { e = JSON.parse(line); } catch { continue; }
|
|
220
218
|
const p = e?.payload;
|
|
@@ -341,9 +339,7 @@ export async function parseCodexSessionFile(filePath, { cutoffMs = -Infinity } =
|
|
|
341
339
|
};
|
|
342
340
|
const copied = forkedCopyFilter();
|
|
343
341
|
let previous = null;
|
|
344
|
-
const
|
|
345
|
-
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
346
|
-
for await (const line of lines) {
|
|
342
|
+
for await (const line of transcriptLines(filePath)) {
|
|
347
343
|
let e;
|
|
348
344
|
try { e = JSON.parse(line); } catch { continue; }
|
|
349
345
|
const p = e?.payload;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { forkedCopyFilter, codexToolEvents } from './
|
|
2
|
+
import { forkedCopyFilter, codexToolEvents } from './parser.js';
|
|
3
3
|
|
|
4
|
-
const { checkReplyLanguage } = createRequire(import.meta.url)('
|
|
4
|
+
const { checkReplyLanguage } = createRequire(import.meta.url)('../reply-language.cjs');
|
|
5
5
|
|
|
6
6
|
/** Ignore copied history and commentary; only the current turn's final reply counts. */
|
|
7
7
|
export function lastCodexTurn(lines, { turnId, sessionId } = {}) {
|