ccsidekick 1.6.0 → 1.6.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.
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/ccsidekick-render.ts
4
- import { readFileSync as readFileSync11 } from "node:fs";
4
+ import { readFileSync as readFileSync12 } from "node:fs";
5
5
 
6
6
  // src/cli/classify.ts
7
7
  import { createHash as createHash4 } from "node:crypto";
8
8
 
9
9
  // src/domain/constants.ts
10
- var COST_TTL_MS = 1500;
10
+ var COST_TTL_MS = 5000;
11
11
  var USAGE_TTL_MS = 300000;
12
+ var CREDS_TTL_MS = 3600000;
12
13
  var BALANCE_FRESHNESS_MS = 300000;
13
14
  var FX_TTL_MS = 604800000;
14
15
  var MIN_RIGHT_WIDTH = 53;
@@ -33,6 +34,8 @@ var HELPFUL_COOLDOWN_MS = 600000;
33
34
  var BURN_WINDOW_MS = 18000000;
34
35
  var COMPACT_URGENT_PCT = 90;
35
36
  var QUOTA_HIGH_PCT = 80;
37
+ var QUOTA_PACE_MIN_PCT = 20;
38
+ var QUOTA_PROJECT_MIN_PCT = 50;
36
39
  var PAY_AS_YOU_GO_CAUTION_PCT = 60;
37
40
  var PAY_AS_YOU_GO_NEAR_PCT = 80;
38
41
  var BALANCE_LOW = 25;
@@ -44,6 +47,17 @@ var SIGNAL_HUE_RANGES = {
44
47
  caution: { min: 20, max: 55 },
45
48
  critical: { wrapMax: 15, wrapMin: 345 }
46
49
  };
50
+ var COMPACT_SOON_PCT = 60;
51
+ var COMPACTION_THRASH_N = 3;
52
+ var CACHE_RATIO_FLOOR = 0.5;
53
+ var CACHE_WARMUP_TURNS = 20;
54
+ var BIG_DIFF_LINES = 1000;
55
+ var UNTRACKED_N = 20;
56
+ var STASH_N = 5;
57
+ var STALE_BRANCH_N = 20;
58
+ var TODO_STALLED_MIN = 30;
59
+ var TIER_THRESHOLDS = [3, 15, 50, 100];
60
+ var PRICING_TIER_THRESHOLD = 200000;
47
61
  // src/domain/enums.ts
48
62
  var MOODS = ["idle", "busy", "happy", "struggling", "recovery"];
49
63
  var PRESSURE_MOODS = ["compact_hint", "block_limit", "weekly_limit"];
@@ -130,6 +144,9 @@ var REACTION_CATEGORIES = [
130
144
  "db_migrate",
131
145
  "dangerous"
132
146
  ];
147
+ var MIN_SEVERITIES = ["low", "medium", "high", "critical"];
148
+ var CHARACTER_MODES = ["fixed", "random"];
149
+ var BANDINGS = ["solid", "cycle"];
133
150
  var isStack = (s) => STACKS.includes(s);
134
151
  var isEventCategory = (s) => EVENT_CATEGORIES.includes(s);
135
152
  // src/domain/ids.ts
