ccsidekick 1.6.1 → 1.6.3

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;
@@ -1385,6 +1427,17 @@ var systemClock = {
1385
1427
  now: () => Date.now(),
1386
1428
  timezone: () => Intl.DateTimeFormat().resolvedOptions().timeZone
1387
1429
  };
1430
+ var fixedClock = (nowMs, tz = "UTC") => ({
1431
+ now: () => nowMs,
1432
+ timezone: () => tz
1433
+ });
1434
+ var resolveClock = (env) => {
1435
+ const raw = env["CCSIDEKICK_NOW"];
1436
+ const nowMs = raw === undefined ? NaN : Number(raw);
1437
+ if (raw === undefined || raw.trim() === "" || !Number.isFinite(nowMs))
1438
+ return systemClock;
1439
+ return fixedClock(nowMs, env["CCSIDEKICK_TZ"] ?? "UTC");
1440
+ };
1388
1441
  // ../../node_modules/.bun/smol-toml@1.6.1/node_modules/smol-toml/dist/error.js
1389
1442
  /*!
1390
1443
  * Copyright (c) Squirrel Chat et al., All rights reserved.
@@ -2493,10 +2546,10 @@ function loadConfig(globalToml = "", projectToml = "") {
2493
2546
  const statusline = section(g, p, "statusline");
2494
2547
  const d = DEFAULT_CONFIG;
2495
2548
  return {
2496
- schema_version: numv(p["schema_version"] ?? g["schema_version"], d.schema_version),
2549
+ schema_version: numv(p["schema_version"], numv(g["schema_version"], d.schema_version)),
2497
2550
  character: {
2498
2551
  enabled: bool(character["enabled"], d.character.enabled),
2499
- mode: oneOf(character["mode"], ["fixed", "random"], d.character.mode),
2552
+ mode: oneOf(character["mode"], CHARACTER_MODES, d.character.mode),
2500
2553
  name: str(character["name"], d.character.name),
2501
2554
  roster: stringArray(character["roster"], d.character.roster)
2502
2555
  },
@@ -2505,7 +2558,7 @@ function loadConfig(globalToml = "", projectToml = "") {
2505
2558
  ...typeof theme["statusline"] === "string" ? { statusline: theme["statusline"] } : {},
2506
2559
  ...typeof theme["logo"] === "string" ? { logo: theme["logo"] } : {},
2507
2560
  ...typeof theme["comment"] === "string" ? { comment: theme["comment"] } : {},
2508
- banding: oneOf(theme["banding"], ["solid", "cycle"], d.theme.banding),
2561
+ banding: oneOf(theme["banding"], BANDINGS, d.theme.banding),
2509
2562
  mood_shift: bool(theme["mood_shift"], d.theme.mood_shift),
2510
2563
  icons: mergeIcons({
2511
2564
  ...obj(obj(g["theme"])["icons"]),
@@ -2515,7 +2568,7 @@ function loadConfig(globalToml = "", projectToml = "") {
2515
2568
  comments: {
2516
2569
  character: bool(comments["character"], d.comments.character),
2517
2570
  helpful: bool(comments["helpful"], d.comments.helpful),
2518
- min_severity: oneOf(comments["min_severity"], ["low", "medium", "high", "critical"], d.comments.min_severity)
2571
+ min_severity: oneOf(comments["min_severity"], MIN_SEVERITIES, d.comments.min_severity)
2519
2572
  },
2520
2573
  network: {
2521
2574
  fx_refresh: bool(network["fx_refresh"], d.network.fx_refresh),
@@ -2645,7 +2698,19 @@ function coerceEntry(v) {
2645
2698
  }
2646
2699
  const projectPath = typeof v["projectPath"] === "string" ? v["projectPath"] : String(record.project);
2647
2700
  const models = Array.isArray(v["models"]) ? v["models"].filter((x) => typeof x === "string") : [];
2648
- return { mtime, size, total, lines, models, projectPath, record };
2701
+ const byteOffset = asNumber(v["byteOffset"]);
2702
+ const headHash = typeof v["headHash"] === "string" ? v["headHash"] : undefined;
2703
+ return {
2704
+ mtime,
2705
+ size,
2706
+ total,
2707
+ lines,
2708
+ models,
2709
+ projectPath,
2710
+ record,
2711
+ ...byteOffset !== undefined ? { byteOffset } : {},
2712
+ ...headHash !== undefined ? { headHash } : {}
2713
+ };
2649
2714
  }
2650
2715
  function coerceByModel(v) {
2651
2716
  if (!isObject(v))
@@ -2702,7 +2767,6 @@ function writeCostCache(root, c) {
2702
2767
  // src/sources/creds.ts
2703
2768
  import { spawnSync as spawnSync2 } from "node:child_process";
2704
2769
  import { readFileSync as readFileSync4 } from "node:fs";
2705
- import { homedir as homedir3 } from "node:os";
2706
2770
  import { join as join5 } from "node:path";
2707
2771
 
2708
2772
  // src/sources/oauthUsage.ts
@@ -2899,7 +2963,6 @@ function readUsage(root, clock, opts) {
2899
2963
  }
2900
2964
 
2901
2965
  // src/sources/creds.ts
2902
- var KEYCHAIN_SERVICE2 = "Claude Code-credentials";
2903
2966
  var SUBSCRIPTIONS = new Set(["pro", "max", "team", "enterprise"]);
2904
2967
  var isObject3 = (v) => typeof v === "object" && v !== null;
2905
2968
  var defaultRunner = (cmd, args) => {
@@ -2916,11 +2979,8 @@ var defaultRunner = (cmd, args) => {
2916
2979
  return "";
2917
2980
  }
2918
2981
  };
2919
- function configBase2() {
2920
- return process.env["CLAUDE_CONFIG_DIR"] ?? join5(homedir3(), ".claude");
2921
- }
2922
2982
  function readBlob(run, base) {
2923
- for (const service of [keychainService(base), KEYCHAIN_SERVICE2]) {
2983
+ for (const service of [keychainService(base), KEYCHAIN_SERVICE]) {
2924
2984
  const keychain = run("security", ["find-generic-password", "-s", service, "-w"]);
2925
2985
  if (keychain.trim() !== "")
2926
2986
  return keychain;
@@ -2930,7 +2990,7 @@ function readBlob(run, base) {
2930
2990
  if (file.trim() !== "")
2931
2991
  return file;
2932
2992
  } catch {}
2933
- const secretTool = run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE2]);
2993
+ const secretTool = run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE]);
2934
2994
  if (secretTool.trim() !== "")
2935
2995
  return secretTool;
2936
2996
  return "";
@@ -2959,7 +3019,7 @@ function readOrgTier(base) {
2959
3019
  }
2960
3020
  function readCreds(run = defaultRunner) {
2961
3021
  try {
2962
- const base = configBase2();
3022
+ const base = configBase();
2963
3023
  const blob = readBlob(run, base);
2964
3024
  if (blob.trim() !== "") {
2965
3025
  const tier = parseSubscription(blob);
@@ -2973,6 +3033,39 @@ function readCreds(run = defaultRunner) {
2973
3033
  return null;
2974
3034
  }
2975
3035
  }
3036
+ var CREDS_CACHE_FILE = "creds.json";
3037
+ function credsCachePath(root) {
3038
+ return join5(cacheDir(root), CREDS_CACHE_FILE);
3039
+ }
3040
+ function coerceCachedCreds(raw) {
3041
+ if (!isObject3(raw) || typeof raw["at"] !== "number")
3042
+ return null;
3043
+ const at = raw["at"];
3044
+ const info = raw["info"];
3045
+ if (info === null)
3046
+ return { info: null, at };
3047
+ if (isObject3(info) && typeof info["present"] === "boolean") {
3048
+ const sub = info["subscriptionType"];
3049
+ return {
3050
+ at,
3051
+ info: typeof sub === "string" && SUBSCRIPTIONS.has(sub) ? { present: info["present"], subscriptionType: sub } : { present: info["present"] }
3052
+ };
3053
+ }
3054
+ return null;
3055
+ }
3056
+ function readCredsCached(root) {
3057
+ return coerceCachedCreds(readJson(credsCachePath(root), undefined))?.info ?? null;
3058
+ }
3059
+ function refreshCreds(root, clock, run = defaultRunner) {
3060
+ const cached = coerceCachedCreds(readJson(credsCachePath(root), undefined));
3061
+ const now = clock.now();
3062
+ if (cached !== null && now - cached.at <= CREDS_TTL_MS)
3063
+ return;
3064
+ const info = readCreds(run);
3065
+ try {
3066
+ atomicWrite(credsCachePath(root), JSON.stringify({ info, at: now }));
3067
+ } catch {}
3068
+ }
2976
3069
  // src/sources/env.ts
2977
3070
  var on = (v) => v === "1" || v === "true";
2978
3071
  function isCustomHost(base) {
@@ -3455,12 +3548,14 @@ function readSubmoduleRollup(run, workTreeRoot) {
3455
3548
  return rollup;
3456
3549
  }
3457
3550
  function readGit(cwd, run = defaultRunner2(cwd)) {
3458
- if (run(["rev-parse", "--is-inside-work-tree"]).trim() !== "true")
3551
+ const probe = run(["rev-parse", "--is-inside-work-tree", "--git-dir", "--show-toplevel"]).split(`
3552
+ `);
3553
+ if ((probe[0] ?? "").trim() !== "true")
3459
3554
  return null;
3460
3555
  const status = parseStatus(run(["status", "--porcelain=v2", "--branch", "--show-stash"]));
3461
- const gitDirRaw = run(["rev-parse", "--git-dir"]).trim();
3556
+ const gitDirRaw = (probe[1] ?? "").trim();
3462
3557
  const gitDir = gitDirRaw === "" ? "" : resolve(cwd, gitDirRaw);
3463
- const workTreeRoot = run(["rev-parse", "--show-toplevel"]).trim();
3558
+ const workTreeRoot = (probe[2] ?? "").trim();
3464
3559
  const tag = run(["describe", "--tags", "--exact-match"]).trim() || undefined;
3465
3560
  const originRepo = normalizeOriginRepo(run(["remote", "get-url", "origin"]));
3466
3561
  const defaultBranch = resolveDefaultBranch(run);
@@ -3507,7 +3602,7 @@ function readGit(cwd, run = defaultRunner2(cwd)) {
3507
3602
  }
3508
3603
  // src/sources/helpfulEnv.ts
3509
3604
  import { readFileSync as readFileSync7 } from "node:fs";
3510
- import { homedir as homedir4 } from "node:os";
3605
+ import { homedir as homedir3 } from "node:os";
3511
3606
  import { delimiter, join as join9 } from "node:path";
3512
3607
  var opt3 = (key, value) => value !== undefined ? { [key]: value } : {};
3513
3608
  function kubeconfigPath() {
@@ -3517,7 +3612,7 @@ function kubeconfigPath() {
3517
3612
  if (first !== undefined)
3518
3613
  return first;
3519
3614
  }
3520
- return join9(homedir4(), ".kube", "config");
3615
+ return join9(homedir3(), ".kube", "config");
3521
3616
  }
3522
3617
  function readKubeContext() {
3523
3618
  try {
@@ -3821,7 +3916,7 @@ function writeState(sessionDir2, s) {
3821
3916
  }, () => {});
3822
3917
  }
3823
3918
  // src/sources/transcript.ts
3824
- import { readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
3919
+ import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync8, readSync, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
3825
3920
  import { basename, dirname as dirname6, join as join12 } from "node:path";
3826
3921
  var asObj = (v) => v !== null && typeof v === "object" ? v : undefined;
3827
3922
  var asStr = (v) => typeof v === "string" ? v : undefined;
@@ -4047,7 +4142,7 @@ function reconstructTodos(lines) {
4047
4142
  }
4048
4143
  function statSafe(p) {
4049
4144
  try {
4050
- const s = statSync3(p);
4145
+ const s = statSync4(p);
4051
4146
  return { mtimeMs: s.mtimeMs, size: s.size };
4052
4147
  } catch {
4053
4148
  return null;
@@ -4060,6 +4155,63 @@ function readSafe(p) {
4060
4155
  return "";
4061
4156
  }
4062
4157
  }
4158
+ function readBufSafe(p) {
4159
+ try {
4160
+ return readFileSync8(p);
4161
+ } catch {
4162
+ return null;
4163
+ }
4164
+ }
4165
+ function readRange(p, start, end) {
4166
+ if (end <= start)
4167
+ return Buffer.alloc(0);
4168
+ let fd;
4169
+ try {
4170
+ fd = openSync2(p, "r");
4171
+ const len = end - start;
4172
+ const buf = Buffer.allocUnsafe(len);
4173
+ let off = 0;
4174
+ while (off < len) {
4175
+ const n = readSync(fd, buf, off, len - off, start + off);
4176
+ if (n === 0)
4177
+ break;
4178
+ off += n;
4179
+ }
4180
+ return off === len ? buf : null;
4181
+ } catch {
4182
+ return null;
4183
+ } finally {
4184
+ if (fd !== undefined)
4185
+ closeSync2(fd);
4186
+ }
4187
+ }
4188
+ var HEAD_HASH_BYTES = 4096;
4189
+ function headHashOf(head) {
4190
+ let h = 2166136261;
4191
+ for (const b of head) {
4192
+ h ^= b;
4193
+ h = Math.imul(h, 16777619);
4194
+ }
4195
+ return (h >>> 0).toString(16);
4196
+ }
4197
+ function makeInternModel(models) {
4198
+ const index = new Map;
4199
+ models.forEach((m, i) => index.set(m, i));
4200
+ return (model) => {
4201
+ let i = index.get(model);
4202
+ if (i === undefined) {
4203
+ i = models.length;
4204
+ models.push(model);
4205
+ index.set(model, i);
4206
+ }
4207
+ return i;
4208
+ };
4209
+ }
4210
+ function lastLineEnd(buf) {
4211
+ const i = buf.lastIndexOf(10);
4212
+ return i < 0 ? 0 : i + 1;
4213
+ }
4214
+ var headLen = (byteOffset) => Math.min(HEAD_HASH_BYTES, byteOffset);
4063
4215
  var EMPTY_TOKENS = {
4064
4216
  input: 0,
4065
4217
  output: 0,
@@ -4150,7 +4302,7 @@ function* walkTree(dir, encodedDir) {
4150
4302
  const full = join12(dir, name);
4151
4303
  let st;
4152
4304
  try {
4153
- st = statSync3(full);
4305
+ st = statSync4(full);
4154
4306
  } catch {
4155
4307
  continue;
4156
4308
  }
@@ -4189,22 +4341,16 @@ function scanBounds(lines) {
4189
4341
  return { sessionId, start, end };
4190
4342
  }
4191
4343
  function priceFile(path, encodedDir, st, price, resolveProject) {
4192
- const lines = parseAll(readSafe(path));
4344
+ const buf = readBufSafe(path) ?? Buffer.alloc(0);
4345
+ const byteOffset = lastLineEnd(buf);
4346
+ const headHash = headHashOf(buf.subarray(0, headLen(byteOffset)));
4347
+ const lines = parseAll(buf.subarray(0, byteOffset).toString("utf8"));
4193
4348
  const { sessionId, start, end } = scanBounds(lines);
4194
4349
  const priced = collapseStreaming(lines);
4195
4350
  const gate = makeDedupGate();
4196
4351
  const costLines = [];
4197
4352
  const models = [];
4198
- const modelIndex = new Map;
4199
- const internModel = (model) => {
4200
- let i = modelIndex.get(model);
4201
- if (i === undefined) {
4202
- i = models.length;
4203
- models.push(model);
4204
- modelIndex.set(model, i);
4205
- }
4206
- return i;
4207
- };
4353
+ const internModel = makeInternModel(models);
4208
4354
  let total = 0;
4209
4355
  let input = 0;
4210
4356
  let output = 0;
@@ -4249,6 +4395,8 @@ function priceFile(path, encodedDir, st, price, resolveProject) {
4249
4395
  return {
4250
4396
  mtime: st.mtimeMs,
4251
4397
  size: st.size,
4398
+ byteOffset,
4399
+ headHash,
4252
4400
  total,
4253
4401
  lines: costLines,
4254
4402
  models,
@@ -4256,6 +4404,138 @@ function priceFile(path, encodedDir, st, price, resolveProject) {
4256
4404
  record
4257
4405
  };
4258
4406
  }
4407
+ function resumeCostFile(cached, path, st, price) {
4408
+ const tail = readAppendedTail(cached, path, st);
4409
+ if (tail === null)
4410
+ return null;
4411
+ if (tail === "partial")
4412
+ return { ...cached, mtime: st.mtimeMs, size: st.size };
4413
+ const models = [...cached.models];
4414
+ const keyIndex = new Map;
4415
+ cached.lines.forEach((l, i) => {
4416
+ if (l.id !== undefined && l.reqId !== undefined)
4417
+ keyIndex.set(`${l.id}|${l.reqId}`, i);
4418
+ });
4419
+ const gate = makeDedupGate();
4420
+ const acc = {
4421
+ lines: [...cached.lines],
4422
+ models,
4423
+ counted: cached.lines.map((l) => gate(l.id, l.reqId, l.sidechain)),
4424
+ keyIndex,
4425
+ gate,
4426
+ internModel: makeInternModel(models),
4427
+ input: cached.record.tokens.input,
4428
+ output: cached.record.tokens.output,
4429
+ cacheRead: cached.record.tokens.cache_read,
4430
+ cacheCreation: cached.record.tokens.cache_creation,
4431
+ total: cached.total,
4432
+ messages: cached.record.messages,
4433
+ start: cached.record.start,
4434
+ end: cached.record.end
4435
+ };
4436
+ for (const l of collapseStreaming(tail.appended))
4437
+ foldAppendedLine(acc, l, price);
4438
+ return {
4439
+ mtime: st.mtimeMs,
4440
+ size: st.size,
4441
+ byteOffset: tail.newByteOffset,
4442
+ headHash: tail.newHeadHash,
4443
+ total: acc.total,
4444
+ lines: acc.lines,
4445
+ models: acc.models,
4446
+ projectPath: cached.projectPath,
4447
+ record: {
4448
+ ...cached.record,
4449
+ start: acc.start,
4450
+ end: acc.end,
4451
+ tokens: {
4452
+ input: acc.input,
4453
+ output: acc.output,
4454
+ cache_read: acc.cacheRead,
4455
+ cache_creation: acc.cacheCreation
4456
+ },
4457
+ messages: acc.messages
4458
+ }
4459
+ };
4460
+ }
4461
+ function costLineOf(internModel, l, cost, totalTok) {
4462
+ return {
4463
+ ...l.id !== undefined ? { id: l.id } : {},
4464
+ ...l.requestId !== undefined ? { reqId: l.requestId } : {},
4465
+ sidechain: l.isSidechain,
4466
+ ts: l.tsMs ?? 0,
4467
+ cost,
4468
+ ...l.model !== undefined ? { m: internModel(l.model) } : {},
4469
+ tok: totalTok
4470
+ };
4471
+ }
4472
+ function readAppendedTail(cached, path, st) {
4473
+ const offset = cached.byteOffset;
4474
+ if (offset === undefined || offset === 0 || st.size < offset)
4475
+ return null;
4476
+ const head = readRange(path, 0, headLen(offset));
4477
+ if (head === null || headHashOf(head) !== cached.headHash)
4478
+ return null;
4479
+ const tailBuf = readRange(path, offset, st.size);
4480
+ if (tailBuf === null)
4481
+ return null;
4482
+ const added = lastLineEnd(tailBuf);
4483
+ if (added === 0)
4484
+ return "partial";
4485
+ const newByteOffset = offset + added;
4486
+ const newHead = headLen(newByteOffset) === head.length ? head : readRange(path, 0, headLen(newByteOffset));
4487
+ if (newHead === null)
4488
+ return null;
4489
+ return {
4490
+ appended: parseAll(tailBuf.subarray(0, added).toString("utf8")),
4491
+ newByteOffset,
4492
+ newHeadHash: headHashOf(newHead)
4493
+ };
4494
+ }
4495
+ function widenBounds(acc, tsMs) {
4496
+ if (tsMs === undefined)
4497
+ return;
4498
+ if (tsMs > acc.end)
4499
+ acc.end = tsMs;
4500
+ if (acc.start === 0 || tsMs < acc.start)
4501
+ acc.start = tsMs;
4502
+ }
4503
+ function foldAppendedLine(acc, l, price) {
4504
+ widenBounds(acc, l.tsMs);
4505
+ const usage = l.usage;
4506
+ if (usage === undefined)
4507
+ return;
4508
+ const t = tokenize(usage);
4509
+ const totalTok = t.input + t.output + t.cache_read + t.c5m + t.c1h;
4510
+ const key = l.id !== undefined && l.requestId !== undefined ? `${l.id}|${l.requestId}` : null;
4511
+ const existing = key !== null ? acc.keyIndex.get(key) : undefined;
4512
+ const old = existing !== undefined ? acc.lines[existing] : undefined;
4513
+ if (existing !== undefined && old !== undefined) {
4514
+ if (totalTok <= (old.tok ?? 0))
4515
+ return;
4516
+ const cost2 = price(usage, l.model ?? "", l.tsMs);
4517
+ if (acc.counted[existing] === true) {
4518
+ acc.output += totalTok - (old.tok ?? 0);
4519
+ acc.total += cost2 - old.cost;
4520
+ }
4521
+ acc.lines[existing] = costLineOf(acc.internModel, l, cost2, totalTok);
4522
+ return;
4523
+ }
4524
+ const cost = price(usage, l.model ?? "", l.tsMs);
4525
+ const isCounted = acc.gate(l.id, l.requestId, l.isSidechain);
4526
+ if (key !== null)
4527
+ acc.keyIndex.set(key, acc.lines.length);
4528
+ acc.lines.push(costLineOf(acc.internModel, l, cost, totalTok));
4529
+ acc.counted.push(isCounted);
4530
+ if (isCounted) {
4531
+ acc.input += t.input;
4532
+ acc.output += t.output;
4533
+ acc.cacheRead += t.cache_read;
4534
+ acc.cacheCreation += t.c5m + t.c1h;
4535
+ acc.total += cost;
4536
+ acc.messages += 1;
4537
+ }
4538
+ }
4259
4539
  function buildAggregate(files, chat) {
4260
4540
  const sessionProject = {};
4261
4541
  const all = [];
@@ -4290,17 +4570,27 @@ function scanCostTree(root, cache, clock, price, resolveProject) {
4290
4570
  return cache;
4291
4571
  const chat = cache.aggregate.chat;
4292
4572
  const files = {};
4573
+ let changed = false;
4293
4574
  for (const { path, encodedDir } of walkJsonl(root)) {
4294
4575
  const st = statSafe(path);
4295
4576
  if (!st)
4296
4577
  continue;
4297
4578
  const cached = cache.files[path];
4298
- files[path] = cached && cached.mtime === st.mtimeMs && cached.size === st.size ? cached : priceFile(path, encodedDir, st, price, resolveProject);
4579
+ if (cached && cached.mtime === st.mtimeMs && cached.size === st.size) {
4580
+ files[path] = cached;
4581
+ } else {
4582
+ const resumed = cached ? resumeCostFile(cached, path, st, price) : null;
4583
+ files[path] = resumed ?? priceFile(path, encodedDir, st, price, resolveProject);
4584
+ changed = true;
4585
+ }
4299
4586
  }
4587
+ const sameFileSet = Object.keys(files).length === Object.keys(cache.files).length;
4588
+ if (!changed && sameFileSet)
4589
+ return { files, aggregate: cache.aggregate, lastScanTs: now };
4300
4590
  return { files, aggregate: buildAggregate(files, chat), lastScanTs: now };
4301
4591
  }
4302
4592
  // src/cli/classify.ts
4303
- var FAIL_RE = /FAILED|Error|✗|not ok|FAIL/;
4593
+ 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/;
4304
4594
  function asObject(v) {
4305
4595
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
4306
4596
  }
@@ -4316,10 +4606,8 @@ function softFail(toolResponse) {
4316
4606
  return false;
4317
4607
  if (r["isError"] === true || r["interrupted"] === true)
4318
4608
  return true;
4319
- const stderr = asString(r["stderr"]);
4320
- if (stderr !== undefined && stderr !== "")
4321
- return true;
4322
- const out = `${asString(r["stdout"]) ?? ""}${stderr ?? ""}`;
4609
+ const out = `${asString(r["stdout"]) ?? ""}
4610
+ ${asString(r["stderr"]) ?? ""}`;
4323
4611
  return FAIL_RE.test(out);
4324
4612
  }
4325
4613
  function commandOf(toolInput) {
@@ -4340,16 +4628,6 @@ function appendClassified(dir, toolName, command, ok, ts) {
4340
4628
  if (c !== null)
4341
4629
  appendEvent(dir, { ts, ...c });
4342
4630
  }
4343
- function appendBatch(dir, calls, ts) {
4344
- for (const call of calls) {
4345
- const c = asObject(call);
4346
- const toolName = c !== undefined ? asString(c["tool_name"]) : undefined;
4347
- if (c === undefined || toolName === undefined)
4348
- continue;
4349
- const ok = !softFail(c["tool_response"]);
4350
- appendClassified(dir, toolName, commandOf(c["tool_input"]), ok, ts);
4351
- }
4352
- }
4353
4631
  function runClassify(stdin, env, clock) {
4354
4632
  try {
4355
4633
  const r = asObject(JSON.parse(stdin));
@@ -4361,12 +4639,6 @@ function runClassify(stdin, env, clock) {
4361
4639
  const dir = sessionDir(ccsidekickRoot(env), session);
4362
4640
  const ts = clock.now();
4363
4641
  const hook = asString(r["hook_event_name"]);
4364
- if (hook === "PostToolBatch") {
4365
- const calls = r["tool_calls"];
4366
- if (Array.isArray(calls))
4367
- appendBatch(dir, calls, ts);
4368
- return;
4369
- }
4370
4642
  const toolName = asString(r["tool_name"]);
4371
4643
  if (toolName === undefined)
4372
4644
  return;
@@ -4379,7 +4651,7 @@ function runClassify(stdin, env, clock) {
4379
4651
  } catch {}
4380
4652
  }
4381
4653
  // src/cli/render.ts
4382
- import { readFileSync as readFileSync10 } from "node:fs";
4654
+ import { readFileSync as readFileSync11 } from "node:fs";
4383
4655
  import { dirname as dirname7, join as join14 } from "node:path";
4384
4656
 
4385
4657
  // src/compose/character.ts
@@ -6096,16 +6368,6 @@ function composeStatusline(ctx) {
6096
6368
  return { fields, providerBadge };
6097
6369
  }
6098
6370
  // src/compose/helpful/catalog.ts
6099
- var COMPACT_SOON_PCT = 60;
6100
- var QUOTA_PROJECT_MIN_PCT = 50;
6101
- var COMPACTION_THRASH_N = 3;
6102
- var CACHE_RATIO_FLOOR = 0.5;
6103
- var CACHE_WARMUP_TURNS = 20;
6104
- var BIG_DIFF_LINES = 1000;
6105
- var UNTRACKED_N = 20;
6106
- var STASH_N = 5;
6107
- var STALE_BRANCH_N = 20;
6108
- var TODO_STALLED_MIN = 30;
6109
6371
  var EFFORT_LOW_LEVEL = "low";
6110
6372
  var PROD_CONTEXT_PATTERN = /prod/i;
6111
6373
  var SECRET_PATTERNS = [
@@ -6280,7 +6542,7 @@ var HELPFUL_CATALOG = [
6280
6542
  category: "quota",
6281
6543
  momentary: false,
6282
6544
  template: "5h limit at {pct}%, {reset} to reset. Save heavy asks for after.",
6283
- test: (i) => i.quota.block !== undefined && i.quota.block.usedPct > QUOTA_HIGH_PCT,
6545
+ test: (i) => i.quota.block !== undefined && i.quota.block.usedPct > QUOTA_HIGH_PCT && i.quota.block.overPace === true,
6284
6546
  render: (i) => `5h limit at ${pct(i.quota.block?.usedPct ?? 0)}, ${bare(i.quota.block?.resetIn)} to reset. Save heavy asks for after.`
6285
6547
  },
6286
6548
  {
@@ -6289,7 +6551,7 @@ var HELPFUL_CATALOG = [
6289
6551
  category: "quota",
6290
6552
  momentary: false,
6291
6553
  template: "Weekly quota {pct}% spent, {reset} to go. Ration the big requests.",
6292
- test: (i) => i.quota.weekly !== undefined && i.quota.weekly.usedPct > QUOTA_HIGH_PCT,
6554
+ test: (i) => i.quota.weekly !== undefined && i.quota.weekly.usedPct > QUOTA_HIGH_PCT && i.quota.weekly.overPace === true,
6293
6555
  render: (i) => `Weekly quota ${pct(i.quota.weekly?.usedPct ?? 0)} spent, ${bare(i.quota.weekly?.resetIn)} to go. Ration the big requests.`
6294
6556
  },
6295
6557
  {
@@ -6298,7 +6560,7 @@ var HELPFUL_CATALOG = [
6298
6560
  category: "quota",
6299
6561
  momentary: false,
6300
6562
  template: "This burn rate empties the 5h block before reset, cutting you off mid-task.",
6301
- test: (i) => i.quota.block !== undefined && i.quota.block.band !== "nominal" && i.quota.block.usedPct > QUOTA_PROJECT_MIN_PCT,
6563
+ test: (i) => i.quota.block !== undefined && i.quota.block.usedPct > QUOTA_PROJECT_MIN_PCT && i.quota.block.overPace === true,
6302
6564
  render: () => "This burn rate empties the 5h block before reset, cutting you off mid-task."
6303
6565
  },
6304
6566
  {
@@ -6307,7 +6569,7 @@ var HELPFUL_CATALOG = [
6307
6569
  category: "quota",
6308
6570
  momentary: false,
6309
6571
  template: "At this pace the weekly quota runs dry before reset, locking you out for days.",
6310
- test: (i) => i.quota.weekly !== undefined && i.quota.weekly.band !== "nominal" && i.quota.weekly.usedPct > QUOTA_PROJECT_MIN_PCT,
6572
+ test: (i) => i.quota.weekly !== undefined && i.quota.weekly.usedPct > QUOTA_PROJECT_MIN_PCT && i.quota.weekly.overPace === true,
6311
6573
  render: () => "At this pace the weekly quota runs dry before reset, locking you out for days."
6312
6574
  },
6313
6575
  {
@@ -6928,16 +7190,33 @@ var PACKS = [
6928
7190
  "yoda"
6929
7191
  ];
6930
7192
  // src/cli/gc.ts
6931
- import { existsSync as existsSync2, readdirSync as readdirSync3, rmSync, statSync as statSync4 } from "node:fs";
7193
+ import {
7194
+ existsSync as existsSync2,
7195
+ mkdirSync as mkdirSync5,
7196
+ readFileSync as readFileSync10,
7197
+ readdirSync as readdirSync3,
7198
+ rmSync,
7199
+ statSync as statSync5,
7200
+ writeFileSync as writeFileSync3
7201
+ } from "node:fs";
6932
7202
  import { join as join13 } from "node:path";
6933
7203
  var DAY_MS = 86400000;
7204
+ var GC_MIN_INTERVAL_MS = 6 * 60 * 60 * 1000;
7205
+ function readGcStamp(path) {
7206
+ try {
7207
+ const n = Number(readFileSync10(path, "utf8"));
7208
+ return Number.isFinite(n) ? n : undefined;
7209
+ } catch {
7210
+ return;
7211
+ }
7212
+ }
6934
7213
  function newestMtime(dir) {
6935
7214
  let newest = 0;
6936
7215
  try {
6937
- newest = statSync4(dir).mtimeMs;
7216
+ newest = statSync5(dir).mtimeMs;
6938
7217
  for (const name of readdirSync3(dir)) {
6939
7218
  try {
6940
- const m = statSync4(join13(dir, name)).mtimeMs;
7219
+ const m = statSync5(join13(dir, name)).mtimeMs;
6941
7220
  if (m > newest)
6942
7221
  newest = m;
6943
7222
  } catch {}
@@ -6957,7 +7236,7 @@ function pruneSessions(root, now) {
6957
7236
  for (const name of names) {
6958
7237
  const dir = join13(sessions, name);
6959
7238
  try {
6960
- if (!statSync4(dir).isDirectory())
7239
+ if (!statSync5(dir).isDirectory())
6961
7240
  continue;
6962
7241
  } catch {
6963
7242
  continue;
@@ -6970,23 +7249,48 @@ function pruneSessions(root, now) {
6970
7249
  function pruneCostCache(root) {
6971
7250
  const cache = readCostCache(root);
6972
7251
  const kept = {};
7252
+ const liveSessions = new Set;
6973
7253
  let dropped = false;
6974
7254
  for (const [path, entry] of Object.entries(cache.files)) {
6975
- if (existsSync2(path))
7255
+ if (existsSync2(path)) {
6976
7256
  kept[path] = entry;
6977
- else
7257
+ liveSessions.add(String(entry.record.session));
7258
+ } else {
6978
7259
  dropped = true;
7260
+ }
7261
+ }
7262
+ const chatKept = {};
7263
+ let chatDropped = false;
7264
+ for (const [session, cost] of Object.entries(cache.aggregate.chat)) {
7265
+ if (liveSessions.has(session))
7266
+ chatKept[session] = cost;
7267
+ else
7268
+ chatDropped = true;
7269
+ }
7270
+ if (dropped || chatDropped) {
7271
+ writeCostCache(root, {
7272
+ ...cache,
7273
+ files: kept,
7274
+ aggregate: { ...cache.aggregate, chat: chatKept }
7275
+ });
6979
7276
  }
6980
- if (dropped)
6981
- writeCostCache(root, { ...cache, files: kept });
6982
7277
  }
6983
7278
  function runGc(root, clock) {
7279
+ const now = clock.now();
7280
+ const stampPath = join13(root, "cache", ".gc-stamp");
7281
+ const last = readGcStamp(stampPath);
7282
+ if (last !== undefined && now - last < GC_MIN_INTERVAL_MS)
7283
+ return;
6984
7284
  try {
6985
- pruneSessions(root, clock.now());
7285
+ pruneSessions(root, now);
6986
7286
  } catch {}
6987
7287
  try {
6988
7288
  pruneCostCache(root);
6989
7289
  } catch {}
7290
+ try {
7291
+ mkdirSync5(join13(root, "cache"), { recursive: true });
7292
+ writeFileSync3(stampPath, String(now));
7293
+ } catch {}
6990
7294
  }
6991
7295
 
6992
7296
  // src/cli/render.ts
@@ -6995,7 +7299,7 @@ var TIER_MILESTONES = new Set(TIER_THRESHOLDS);
6995
7299
  var YEAR_MS = 365 * 86400000;
6996
7300
  function readTextSafe(path) {
6997
7301
  try {
6998
- return readFileSync10(path, "utf8");
7302
+ return readFileSync11(path, "utf8");
6999
7303
  } catch {
7000
7304
  return "";
7001
7305
  }
@@ -7030,19 +7334,29 @@ function applyThemeIcons(fields, theme, config) {
7030
7334
  });
7031
7335
  }
7032
7336
  function readGated(overrides, envInputs, config, root, dir, markers, clock) {
7033
- const creds = overrides?.creds !== undefined ? overrides.creds : envInputs.hasApiKey || config.network.usage_fetch ? readCreds() : null;
7337
+ const creds = overrides?.creds !== undefined ? overrides.creds : envInputs.hasApiKey || config.network.usage_fetch ? readCredsCached(root) : null;
7034
7338
  const usage = overrides?.usage !== undefined ? overrides.usage : config.network.usage_fetch ? readUsageCached(root) : null;
7035
7339
  const balance = overrides?.balance !== undefined ? overrides.balance : config.network.balance_path !== "" ? readBalance(config.network.balance_path, clock) : null;
7036
7340
  const helpfulEnv = needsHelpfulEnv(markers) ? readHelpfulEnv(dir) : EMPTY_HELPFUL_ENV;
7037
7341
  return { creds, usage, balance, helpfulEnv };
7038
7342
  }
7039
7343
  var DEFAULT_EMBLEM = "❝";
7040
- function lookupPackTheme(name) {
7041
- const loaded = loadPack(name);
7344
+ function packThemeOf(loaded) {
7042
7345
  if (!loaded.ok || loaded.pack.theme === undefined)
7043
7346
  return null;
7044
7347
  return { displayName: loaded.pack.displayName, ...loaded.pack.theme };
7045
7348
  }
7349
+ function makePackThemeLookup(persona, loaded, load = loadPack) {
7350
+ const memo = new Map;
7351
+ return (name) => {
7352
+ const cached = memo.get(name);
7353
+ if (cached !== undefined || memo.has(name))
7354
+ return cached ?? null;
7355
+ const theme = packThemeOf(name === persona ? loaded : load(name));
7356
+ memo.set(name, theme);
7357
+ return theme;
7358
+ };
7359
+ }
7046
7360
  var NOOP = () => {};
7047
7361
  function runRender(stdin, env, term, clock, overrides) {
7048
7362
  try {
@@ -7105,8 +7419,8 @@ function build(stdin, env, term, clock, overrides) {
7105
7419
  const stacks = deriveStacks(markers, events);
7106
7420
  const fresh = freshestEvent(events);
7107
7421
  const stack = pickStack(stacks, fresh?.stack);
7108
- const familiarity = deriveFamiliarity(attribution, persona, scannedCache, project, clock);
7109
- const theme = resolveTheme(config, lookupPackTheme, persona);
7422
+ const familiarity = deriveFamiliarity(attribution, persona, scannedCache, project, clock, String(session));
7423
+ const theme = resolveTheme(config, makePackThemeLookup(persona, loaded), persona);
7110
7424
  const composeInputs = {
7111
7425
  provider,
7112
7426
  model,
@@ -7207,6 +7521,10 @@ function build(stdin, env, term, clock, overrides) {
7207
7521
  }
7208
7522
  swallow(() => {
7209
7523
  const payloadChat = payload.cost?.total_cost_usd;
7524
+ const rescanned = scannedCache !== costCache;
7525
+ const chatChanged = !isDefault && payloadChat !== undefined && scannedCache.aggregate.chat[String(session)] !== payloadChat;
7526
+ if (!rescanned && !chatChanged)
7527
+ return;
7210
7528
  const toWrite = !isDefault && payloadChat !== undefined ? {
7211
7529
  ...scannedCache,
7212
7530
  aggregate: {
@@ -7230,6 +7548,10 @@ function build(stdin, env, term, clock, overrides) {
7230
7548
  swallow(() => {
7231
7549
  readUsage(root, clock, { enabled: config.network.usage_fetch });
7232
7550
  });
7551
+ swallow(() => {
7552
+ if (envInputs.hasApiKey || config.network.usage_fetch)
7553
+ refreshCreds(root, clock);
7554
+ });
7233
7555
  swallow(() => {
7234
7556
  runGc(root, clock);
7235
7557
  });
@@ -7251,7 +7573,7 @@ function dispatchRender() {
7251
7573
  noColor: process.env["NO_COLOR"] !== undefined,
7252
7574
  isTTY: true
7253
7575
  };
7254
- const { line, persist } = runRender(readFileSync11(0, "utf8"), process.env, term, systemClock);
7576
+ const { line, persist } = runRender(readFileSync12(0, "utf8"), process.env, term, resolveClock(process.env));
7255
7577
  process.stdout.write(`${line}
7256
7578
  `);
7257
7579
  persist();
@@ -7259,7 +7581,7 @@ function dispatchRender() {
7259
7581
  }
7260
7582
  function dispatchClassify() {
7261
7583
  try {
7262
- runClassify(readFileSync11(0, "utf8"), process.env, systemClock);
7584
+ runClassify(readFileSync12(0, "utf8"), process.env, systemClock);
7263
7585
  } catch {} finally {
7264
7586
  process.exit(0);
7265
7587
  }