claude-token-saver 3.5.0 → 3.5.2
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/bin/cli.js +12 -0
- package/package.json +1 -1
- package/src/brief.js +32 -15
- package/src/installer.js +10 -0
package/bin/cli.js
CHANGED
|
@@ -572,6 +572,7 @@ async function main() {
|
|
|
572
572
|
// stay fast) — reads the cache, kicks a detached refresh when stale, and
|
|
573
573
|
// prints delegation-candidate context for the new session.
|
|
574
574
|
if (hasFlag('--hook')) {
|
|
575
|
+
const hookCtx = readStdinJson() || {};
|
|
575
576
|
let cache = rs.readRouteScan();
|
|
576
577
|
if (await rs.shouldRescan(cache)) {
|
|
577
578
|
try {
|
|
@@ -615,7 +616,18 @@ async function main() {
|
|
|
615
616
|
}
|
|
616
617
|
lines.push(' 제거: claude-token-saver route-scan rules rm <N>');
|
|
617
618
|
}
|
|
619
|
+
lines.push('위 내용은 사용자의 첫 메시지 주제와 무관하더라도(단순 인사여도) 첫 응답 말미에 `※ [claude-token-saver]` 라벨을 달아 요약 브리핑하세요 — 사용자가 물어볼 때까지 기다리지 말 것.');
|
|
618
620
|
console.log(lines.join('\n'));
|
|
621
|
+
// Record what was actually briefed so the UserPromptSubmit brief hook
|
|
622
|
+
// suppresses exactly these — a candidate landing after this read (e.g.
|
|
623
|
+
// the detached rescan above finishing) still gets briefed next prompt.
|
|
624
|
+
try {
|
|
625
|
+
const { seedSessionBriefed } = await import('../src/brief.js');
|
|
626
|
+
seedSessionBriefed(hookCtx.session_id, [
|
|
627
|
+
...open.map((c) => `route|${c.signature}`),
|
|
628
|
+
...reviewRules.map((r) => `health|${r.signature}|${r.scope}`),
|
|
629
|
+
]);
|
|
630
|
+
} catch { /* best-effort — worst case is one duplicate brief */ }
|
|
619
631
|
return;
|
|
620
632
|
}
|
|
621
633
|
|
package/package.json
CHANGED
package/src/brief.js
CHANGED
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
* transcript_path) and the "already briefed" markers are keyed by session_id.
|
|
15
15
|
*
|
|
16
16
|
* State: <stateDir>/brief-state.json
|
|
17
|
-
* { sessions: { [session_id]: { ts, ctxTier,
|
|
17
|
+
* { sessions: { [session_id]: { ts, ctxTier, briefed: [signature] } } }
|
|
18
18
|
* Sessions untouched for 7 days are pruned on every write.
|
|
19
|
+
*
|
|
20
|
+
* Division of labor with the SessionStart hook: SessionStart prints the
|
|
21
|
+
* session-start snapshot and records the signatures it actually briefed via
|
|
22
|
+
* seedSessionBriefed(); this hook emits anything NOT in that record. A
|
|
23
|
+
* candidate that lands after the SessionStart read (e.g. the detached rescan
|
|
24
|
+
* finishing between session start and the first prompt) is therefore briefed
|
|
25
|
+
* on the next prompt instead of being lost for the whole session.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
@@ -109,20 +116,34 @@ function ctxTierOf(pct) {
|
|
|
109
116
|
|
|
110
117
|
const fmtK = (n) => `${Math.round(n / 1000)}k`;
|
|
111
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Called by the SessionStart hook after it prints the session-start snapshot:
|
|
121
|
+
* records the signatures it actually briefed so runBrief suppresses exactly
|
|
122
|
+
* those and nothing more. Signature format matches runBrief:
|
|
123
|
+
* `route|<sig>` / `health|<sig>|<scope>`.
|
|
124
|
+
*/
|
|
125
|
+
export function seedSessionBriefed(sessionId, signatures, now = Date.now()) {
|
|
126
|
+
if (!sessionId || !Array.isArray(signatures) || signatures.length === 0) return;
|
|
127
|
+
const state = loadState();
|
|
128
|
+
const s = state.sessions[sessionId] || { ctxTier: 0, briefed: [] };
|
|
129
|
+
s.briefed = [...new Set([...(s.briefed || []), ...signatures])];
|
|
130
|
+
s.ts = now;
|
|
131
|
+
state.sessions[sessionId] = s;
|
|
132
|
+
saveState(state, now);
|
|
133
|
+
}
|
|
134
|
+
|
|
112
135
|
/**
|
|
113
136
|
* Compute the briefing for one prompt-submit event. Returns the text to
|
|
114
137
|
* inject, or null when nothing new happened. Mutates + persists state.
|
|
115
138
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* Context tiers are NOT seeded: a session that starts (or resumes) already
|
|
120
|
-
* past a threshold still deserves the warning once.
|
|
139
|
+
* Route/rule-health signatures already briefed by the SessionStart hook
|
|
140
|
+
* (recorded via seedSessionBriefed) are skipped; every other fresh signature
|
|
141
|
+
* is emitted, including on the session's first event.
|
|
121
142
|
*/
|
|
122
143
|
export async function runBrief({ sessionId, transcriptPath, now = Date.now() }) {
|
|
123
144
|
if (!sessionId) return null;
|
|
124
145
|
const state = loadState();
|
|
125
|
-
const s = state.sessions[sessionId] || { ctxTier: 0,
|
|
146
|
+
const s = state.sessions[sessionId] || { ctxTier: 0, briefed: [] };
|
|
126
147
|
const items = [];
|
|
127
148
|
|
|
128
149
|
// ── context tier crossing (per-session) ──
|
|
@@ -158,16 +179,12 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
|
|
|
158
179
|
briefed.add(sig);
|
|
159
180
|
fresh.push(['health', r]);
|
|
160
181
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
: `승인된 위임 룰의 최근 에러율이 기준(20%)을 넘었습니다 — "${x.label}" (statusline의 rule-health 칩). 조건 좁히기/제거를 사용자와 상의하세요: claude-token-saver route-scan rules`);
|
|
166
|
-
}
|
|
182
|
+
for (const [kind, x] of fresh) {
|
|
183
|
+
items.push(kind === 'route'
|
|
184
|
+
? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록: claude-token-saver harness promote R${x.id} --project|--global (적용 범위는 사용자에게 확인) / 무시: route-scan dismiss ${x.id}`
|
|
185
|
+
: `승인된 위임 룰의 최근 에러율이 기준(20%)을 넘었습니다 — "${x.label}" (statusline의 rule-health 칩). 조건 좁히기/제거를 사용자와 상의하세요: claude-token-saver route-scan rules`);
|
|
167
186
|
}
|
|
168
|
-
// First event: session-start snapshot is SessionStart's job — swallow it.
|
|
169
187
|
s.briefed = [...briefed];
|
|
170
|
-
s.seeded = true;
|
|
171
188
|
} catch { /* caches unreadable — ctx briefing above still applies */ }
|
|
172
189
|
|
|
173
190
|
s.ts = now;
|
package/src/installer.js
CHANGED
|
@@ -223,6 +223,11 @@ export function installSessionStartHook() {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
settings.hooks = settings.hooks || {};
|
|
226
|
+
// A present-but-non-array value is schema-invalid, but it's the user's
|
|
227
|
+
// data — back off instead of silently replacing it.
|
|
228
|
+
if (settings.hooks.SessionStart !== undefined && !Array.isArray(settings.hooks.SessionStart)) {
|
|
229
|
+
return { path: file, action: 'skipped', reason: 'hooks.SessionStart is not an array — fix settings.json manually' };
|
|
230
|
+
}
|
|
226
231
|
const list = Array.isArray(settings.hooks.SessionStart) ? settings.hooks.SessionStart : [];
|
|
227
232
|
const already = list.some((m) =>
|
|
228
233
|
Array.isArray(m?.hooks) && m.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('route-scan --hook')),
|
|
@@ -260,6 +265,11 @@ export function installBriefHook() {
|
|
|
260
265
|
}
|
|
261
266
|
|
|
262
267
|
settings.hooks = settings.hooks || {};
|
|
268
|
+
// A present-but-non-array value is schema-invalid, but it's the user's
|
|
269
|
+
// data — back off instead of silently replacing it.
|
|
270
|
+
if (settings.hooks.UserPromptSubmit !== undefined && !Array.isArray(settings.hooks.UserPromptSubmit)) {
|
|
271
|
+
return { path: file, action: 'skipped', reason: 'hooks.UserPromptSubmit is not an array — fix settings.json manually' };
|
|
272
|
+
}
|
|
263
273
|
const list = Array.isArray(settings.hooks.UserPromptSubmit) ? settings.hooks.UserPromptSubmit : [];
|
|
264
274
|
const already = list.some((m) =>
|
|
265
275
|
Array.isArray(m?.hooks) && m.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('brief --hook')),
|