claude-token-saver 3.10.0 โ†’ 3.11.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.en.md CHANGED
@@ -77,6 +77,7 @@ The statusline appears at the bottom of Claude Code right away. If auto-registra
77
77
  | `โœฆ current` / `๐Ÿ“… weekly` | 5-hour / 7-day rate-limit window usage + reset time |
78
78
  | `๐Ÿ“ฆ` | Context usage (e.g. `Ctx 68% of 1M`) โ€” colored by fill. Current models default to 1M with no premium, but token volume itself drives per-turn cost and 5H/7D burn |
79
79
  | `๐Ÿ’ฐ` | Cumulative savings from prompt caching |
80
+ | `๐Ÿ”€` | Cumulative savings from delegation โ€” what running work on a cheaper tier saved, a different number from `๐Ÿ’ฐ`. Hidden until a delegation has actually been measured |
80
81
 
81
82
  When something is wrong, a **warning chip leads the line**:
82
83
 
@@ -300,6 +301,10 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
300
301
 
301
302
  ## Release notes
302
303
 
304
+ ### v3.11.0 (2026-08-21)
305
+ - **Delegation savings chip in the statusline** โ€” `๐Ÿ”€ Delegated $3.2`. The existing `๐Ÿ’ฐ Cache saved` covers the prompt cache; this one is what running work on a cheaper tier saved. Zero or missing data hides the chip entirely, so nothing changes for direct-API users. The statusline only reads `model-rules.json` โ€” it never triggers a scan (it re-renders every few seconds). Segment name: `delegated`.
306
+ - **Role-learning mis-classification fixed** โ€” subagent records sometimes land in the parent transcript without an `isSidechain` flag, and that circumstantial evidence could outvote stated evidence, confirming a haiku profile as the session model (measured: 16 against 1, agreement exactly at the 80% line). Stated evidence (`Task(model:)` parameters, agent-definition frontmatter) and inferred evidence are now tallied separately, and an inference is not adopted when stated evidence contradicts it. Undecided ids stay `unknown` and leave the aggregate.
307
+
303
308
  ### v3.10.0 (2026-08-20)
304
309
  - **Model tiers are detected again behind a Bedrock / LiteLLM gateway** โ€” when the transcript's model id is an inference-profile ARN there is no `opus` or `haiku` in the string, so it fell back to Sonnet. Since `worthDelegating()` requires `rank > target`, **every T1 rule was rejected**, savings aggregated to zero, and cost was under-counted by roughly 1.67x. The profile id is now learned as a role (parent `Task` call joined to the subagent run by `toolUseId`) and mapped back to the alias your environment declares. The pricing table, the ranks, and the tiering logic are untouched.
305
310
  - **No confident mapping means no guess** โ€” under three observations, or below 80% agreement, the id stays `unknown` and leaves the delegation aggregate. Quietly calling it Sonnet was the worse failure.
package/README.md CHANGED
@@ -76,6 +76,7 @@ npm i -g claude-token-saver
76
76
  | `โœฆ current` / `๐Ÿ“… weekly` | 5์‹œ๊ฐ„ / 7์ผ rate-limit ์œˆ๋„ ์‚ฌ์šฉ๋ฅ  + ๋ฆฌ์…‹ ์‹œ๊ฐ |
77
77
  | `๐Ÿ“ฆ` | ์ปจํ…์ŠคํŠธ ์‚ฌ์šฉ๋ฅ  (์˜ˆ: `Ctx 68% of 1M`) โ€” ์‚ฌ์šฉ๋ฅ  ๊ธฐ์ค€ ๋…น/ํ™ฉ/์ . ํ˜„์žฌ ๋ชจ๋ธ์€ 1M์ด ๊ธฐ๋ณธยทํ”„๋ฆฌ๋ฏธ์—„ ์—†์Œ์ด์ง€๋งŒ, ํ† ํฐ๋Ÿ‰ ์ž์ฒด๊ฐ€ ํ„ด๋‹น ๋น„์šฉ๊ณผ 5H/7D ํ•œ๋„๋ฅผ ํƒœ์›๋‹ˆ๋‹ค |
