claude-token-saver 3.43.0 → 3.44.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/bin/cli.js CHANGED
@@ -209,7 +209,7 @@ async function main() {
209
209
  // sprag route-scan --hook # SessionStart hook mode (context injection)
210
210
  // sprag route-scan dismiss <N> # mute candidate R<N>
211
211
  // Promote a candidate to a ratchet rule (scope is always explicit):
212
- // sprag harness promote R<N> --project|--global
212
+ // sprag harness promote R<N> --global|--project
213
213
  // brief --hook — UserPromptSubmit hook mode: per-session, change-triggered
214
214
  // briefing of state the statusline can only chip (ctx tier crossings,
215
215
  // mid-session route/rule-health changes). Silent when nothing changed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.43.0",
3
+ "version": "3.44.0",
4
4
  "description": "Legacy name of sprag-cli (Sprag) - same tool, kept publishing so existing installs stay current. Prefer: npm i -g sprag-cli",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,8 +15,8 @@
15
15
  "en": "When a web fetch/search is blocked by robots or user-agent checks ('unable to fetch' and similar) → retry with curl carrying a browser UA header (Mozilla/5.0 ... Chrome/...). Sites like reddit also answer on their .json endpoints with that UA"
16
16
  },
17
17
  {
18
- "ko": "ratchet 룰 promote 호출 직전 사용자에게 scope(project/global)를 먼저 묻는다 — Bash 환경은 non-TTY라 CLI readline 프롬프트가 안 뜨므로, 호출자(LLM)가 대신 묻고 --project/--global 플래그를 명시해 실행",
19
- "en": "Ask the user for the scope (project or global) before calling ratchet promote — the Bash environment is non-TTY, so the CLI's readline prompt never appears; the caller (the model) has to ask and pass --project/--global explicitly"
18
+ "ko": "ratchet 룰 promote 호출 직전 사용자에게 scope(global/project)를 먼저 묻는다 — Bash 환경은 non-TTY라 CLI readline 프롬프트가 안 뜨므로, 호출자(LLM)가 대신 묻고 --global/--project 플래그를 명시해 실행",
19
+ "en": "Ask the user for the scope (global or project) before calling ratchet promote — the Bash environment is non-TTY, so the CLI's readline prompt never appears; the caller (the model) has to ask and pass --global/--project explicitly"
20
20
  },
21
21
  {
22
22
  "ko": "자동화 도구가 grep·정규식으로 찾는 마커 텍스트(헤더·앵커·키워드 라인)는 리팩터링·압축·수정 금지 — 정확 매칭 검출은 한 글자만 바뀌어도 깨짐",
package/src/brief.js CHANGED
@@ -226,7 +226,7 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
226
226
  }
227
227
  for (const [kind, x] of fresh) {
228
228
  items.push(kind === 'route'
229
- ? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록: sprag harness promote R${x.id} --project|--global (적용 범위는 사용자에게 확인) / 무시: route-scan dismiss ${x.id}`
229
+ ? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록: sprag harness promote R${x.id} --global|--project (적용 범위는 사용자에게 확인) / 무시: route-scan dismiss ${x.id}`
230
230
  : `승인된 위임 룰의 최근 에러율이 기준(20%)을 넘었습니다 — "${x.label}" (${x.healthSource === 'delegated' ? `위임 실행 ${x.delegatedRuns}건 중 에러율 ${Math.round((x.delegatedErrRate || 0) * 100)}%` : `유사 에피소드 기준 에러율 ${Math.round((x.errRate || 0) * 100)}%`}, statusline의 rule-health 칩). 조건 좁히기/제거를 사용자와 상의하세요: sprag route-scan rules`);
231
231
  }
232
232
  s.briefed = [...briefed];
@@ -7,7 +7,7 @@
7
7
  * sprag route-scan --hook # SessionStart hook mode (context injection)
8
8
  * sprag route-scan dismiss <N> # mute candidate R<N>
9
9
  * Promote a candidate to a ratchet rule (scope is always explicit):
10
- * sprag harness promote R<N> --project|--global
10
+ * sprag harness promote R<N> --global|--project
11
11
  * brief --hook — UserPromptSubmit hook mode: per-session, change-triggered
12
12
  * briefing of state the statusline can only chip (ctx tier crossings,
13
13
  * mid-session route/rule-health changes). Silent when nothing changed.
@@ -356,11 +356,11 @@ export async function run({ args, hasFlag, numArg }) {
356
356
  }