@@ -324,7 +341,6 @@ var pricing_default = [
324
341
 
325
342
  // src/derived/pricing.ts
326
343
  var PER_MILLION = 1e6;
327
- var TIER_THRESHOLD = 200000;
328
344
  var RAW = pricing_default;
329
345
  var TABLE = (() => {
330
346
  const byKey = new Map;
@@ -403,9 +419,9 @@ function resolvePrice(modelId, aliases = NO_ALIASES, atMs) {
403
419
  return rows ? pickRow(rows, atMs) : null;
404
420
  }
405
421
  function tiered(tokens, base, above) {
406
- if (tokens <= TIER_THRESHOLD || above === undefined)
422
+ if (tokens <= PRICING_TIER_THRESHOLD || above === undefined)
407
423
  return tokens * base;
408
- return TIER_THRESHOLD * base + (tokens - TIER_THRESHOLD) * above;
424
+ return PRICING_TIER_THRESHOLD * base + (tokens - PRICING_TIER_THRESHOLD) * above;
409
425
  }
410
426
  function priceMessage(usage, modelId, aliases = NO_ALIASES, atMs) {
411
427
  const row = resolvePrice(modelId, aliases, atMs);
@@ -480,6 +496,7 @@ function joinRecords(attribution, cache, tz) {
480
496
  for (const [session, g] of groupBySession(cache)) {
481
497
  const attr = attribution[session];
482
498
  out.push({
499
+ session,
483
500
  character: attr?.character ?? "unknown",
484
501
  project: attr?.project ?? g.projectRaw,
485
502
  start: g.start,
@@ -489,7 +506,6 @@ function joinRecords(attribution, cache, tz) {
489
506
  }
490
507
  return out;
491
508
  }
492
- var TIER_THRESHOLDS = [3, 15, 50, 100];
493
509
  function tierForCount(n) {
494
510
  const [t1, t2, t3, t4] = TIER_THRESHOLDS;
495
511
  if (n < t1)
@@ -516,7 +532,7 @@ function currentStreak(activeDays, today) {
516
532
  }
517
533
  return streak;
518
534
  }
519
- function deriveFamiliarity(attribution, character, cache, project, clock) {
535
+ function deriveFamiliarity(attribution, character, cache, project, clock, currentSession = "") {
520
536
  const tz = clock.timezone();
521
537
  const records = joinRecords(attribution, cache, tz);
522
538
  const seenProject = records.some((r) => r.project === String(project));
@@ -528,10 +544,12 @@ function deriveFamiliarity(attribution, character, cache, project, clock) {
528
544
  let lastEnd = Number.NEGATIVE_INFINITY;
529
545
  let workingSinceMs = Number.POSITIVE_INFINITY;
530
546
  for (const r of mine) {
531
- if (r.end > lastEnd)
532
- lastEnd = r.end;
533
547
  if (r.start < workingSinceMs)
534
548
  workingSinceMs = r.start;
549
+ if (r.session === currentSession)
550
+ continue;
551
+ if (r.end > lastEnd)
552
+ lastEnd = r.end;
535
553
  }
536
554
  const daysSinceLastSession = Number.isFinite(lastEnd) ? Math.floor((clock.now() - lastEnd) / MS_PER_DAY) : Number.POSITIVE_INFINITY;
537
555
  return {
@@ -757,7 +775,6 @@ function classify(toolName, command, ok) {
757
775
  return category !== undefined ? { category } : null;
758
776
  }
759
777
  // src/derived/signals.ts
760
- var QUOTA_CRITICAL_PCT = 80;
761
778
  function contextBand(pct) {
762
779
  if (pct < 34)
763
780
  return "nominal";
@@ -766,9 +783,9 @@ function contextBand(pct) {
766
783
  return "critical";
767
784
  }
768
785
  function quotaBand(usedPct, resetsAtMs, windowMs, nowMs) {
769
- if (usedPct >= QUOTA_CRITICAL_PCT)
786
+ if (usedPct >= QUOTA_HIGH_PCT)
770
787
  return "critical";
771
- if (resetsAtMs === undefined)
788
+ if (resetsAtMs === undefined || usedPct < QUOTA_PACE_MIN_PCT)
772
789
  return contextBand(usedPct);
773
790
  const usedFraction = usedPct / 100;
774
791
  const elapsedFraction = Math.max((nowMs - (resetsAtMs - windowMs)) / windowMs, 0.01);
@@ -779,6 +796,13 @@ function quotaBand(usedPct, resetsAtMs, windowMs, nowMs) {
779
796
  return "caution";
780
797
  return "critical";
781
798
  }
799
+ function overQuotaPace(usedPct, resetsAtMs, windowMs, nowMs) {
800
+ if (resetsAtMs === undefined)
801
+ return false;
802
+ const usedFraction = usedPct / 100;
803
+ const elapsedFraction = Math.max((nowMs - (resetsAtMs - windowMs)) / windowMs, 0.01);
804
+ return usedFraction / elapsedFraction > 1.5;
805
+ }
782
806
 
783
807
  // src/derived/context.ts
784
808
  function deriveContext(payload, scan) {
@@ -1149,12 +1173,13 @@ function buildWindow(payloadQuota, oauthQuota, windowMs, clock) {
1149
1173
  return;
1150
1174
  }
1151
1175
  const band = quotaBand(usedPct, resetsAtMs, windowMs, clock.now());
1176
+ const overPace = overQuotaPace(usedPct, resetsAtMs, windowMs, clock.now());
1152
1177
  if (resetsAtMs === undefined)
1153
- return { usedPct, band };
1178
+ return { usedPct, band, overPace };
1154
1179
  const left = resetsAtMs - clock.now();
1155
1180
  if (left <= 0)
1156
- return { usedPct, band };
1157
- return { usedPct, band, resetIn: fmtLeft(left) };
1181
+ return { usedPct, band, overPace };
1182
+ return { usedPct, band, overPace, resetIn: fmtLeft(left) };
1158
1183
  }
1159
1184
  function buildPayg(usage) {
1160
1185
  const extra = usage?.extra_usage;
@@ -1252,7 +1277,7 @@ function readJson(path, fallback) {
1252
1277
  }
1253
1278
 
1254
1279
  // src/sources/storage/withLock.ts
1255
- import { closeSync, mkdirSync as mkdirSync2, openSync, statSync, unlinkSync } from "node:fs";
1280
+ import { closeSync, mkdirSync as mkdirSync2, openSync, renameSync as renameSync2, statSync, unlinkSync } from "node:fs";
1256
1281
  import { dirname as dirname2 } from "node:path";
1257
1282
  var STALE_MS = 30000;
1258
1283
  function withLock(lockPath, fn, readOnly) {
@@ -1267,8 +1292,12 @@ function withLock(lockPath, fn, readOnly) {
1267
1292
  } catch {
1268
1293
  try {
1269
1294
  if (Date.now() - statSync(lockPath).mtimeMs > STALE_MS) {
1270
- unlinkSync(lockPath);
1295
+ const aside = `${lockPath}.stale`;
1296
+ renameSync2(lockPath, aside);
1271
1297
  fd = openSync(lockPath, "wx");
1298
+ try {
1299
+ unlinkSync(aside);
1300
+ } catch {}
1272
1301
  }
1273
1302
  } catch {}
1274
1303
  }
@@ -1355,14 +1384,27 @@ function upsertAttribution(root, sessionId, rec) {
1355
1384
  const path = storePath(root);
1356
1385
  withLock(`${path}.lock`, () => {
1357
1386
  const store = coerceStore(readJson(path, undefined));
1387
+ const existing = store[sessionId];
1388
+ if (existing !== undefined && existing.project === rec.project && existing.character === rec.character) {
1389
+ return;
1390
+ }
1358
1391
  store[sessionId] = rec.updatedMs !== undefined ? { project: rec.project, character: rec.character, updatedMs: rec.updatedMs } : { project: rec.project, character: rec.character };
1359
1392
  atomicWrite(path, JSON.stringify(store));
1360
1393
  }, () => {});
1361
1394
  }
1362
1395
  // src/sources/balance.ts
1396
+ import { statSync as statSync3 } from "node:fs";
1397
+ var BALANCE_MAX_BYTES = 64 * 1024;
1363
1398
  function readBalance(path, clock) {
1364
1399
  if (path === "")
1365
1400
  return null;
1401
+ try {
1402
+ const st = statSync3(path);
1403
+ if (!st.isFile() || st.size > BALANCE_MAX_BYTES)
1404
+ return null;
1405
+ } catch {
1406
+ return null;
1407
+ }
1366
1408
  const raw = readJson(path, undefined);
1367
1409
  if (typeof raw !== "object" || raw === null)
1368
1410
  return null;
@@ -2443,7 +2485,7 @@ var DEFAULT_CONFIG = {
2443
2485
  schema_version: 1,
2444
2486
  character: { enabled: true, mode: "random", name: "spiderman", roster: [] },
2445
2487
  theme: { name: "character", banding: "solid", mood_shift: false, icons: {} },
2446
- comments: { character: true, helpful: true, min_severity: "low" },
2488
+ comments: { character: true, helpful: true, min_severity: "medium" },
2447
2489
  network: { fx_refresh: false, usage_fetch: false, balance_path: "" },
2448
2490
  statusline: { currency: defaultCurrency(), widgets: DEFAULT_WIDGETS }
2449
2491
  };
@@ -2493,10 +2535,10 @@ function loadConfig(globalToml = "", projectToml = "") {
2493
2535
  const statusline = section(g, p, "statusline");
2494
2536
  const d = DEFAULT_CONFIG;
2495
2537
  return {
2496
- schema_version: numv(p["schema_version"] ?? g["schema_version"], d.schema_version),
2538
+ schema_version: numv(p["schema_version"], numv(g["schema_version"], d.schema_version)),
2497
2539
  character: {
2498
2540
  enabled: bool(character["enabled"], d.character.enabled),
2499
- mode: oneOf(character["mode"], ["fixed", "random"], d.character.mode),
2541
+ mode: oneOf(character["mode"], CHARACTER_MODES, d.character.mode),
2500
2542
  name: str(character["name"], d.character.name),
2501
2543
  roster: stringArray(character["roster"], d.character.roster)
2502
2544
  },
@@ -2505,7 +2547,7 @@ function loadConfig(globalToml = "", projectToml = "") {
2505
2547
  ...typeof theme["statusline"] === "string" ? { statusline: theme["statusline"] } : {},
2506
2548
  ...typeof theme["logo"] === "string" ? { logo: theme["logo"] } : {},
2507
2549
  ...typeof theme["comment"] === "string" ? { comment: theme["comment"] } : {},
2508
- banding: oneOf(theme["banding"], ["solid", "cycle"], d.theme.banding),
2550
+ banding: oneOf(theme["banding"], BANDINGS, d.theme.banding),
2509
2551
  mood_shift: bool(theme["mood_shift"], d.theme.mood_shift),
2510
2552
  icons: mergeIcons({
2511
2553
  ...obj(obj(g["theme"])["icons"]),
@@ -2515,7 +2557,7 @@ function loadConfig(globalToml = "", projectToml = "") {
2515
2557
  comments: {
2516
2558
  character: bool(comments["character"], d.comments.character),
2517
2559
  helpful: bool(comments["helpful"], d.comments.helpful),
2518
- min_severity: oneOf(comments["min_severity"], ["low", "medium", "high", "critical"], d.comments.min_severity)
2560
+ min_severity: oneOf(comments["min_severity"], MIN_SEVERITIES, d.comments.min_severity)
2519
2561
  },
2520
2562
  network: {
2521
2563
  fx_refresh: bool(network["fx_refresh"], d.network.fx_refresh),
@@ -2645,7 +2687,19 @@ function coerceEntry(v) {
2645
2687
  }
2646
2688
  const projectPath = typeof v["projectPath"] === "string" ? v["projectPath"] : String(record.project);
2647
2689
  const models = Array.isArray(v["models"]) ? v["models"].filter((x) => typeof x === "string") : [];
2648
- return { mtime, size, total, lines, models, projectPath, record };
2690
+ const byteOffset = asNumber(v["byteOffset"]);
2691
+ const headHash = typeof v["headHash"] === "string" ? v["headHash"] : undefined;
2692
+ return {
2693
+ mtime,
2694
+ size,
2695
+ total,
2696
+ lines,
2697
+ models,
2698
+ projectPath,
2699
+ record,
2700
+ ...byteOffset !== undefined ? { byteOffset } : {},
2701
+ ...headHash !== undefined ? { headHash } : {}
2702
+ };
2649
2703
  }
2650
2704
  function coerceByModel(v) {
2651
2705
  if (!isObject(v))
@@ -2702,7 +2756,6 @@ function writeCostCache(root, c) {
2702
2756
  // src/sources/creds.ts
2703
2757
  import { spawnSync as spawnSync2 } from "node:child_process";
2704
2758
  import { readFileSync as readFileSync4 } from "node:fs";
2705
- import { homedir as homedir3 } from "node:os";
2706
2759
  import { join as join5 } from "node:path";
2707
2760
 
2708
2761
  // src/sources/oauthUsage.ts
@@ -2899,7 +2952,6 @@ function readUsage(root, clock, opts) {
2899
2952
  }
2900
2953
 
2901
2954
  // src/sources/creds.ts
2902
- var KEYCHAIN_SERVICE2 = "Claude Code-credentials";
2903
2955
  var SUBSCRIPTIONS = new Set(["pro", "max", "team", "enterprise"]);
2904
2956
  var isObject3 = (v) => typeof v === "object" && v !== null;
2905
2957
  var defaultRunner = (cmd, args) => {
@@ -2916,11 +2968,8 @@ var defaultRunner = (cmd, args) => {
2916
2968
  return "";
2917
2969
  }
2918
2970
  };
2919
- function configBase2() {
2920
- return process.env["CLAUDE_CONFIG_DIR"] ?? join5(homedir3(), ".claude");
2921
- }
2922
2971
  function readBlob(run, base) {
2923
- for (const service of [keychainService(base), KEYCHAIN_SERVICE2]) {
2972
+ for (const service of [keychainService(base), KEYCHAIN_SERVICE]) {
2924
2973
  const keychain = run("security", ["find-generic-password", "-s", service, "-w"]);
2925
2974
  if (keychain.trim() !== "")
2926
2975
  return keychain;
@@ -2930,7 +2979,7 @@ function readBlob(run, base) {
2930
2979
  if (file.trim() !== "")
2931
2980
  return file;
2932
2981
  } catch {}
2933
- const secretTool = run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE2]);
2982
+ const secretTool = run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE]);
2934
2983
  if (secretTool.trim() !== "")
2935
2984
  return secretTool;
2936
2985
  return "";
@@ -2959,7 +3008,7 @@ function readOrgTier(base) {
2959
3008
  }
2960
3009
  function readCreds(run = defaultRunner) {
2961
3010
  try {
2962
- const base = configBase2();
3011
+ const base = configBase();
2963
3012
  const blob = readBlob(run, base);
2964
3013
  if (blob.trim() !== "") {
2965
3014
  const tier = parseSubscription(blob);
@@ -2973,6 +3022,39 @@ function readCreds(run = defaultRunner) {
2973
3022
  return null;
2974
3023
  }
2975
3024
  }
3025
+ var CREDS_CACHE_FILE = "creds.json";
3026
+ function credsCachePath(root) {
3027
+ return join5(cacheDir(root), CREDS_CACHE_FILE);
3028
+ }
3029
+ function coerceCachedCreds(raw) {
3030
+ if (!isObject3(raw) || typeof raw["at"] !== "number")
3031
+ return null;
3032
+ const at = raw["at"];
3033
+ const info = raw["info"];
3034
+ if (info === null)
3035
+ return { info: null, at };
3036
+ if (isObject3(info) && typeof info["present"] === "boolean") {
3037
+ const sub = info["subscriptionType"];
3038
+ return {
3039
+ at,
3040
+ info: typeof sub === "string" && SUBSCRIPTIONS.has(sub) ? { present: info["present"], subscriptionType: sub } : { present: info["present"] }
3041
+ };
3042
+ }
3043
+ return null;
3044
+ }
3045
+ function readCredsCached(root) {
3046
+ return coerceCachedCreds(readJson(credsCachePath(root), undefined))?.info ?? null;
3047
+ }
3048
+ function refreshCreds(root, clock, run = defaultRunner) {
3049
+ const cached = coerceCachedCreds(readJson(credsCachePath(root), undefined));
3050
+ const now = clock.now();
3051
+ if (cached !== null && now - cached.at <= CREDS_TTL_MS)
3052
+ return;
3053
+ const info = readCreds(run);
3054
+ try {
3055
+ atomicWrite(credsCachePath(root), JSON.stringify({ info, at: now }));
3056
+ } catch {}
3057
+ }
2976
3058
  // src/sources/env.ts
2977
3059
  var on = (v) => v === "1" || v === "true";
2978
3060
  function isCustomHost(base) {
@@ -3455,12 +3537,14 @@ function readSubmoduleRollup(run, workTreeRoot) {
3455
3537
  return rollup;
3456
3538
  }
3457
3539
  function readGit(cwd, run = defaultRunner2(cwd)) {
3458
- if (run(["rev-parse", "--is-inside-work-tree"]).trim() !== "true")
3540
+ const probe = run(["rev-parse", "--is-inside-work-tree", "--git-dir", "--show-toplevel"]).split(`
3541
+ `);
3542
+ if ((probe[0] ?? "").trim() !== "true")
3459
3543
  return null;
3460
3544
  const status = parseStatus(run(["status", "--porcelain=v2", "--branch", "--show-stash"]));
3461
- const gitDirRaw = run(["rev-parse", "--git-dir"]).trim();
3545
+ const gitDirRaw = (probe[1] ?? "").trim();
3462
3546
  const gitDir = gitDirRaw === "" ? "" : resolve(cwd, gitDirRaw);
3463
- const workTreeRoot = run(["rev-parse", "--show-toplevel"]).trim();
3547
+ const workTreeRoot = (probe[2] ?? "").trim();
3464
3548
  const tag = run(["describe", "--tags", "--exact-match"]).trim() || undefined;
3465
3549
  const originRepo = normalizeOriginRepo(run(["remote", "get-url", "origin"]));
3466
3550
  const defaultBranch = resolveDefaultBranch(run);
@@ -3471,6 +3555,12 @@ function readGit(cwd, run = defaultRunner2(cwd)) {
3471
3555
  } = parseNumstat(run(["diff", "HEAD", "--numstat"]));
3472
3556
  const rollup = countSubmodules(workTreeRoot) > 0 ? readSubmoduleRollup(run, workTreeRoot) : null;
3473
3557
  const behindDefault = readBehindDefault(run, status.branch, defaultBranch);
3558
+ const remoteBranchExists = status.branch !== undefined && !status.upstream ? run([
3559
+ "rev-parse",
3560
+ "--verify",
3561
+ "--quiet",
3562
+ `refs/remotes/origin/${status.branch}`
3563
+ ]).trim() !== "" : false;
3474
3564
  return {
3475
3565
  ...opt2("branch", status.branch),
3476
3566
  ...opt2("sha", status.sha),
@@ -3487,6 +3577,7 @@ function readGit(cwd, run = defaultRunner2(cwd)) {
3487
3577
  submoduleBranches: rollup?.branches ?? [],
3488
3578
  upstream: status.upstream,
3489
3579
  upstreamGone: status.upstreamGone,
3580
+ remoteBranchExists,
3490
3581
  ...opt2("originRepo", originRepo),
3491
3582
  ...opt2("defaultBranch", defaultBranch),
3492
3583
  ...opt2("root", workTreeRoot === "" ? undefined : workTreeRoot),
@@ -3500,7 +3591,7 @@ function readGit(cwd, run = defaultRunner2(cwd)) {
3500
3591
  }
3501
3592
  // src/sources/helpfulEnv.ts
3502
3593
  import { readFileSync as readFileSync7 } from "node:fs";
3503
- import { homedir as homedir4 } from "node:os";
3594
+ import { homedir as homedir3 } from "node:os";
3504
3595
  import { delimiter, join as join9 } from "node:path";
3505
3596
  var opt3 = (key, value) => value !== undefined ? { [key]: value } : {};
3506
3597
  function kubeconfigPath() {
@@ -3510,7 +3601,7 @@ function kubeconfigPath() {
3510
3601
  if (first !== undefined)
3511
3602
  return first;
3512
3603
  }
3513
- return join9(homedir4(), ".kube", "config");
3604
+ return join9(homedir3(), ".kube", "config");
3514
3605
  }
3515
3606
  function readKubeContext() {
3516
3607
  try {
@@ -3814,7 +3905,7 @@ function writeState(sessionDir2, s) {
3814
3905
  }, () => {});
3815
3906
  }
3816
3907
  // src/sources/transcript.ts
3817
- import { readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
3908
+ import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync8, readSync, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
3818
3909
  import { basename, dirname as dirname6, join as join12 } from "node:path";
3819
3910
  var asObj = (v) => v !== null && typeof v === "object" ? v : undefined;
3820
3911
  var asStr = (v) => typeof v === "string" ? v : undefined;
@@ -4040,7 +4131,7 @@ function reconstructTodos(lines) {
4040
4131
  }
4041
4132
  function statSafe(p) {
4042
4133
  try {
4043
- const s = statSync3(p);
4134
+ const s = statSync4(p);
4044
4135
  return { mtimeMs: s.mtimeMs, size: s.size };
4045
4136
  } catch {
4046
4137
  return null;
@@ -4053,6 +4144,63 @@ function readSafe(p) {
4053
4144
  return "";
4054
4145
  }
4055
4146
  }
4147
+ function readBufSafe(p) {
4148
+ try {
4149
+ return readFileSync8(p);
4150
+ } catch {
4151
+ return null;
4152
+ }
4153
+ }
4154
+ function readRange(p, start, end) {
4155
+ if (end <= start)
4156
+ return Buffer.alloc(0);
4157
+ let fd;
4158
+ try {
4159
+ fd = openSync2(p, "r");
4160
+ const len = end - start;
4161
+ const buf = Buffer.allocUnsafe(len);
4162
+ let off = 0;
4163
+ while (off < len) {
4164
+ const n = readSync(fd, buf, off, len - off, start + off);
4165
+ if (n === 0)
4166
+ break;
4167
+ off += n;
4168
+ }
4169
+ return off === len ? buf : null;
4170
+ } catch {
4171
+ return null;
4172
+ } finally {
4173
+ if (fd !== undefined)
4174
+ closeSync2(fd);
4175
+ }
4176
+ }
4177
+ var HEAD_HASH_BYTES = 4096;
4178
+ function headHashOf(head) {
4179
+ let h = 2166136261;
4180
+ for (const b of head) {
4181
+ h ^= b;
4182
+ h = Math.imul(h, 16777619);
4183
+ }
4184
+ return (h >>> 0).toString(16);
4185
+ }
4186
+ function makeInternModel(models) {
4187
+ const index = new Map;
4188
+ models.forEach((m, i) => index.set(m, i));
4189
+ return (model) => {
4190
+ let i = index.get(model);
4191
+ if (i === undefined) {
4192
+ i = models.length;
4193
+ models.push(model);
4194
+ index.set(model, i);
4195
+ }
4196
+ return i;
4197
+ };
4198
+ }
4199
+ function lastLineEnd(buf) {
4200
+ const i = buf.lastIndexOf(10);
4201
+ return i < 0 ? 0 : i + 1;
4202
+ }
4203
+ var headLen = (byteOffset) => Math.min(HEAD_HASH_BYTES, byteOffset);
4056
4204
  var EMPTY_TOKENS = {
4057
4205
  input: 0,
4058
4206
  output: 0,
@@ -4143,7 +4291,7 @@ function* walkTree(dir, encodedDir) {
4143
4291
  const full = join12(dir, name);
4144
4292
  let st;
4145
4293
  try {
4146
- st = statSync3(full);
4294
+ st = statSync4(full);
4147
4295
  } catch {
4148
4296
  continue;
4149
4297
  }
@@ -4182,22 +4330,16 @@ function scanBounds(lines) {
4182
4330
  return { sessionId, start, end };
4183
4331
  }
4184
4332
  function priceFile(path, encodedDir, st, price, resolveProject) {
4185
- const lines = parseAll(readSafe(path));
4333
+ const buf = readBufSafe(path) ?? Buffer.alloc(0);
4334
+ const byteOffset = lastLineEnd(buf);
4335
+ const headHash = headHashOf(buf.subarray(0, headLen(byteOffset)));
4336
+ const lines = parseAll(buf.subarray(0, byteOffset).toString("utf8"));
4186
4337
  const { sessionId, start, end } = scanBounds(lines);
4187
4338
  const priced = collapseStreaming(lines);
4188
4339
  const gate = makeDedupGate();
4189
4340
  const costLines = [];
4190
4341
  const models = [];
4191
- const modelIndex = new Map;
4192
- const internModel = (model) => {
4193
- let i = modelIndex.get(model);
4194
- if (i === undefined) {
4195
- i = models.length;
4196
- models.push(model);
4197
- modelIndex.set(model, i);
4198
- }
4199
- return i;
4200
- };
4342
+ const internModel = makeInternModel(models);
4201
4343
  let total = 0;
4202
4344
  let input = 0;
4203
4345
  let output = 0;
@@ -4242,6 +4384,8 @@ function priceFile(path, encodedDir, st, price, resolveProject) {
4242
4384
  return {
4243
4385
  mtime: st.mtimeMs,
4244
4386
  size: st.size,
4387
+ byteOffset,
4388
+ headHash,
4245
4389
  total,
4246
4390
  lines: costLines,
4247
4391
  models,
@@ -4249,6 +4393,138 @@ function priceFile(path, encodedDir, st, price, resolveProject) {
4249
4393
  record
4250
4394
  };
4251
4395
  }
4396
+ function resumeCostFile(cached, path, st, price) {
4397
+ const tail = readAppendedTail(cached, path, st);
4398
+ if (tail === null)
4399
+ return null;
4400
+ if (tail === "partial")
4401
+ return { ...cached, mtime: st.mtimeMs, size: st.size };
4402
+ const models = [...cached.models];
4403
+ const keyIndex = new Map;
4404
+ cached.lines.forEach((l, i) => {
4405
+ if (l.id !== undefined && l.reqId !== undefined)
4406
+ keyIndex.set(`${l.id}|${l.reqId}`, i);
4407
+ });
4408
+ const gate = makeDedupGate();
4409
+ const acc = {
4410
+ lines: [...cached.lines],
4411
+ models,
4412
+ counted: cached.lines.map((l) => gate(l.id, l.reqId, l.sidechain)),
4413
+ keyIndex,
4414
+ gate,
4415
+ internModel: makeInternModel(models),
4416
+ input: cached.record.tokens.input,
4417
+ output: cached.record.tokens.output,
4418
+ cacheRead: cached.record.tokens.cache_read,
4419
+ cacheCreation: cached.record.tokens.cache_creation,
4420
+ total: cached.total,
4421
+ messages: cached.record.messages,
4422
+ start: cached.record.start,
4423
+ end: cached.record.end
4424
+ };
4425
+ for (const l of collapseStreaming(tail.appended))
4426
+ foldAppendedLine(acc, l, price);
4427
+ return {
4428
+ mtime: st.mtimeMs,
4429
+ size: st.size,
4430
+ byteOffset: tail.newByteOffset,
4431
+ headHash: tail.newHeadHash,
4432
+ total: acc.total,
4433
+ lines: acc.lines,
4434
+ models: acc.models,
4435
+ projectPath: cached.projectPath,
4436
+ record: {
4437
+ ...cached.record,
4438
+ start: acc.start,
4439
+ end: acc.end,
4440
+ tokens: {
4441
+ input: acc.input,
4442
+ output: acc.output,
4443
+ cache_read: acc.cacheRead,
4444
+ cache_creation: acc.cacheCreation
4445
+ },
4446
+ messages: acc.messages
4447
+ }
4448
+ };
4449
+ }
4450
+ function costLineOf(internModel, l, cost, totalTok) {
4451
+ return {
4452
+ ...l.id !== undefined ? { id: l.id } : {},
4453
+ ...l.requestId !== undefined ? { reqId: l.requestId } : {},
4454
+ sidechain: l.isSidechain,
4455
+ ts: l.tsMs ?? 0,
4456
+ cost,
4457
+ ...l.model !== undefined ? { m: internModel(l.model) } : {},
4458
+ tok: totalTok
4459
+ };
4460
+ }
4461
+ function readAppendedTail(cached, path, st) {
4462
+ const offset = cached.byteOffset;
4463
+ if (offset === undefined || offset === 0 || st.size < offset)
4464
+ return null;
4465
+ const head = readRange(path, 0, headLen(offset));
4466
+ if (head === null || headHashOf(head) !== cached.headHash)
4467
+ return null;
4468
+ const tailBuf = readRange(path, offset, st.size);
4469
+ if (tailBuf === null)
4470
+ return null;
4471
+ const added = lastLineEnd(tailBuf);
4472
+ if (added === 0)
4473
+ return "partial";
4474
+ const newByteOffset = offset + added;
4475
+ const newHead = headLen(newByteOffset) === head.length ? head : readRange(path, 0, headLen(newByteOffset));
4476
+ if (newHead === null)
4477
+ return null;
4478
+ return {
4479
+ appended: parseAll(tailBuf.subarray(0, added).toString("utf8")),
4480
+ newByteOffset,
4481
+ newHeadHash: headHashOf(newHead)
4482
+ };
4483
+ }
4484
+ function widenBounds(acc, tsMs) {
4485
+ if (tsMs === undefined)
4486
+ return;
4487
+ if (tsMs > acc.end)
4488
+ acc.end = tsMs;
4489
+ if (acc.start === 0 || tsMs < acc.start)
4490
+ acc.start = tsMs;
4491
+ }
4492
+ function foldAppendedLine(acc, l, price) {
4493
+ widenBounds(acc, l.tsMs);
4494
+ const usage = l.usage;
4495
+ if (usage === undefined)
4496
+ return;
4497
+ const t = tokenize(usage);
4498
+ const totalTok = t.input + t.output + t.cache_read + t.c5m + t.c1h;
4499
+ const key = l.id !== undefined && l.requestId !== undefined ? `${l.id}|${l.requestId}` : null;
4500
+ const existing = key !== null ? acc.keyIndex.get(key) : undefined;
4501
+ const old = existing !== undefined ? acc.lines[existing] : undefined;
4502
+ if (existing !== undefined && old !== undefined) {
4503
+ if (totalTok <= (old.tok ?? 0))
4504
+ return;
4505
+ const cost2 = price(usage, l.model ?? "", l.tsMs);
4506
+ if (acc.counted[existing] === true) {
4507
+ acc.output += totalTok - (old.tok ?? 0);
4508
+ acc.total += cost2 - old.cost;
4509
+ }
4510
+ acc.lines[existing] = costLineOf(acc.internModel, l, cost2, totalTok);
4511
+ return;
4512
+ }
4513
+ const cost = price(usage, l.model ?? "", l.tsMs);
4514
+ const isCounted = acc.gate(l.id, l.requestId, l.isSidechain);
4515
+ if (key !== null)
4516
+ acc.keyIndex.set(key, acc.lines.length);
4517
+ acc.lines.push(costLineOf(acc.internModel, l, cost, totalTok));
4518
+ acc.counted.push(isCounted);
4519
+ if (isCounted) {
4520
+ acc.input += t.input;
4521
+ acc.output += t.output;
4522
+ acc.cacheRead += t.cache_read;
4523
+ acc.cacheCreation += t.c5m + t.c1h;
4524
+ acc.total += cost;
4525
+ acc.messages += 1;
4526
+ }
4527
+ }
4252
4528
  function buildAggregate(files, chat) {
4253
4529
  const sessionProject = {};
4254
4530
  const all = [];
@@ -4283,17 +4559,27 @@ function scanCostTree(root, cache, clock, price, resolveProject) {
4283
4559
  return cache;
4284
4560
  const chat = cache.aggregate.chat;
4285
4561
  const files = {};
4562
+ let changed = false;
4286
4563
  for (const { path, encodedDir } of walkJsonl(root)) {
4287
4564
  const st = statSafe(path);
4288
4565
  if (!st)
4289
4566
  continue;
4290
4567
  const cached = cache.files[path];
4291
- files[path] = cached && cached.mtime === st.mtimeMs && cached.size === st.size ? cached : priceFile(path, encodedDir, st, price, resolveProject);
4568
+ if (cached && cached.mtime === st.mtimeMs && cached.size === st.size) {
4569
+ files[path] = cached;
4570
+ } else {
4571
+ const resumed = cached ? resumeCostFile(cached, path, st, price) : null;
4572
+ files[path] = resumed ?? priceFile(path, encodedDir, st, price, resolveProject);
4573
+ changed = true;
4574
+ }
4292
4575
  }
4576
+ const sameFileSet = Object.keys(files).length === Object.keys(cache.files).length;
4577
+ if (!changed && sameFileSet)
4578
+ return { files, aggregate: cache.aggregate, lastScanTs: now };
4293
4579
  return { files, aggregate: buildAggregate(files, chat), lastScanTs: now };
4294
4580
  }
4295
4581
  // src/cli/classify.ts
4296
- var FAIL_RE = /FAILED|Error|✗|not ok|FAIL/;
4582
+ var FAIL_RE = /\bFAILED\b|\bFAIL\b|✗|✕|\bnot ok\b|error:|error\[|error TS\d|error CS\d|\bpanic:|Traceback \(most recent call last\)|[1-9]\d* (?:failed|failing|errors?)\b/;
4297
4583
  function asObject(v) {
4298
4584
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
4299
4585
  }
@@ -4309,10 +4595,8 @@ function softFail(toolResponse) {
4309
4595
  return false;
4310
4596
  if (r["isError"] === true || r["interrupted"] === true)
4311
4597
  return true;
4312
- const stderr = asString(r["stderr"]);
4313
- if (stderr !== undefined && stderr !== "")
4314
- return true;
4315
- const out = `${asString(r["stdout"]) ?? ""}${stderr ?? ""}`;
4598
+ const out = `${asString(r["stdout"]) ?? ""}
4599
+ ${asString(r["stderr"]) ?? ""}`;
4316
4600
  return FAIL_RE.test(out);
4317
4601
  }
4318
4602
  function commandOf(toolInput) {
@@ -4333,16 +4617,6 @@ function appendClassified(dir, toolName, command, ok, ts) {
4333
4617
  if (c !== null)
4334
4618
  appendEvent(dir, { ts, ...c });
4335
4619
  }
4336
- function appendBatch(dir, calls, ts) {
4337
- for (const call of calls) {
4338
- const c = asObject(call);
4339
- const toolName = c !== undefined ? asString(c["tool_name"]) : undefined;
4340
- if (c === undefined || toolName === undefined)
4341
- continue;
4342
- const ok = !softFail(c["tool_response"]);
4343
- appendClassified(dir, toolName, commandOf(c["tool_input"]), ok, ts);
4344
- }
4345
- }
4346
4620
  function runClassify(stdin, env, clock) {
4347
4621
  try {
4348
4622
  const r = asObject(JSON.parse(stdin));
@@ -4354,12 +4628,6 @@ function runClassify(stdin, env, clock) {
4354
4628
  const dir = sessionDir(ccsidekickRoot(env), session);
4355
4629
  const ts = clock.now();
4356
4630
  const hook = asString(r["hook_event_name"]);
4357
- if (hook === "PostToolBatch") {
4358
- const calls = r["tool_calls"];
4359
- if (Array.isArray(calls))
4360
- appendBatch(dir, calls, ts);
4361
- return;
4362
- }
4363
4631
  const toolName = asString(r["tool_name"]);
4364
4632
  if (toolName === undefined)
4365
4633
  return;
@@ -4372,7 +4640,7 @@ function runClassify(stdin, env, clock) {
4372
4640
  } catch {}
4373
4641
  }
4374
4642
  // src/cli/render.ts
4375
- import { readFileSync as readFileSync10 } from "node:fs";
4643
+ import { readFileSync as readFileSync11 } from "node:fs";
4376
4644
  import { dirname as dirname7, join as join14 } from "node:path";
4377
4645
 
4378
4646
  // src/compose/character.ts
@@ -6089,16 +6357,6 @@ function composeStatusline(ctx) {
6089
6357
  return { fields, providerBadge };
6090
6358
  }
6091
6359
  // src/compose/helpful/catalog.ts
6092
- var COMPACT_SOON_PCT = 60;
6093
- var QUOTA_PROJECT_MIN_PCT = 50;
6094
- var COMPACTION_THRASH_N = 3;
6095
- var CACHE_RATIO_FLOOR = 0.5;
6096
- var CACHE_WARMUP_TURNS = 20;
6097
- var BIG_DIFF_LINES = 1000;
6098
- var UNTRACKED_N = 20;
6099
- var STASH_N = 5;
6100
- var STALE_BRANCH_N = 20;
6101
- var TODO_STALLED_MIN = 30;
6102
6360
  var EFFORT_LOW_LEVEL = "low";
6103
6361
  var PROD_CONTEXT_PATTERN = /prod/i;
6104
6362
  var SECRET_PATTERNS = [
@@ -6260,7 +6518,7 @@ var HELPFUL_CATALOG = [
6260
6518
  },
6261
6519
  {
6262
6520
  id: "pay_as_you_go_active",
6263
- severity: "medium",
6521
+ severity: "low",
6264
6522
  category: "billing",
6265
6523
  momentary: false,
6266
6524
  template: "Pay-as-you-go is on. Every request now bills on top of your plan.",
@@ -6273,7 +6531,7 @@ var HELPFUL_CATALOG = [
6273
6531
  category: "quota",
6274
6532
  momentary: false,
6275
6533
  template: "5h limit at {pct}%, {reset} to reset. Save heavy asks for after.",
6276
- test: (i) => i.quota.block !== undefined && i.quota.block.usedPct > QUOTA_HIGH_PCT,
6534
+ test: (i) => i.quota.block !== undefined && i.quota.block.usedPct > QUOTA_HIGH_PCT && i.quota.block.overPace === true,
6277
6535
  render: (i) => `5h limit at ${pct(i.quota.block?.usedPct ?? 0)}, ${bare(i.quota.block?.resetIn)} to reset. Save heavy asks for after.`
6278
6536
  },
6279
6537
  {
@@ -6282,7 +6540,7 @@ var HELPFUL_CATALOG = [
6282
6540
  category: "quota",
6283
6541
  momentary: false,
6284
6542
  template: "Weekly quota {pct}% spent, {reset} to go. Ration the big requests.",
6285
- test: (i) => i.quota.weekly !== undefined && i.quota.weekly.usedPct > QUOTA_HIGH_PCT,
6543
+ test: (i) => i.quota.weekly !== undefined && i.quota.weekly.usedPct > QUOTA_HIGH_PCT && i.quota.weekly.overPace === true,
6286
6544
  render: (i) => `Weekly quota ${pct(i.quota.weekly?.usedPct ?? 0)} spent, ${bare(i.quota.weekly?.resetIn)} to go. Ration the big requests.`
6287
6545
  },
6288
6546
  {
@@ -6291,7 +6549,7 @@ var HELPFUL_CATALOG = [
6291
6549
  category: "quota",
6292
6550
  momentary: false,
6293
6551
  template: "This burn rate empties the 5h block before reset, cutting you off mid-task.",
6294
- test: (i) => i.quota.block !== undefined && i.quota.block.band !== "nominal" && i.quota.block.usedPct > QUOTA_PROJECT_MIN_PCT,
6552
+ test: (i) => i.quota.block !== undefined && i.quota.block.usedPct > QUOTA_PROJECT_MIN_PCT && i.quota.block.overPace === true,
6295
6553
  render: () => "This burn rate empties the 5h block before reset, cutting you off mid-task."
6296
6554
  },
6297
6555
  {
@@ -6300,7 +6558,7 @@ var HELPFUL_CATALOG = [
6300
6558
  category: "quota",
6301
6559
  momentary: false,
6302
6560
  template: "At this pace the weekly quota runs dry before reset, locking you out for days.",
6303
- test: (i) => i.quota.weekly !== undefined && i.quota.weekly.band !== "nominal" && i.quota.weekly.usedPct > QUOTA_PROJECT_MIN_PCT,
6561
+ test: (i) => i.quota.weekly !== undefined && i.quota.weekly.usedPct > QUOTA_PROJECT_MIN_PCT && i.quota.weekly.overPace === true,
6304
6562
  render: () => "At this pace the weekly quota runs dry before reset, locking you out for days."
6305
6563
  },
6306
6564
  {
@@ -6317,9 +6575,9 @@ var HELPFUL_CATALOG = [
6317
6575
  severity: "high",
6318
6576
  category: "context",
6319
6577
  momentary: false,
6320
- template: "{pct}% full with uncommitted work. Commit before a compact buries it.",
6578
+ template: "{pct}% full with uncommitted work. Commit now while the context is fresh.",
6321
6579
  test: (i) => i.context.usedPct > COMPACT_SOON_PCT && dirty(i.git),
6322
- render: (i) => `${pct(i.context.usedPct)} full with uncommitted work. Commit before a compact buries it.`
6580
+ render: (i) => `${pct(i.context.usedPct)} full with uncommitted work. Commit now while the context is fresh.`
6323
6581
  },
6324
6582
  {
6325
6583
  id: "compaction_thrash",
@@ -6435,7 +6693,7 @@ var HELPFUL_CATALOG = [
6435
6693
  category: "git",
6436
6694
  momentary: false,
6437
6695
  template: "No upstream set. `git push -u origin HEAD` to track it.",
6438
- test: (i) => i.git !== null && i.git.branch !== undefined && !i.git.upstream,
6696
+ test: (i) => i.git !== null && i.git.branch !== undefined && !i.git.upstream && !i.git.remoteBranchExists,
6439
6697
  render: () => "No upstream set. `git push -u origin HEAD` to track it."
6440
6698
  },
6441
6699
  {
@@ -6512,7 +6770,7 @@ var HELPFUL_CATALOG = [
6512
6770
  },
6513
6771
  {
6514
6772
  id: "detached_head",
6515
- severity: "low",
6773
+ severity: "medium",
6516
6774
  category: "git",
6517
6775
  momentary: false,
6518
6776
  template: "Detached HEAD. Branch before committing or new commits can vanish.",
@@ -6530,7 +6788,7 @@ var HELPFUL_CATALOG = [
6530
6788
  },
6531
6789
  {
6532
6790
  id: "effort_low",
6533
- severity: "low",
6791
+ severity: "medium",
6534
6792
  category: "workflow",
6535
6793
  momentary: false,
6536
6794
  template: "Effort on low. Hard problems get shallow answers.",
@@ -6921,16 +7179,33 @@ var PACKS = [
6921
7179
  "yoda"
6922
7180
  ];
6923
7181
  // src/cli/gc.ts
6924
- import { existsSync as existsSync2, readdirSync as readdirSync3, rmSync, statSync as statSync4 } from "node:fs";
7182
+ import {
7183
+ existsSync as existsSync2,
7184
+ mkdirSync as mkdirSync5,
7185
+ readFileSync as readFileSync10,
7186
+ readdirSync as readdirSync3,
7187
+ rmSync,
7188
+ statSync as statSync5,
7189
+ writeFileSync as writeFileSync3
7190
+ } from "node:fs";
6925
7191
  import { join as join13 } from "node:path";
6926
7192
  var DAY_MS = 86400000;
7193
+ var GC_MIN_INTERVAL_MS = 6 * 60 * 60 * 1000;
7194
+ function readGcStamp(path) {
7195
+ try {
7196
+ const n = Number(readFileSync10(path, "utf8"));
7197
+ return Number.isFinite(n) ? n : undefined;
7198
+ } catch {
7199
+ return;
7200
+ }
7201
+ }
6927
7202
  function newestMtime(dir) {
6928
7203
  let newest = 0;
6929
7204
  try {
6930
- newest = statSync4(dir).mtimeMs;
7205
+ newest = statSync5(dir).mtimeMs;
6931
7206
  for (const name of readdirSync3(dir)) {
6932
7207
  try {
6933
- const m = statSync4(join13(dir, name)).mtimeMs;
7208
+ const m = statSync5(join13(dir, name)).mtimeMs;
6934
7209
  if (m > newest)
6935
7210
  newest = m;
6936
7211
  } catch {}
@@ -6950,7 +7225,7 @@ function pruneSessions(root, now) {
6950
7225
  for (const name of names) {
6951
7226
  const dir = join13(sessions, name);
6952
7227
  try {
6953
- if (!statSync4(dir).isDirectory())
7228
+ if (!statSync5(dir).isDirectory())
6954
7229
  continue;
6955
7230
  } catch {
6956
7231
  continue;
@@ -6963,23 +7238,48 @@ function pruneSessions(root, now) {
6963
7238
  function pruneCostCache(root) {
6964
7239
  const cache = readCostCache(root);
6965
7240
  const kept = {};
7241
+ const liveSessions = new Set;
6966
7242
  let dropped = false;
6967
7243
  for (const [path, entry] of Object.entries(cache.files)) {
6968
- if (existsSync2(path))
7244
+ if (existsSync2(path)) {
6969
7245
  kept[path] = entry;
6970
- else
7246
+ liveSessions.add(String(entry.record.session));
7247
+ } else {
6971
7248
  dropped = true;
7249
+ }
7250
+ }
7251
+ const chatKept = {};
7252
+ let chatDropped = false;
7253
+ for (const [session, cost] of Object.entries(cache.aggregate.chat)) {
7254
+ if (liveSessions.has(session))
7255
+ chatKept[session] = cost;
7256
+ else
7257
+ chatDropped = true;
7258
+ }
7259
+ if (dropped || chatDropped) {
7260
+ writeCostCache(root, {
7261
+ ...cache,
7262
+ files: kept,
7263
+ aggregate: { ...cache.aggregate, chat: chatKept }
7264
+ });
6972
7265
  }
6973
- if (dropped)
6974
- writeCostCache(root, { ...cache, files: kept });
6975
7266
  }
6976
7267
  function runGc(root, clock) {
7268
+ const now = clock.now();
7269
+ const stampPath = join13(root, "cache", ".gc-stamp");
7270
+ const last = readGcStamp(stampPath);
7271
+ if (last !== undefined && now - last < GC_MIN_INTERVAL_MS)
7272
+ return;
6977
7273
  try {
6978
- pruneSessions(root, clock.now());
7274
+ pruneSessions(root, now);
6979
7275
  } catch {}
6980
7276
  try {
6981
7277
  pruneCostCache(root);
6982
7278
  } catch {}
7279
+ try {
7280
+ mkdirSync5(join13(root, "cache"), { recursive: true });
7281
+ writeFileSync3(stampPath, String(now));
7282
+ } catch {}
6983
7283
  }
6984
7284
 
6985
7285
  // src/cli/render.ts
@@ -6988,7 +7288,7 @@ var TIER_MILESTONES = new Set(TIER_THRESHOLDS);
6988
7288
  var YEAR_MS = 365 * 86400000;
6989
7289
  function readTextSafe(path) {
6990
7290
  try {
6991
- return readFileSync10(path, "utf8");
7291
+ return readFileSync11(path, "utf8");
6992
7292
  } catch {
6993
7293
  return "";
6994
7294
  }
@@ -7023,19 +7323,29 @@ function applyThemeIcons(fields, theme, config) {
7023
7323
  });
7024
7324
  }
7025
7325
  function readGated(overrides, envInputs, config, root, dir, markers, clock) {
7026
- const creds = overrides?.creds !== undefined ? overrides.creds : envInputs.hasApiKey || config.network.usage_fetch ? readCreds() : null;
7326
+ const creds = overrides?.creds !== undefined ? overrides.creds : envInputs.hasApiKey || config.network.usage_fetch ? readCredsCached(root) : null;
7027
7327
  const usage = overrides?.usage !== undefined ? overrides.usage : config.network.usage_fetch ? readUsageCached(root) : null;
7028
7328
  const balance = overrides?.balance !== undefined ? overrides.balance : config.network.balance_path !== "" ? readBalance(config.network.balance_path, clock) : null;
7029
7329
  const helpfulEnv = needsHelpfulEnv(markers) ? readHelpfulEnv(dir) : EMPTY_HELPFUL_ENV;
7030
7330
  return { creds, usage, balance, helpfulEnv };
7031
7331
  }
7032
7332
  var DEFAULT_EMBLEM = "❝";
7033
- function lookupPackTheme(name) {
7034
- const loaded = loadPack(name);
7333
+ function packThemeOf(loaded) {
7035
7334
  if (!loaded.ok || loaded.pack.theme === undefined)
7036
7335
  return null;
7037
7336
  return { displayName: loaded.pack.displayName, ...loaded.pack.theme };
7038
7337
  }
7338
+ function makePackThemeLookup(persona, loaded, load = loadPack) {
7339
+ const memo = new Map;
7340
+ return (name) => {
7341
+ const cached = memo.get(name);
7342
+ if (cached !== undefined || memo.has(name))
7343
+ return cached ?? null;
7344
+ const theme = packThemeOf(name === persona ? loaded : load(name));
7345
+ memo.set(name, theme);
7346
+ return theme;
7347
+ };
7348
+ }
7039
7349
  var NOOP = () => {};
7040
7350
  function runRender(stdin, env, term, clock, overrides) {
7041
7351
  try {
@@ -7098,8 +7408,8 @@ function build(stdin, env, term, clock, overrides) {
7098
7408
  const stacks = deriveStacks(markers, events);
7099
7409
  const fresh = freshestEvent(events);
7100
7410
  const stack = pickStack(stacks, fresh?.stack);
7101
- const familiarity = deriveFamiliarity(attribution, persona, scannedCache, project, clock);
7102
- const theme = resolveTheme(config, lookupPackTheme, persona);
7411
+ const familiarity = deriveFamiliarity(attribution, persona, scannedCache, project, clock, String(session));
7412
+ const theme = resolveTheme(config, makePackThemeLookup(persona, loaded), persona);
7103
7413
  const composeInputs = {
7104
7414
  provider,
7105
7415
  model,
@@ -7200,6 +7510,10 @@ function build(stdin, env, term, clock, overrides) {
7200
7510
  }
7201
7511
  swallow(() => {
7202
7512
  const payloadChat = payload.cost?.total_cost_usd;
7513
+ const rescanned = scannedCache !== costCache;
7514
+ const chatChanged = !isDefault && payloadChat !== undefined && scannedCache.aggregate.chat[String(session)] !== payloadChat;
7515
+ if (!rescanned && !chatChanged)
7516
+ return;
7203
7517
  const toWrite = !isDefault && payloadChat !== undefined ? {
7204
7518
  ...scannedCache,
7205
7519
  aggregate: {
@@ -7223,6 +7537,10 @@ function build(stdin, env, term, clock, overrides) {
7223
7537
  swallow(() => {
7224
7538
  readUsage(root, clock, { enabled: config.network.usage_fetch });
7225
7539
  });
7540
+ swallow(() => {
7541
+ if (envInputs.hasApiKey || config.network.usage_fetch)
7542
+ refreshCreds(root, clock);
7543
+ });
7226
7544
  swallow(() => {
7227
7545
  runGc(root, clock);
7228
7546
  });
@@ -7244,7 +7562,7 @@ function dispatchRender() {
7244
7562
  noColor: process.env["NO_COLOR"] !== undefined,
7245
7563
  isTTY: true
7246
7564
  };
7247
- const { line, persist } = runRender(readFileSync11(0, "utf8"), process.env, term, systemClock);
7565
+ const { line, persist } = runRender(readFileSync12(0, "utf8"), process.env, term, systemClock);
7248
7566
  process.stdout.write(`${line}
7249
7567
  `);
7250
7568
  persist();
@@ -7252,7 +7570,7 @@ function dispatchRender() {
7252
7570
  }
7253
7571
  function dispatchClassify() {
7254
7572
  try {
7255
- runClassify(readFileSync11(0, "utf8"), process.env, systemClock);
7573
+ runClassify(readFileSync12(0, "utf8"), process.env, systemClock);
7256
7574
  } catch {} finally {
7257
7575
  process.exit(0);
7258
7576
  }