78
78
  | `๐Ÿ’ฐ` | ์บ์‹œ๊ฐ€ ์ ˆ์•ฝํ•ด์ค€ ๋ˆ„์  ๊ธˆ์•ก |
79
+ | `๐Ÿ”€` | ์œ„์ž„์ด ์ ˆ์•ฝํ•ด์ค€ ๋ˆ„์  ๊ธˆ์•ก โ€” ๋” ์‹ผ ํ‹ฐ์–ด๋กœ ๋‚ด๋ ค์„œ ์•„๋‚€ ๋ชซ์ด๋ฉฐ `๐Ÿ’ฐ`์™€๋Š” ๋‹ค๋ฅธ ์ˆ˜์น˜์ž…๋‹ˆ๋‹ค. ์‹ค์ธก ์œ„์ž„์ด ์—†์œผ๋ฉด ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค |
79
80
 
80
81
  ๋ฌธ์ œ๊ฐ€ ๊ฐ์ง€๋˜๋ฉด **๊ฒฝ๊ณ  ์นฉ์ด ๋งจ ์•ž์—** ๋ถ™์Šต๋‹ˆ๋‹ค:
81
82
 
@@ -256,6 +257,10 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
256
257
 
257
258
  ## ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ
258
259
 
260
+ ### v3.11.0 (2026-08-21)
261
+ - **statusline์— ์œ„์ž„ ์ ˆ๊ฐ ์นฉ ์ถ”๊ฐ€** โ€” `๐Ÿ”€ Delegated $3.2`. ๊ธฐ์กด `๐Ÿ’ฐ Cache saved`๋Š” ํ”„๋กฌํ”„ํŠธ ์บ์‹œ ์ ˆ๊ฐ์ด๊ณ , ์ƒˆ ์นฉ์€ **๋” ์‹ผ ํ‹ฐ์–ด๋กœ ๋‚ด๋ ค์„œ ์•„๋‚€ ๊ธˆ์•ก**์ž…๋‹ˆ๋‹ค. ๊ฐ’์ด 0์ด๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์นฉ์„ ์•„์˜ˆ ๊ทธ๋ฆฌ์ง€ ์•Š์•„ ์ง์ ‘ API ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋Š˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. statusline์€ `model-rules.json`์„ ์ฝ๊ธฐ๋งŒ ํ•˜๋ฉฐ ์Šค์บ”์„ ๋Œ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค(5์ดˆ๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋Š” ์ž๋ฆฌ์ž…๋‹ˆ๋‹ค). ์„ธ๊ทธ๋จผํŠธ ์ด๋ฆ„์€ `delegated`์ž…๋‹ˆ๋‹ค.
262
+ - **์—ญํ•  ํ•™์Šต์˜ ์˜ค๋ถ„๋ฅ˜ ์ˆ˜์ •** โ€” ์„œ๋ธŒ์—์ด์ „ํŠธ ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ถ€๋ชจ ํŠธ๋žœ์Šคํฌ๋ฆฝํŠธ์— `isSidechain` ์—†์ด ์„ž์—ฌ ๋“ค์–ด์˜ค๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด, ๊ทธ ์ •ํ™ฉ ์ฆ๊ฑฐ๊ฐ€ ๋ช…์‹œ ์ฆ๊ฑฐ๋ฅผ ์ด๊ฒจ haiku ํ”„๋กœํŒŒ์ผ์ด ์„ธ์…˜ ๋ชจ๋ธ๋กœ ํ™•์ •๋˜๊ณค ํ–ˆ์Šต๋‹ˆ๋‹ค(์‹ค์ธก 16๊ฑด ๋Œ€ 1๊ฑด, ํ•ฉ์˜์œจ ์ •ํ™•ํžˆ 80%). ์ด์ œ **๋ช…์‹œ ์ฆ๊ฑฐ(`Task(model:)` ํŒŒ๋ผ๋ฏธํ„ฐยท์—์ด์ „ํŠธ ์ •์˜ frontmatter)์™€ ์ •ํ™ฉ ์ฆ๊ฑฐ(sidechain ํ”Œ๋ž˜๊ทธ ๋ถ€์žฌ)๋ฅผ ๋ถ„๋ฆฌํ•ด ์ง‘๊ณ„**ํ•˜๊ณ , ๋ช…์‹œ ์ฆ๊ฑฐ๊ฐ€ ์ด๋ฅผ ๋ฐ˜๋ฐ•ํ•˜๋ฉด ์ •ํ™ฉ ์ถ”๋ก ์„ ์ฑ„ํƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ™•์ •์ด ์•ˆ ๋˜๋ฉด `unknown`์œผ๋กœ ๋‚จ์•„ ์ง‘๊ณ„์—์„œ ๋น ์ง‘๋‹ˆ๋‹ค.
263
+
259
264
  ### v3.10.0 (2026-08-20)