357
357
  if (lang === 'ko') {
358
358
  lines.push('등록하면 다음 세션부터 자동 위임됩니다. 사용자에게 등록 여부를 물을 때 위 룰 원문을 그대로 보여주고, 적용 범위까지 확인한 뒤 실행하세요:');
359
- lines.push(' sprag harness promote R<N> --project|--global # 적용 범위는 반드시 사용자에게 확인');
359
+ lines.push(' sprag harness promote R<N> --global|--project # 적용 범위는 반드시 사용자에게 확인');
360
360
  lines.push(' sprag route-scan dismiss <N> # 사용자가 원치 않으면');
361
361
  } else {
362
362
  lines.push('Once registered, delegation happens automatically from the next session. Show the user the rule text verbatim, confirm the scope with them, then run:');
363
- lines.push(' sprag harness promote R<N> --project|--global # ALWAYS confirm the scope with the user first');
363
+ lines.push(' sprag harness promote R<N> --global|--project # ALWAYS confirm the scope with the user first');
364
364
  lines.push(' sprag route-scan dismiss <N> # if they do not want it');
365
365
  }
366
366
  }
@@ -446,7 +446,7 @@ export async function run({ args, hasFlag, numArg }) {
446
446
  }
447
447
  console.log('');
448
448
  console.log(lang === 'ko' ? '등록 / 무시:' : 'Promote / dismiss:');
449
- console.log(' sprag harness promote R<N> --project|--global');
449
+ console.log(' sprag harness promote R<N> --global|--project');
450
450
  console.log(' sprag route-scan dismiss <N>');
451
451
  // 최초 1회만 — 매번 찍으면 도구가 광고판이 된다 (CTS_NO_NOTE=1 로 끔)
452
452
  (await import('../first-run-note.js')).printOnce('route-scan', lang);
@@ -25,7 +25,8 @@ function runCommand(cmd) {
25
25
  }
26
26
 
