sprag-cli 3.58.1 → 3.60.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 +17 -0
- package/README.md +23 -0
- package/bin/cli.js +59 -4
- package/package.json +1 -1
- package/src/caps-cache.js +3 -3
- package/src/codex-brief.js +4 -5
- package/src/codex-cache-policy.js +3 -3
- package/src/codex-delegation.js +3 -3
- package/src/codex-doc2md-ledger.js +9 -4
- package/src/codex-hooks.js +3 -3
- package/src/codex-installer.js +3 -3
- package/src/codex-ledger.js +13 -8
- package/src/codex-panel-session.js +3 -3
- package/src/codex-panel-state.js +51 -5
- package/src/codex-route-scan.js +3 -3
- package/src/commands/codex.js +2 -0
- package/src/commands/compact-window.js +6 -2
- package/src/commands/install.js +4 -1
- package/src/commands/korean.js +69 -3
- package/src/commands/route-scan.js +25 -4
- package/src/commands/saved.js +154 -0
- package/src/ctx-window-cache.js +14 -0
- package/src/doc2md-ledger.cjs +7 -1
- package/src/formatters/statusline.js +167 -18
- package/src/harness-analyzer.cjs +8 -2
- package/src/harness.js +32 -23
- package/src/installer.js +52 -0
- package/src/model-alias.js +3 -3
- package/src/model-rules.js +3 -1
- package/src/preset-ratchet.js +3 -3
- package/src/reply-language.cjs +167 -0
- package/src/route-inject.js +43 -3
- package/src/route-scan.js +51 -11
- package/src/saved-reset.cjs +118 -0
- package/src/savings-ledger.js +12 -4
- package/src/session-cache.js +12 -4
- package/src/state-file.js +99 -2
- package/src/statusline-data.js +97 -0
- package/src/update-check.js +46 -6
package/src/commands/korean.js
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
* wrote:
|
|
21
21
|
* sprag korean lint block|warn|off # how findings are handled
|
|
22
22
|
* sprag korean lint <file...> # check files on disk
|
|
23
|
+
*
|
|
24
|
+
* The third half is a Stop hook for the one slip a write-time check cannot
|
|
25
|
+
* see: an English reply to a Korean prompt, which writes no file at all.
|
|
26
|
+
* sprag korean reply on|off # toggle it (default on)
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
// Enforcement is only useful if it is on by default — a check the user has to
|
|
@@ -39,6 +43,12 @@ function koreanLintScope(cfg) {
|
|
|
39
43
|
return cfg?.koreanStyle?.lintScope === 'prose' ? 'prose' : 'all';
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
// Default on, like the lint: a check the user has to discover is the same
|
|
47
|
+
// hole in a different shape. Only an explicit false turns it off.
|
|
48
|
+
function koreanReplyCheckOn(cfg) {
|
|
49
|
+
return cfg?.koreanReplyCheck !== false;
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
export async function run({ args, hasFlag }) {
|
|
43
53
|
const sub = args[1] || 'status';
|
|
44
54
|
const ks = await import('../korean-style.js');
|
|
@@ -72,6 +82,55 @@ export async function run({ args, hasFlag }) {
|
|
|
72
82
|
return;
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
// Stop hook. Fires when the model finishes a turn; if the reply is English
|
|
86
|
+
// to a Korean prompt, block the stop so the model answers again in Korean.
|
|
87
|
+
// Imports stay minimal on purpose: this runs after every turn.
|
|
88
|
+
if (hasFlag?.('--reply-hook') || sub === '--reply-hook') {
|
|
89
|
+
const { readStdinJson } = await import('../stdin-payload.js');
|
|
90
|
+
const payload = readStdinJson();
|
|
91
|
+
if (!payload) return;
|
|
92
|
+
// Claude Code sets stop_hook_active while the model is already continuing
|
|
93
|
+
// because of a Stop hook. Blocking again would loop forever.
|
|
94
|
+
if (payload.stop_hook_active) return;
|
|
95
|
+
if (!ks.koreanStyleEnabled()) return;
|
|
96
|
+
if (!koreanReplyCheckOn(loadConfig())) return;
|
|
97
|
+
const { createRequire } = await import('node:module');
|
|
98
|
+
const found = createRequire(import.meta.url)('../reply-language.cjs').checkReplyLanguage(payload.transcript_path);
|
|
99
|
+
if (!found) return;
|
|
100
|
+
console.log(JSON.stringify({
|
|
101
|
+
decision: 'block',
|
|
102
|
+
reason: '사용자는 한국어로 질문했는데 마지막 답변이 영어로 작성됐습니다. 같은 내용을 한국어로 다시 답하십시오. 코드, 명령어, 오류 원문, 커밋 메시지처럼 원문을 유지해야 하는 부분은 그대로 둡니다.',
|
|
103
|
+
}));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Toggle: `korean reply on|off`. Off keeps the hook installed and makes it
|
|
108
|
+
// a no-op, the same way lint mode `off` works; `korean off` removes it.
|
|
109
|
+
if (sub === 'reply') {
|
|
110
|
+
const cfg = loadConfig();
|
|
111
|
+
const next = args[2];
|
|
112
|
+
if (!next) {
|
|
113
|
+
console.log(lang === 'ko'
|
|
114
|
+
? `답변 언어 검사: ${koreanReplyCheckOn(cfg) ? '켜짐' : '꺼짐'}`
|
|
115
|
+
: `Reply-language check: ${koreanReplyCheckOn(cfg) ? 'on' : 'off'}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!['on', 'off'].includes(next)) {
|
|
119
|
+
console.error('Usage: sprag korean reply [on|off]');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
cfg.koreanReplyCheck = next === 'on';
|
|
123
|
+
saveConfig(cfg);
|
|
124
|
+
console.log(next === 'on'
|
|
125
|
+
? (lang === 'ko'
|
|
126
|
+
? '답변 언어 검사를 켰습니다. 한국어 질문에 영어로 답하면 모델에게 되돌려 보내 다시 답하게 합니다.'
|
|
127
|
+
: 'Reply-language check is on. An English reply to a Korean prompt is sent back for a Korean answer.')
|
|
128
|
+
: (lang === 'ko'
|
|
129
|
+
? '답변 언어 검사를 껐습니다. 훅은 남아 있지만 아무 일도 하지 않습니다.'
|
|
130
|
+
: 'Reply-language check is off. The hook stays installed and does nothing.'));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
75
134
|
// Manual run over files already on disk: `korean lint docs/*.md`.
|
|
76
135
|
if (sub === 'lint' && args.length > 2 && !['on', 'off', 'warn', 'block', 'scope'].includes(args[2])) {
|
|
77
136
|
const { readFileSync } = await import('node:fs');
|
|
@@ -116,8 +175,8 @@ export async function run({ args, hasFlag }) {
|
|
|
116
175
|
saveConfig(cfg);
|
|
117
176
|
console.log(lang === 'ko'
|
|
118
177
|
? (next === 'all'
|
|
119
|
-
? '검사
|
|
120
|
-
: '검사
|
|
178
|
+
? '검사 범위를 all 로 바꿨습니다. 문서와 코드 주석, UI 문자열까지 세션이 쓴 모든 텍스트 파일을 검사합니다.'
|
|
179
|
+
: '검사 범위를 prose 로 좁혔습니다. 마크다운과 텍스트 문서만 검사합니다.')
|
|
121
180
|
: (next === 'all'
|
|
122
181
|
? 'Check scope: all — every text file the session writes, comments and UI strings included.'
|
|
123
182
|
: 'Check scope: prose — documents only.'));
|
|
@@ -163,8 +222,13 @@ export async function run({ args, hasFlag }) {
|
|
|
163
222
|
if (sub === 'on' || sub === 'off') {
|
|
164
223
|
const enabled = sub === 'on';
|
|
165
224
|
ks.setKoreanStyleEnabled(enabled);
|
|
166
|
-
const {
|
|
225
|
+
const {
|
|
226
|
+
installKoreanLintHook, removeKoreanLintHook,
|
|
227
|
+
installReplyLanguageHook, removeReplyLanguageHook,
|
|
228
|
+
} = await import('../installer.js');
|
|
167
229
|
const hook = enabled ? installKoreanLintHook() : removeKoreanLintHook();
|
|
230
|
+
if (enabled) installReplyLanguageHook();
|
|
231
|
+
else removeReplyLanguageHook();
|
|
168
232
|
if (enabled) {
|
|
169
233
|
console.log(lang === 'ko'
|
|
170
234
|
? '한국어 문체 지침을 켰습니다. 다음 세션부터 모든 프로젝트에 적용됩니다.'
|
|
@@ -202,6 +266,7 @@ export async function run({ args, hasFlag }) {
|
|
|
202
266
|
console.log(`한국어 문체 지침: ${on ? '켜짐' : '꺼짐'}`);
|
|
203
267
|
console.log(` 비용: 세션당 약 ${tokens} 토큰 (세션 시작 1회 주입)`);
|
|
204
268
|
console.log(` 쓰기 시점 검사: ${koreanLintMode(loadConfig())}, 범위 ${koreanLintScope(loadConfig())} (바꾸려면 sprag korean lint block|warn|off, korean lint scope all|prose)`);
|
|
269
|
+
console.log(` 답변 언어 검사: ${koreanReplyCheckOn(loadConfig()) ? '켜짐' : '꺼짐'} (바꾸려면 sprag korean reply on|off)`);
|
|
205
270
|
console.log(` 출처: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
206
271
|
console.log(` 라이선스 전문: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
|
|
207
272
|
console.log(on
|
|
@@ -211,6 +276,7 @@ export async function run({ args, hasFlag }) {
|
|
|
211
276
|
console.log(`Korean writing guidance: ${on ? 'on' : 'off'}`);
|
|
212
277
|
console.log(` Cost: ~${tokens} tokens per session (injected once at session start)`);
|
|
213
278
|
console.log(` Write-time check: ${koreanLintMode(loadConfig())}, scope ${koreanLintScope(loadConfig())} (change with: sprag korean lint block|warn|off, korean lint scope all|prose)`);
|
|
279
|
+
console.log(` Reply-language check: ${koreanReplyCheckOn(loadConfig()) ? 'on' : 'off'} (change with: sprag korean reply on|off)`);
|
|
214
280
|
console.log(` Source: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
215
281
|
console.log(` License text: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
|
|
216
282
|
console.log(on
|
|
@@ -56,9 +56,22 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
56
56
|
// which model actually ran it.
|
|
57
57
|
if (args[1] === 'savings') {
|
|
58
58
|
const { loadLedger, delegationSavedTotals } = await import('../savings-ledger.js');
|
|
59
|
+
// Runs from before `sprag saved reset` are kept in the ledger but not listed.
|
|
60
|
+
const { readResetMarks, isBeforeReset } = await import('../saved-reset.cjs');
|
|
61
|
+
const { userDataDir } = await import('../paths.js');
|
|
62
|
+
const since = readResetMarks(userDataDir()).routing;
|
|
59
63
|
const events = Object.entries(loadLedger().events)
|
|
60
64
|
.map(([key, e]) => ({ key, ...e }))
|
|
65
|
+
.filter((e) => !isBeforeReset(e.ts, since))
|
|
61
66
|
.sort((a, b) => b.ts - a.ts);
|
|
67
|
+
if (events.length === 0 && since !== null) {
|
|
68
|
+
// After a reset an empty list is the expected state, not a setup gap.
|
|
69
|
+
const at = new Date(since).toLocaleString(lang === 'ko' ? 'ko-KR' : 'en-US');
|
|
70
|
+
console.log(lang === 'ko'
|
|
71
|
+
? `${at}에 카운터를 초기화한 뒤로 기록된 라우팅 절감이 아직 없습니다. 되돌리려면 \`sprag saved undo\` 를 실행하십시오.`
|
|
72
|
+
: `No routing savings recorded since the counter was reset at ${at}. Run \`sprag saved undo\` to restore it.`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
62
75
|
if (events.length === 0) {
|
|
63
76
|
console.log(lang === 'ko'
|
|
64
77
|
? '기록된 라우팅 절감 없음. 승격된 룰이 실제로 위임을 일으킨 뒤 `route-scan --refresh` 를 돌리면 채워집니다.'
|
|
@@ -72,6 +85,12 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
72
85
|
console.log(lang === 'ko'
|
|
73
86
|
? `🔀 라우팅 절감 누적 ${money(t.total)} (최근 7일 ${money(t.week)} · 30일 ${money(t.month)})`
|
|
74
87
|
: `🔀 Routing saved, lifetime ${money(t.total)} (last 7d ${money(t.week)} · 30d ${money(t.month)})`);
|
|
88
|
+
if (since !== null) {
|
|
89
|
+
const at = new Date(since).toLocaleString(lang === 'ko' ? 'ko-KR' : 'en-US');
|
|
90
|
+
console.log(lang === 'ko'
|
|
91
|
+
? ` ${at}에 카운터를 초기화했습니다. 이후 기록만 집계합니다.`
|
|
92
|
+
: ` Counter reset at ${at}; only later runs are counted.`);
|
|
93
|
+
}
|
|
75
94
|
|
|
76
95
|
// Per model-pair rollup first: the "what moved where" question is what
|
|
77
96
|
// this view exists to answer, and it is easier to read than the log.
|
|
@@ -341,12 +360,14 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
341
360
|
// — the refresh runs detached off the statusline path, never here.
|
|
342
361
|
let updateBlock = null;
|
|
343
362
|
try {
|
|
344
|
-
const { updateStatus, maybeSpawnUpdateCheck } = await import('../update-check.js');
|
|
363
|
+
const { updateStatus, maybeSpawnUpdateCheck, SESSION_START_CHECK_INTERVAL_MS } = await import('../update-check.js');
|
|
345
364
|
const pkgVersion = readPackageVersion();
|
|
346
365
|
// Session start is the one moment where a stale cache matters most, so
|
|
347
|
-
// kick the detached refresh here too
|
|
348
|
-
// statusline
|
|
349
|
-
|
|
366
|
+
// kick the detached refresh here too, on a shorter interval than the
|
|
367
|
+
// statusline's 24h: releases can land several times a day, and a
|
|
368
|
+
// session opened that afternoon should hear about them. It lands in
|
|
369
|
+
// time for the statusline and for the next session, not for this line.
|
|
370
|
+
if (pkgVersion) maybeSpawnUpdateCheck(pkgVersion, { intervalMs: SESSION_START_CHECK_INTERVAL_MS });
|
|
350
371
|
const u = pkgVersion ? updateStatus(pkgVersion) : null;
|
|
351
372
|
// `dismissed` is the whole point of asking once: a user who said no to
|
|
352
373
|
// this version must not be asked again every `/clear`.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subcommand: saved — a stopwatch reset for the lifetime "saved" counters.
|
|
3
|
+
*
|
|
4
|
+
* sprag saved # totals since the last reset
|
|
5
|
+
* sprag saved reset [routing|doc2md|all] # start counting from now
|
|
6
|
+
* sprag saved undo # put the previous counters back
|
|
7
|
+
* (add --agent codex for the Codex counters: codex-routing, codex-docs)
|
|
8
|
+
*
|
|
9
|
+
* Nothing is deleted. A reset only records a timestamp, and the totals count
|
|
10
|
+
* events from that moment on; see src/saved-reset.cjs.
|
|
11
|
+
*
|
|
12
|
+
* The routing mark also bounds the per-rule `saved ~$` figures in
|
|
13
|
+
* model-rules.json and ratchet-model.md, which are rebuilt by a scan rather than
|
|
14
|
+
* summed from the ledger. So a reset or undo that touches `routing` finishes by
|
|
15
|
+
* running that scan; see refreshRuleSavings.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { debug } from '../debug.js';
|
|
19
|
+
|
|
20
|
+
const SCOPE_SETS = {
|
|
21
|
+
claude: ['routing', 'doc2md'],
|
|
22
|
+
codex: ['codex-routing', 'codex-docs'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Totals since each scope's mark: { usd } for money counters, { docs } for the Codex document counter. */
|
|
26
|
+
async function currentTotals(scope, dir) {
|
|
27
|
+
if (scope === 'routing') {
|
|
28
|
+
const { delegationSavedTotals } = await import('../savings-ledger.js');
|
|
29
|
+
return { usd: delegationSavedTotals(Date.now(), dir).total };
|
|
30
|
+
}
|
|
31
|
+
if (scope === 'doc2md') {
|
|
32
|
+
const { doc2mdSavedTotals } = await import('../doc2md-ledger.cjs');
|
|
33
|
+
return { usd: doc2mdSavedTotals(dir).total };
|
|
34
|
+
}
|
|
35
|
+
if (scope === 'codex-routing') {
|
|
36
|
+
const { codexRoutingSavedTotals } = await import('../codex-ledger.js');
|
|
37
|
+
return { usd: codexRoutingSavedTotals({ dir }).total };
|
|
38
|
+
}
|
|
39
|
+
const { codexDocumentTotals } = await import('../codex-doc2md-ledger.js');
|
|
40
|
+
return { docs: codexDocumentTotals({ dir }).docs };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Rebuild each rule's savedUsd and re-render ratchet-model.md after the routing
|
|
45
|
+
* mark moved. The 14-day scan does both (it reads the mark, refreshes the
|
|
46
|
+
* registry, writes the managed files) in about half a second, so it is reused
|
|
47
|
+
* rather than duplicated here.
|
|
48
|
+
*
|
|
49
|
+
* Best-effort by design: the mark is already saved, and a failed rescan must not
|
|
50
|
+
* turn a successful reset into an error. It says so in one line instead, since
|
|
51
|
+
* the figures then stay stale until the next scan.
|
|
52
|
+
*
|
|
53
|
+
* @param {boolean} ko
|
|
54
|
+
* @param {Function} [rescan] injected by tests; defaults to runRouteScan
|
|
55
|
+
*/
|
|
56
|
+
async function refreshRuleSavings(ko, rescan) {
|
|
57
|
+
try {
|
|
58
|
+
const scan = rescan || (await import('../route-scan.js')).runRouteScan;
|
|
59
|
+
await scan({ days: 14 });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
debug('saved:rule-savings', e);
|
|
62
|
+
console.log(ko
|
|
63
|
+
? '룰별 절감액은 다음 route-scan 때 다시 계산됩니다.'
|
|
64
|
+
: 'Per-rule savings will be recalculated at the next route-scan.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function run({ args, hasFlag, agent = 'claude', dir: dirArg, rescan }) {
|
|
69
|
+
const { userLanguage } = await import('../config.js');
|
|
70
|
+
const { userDataDir } = await import('../paths.js');
|
|
71
|
+
const { readResetMarks, resetSaved, undoReset } = await import('../saved-reset.cjs');
|
|
72
|
+
const ko = userLanguage() === 'ko';
|
|
73
|
+
const dir = dirArg || userDataDir();
|
|
74
|
+
const scopes = SCOPE_SETS[agent === 'codex' ? 'codex' : 'claude'];
|
|
75
|
+
const suffix = agent === 'codex' ? ' --agent codex' : '';
|
|
76
|
+
|
|
77
|
+
const money = (v) => (v < 0 ? `-$${(-v).toFixed(2)}` : `$${v.toFixed(2)}`);
|
|
78
|
+
const fmtTotal = (t) => (t.docs !== undefined ? (ko ? `${t.docs}건` : `${t.docs} doc(s)`) : money(t.usd));
|
|
79
|
+
const fmtTime = (ms) => new Date(ms).toLocaleString(ko ? 'ko-KR' : 'en-US');
|
|
80
|
+
const label = (scope) => ({
|
|
81
|
+
routing: ko ? '라우팅 절감' : 'Routing saved',
|
|
82
|
+
doc2md: ko ? '문서 변환 절감' : 'Document conversion saved',
|
|
83
|
+
'codex-routing': ko ? 'Codex 라우팅 절감' : 'Codex routing saved',
|
|
84
|
+
'codex-docs': ko ? 'Codex 문서 변환' : 'Codex documents converted',
|
|
85
|
+
})[scope];
|
|
86
|
+
|
|
87
|
+
const sub = args[1];
|
|
88
|
+
|
|
89
|
+
if (sub === undefined) {
|
|
90
|
+
const marks = readResetMarks(dir);
|
|
91
|
+
for (const s of scopes) {
|
|
92
|
+
const t = await currentTotals(s, dir);
|
|
93
|
+
const since = marks[s] === null
|
|
94
|
+
? (ko ? '초기화한 적 없음' : 'not reset')
|
|
95
|
+
: (ko ? `${fmtTime(marks[s])} 이후` : `since ${fmtTime(marks[s])}`);
|
|
96
|
+
console.log(`${label(s)}: ${fmtTotal(t)} (${since})`);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (sub === 'reset') {
|
|
102
|
+
const arg = args[2] || 'all';
|
|
103
|
+
const base = agent === 'codex' ? { routing: 'codex-routing', doc2md: 'codex-docs' } : { routing: 'routing', doc2md: 'doc2md' };
|
|
104
|
+
let targets;
|
|
105
|
+
if (arg === 'all') targets = scopes;
|
|
106
|
+
else if (scopes.includes(arg)) targets = [arg];
|
|
107
|
+
else if (base[arg]) targets = [base[arg]];
|
|
108
|
+
else {
|
|
109
|
+
console.error(ko
|
|
110
|
+
? `알 수 없는 범위입니다: ${arg}. 사용할 수 있는 범위는 ${scopes.join(', ')}, all 입니다.`
|
|
111
|
+
: `Unknown scope: ${arg}. Valid scopes are ${scopes.join(', ')}, all.`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const before = {};
|
|
115
|
+
for (const s of targets) before[s] = await currentTotals(s, dir);
|
|
116
|
+
resetSaved(targets, { dir });
|
|
117
|
+
for (const s of targets) {
|
|
118
|
+
console.log(ko
|
|
119
|
+
? `${label(s)} 카운터를 0으로 초기화했습니다. 이전 합계는 ${fmtTotal(before[s])}입니다.`
|
|
120
|
+
: `Reset the ${label(s)} counter to zero. The previous total was ${fmtTotal(before[s])}.`);
|
|
121
|
+
}
|
|
122
|
+
console.log(ko
|
|
123
|
+
? `원장 기록은 지우지 않았습니다. \`sprag saved undo${suffix}\`로 되돌릴 수 있습니다.`
|
|
124
|
+
: `No ledger data was deleted. Run \`sprag saved undo${suffix}\` to restore the previous counters.`);
|
|
125
|
+
// Codex keeps no per-rule figure (its rules are a separate store with no
|
|
126
|
+
// savedUsd), so only the Claude `routing` scope has anything to recompute.
|
|
127
|
+
if (targets.includes('routing')) await refreshRuleSavings(ko, rescan);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (sub === 'undo') {
|
|
132
|
+
const undone = undoReset({ dir });
|
|
133
|
+
if (!undone) {
|
|
134
|
+
console.log(ko ? '되돌릴 초기화 기록이 없습니다.' : 'There is no reset to undo.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const s of undone.scopes) {
|
|
138
|
+
const t = await currentTotals(s, dir);
|
|
139
|
+
console.log(ko
|
|
140
|
+
? `${label(s)} 카운터를 되돌렸습니다. 현재 합계는 ${fmtTotal(t)}입니다.`
|
|
141
|
+
: `Restored the ${label(s)} counter. The total is now ${fmtTotal(t)}.`);
|
|
142
|
+
}
|
|
143
|
+
// Keyed on what was undone, not on --agent: undo pops the latest reset
|
|
144
|
+
// whichever agent made it, so a bare `undo --agent codex` can restore
|
|
145
|
+
// the Claude routing mark and the rule figures must follow it.
|
|
146
|
+
if (undone.scopes.includes('routing')) await refreshRuleSavings(ko, rescan);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.error(ko
|
|
151
|
+
? `알 수 없는 하위 명령입니다: ${sub}. 사용법은 sprag saved [reset|undo] 입니다.`
|
|
152
|
+
: `Unknown subcommand: ${sub}. Usage: sprag saved [reset|undo].`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
package/src/ctx-window-cache.js
CHANGED
|
@@ -59,3 +59,17 @@ export function liveWindowFor(sessionId) {
|
|
|
59
59
|
const s = load().sessions[sessionId];
|
|
60
60
|
return s && validSize(s.size) ? s.size : null;
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The window most recently reported by any session's statusline, or null.
|
|
65
|
+
* `sprag compact-window` runs outside a session and has no payload of its own;
|
|
66
|
+
* judged by the settings model id alone, a bare `opus` (1M by default on Opus
|
|
67
|
+
* 5.5, with no `[1m]` suffix) read as 200k while the statusline said 1M.
|
|
68
|
+
*/
|
|
69
|
+
export function latestLiveWindow() {
|
|
70
|
+
let best = null;
|
|
71
|
+
for (const s of Object.values(load().sessions)) {
|
|
72
|
+
if (s && validSize(s.size) && Number.isFinite(s.ts) && (!best || s.ts > best.ts)) best = s;
|
|
73
|
+
}
|
|
74
|
+
return best ? best.size : null;
|
|
75
|
+
}
|
package/src/doc2md-ledger.cjs
CHANGED
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
const fs = require('node:fs');
|
|
71
71
|
const path = require('node:path');
|
|
72
|
+
const { readResetMarks, isBeforeReset } = require('./saved-reset.cjs');
|
|
72
73
|
|
|
73
74
|
const WEEK_MS = 7 * 24 * 3600 * 1000;
|
|
74
75
|
const MONTH_MS = 30 * 24 * 3600 * 1000;
|
|
@@ -185,13 +186,18 @@ function recordConversion(userDataDir, event) {
|
|
|
185
186
|
* the lifetime rollup per format, priciest first, then most-converted. Never
|
|
186
187
|
* throws; an unreadable ledger yields zeros, and the statusline hides the
|
|
187
188
|
* chip on a zero.
|
|
189
|
+
*
|
|
190
|
+
* Events before the `doc2md` reset mark (`sprag saved reset`) are skipped in
|
|
191
|
+
* every figure, and `since` reports the mark (null when never reset).
|
|
188
192
|
*/
|
|
189
193
|
function doc2mdSavedTotals(userDataDir, now = Date.now()) {
|
|
190
|
-
const
|
|
194
|
+
const since = readResetMarks(userDataDir).doc2md;
|
|
195
|
+
const empty = () => ({ week: 0, month: 0, total: 0, docs: 0, tokens: 0, byExt: [], since });
|
|
191
196
|
const totals = empty();
|
|
192
197
|
const byExt = new Map();
|
|
193
198
|
try {
|
|
194
199
|
for (const e of Object.values(loadLedger(userDataDir).events)) {
|
|
200
|
+
if (isBeforeReset(e.ts, since)) continue;
|
|
195
201
|
const usd = Number(e.usd) || 0;
|
|
196
202
|
totals.total += usd;
|
|
197
203
|
totals.docs += 1;
|