claude-token-saver 2.13.3 β†’ 2.14.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
@@ -488,10 +488,35 @@ async function main() {
488
488
  }
489
489
 
490
490
  if (sub === 'promote') {
491
- const raw = args.slice(2).join(' ').trim();
491
+ // Parse scope flags before stripping. Accepts: --global, --project,
492
+ // --scope=global|project, --scope global|project
493
+ const promoteArgs = args.slice(2);
494
+ let scope = null;
495
+ const scopeFlags = new Set();
496
+ for (let i = 0; i < promoteArgs.length; i++) {
497
+ const a = promoteArgs[i];
498
+ if (a === '--global') { scope = 'global'; scopeFlags.add(i); }
499
+ else if (a === '--project') { scope = 'project'; scopeFlags.add(i); }
500
+ else if (a === '--scope' && promoteArgs[i + 1]) {
501
+ const v = promoteArgs[i + 1];
502
+ if (v !== 'global' && v !== 'project') {
503
+ console.error(`Invalid --scope value: ${v} (expected "global" or "project")`);
504
+ process.exit(1);
505
+ }
506
+ scope = v; scopeFlags.add(i); scopeFlags.add(i + 1); i++;
507
+ } else if (a.startsWith('--scope=')) {
508
+ const v = a.slice('--scope='.length);
509
+ if (v !== 'global' && v !== 'project') {
510
+ console.error(`Invalid --scope value: ${v} (expected "global" or "project")`);
511
+ process.exit(1);
512
+ }
513
+ scope = v; scopeFlags.add(i);
514
+ }
515
+ }
516
+ const raw = promoteArgs.filter((_, i) => !scopeFlags.has(i)).join(' ').trim();
492
517
  if (!raw) {
493
- console.error('Usage: claude-token-saver harness promote <N> # from statusline πŸ…·βš  ratchet? #N');
494
- console.error(' or: claude-token-saver harness promote "<rule text>"');
518
+ console.error('Usage: claude-token-saver harness promote [--global|--project] <N> # from statusline πŸ…·βš  ratchet? #N');
519
+ console.error(' or: claude-token-saver harness promote [--global|--project] "<rule text>"');
495
520
  process.exit(1);
496
521
  }
497
522
  let rule = raw;
@@ -515,8 +540,33 @@ async function main() {
515
540
  }
516
541
  rule = `반볡 감지 Γ—${cand.count}: ${cand.pattern} β€” TODO: μ›μΈΒ·μ˜ˆλ°©μ±… ν•œ μ€„λ‘œ`;
517
542
  }
518
- const r = harnessPromote(rule);
519
- console.log(`Appended to ${r.path}:`);
543
+ // Scope resolution: explicit flag wins. Otherwise prompt interactively
544
+ // when running on a TTY; in non-TTY (CI/scripts) require an explicit
545
+ // flag so the choice is never silently made for the caller.
546
+ if (!scope) {
547
+ if (process.stdin.isTTY && process.stdout.isTTY) {
548
+ const readline = await import('node:readline');
549
+ const { homedir: hd } = await import('node:os');
550
+ const { findProjectRoot: fpr } = await import('../src/harness.js');
551
+ const projPath = `${fpr()}/.claude/ratchet.md`;
552
+ const globPath = `${hd()}/.claude/ratchet.md`;
553
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
554
+ const ask = (q) => new Promise((res) => rl.question(q, res));
555
+ console.log('Where should this rule live?');
556
+ console.log(` [1] project (${projPath})`);
557
+ console.log(` [2] global (${globPath})`);
558
+ const ans = (await ask('Choose [1/2] (default 1): ')).trim();
559
+ rl.close();
560
+ scope = (ans === '2' || ans.toLowerCase() === 'global' || ans.toLowerCase() === 'g')
561
+ ? 'global' : 'project';
562
+ } else {
563
+ console.error('Scope required in non-interactive mode.');
564
+ console.error('Pass --project or --global (or --scope=project|global).');
565
+ process.exit(1);
566
+ }
567
+ }
568
+ const r = harnessPromote(rule, { scope });
569
+ console.log(`Appended to ${r.path} [${r.scope}]:`);
520
570
  console.log(` - ${rule}`);
