claude-token-saver 3.23.0 → 3.23.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.23.0",
3
+ "version": "3.23.1",
4
4
  "description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet — post-hoc session analysis, no realtime router, no extra LLM calls.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -161,6 +161,14 @@ export async function run({ args, hasFlag, numArg }) {
161
161
  // one rule and the file carries another.
162
162
  const mrHook = await import('../model-rules.js');
163
163
  const composed = (base, c) => mrHook.composeRuleText(base, c, lang);
164
+ // Re-render ratchet-model.md from THIS version's template. Rule text is
165
+ // composed in code, so an upgrade that rewords it leaves every existing
166
+ // file stale, and the rewrite used to ride along with refreshModelRules
167
+ // — which only runs on a rescan. A session that starts on a warm cache
168
+ // would keep reading the old wording indefinitely. The call writes only
169
+ // when the rendering differs, so the common case is a read and a string
170
+ // compare.
171
+ try { mrHook.syncAllFiles(); } catch (e) { debug('route-scan:sync-ratchet', e); }
164
172
  // Registered rules whose delegated-category error rate crossed the
165
173
  // health threshold since promotion — the user approved these, so a
166
174
  // status change must be briefed, not just written into the md file.
@@ -288,8 +288,15 @@ export function modelRatchetPathFor(scope, targetRoot) {
288
288
 
289
289
  /**
290
290
  * Regenerate ratchet-model.md for every target that carries model rules.
291
- * A target whose rules are all gone gets its file removed (it's fully
292
- * tool-owned, so deletion is safe).
291
+ * A target whose rules are all gone gets its file emptied not deleted,
292
+ * because CLAUDE.md imports the path (see the note at the loop below).
293
+ *
294
+ * Writes are content-conditional: a target whose rendering already matches
295
+ * what is on disk is left alone, and the returned list names only the files
296
+ * that actually changed. That makes the call cheap enough to run on a cache
297
+ * hit, which is what closes the upgrade gap — the file is rendered from THIS
298
+ * version's template, so an upgrade that reworded the rules reaches the disk
299
+ * on the next session rather than waiting for a rescan to happen to fire.
293
300
  */
294
301
  export function syncAllFiles({ previousPaths = [] } = {}) {
295
302
  const data = loadModelRules();
@@ -299,19 +306,30 @@ export function syncAllFiles({ previousPaths = [] } = {}) {
299
306
  if (!byPath.has(p)) byPath.set(p, []);
300
307
  byPath.get(p).push(r);
301
308
  }
302
- const written = [];
303
- for (const [p, rules] of byPath) {
309
+ // Skip the write when the file already says exactly this. An unreadable or
310
+ // missing file reads as "" and therefore always differs, which is the right
311
+ // outcome — it gets written.
312
+ const writeIfChanged = (p, text) => {
313
+ try {
314
+ if (existsSync(p) && readFileSync(p, 'utf8') === text) return false;
315
+ } catch { /* unreadable → fall through and rewrite it */ }
304
316
  try {
305
317
  mkdirSync(dirname(p), { recursive: true });
306
- writeFileSync(p, renderModelRatchet(rules));
307
- written.push(p);
308
- } catch { /* unwritable target — skip, registry stays authoritative */ }
318
+ writeFileSync(p, text);
319
+ return true;
320
+ } catch {
321
+ return false; // unwritable target — skip, registry stays authoritative
322
+ }
323
+ };
324
+ const written = [];
325
+ for (const [p, rules] of byPath) {
326
+ if (writeIfChanged(p, renderModelRatchet(rules))) written.push(p);
309
327
  }
310
328
  // A target that lost its last rule is emptied, NOT deleted: CLAUDE.md
311
329
  // imports this path, and a dangling `@` import is worse than an empty file.
312
330
  for (const p of previousPaths) {
313
331
  if (!byPath.has(p) && existsSync(p)) {
314
- try { writeFileSync(p, renderModelRatchet([])); } catch { /* stale content; regenerated next sync */ }
332
+ if (writeIfChanged(p, renderModelRatchet([]))) written.push(p);
315
333
  }
316
334
  }
317
335
  return written;