27
27
  export async function run({ hasFlag, version }) {
28
- const { updateStatus, refreshUpdateState, upgradeCommand } = await import('../update-check.js');
28
+ const { updateStatus, refreshUpdateState, upgradeCommand, INSTALLED_UNDER_LEGACY_NAME } =
29
+ await import('../update-check.js');
29
30
  const { userLanguage } = await import('../config.js');
30
31
  const lang = userLanguage();
31
32
  const cmd = upgradeCommand();
@@ -40,6 +41,25 @@ export async function run({ hasFlag, version }) {
40
41
  // network round trip is fine here.
41
42
  await refreshUpdateState(version);
42
43
  const s = updateStatus(version);
44
+ // A legacy-name copy has somewhere to go even on the newest version: the
45
+ // package was renamed, and only the new name keeps receiving releases.
46
+ if (!s.available && INSTALLED_UNDER_LEGACY_NAME) {
47
+ console.log(lang === 'ko'
48
+ ? `패키지 이름이 claude-token-saver 에서 sprag-cli 로 바뀌었습니다. 버전은 최신(v${version})이지만, 앞으로의 릴리스를 계속 받으려면 새 이름으로 옮겨야 합니다.`
49
+ : `The package was renamed from claude-token-saver to sprag-cli. You are on the latest version (v${version}), but future releases only ship under the new name.`);
50
+ console.log(`$ ${cmd}`);
51
+ const migrateCode = await runCommand(cmd);
52
+ if (migrateCode !== 0) {
53
+ console.error(lang === 'ko'
54
+ ? `이전 명령이 종료 코드 ${migrateCode}로 실패했습니다. 위 출력을 확인하십시오.`
55
+ : `Migration command failed with exit code ${migrateCode}. Check the output above.`);
56
+ process.exit(migrateCode);
57
+ }
58
+ console.log(lang === 'ko'
59
+ ? '이전이 끝났습니다. 새 셸에서 sprag --version 으로 확인하십시오.'
60
+ : 'Migration done. Confirm with sprag --version in a fresh shell.');
61
+ return;
62
+ }
43
63
  if (!s.available) {
44
64
  console.log(lang === 'ko'
45
65
  ? `이미 최신 버전입니다 (v${version}). 설치할 것이 없습니다.`
@@ -48,8 +68,12 @@ export async function run({ hasFlag, version }) {
48
68
  }
49
69
 
50
70
  console.log(lang === 'ko'
51
- ? `v${version} → ${s.latest} 로 업그레이드합니다.`
52
- : `Upgrading v${version} → ${s.latest}.`);
71
+ ? (INSTALLED_UNDER_LEGACY_NAME
72
+ ? `v${version} → ${s.latest} 로 업그레이드하면서 새 패키지 이름(sprag-cli)으로 함께 옮깁니다.`
73
+ : `v${version} → ${s.latest} 로 업그레이드합니다.`)
74
+ : (INSTALLED_UNDER_LEGACY_NAME
75
+ ? `Upgrading v${version} → ${s.latest} and moving to the new package name (sprag-cli).`
76
+ : `Upgrading v${version} → ${s.latest}.`));
53
77
  console.log(`$ ${cmd}`);
54
78
  const code = await runCommand(cmd);
55
79
  if (code !== 0) {
@@ -60,9 +60,10 @@ ${HARNESS_SECTIONS[0].heading}
60
60
  - 같은 에러·오해·반복 작업이 한 번 더 발생하면 즉시 \`.claude/ratchet.md\`에
61
61
  "조건 → 행동" 한 줄로 룰 추가.
62
62
  - sprag가 후보를 감지하면 statusline에 \`🅷⚠ ratchet?\`로 알림.
63
- \`sprag harness promote "<rule>" --project|--global\`로 승인.
64
- - **scope는 항상 사용자에게 먼저 물어볼 것** — 프로젝트 한정이면 \`--project\`,
65
- 도구·환경 일반 룰이면 \`--global\`(\`~/.claude/ratchet.md\`). Bash 환경은
63
+ \`sprag harness promote "<rule>" --global|--project\`로 승인.
64
+ - **scope는 항상 사용자에게 먼저 물어볼 것.** 선택지는 글로벌을 먼저 제시한다.
65
+ 도구·환경 일반 룰이면 \`--global\`(\`~/.claude/ratchet.md\`), 프로젝트 한정이면
66
+ \`--project\`. Bash 환경은
66
67
  non-TTY라 CLI의 readline 프롬프트가 안 뜨므로, 호출자(LLM)가 직접 묻고
67
68
  플래그를 명시해야 함. 묻지 않고 기본값으로 등록하지 말 것.
68
69
  - 승인된 룰은 이 블록 맨 아래 \`@\` import로 매 세션 로드된다 — 그 import 라인을
@@ -109,7 +110,7 @@ ${HARNESS_SECTIONS[4].heading}
109
110
 
110
111
  📌 운영:
111
112
  - \`sprag harness check\` — 현재 셋업 점수
112
- - \`sprag harness promote "<룰>" --project|--global\` — ratchet에 룰 추가 (scope는 사용자에게 먼저 물어볼 것)
113
+ - \`sprag harness promote "<룰>" --global|--project\` — ratchet에 룰 추가 (scope는 사용자에게 먼저 물어볼 것)
113
114
  - \`sprag harness off\` — statusline 표시 끄기
114
115
 
115
116
  ---
package/src/route-scan.js CHANGED
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Pipeline position (per design discussion): this scan is NOT a real-time
16
16
  * router — it is a session-boundary calibrator that feeds the existing
17
- * ratchet promote flow (`harness promote R<N> --project|--global`).
17
+ * ratchet promote flow (`harness promote R<N> --global|--project`).
18
18
  */
19
19
 
20
20
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
package/src/seed-rules.js CHANGED
@@ -243,7 +243,9 @@ export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot()
243
243
  const lines = [];
244
244
  if (lang === 'ko') {
245
245
  lines.push(`[claude-token-saver seed] 아직 등록되지 않은 추천 룰이 ${pending.length}건 있습니다 (패키지 동봉 프리셋).`);
246
+ lines.push('**이 세션의 첫 응답에서 다른 작업보다 먼저 1번 룰의 등록 여부를 물어보십시오.** 사용자가 먼저 꺼낸 용건이 있으면 그 답변을 마친 직후에 이어서 묻고, 용건이 끝나기를 기다리지 마십시오. 사용자가 이 안내 문구에 대해 질문할 때까지 미루면 안 됩니다.');
246
247
  lines.push('사용자에게 **한 건씩 순서대로** 보여 주고 등록할지 물어보십시오. 한 번에 전체 등록을 권하지 말고, 답을 받은 뒤 바로 아래 명령을 실행하십시오.');
248
+ lines.push('적용 범위를 물을 때는 선택지를 **글로벌(모든 프로젝트)을 먼저, 이 프로젝트만을 그다음**으로 제시하십시오.');
247
249
  if (models.length > 0) {
248
250
  lines.push(` · 모델 피팅 룰 ${models.length}건 — 등록하면 해당 유형 작업이 더 싼 서브에이전트로 위임됩니다.`);
249
251
  for (const s of models) {
@@ -260,7 +262,9 @@ export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot()
260
262
  lines.push(' (사용자가 "전부 등록"이라고 답하면 `seed accept all --global` 을 쓸 수 있습니다)');
261
263
  } else {
262
264
  lines.push(`[claude-token-saver seed] ${pending.length} recommended rule(s) from the bundled presets are not registered yet.`);
265
+ lines.push('**Ask about rule 1 in your very first reply of this session, before anything else.** If the user opened with their own request, answer it and then ask right away; do not wait for their task to finish, and never wait until they ask about this notice.');
263
266
  lines.push('Walk the user through them **one at a time** and ask about each. Do not push the whole set at once; run the matching command as soon as they answer.');
267
+ lines.push('When you ask about the scope, offer **global (all projects) first, this project only second**.');
264
268
  if (models.length > 0) {
265
269
  lines.push(` · ${models.length} model-fitting rule(s) — once registered, that kind of work goes to a cheaper subagent.`);
266
270
  for (const s of models) {
@@ -23,10 +23,15 @@ import { fileURLToPath } from 'node:url';
23
23
  import { userDataDir } from './paths.js';
24
24
  import { debug } from './debug.js';
25
25
 
26
- // Whichever name this copy was installed under (sprag-cli or the legacy
27
- // claude-token-saver) is the one whose registry entry must be consulted.
26
+ // This copy may be installed under the canonical name or under the legacy one.
27
+ // Releases go out under the canonical name, so that is the entry the registry
28
+ // check consults either way — a legacy copy must keep hearing about new
29
+ // versions even after the legacy name stops receiving publishes.
28
30
  import { createRequire } from 'node:module';
29
31
  const PKG_NAME = createRequire(import.meta.url)('../package.json').name;
32
+ const CANONICAL_NAME = 'sprag-cli';
33
+ const LEGACY_NAME = 'claude-token-saver';
34
+ const IS_LEGACY = PKG_NAME === LEGACY_NAME;
30
35
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h — the registry is not a health endpoint
31
36
  const FETCH_TIMEOUT_MS = 5000;
32
37
 
@@ -148,7 +153,7 @@ export async function refreshUpdateState(currentVersion) {
148
153
  try {
149
154
  // The `latest` dist-tag endpoint returns a few hundred bytes, unlike the
150
155
  // full packument which is megabytes for a package with this many releases.
151
- const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, {
156
+ const res = await fetch(`https://registry.npmjs.org/${CANONICAL_NAME}/latest`, {
152
157
  signal: controller.signal,
153
158
  headers: { accept: 'application/json' },
154
159
  });
@@ -194,6 +199,15 @@ export function dismissUpdate(version) {
194
199
  */
195
200
  export function upgradeCommand() {
196
201
  const here = dirname(fileURLToPath(import.meta.url));
202
+ // A legacy copy upgrades by moving to the canonical name. Installing the new
203
+ // name alone would leave two copies fighting over the same `sprag` binary,
204
+ // so the old one comes off first.
205
+ if (IS_LEGACY) {
206
+ if (here.includes('/pnpm/')) return `pnpm remove -g ${LEGACY_NAME} && pnpm add -g ${CANONICAL_NAME}@latest`;
207
+ if (here.includes('/.bun/')) return `bun remove -g ${LEGACY_NAME} && bun add -g ${CANONICAL_NAME}@latest`;
208
+ if (here.includes('/.yarn/')) return `yarn global remove ${LEGACY_NAME} && yarn global add ${CANONICAL_NAME}@latest`;
209
+ return `npm uninstall -g ${LEGACY_NAME} && npm install -g ${CANONICAL_NAME}@latest`;
210
+ }
197
211
  if (here.includes('/pnpm/')) return `pnpm add -g ${PKG_NAME}@latest`;
198
212
  if (here.includes('/.bun/')) return `bun add -g ${PKG_NAME}@latest`;
199
213
  if (here.includes('/.yarn/')) return `yarn global add ${PKG_NAME}@latest`;
@@ -202,3 +216,6 @@ export function upgradeCommand() {
202
216
 
203
217
  export const UPDATE_CHECK_INTERVAL_MS = CHECK_INTERVAL_MS;
204
218
  export const PACKAGE_NAME = PKG_NAME;
219
+ export const CANONICAL_PACKAGE_NAME = CANONICAL_NAME;
220
+ /** True when this copy was installed under the legacy package name. */
221
+ export const INSTALLED_UNDER_LEGACY_NAME = IS_LEGACY;