521
571
  if (/^\d+$/.test(raw)) {
522
572
  console.log('\nπŸ‘‰ ratchet.mdλ₯Ό μ—΄μ–΄ TODO 뢀뢄을 μ‹€μ œ 룰둜 λ‹€λ“¬μ–΄μ£Όμ„Έμš”.');
@@ -570,21 +620,30 @@ async function main() {
570
620
  }
571
621
 
572
622
  if (sub === 'list' || sub === 'ls') {
573
- const { path, rules } = harnessListRules();
574
- if (!rules.length) {
575
- console.log(`No ratchet rules in ${path}`);
576
- return;
577
- }
578
- console.log(`πŸ“‹ Ratchet rules β€” ${path}\n`);
579
- for (const r of rules) console.log(` #${r.index} ${r.text}`);
580
- console.log('\nRemove with: claude-token-saver harness rm <N>');
623
+ const wantGlobal = hasFlag('--global');
624
+ const wantProject = hasFlag('--project') || !wantGlobal;
625
+ const print = (scope) => {
626
+ const { path, rules } = harnessListRules({ scope });
627
+ if (!rules.length) {
628
+ console.log(`No ratchet rules in ${path} [${scope}]`);
629
+ return;
630
+ }
631
+ console.log(`πŸ“‹ Ratchet rules [${scope}] β€” ${path}\n`);
632
+ for (const r of rules) console.log(` #${r.index} ${r.text}`);
633
+ console.log('');
634
+ };
635
+ if (wantProject) print('project');
636
+ if (wantGlobal) print('global');
637
+ console.log('Remove with: claude-token-saver harness rm [--global|--project] <N>');
581
638
  return;
582
639
  }
583
640
 
584
641
  if (sub === 'rm') {
585
- const raw = (args[2] || '').trim();
642
+ const rmScope = hasFlag('--global') ? 'global' : 'project';
643
+ const rmArgs = args.slice(2).filter((a) => a !== '--global' && a !== '--project');
644
+ const raw = (rmArgs[0] || '').trim();
586
645
  if (!/^\d+$/.test(raw)) {
587
- console.error('Usage: claude-token-saver harness rm <N> # N from `harness list`');
646
+ console.error('Usage: claude-token-saver harness rm [--global|--project] <N> # N from `harness list`');
588
647
  process.exit(1);
589
648
  }
590
649
  const n = parseInt(raw, 10);
@@ -598,7 +657,7 @@ async function main() {
598
657
  console.log(' - 룰이 λ„ˆλ¬΄ μ’μ•„μ„œ 거의 λ°œλ™ μ•ˆ λ˜λ‚˜? β†’ κ·Έλƒ₯ 두기 (λΉ„μš© 0)');
599
658
  console.log(' - 정말 잘λͺ»λœ 룰이라 ν™•μ‹ ? β†’ κ·Έλ•Œλ§Œ μ‚­μ œ');
600
659
  console.log('');
601
- const r = harnessRmRule(n);
660
+ const r = harnessRmRule(n, { scope: rmScope });
602
661
  if (!r.ok) {
603
662
  console.error(`❌ ${r.error}`);
604
663
  if (r.rules) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.13.3",
3
+ "version": "2.14.0",
4
4
  "description": "Save tokens on Claude Code β€” spike diagnosis, 1M-context detection, TTL countdown, statusline. (formerly claude-cache-monitor)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/harness.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
11
11
  import { join, resolve, dirname } from 'node:path';
12
+ import { homedir } from 'node:os';
12
13
  import { createRequire } from 'node:module';
13
14
  import {
14
15
  HARNESS_SECTIONS,
@@ -58,6 +59,14 @@ function ratchetMdPath(root) {
58
59
  return join(root, '.claude', 'ratchet.md');
59
60
  }
60
61
 
62
+ function globalRatchetMdPath() {
63
+ return join(homedir(), '.claude', 'ratchet.md');
64
+ }
65
+
66
+ function resolveRatchetPath(scope, root) {
67
+ return scope === 'global' ? globalRatchetMdPath() : ratchetMdPath(root);
68
+ }
69
+
61
70
  /**
62
71
  * Count how many of the 5 harness sections appear in the project's CLAUDE.md.
63
72
  * Returns { configured, total, missing, hasBlock }. Cheap enough to call from
@@ -217,8 +226,8 @@ export function harnessUninit({ root = findProjectRoot(), purgeRatchet = false }
217
226
  * harness promote β€” append a one-line rule to .claude/ratchet.md.
218
227
  * Creates the file from the initial template if missing.
219
228
  */
220
- export function harnessPromote(ruleText, { root = findProjectRoot() } = {}) {
221
- const rmPath = ratchetMdPath(root);
229
+ export function harnessPromote(ruleText, { root = findProjectRoot(), scope = 'project' } = {}) {
230
+ const rmPath = resolveRatchetPath(scope, root);
222
231
  let existing = '';
223
232
  if (existsSync(rmPath)) {
224
233
  existing = readFileSync(rmPath, 'utf8');
@@ -228,15 +237,15 @@ export function harnessPromote(ruleText, { root = findProjectRoot() } = {}) {
228
237
  }
229
238
  const next = appendRatchetRule(existing, ruleText);
230
239
  writeFileSync(rmPath, next);
231
- return { path: rmPath, root };
240
+ return { path: rmPath, root, scope };
232
241
  }
233
242
 
234
243
  /**
235
244
  * harness list β€” return numbered ratchet rules from .claude/ratchet.md.
236
245
  * Numbering is 1-based and matches `harness rm <N>`.
237
246
  */
238
- export function harnessListRules({ root = findProjectRoot() } = {}) {
239
- const rmPath = ratchetMdPath(root);
247
+ export function harnessListRules({ root = findProjectRoot(), scope = 'project' } = {}) {
248
+ const rmPath = resolveRatchetPath(scope, root);
240
249
  if (!existsSync(rmPath)) return { path: rmPath, rules: [] };
241
250
  const lines = readFileSync(rmPath, 'utf8').split('\n');
242
251
  const rules = [];
@@ -260,8 +269,8 @@ export function harnessListRules({ root = findProjectRoot() } = {}) {
260
269
  * value is one-way accumulation; deleting should feel deliberate. The CLI
261
270
  * surfaces a "narrow the condition instead" reminder around this call.
262
271
  */
263
- export function harnessRmRule(n, { root = findProjectRoot() } = {}) {
264
- const { path: rmPath, rules } = harnessListRules({ root });
272
+ export function harnessRmRule(n, { root = findProjectRoot(), scope = 'project' } = {}) {
273
+ const { path: rmPath, rules } = harnessListRules({ root, scope });
265
274
  if (!existsSync(rmPath)) {
266
275
  return { ok: false, error: `ratchet.md not found at ${rmPath}` };
267
276
  }