260
265
  - **๊ฒŒ์ดํŠธ์›จ์ด(BedrockยทLiteLLM) ํ™˜๊ฒฝ์—์„œ ๋ชจ๋ธ ํ‹ฐ์–ด๋ฅผ ๋‹ค์‹œ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค** โ€” ๋กœ๊ทธ์˜ ๋ชจ๋ธ๋ช…์ด ์ถ”๋ก  ํ”„๋กœํŒŒ์ผ ARN์ด๋ฉด `opus`ยท`haiku` ๋‹จ์„œ๊ฐ€ ์—†์–ด Sonnet์œผ๋กœ ํด๋ฐฑํ–ˆ๊ณ , `worthDelegating()`์ด `rank > target`์„ ์š”๊ตฌํ•˜๋ฏ€๋กœ **T1 ์œ„์ž„์ด ์ „๋ถ€ ๊ธฐ๊ฐ**๋์Šต๋‹ˆ๋‹ค. ์ ˆ๊ฐ ์ง‘๊ณ„๋Š” 0, ๋น„์šฉ์€ ์•ฝ 1.67๋ฐฐ ๊ณผ์†Œ ๊ณ„์ƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ํ”„๋กœํŒŒ์ผ ID๋ฅผ ์—ญํ• ๋กœ ํ•™์Šตํ•ด(๋ถ€๋ชจ `Task` ํ˜ธ์ถœ โ†” ์„œ๋ธŒ์—์ด์ „ํŠธ `toolUseId` ์ •ํ™• ์กฐ์ธ) ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ ์–ธํ•œ ๋ณ„์นญ์œผ๋กœ ๋˜๋Œ๋ฆฝ๋‹ˆ๋‹ค. ๊ฐ€๊ฒฉํ‘œยท๋žญํฌยทํŒ์ • ๋กœ์ง์€ ๊ทธ๋Œ€๋กœ์ž…๋‹ˆ๋‹ค.
261
266
  - **ํ™•์‹ ์ด ์—†์œผ๋ฉด ์ˆจ๊ธฐ์ง€ ์•Š๊ณ  ๋“œ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค** โ€” ๊ด€์ธก 3๊ฑด ๋ฏธ๋งŒ์ด๊ฑฐ๋‚˜ ์—ญํ•  ๋™์˜์œจ 80% ๋ฏธ๋งŒ์ด๋ฉด `unknown`์œผ๋กœ ๋‘๊ณ  ์œ„์ž„ ์ง‘๊ณ„์—์„œ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. Sonnet์œผ๋กœ ์กฐ์šฉํžˆ ํ‹€๋ฆฌ๋˜ ๊ธฐ์กด ๋™์ž‘์ด ๋” ๋‚˜๋นด์Šต๋‹ˆ๋‹ค.
package/bin/cli.js CHANGED
@@ -433,6 +433,17 @@ async function main() {
433
433
  }
434
434
  const lastActivity = Math.max(otherLastActivity, currentSessionLastUser);
435
435
 
436
+ // What delegation has measurably saved, read straight from the registry
437
+ // route-scan maintains. A lookup, never a scan: the statusline re-renders
438
+ // every few seconds and a scan parses tens of MB of transcripts.
439
+ let delegationSaved = 0;
440
+ try {
441
+ const { delegationSavedUsd } = await import('../src/model-rules.js');
442
+ delegationSaved = delegationSavedUsd();
443
+ } catch (e) {
444
+ debug('model-rules:saved', e); // an unreadable registry just hides the chip
445
+ }
446
+
436
447
  const data = {
437
448
  summary: sum,
438
449
  trend,
@@ -447,6 +458,7 @@ async function main() {
447
458
  spikeChip,
448
459
  caps,
449
460
  model,
461
+ delegationSaved,
450
462
  };
451
463
 
452
464
  let output;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
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": {
@@ -211,7 +211,7 @@ export function formatNoSession({ caps = null, model = null, windowLabel = '' }
211
211
  * @param {boolean} [opts.verbose=false] - longer layout with labels
212
212
  * @param {boolean} [opts.timer=true] - show TTL countdown segment
213
213
  * @param {'text'|'icon'} [opts.mode='text'] - label style. 'icon' uses ๐Ÿง  โณ ๐Ÿ’ฐ instead of word labels.
214
- * @param {string[]|null} [opts.segments] - whitelist of segments to render. Names: cap-warn, spike, harness, model, hit, ttl, saved, ctx, period, plus per-window keys (`five_hour`, `seven_day`, โ€ฆ). `5h`/`7d` are kept as aliases for back-compat. Null/undefined = all.
214
+ * @param {string[]|null} [opts.segments] - whitelist of segments to render. Names: cap-warn, spike, harness, model, hit, ttl, saved, delegated, ctx, period, plus per-window keys (`five_hour`, `seven_day`, โ€ฆ). `5h`/`7d` are kept as aliases for back-compat. Null/undefined = all.
215
215
  */
216
216
  export function formatReport(data, { color = true, verbose = false, timer = true, mode = 'text', segments = null } = {}) {
217
217
  const { summary, ttl, cost, options, lastActivity, contextWindow, ctxLive, spikeChip, caps, model } = data;
@@ -255,6 +255,21 @@ export function formatReport(data, { color = true, verbose = false, timer = true
255
255
  : 'Cache saved';
256
256
  const saveSeg = `${c(CYAN)}${saveLabel}${c(RESET)} ${formatMoney(savings)}`;
257
257
 
258
+ // Delegation savings โ€” a DIFFERENT number from "Cache saved" above, which
259
+ // covers the prompt cache only. This one is what running work on a cheaper
260
+ // tier saved, summed from the rule registry route-scan maintains. Hidden
261
+ // when zero or absent: a permanent "$0" is noise for direct-API users and
262
+ // for anyone who has not delegated yet.
263
+ // text: "Delegated $3.2" | same in verbose
264
+ // icon: "๐Ÿ”€ $3.2" | verbose: "๐Ÿ”€ Delegated $3.2"
265
+ const delegationSaved = Number(data.delegationSaved) || 0;
266
+ const delegateLabel = isIcon
267
+ ? (verbose ? '๐Ÿ”€ Delegated' : '๐Ÿ”€')
268
+ : 'Delegated';
269
+ const delegateSeg = delegationSaved > 0
270
+ ? `${c(GREEN)}${delegateLabel}${c(RESET)} ${formatMoney(delegationSaved)}`
271
+ : null;
272
+
258
273
  // Period label honors hour-precision configs (`mode 6h` โ†’ "6h", `mode 1d` โ†’ "1d").
259
274
  // Fall back to legacy `${days}d` when callers haven't supplied a label.
260
275
  const periodLabel = options.windowLabel || `${options.days}d`;
@@ -482,6 +497,8 @@ export function formatReport(data, { color = true, verbose = false, timer = true
482
497
  // it sits near the tail. The period label closes the line as a quiet
483
498
  // timeframe footer.
484
499
  if (want('saved')) segs.push(saveSeg);
500
+ // Both savings numbers sit side by side, then the period label closes the line.
501
+ if (delegateSeg && want('delegated')) segs.push(delegateSeg);
485
502
  if (want('period')) segs.push(periodSeg);
486
503
  // Trailing erase-to-end-of-line so any leftover characters from a previous
487
504
  // (longer) statusline render don't bleed into ours. \x1b[K is the standard
@@ -200,10 +200,21 @@ function roleFromAgentType(agentType, cache) {
200
200
  return role;
201
201
  }
202
202
 
203
- function addVote(votes, pid, role) {
203
+ /**
204
+ * Record one observation.
205
+ *
206
+ * `kind` matters more than the count. An 'explicit' vote comes from a stated
207
+ * model โ€” a `Task(model: "haiku")` parameter or an agent definition's
208
+ * frontmatter. An 'inferred' vote is circumstantial: the record carried no
209
+ * sidechain flag, so it is *probably* the parent session's own model. Mixing
210
+ * the two by volume let 4 inferred votes outrank 1 explicit one and stamped a
211
+ * haiku profile as 'main' โ€” see tallyVotes.
212
+ */
213
+ function addVote(votes, pid, role, kind) {
204
214
  if (!pid || !role) return;
205
- const v = (votes[pid] ||= {});
206
- v[role] = (v[role] || 0) + 1;
215
+ const v = (votes[pid] ||= { explicit: {}, inferred: {} });
216
+ const bucket = v[kind] || (v[kind] = {});
217
+ bucket[role] = (bucket[role] || 0) + 1;
207
218
  }
208
219
 
209
220
  /**
@@ -235,7 +246,9 @@ async function scanMainTranscript(path, votes, requestedByToolUse, agentTypeCach
235
246
  const pid = profileIdFrom(msg.model);
236
247
  if (pid) {
237
248
  sawGateway = true;
238
- addVote(votes, pid, 'main');
249
+ // Circumstantial: a subagent's records also land in the parent file
250
+ // without the flag often enough to outvote real evidence.
251
+ addVote(votes, pid, 'main', 'inferred');
239
252
  }
240
253
  }
241
254
 
@@ -289,18 +302,76 @@ async function subagentFiles(sessionPath) {
289
302
  }
290
303
  }
291
304
 
292
- /** Decide a role per profile id once the votes are numerous and consistent. */
305
+ /**
306
+ * Whether two roles describe the same model. 'main' and 'opus' routinely do:
307
+ * the session model is the opus alias on a default setup, so a Task that asked
308
+ * for opus does not contradict "this is the parent's own profile".
309
+ */
310
+ function rolesAgree(a, b) {
311
+ if (!a || !b) return false;
312
+ if (a === b) return true;
313
+ const pair = new Set([a, b]);
314
+ return pair.has('main') && pair.has('opus');
315
+ }
316
+
317
+ /** Winner of one bucket, with the counts needed to judge confidence. */
318
+ function topRole(tally) {
319
+ let role = null;
320
+ let top = 0;
321
+ let total = 0;
322
+ for (const [r, n] of Object.entries(tally || {})) {
323
+ total += n;
324
+ if (n > top) { top = n; role = r; }
325
+ }
326
+ return { role, top, total };
327
+ }
328
+
329
+ /**
330
+ * Decide a role per profile id.
331
+ *
332
+ * Explicit evidence decides alone whenever there is enough of it; inferred
333
+ * evidence is only consulted when the explicit bucket is too thin. Counting
334
+ * both together is what produced the original mis-classification: a haiku
335
+ * profile appeared 16 times in parent transcripts without a sidechain flag
336
+ * against 1,702 times as a subagent, and those 16 inferred 'main' votes beat
337
+ * the single explicit 'haiku' one at exactly the 80% line.
338
+ *
339
+ * When neither bucket is decisive the id stays unresolved. An 'unknown' that
340
+ * drops out of the aggregate beats a confident wrong answer that silently
341
+ * re-tiers every run on that profile.
342
+ */
293
343
  export function tallyVotes(votes, { minVotes = MIN_VOTES, minAgreement = MIN_AGREEMENT } = {}) {
294
344
  const learned = {};
295
- for (const [pid, tally] of Object.entries(votes)) {
296
- const total = Object.values(tally).reduce((a, b) => a + b, 0);
345
+ for (const [pid, buckets] of Object.entries(votes)) {
346
+ // Legacy flat shape (`{ haiku: 3 }`) is read as explicit evidence.
347
+ const split = buckets && (buckets.explicit || buckets.inferred)
348
+ ? { explicit: buckets.explicit || {}, inferred: buckets.inferred || {} }
349
+ : { explicit: buckets || {}, inferred: {} };
350
+
351
+ const explicit = topRole(split.explicit);
352
+ const inferred = topRole(split.inferred);
353
+
297
354
  let role = null;
298
- let top = 0;
299
- for (const [r, n] of Object.entries(tally)) {
300
- if (n > top) { top = n; role = r; }
355
+ let source = null;
356
+ if (explicit.total >= minVotes && explicit.top / explicit.total >= minAgreement) {
357
+ role = explicit.role;
358
+ source = 'explicit';
359
+ } else if (inferred.total >= minVotes
360
+ && inferred.top / inferred.total >= minAgreement
361
+ && (explicit.total === 0 || rolesAgree(explicit.role, inferred.role))) {
362
+ // Thin explicit evidence still vetoes a contradicting inference: one
363
+ // stated `Task(model: "haiku")` outweighs any number of "no sidechain
364
+ // flag, so probably the session model" observations.
365
+ role = inferred.role;
366
+ source = 'inferred';
301
367
  }
302
- const confident = total >= minVotes && top / total >= minAgreement;
303
- learned[pid] = { role: confident ? role : null, votes: tally, total };
368
+
369
+ learned[pid] = {
370
+ role,
371
+ source,
372
+ votes: split,
373
+ total: explicit.total + inferred.total,
374
+ };
304
375
  }
305
376
  return learned;
306
377
  }
@@ -345,7 +416,9 @@ export async function learnProfileMapping({ sessionPaths = [], maxSessions = 40
345
416
  const pid = await subagentProfileId(jsonl);
346
417
  if (!pid) continue;
347
418
  sawGateway = true;
348
- addVote(votes, pid, role);
419
+ // Stated evidence: the Task call named this tier, or the agent
420
+ // definition it used did.
421
+ addVote(votes, pid, role, 'explicit');
349
422
  }
350
423
  if (sawGateway) gateway = true;
351
424
  }
@@ -100,6 +100,27 @@ export function loadModelRules() {
100
100
  }
101
101
  }
102
102
 
103
+ /**
104
+ * USD that measured delegations have saved, summed across registered rules.
105
+ * Reads the file route-scan already wrote โ€” the statusline refreshes every few
106
+ * seconds and must never start a scan of its own (a scan parses tens of MB of
107
+ * transcripts).
108
+ *
109
+ * Rules with no measured delegation contribute nothing, and an unreadable
110
+ * registry returns 0 rather than throwing: the statusline reads 0 as "draw no
111
+ * chip", which is the right outcome for anyone who never delegates.
112
+ */
113
+ export function delegationSavedUsd() {
114
+ try {
115
+ return loadModelRules().rules.reduce(
116
+ (sum, r) => sum + (r.delegatedRuns ? (Number(r.savedUsd) || 0) : 0),
117
+ 0,
118
+ );
119
+ } catch {
120
+ return 0;
121
+ }
122
+ }
123
+
103
124
  export function saveModelRules(data) {
104
125
  const dir = stateDir();
105
126
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });