ccc-notifier 0.6.3 → 0.8.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.
Files changed (30) hide show
  1. package/README.md +5 -0
  2. package/dist/{budget-V7CCMJHF.js → budget-U37CCG4Y.js} +2 -2
  3. package/dist/chunk-3M5UQK76.js +92 -0
  4. package/dist/chunk-6IVSZ3S4.js +22 -0
  5. package/dist/{sweep-CONNNBK7.js → chunk-C7QOYVGT.js} +324 -120
  6. package/dist/{chunk-PYU7CAK2.js → chunk-DSTGXYSY.js} +44 -11
  7. package/dist/{chunk-D4D76RGQ.js → chunk-GAOGTALW.js} +361 -20
  8. package/dist/{chunk-T6Z2ZAN4.js → chunk-ML5H43NH.js} +1 -1
  9. package/dist/{chunk-27SJELD2.js → chunk-PXMXEFM7.js} +22 -107
  10. package/dist/{chunk-6HTETN26.js → chunk-Q45AAPJY.js} +18 -1
  11. package/dist/{chunk-OXXDZZPJ.js → chunk-RE72XBXB.js} +4 -6
  12. package/dist/{chunk-NV5UOHJA.js → chunk-TTD3BGOL.js} +5 -5
  13. package/dist/{chunk-OOAC5ULQ.js → chunk-WONMZ466.js} +184 -4
  14. package/dist/{chunk-J5QAYTFE.js → chunk-XOGYLFX3.js} +12 -1
  15. package/dist/chunk-YH45W7TA.js +479 -0
  16. package/dist/cli.js +287 -30
  17. package/dist/{dashboard-O7LJLYM3.js → dashboard-YG4H3GLA.js} +6 -4
  18. package/dist/{history-DL4SG3PG.js → history-W34EJOVV.js} +5 -3
  19. package/dist/{mute-UIR5P5N5.js → mute-23BWQXS3.js} +2 -2
  20. package/dist/reset-cursors-UVQXFN5Y.js +44 -0
  21. package/dist/scan-QB2N2O4Q.js +94 -0
  22. package/dist/{setup-I3JNGWZG.js → setup-2QDA3OWE.js} +5 -6
  23. package/dist/{subagent-store-US4TJJQR.js → subagent-store-USG3WJEB.js} +1 -1
  24. package/dist/sweep-OLI2LIXJ.js +22 -0
  25. package/dist/track-TIINM4ZR.js +368 -0
  26. package/package.json +1 -1
  27. package/dist/chunk-HLJAJS2W.js +0 -55
  28. package/dist/chunk-HTYUYKFW.js +0 -21
  29. package/dist/chunk-OYD6H3ZZ.js +0 -124
  30. package/dist/track-RBLLR25M.js +0 -257
@@ -1,100 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  paths
4
- } from "./chunk-OOAC5ULQ.js";
4
+ } from "./chunk-WONMZ466.js";
5
5
 
6
- // src/dashboard-state.ts
6
+ // src/data-lock.ts
7
+ import { randomUUID } from "crypto";
8
+ import { hostname } from "os";
7
9
  import {
8
- closeSync,
9
10
  existsSync,
10
- openSync,
11
+ mkdirSync,
11
12
  readFileSync,
12
- readSync,
13
13
  renameSync,
14
14
  rmSync,
15
15
  writeFileSync
16
16
  } from "fs";
17
- import { randomUUID } from "crypto";
18
- function localDate(now) {
19
- const y = now.getFullYear();
20
- const m = String(now.getMonth() + 1).padStart(2, "0");
21
- const d = String(now.getDate()).padStart(2, "0");
22
- return `${y}-${m}-${d}`;
23
- }
24
- function timeZone() {
25
- return Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
26
- }
27
- function makeFullDashboardState(now = /* @__PURE__ */ new Date()) {
28
- return { localDate: localDate(now), timeZone: timeZone(), generatedAt: now.toISOString() };
29
- }
30
- function readState() {
31
- const file = paths().dashboardFullStateFile;
32
- if (!existsSync(file)) return null;
33
- try {
34
- const value = JSON.parse(readFileSync(file, "utf8"));
35
- if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
36
- const v = value;
37
- if (typeof v.localDate !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v.localDate) || typeof v.timeZone !== "string" || v.timeZone.length === 0 || typeof v.generatedAt !== "string" || !Number.isFinite(Date.parse(v.generatedAt))) {
38
- return null;
39
- }
40
- return v;
41
- } catch {
42
- return null;
43
- }
44
- }
45
- function isFullDashboardDue(now = /* @__PURE__ */ new Date()) {
46
- const p = paths();
47
- if (!existsSync(p.fullDashboardFile)) return true;
48
- try {
49
- const fd = openSync(p.fullDashboardFile, "r");
50
- try {
51
- const head = Buffer.alloc(512);
52
- const n = readSync(fd, head, 0, head.length, 0);
53
- if (head.toString("utf8", 0, n).includes('name="cccn-placeholder"')) return true;
54
- } finally {
55
- closeSync(fd);
56
- }
57
- } catch {
58
- return true;
59
- }
60
- const state = readState();
61
- if (state === null) return true;
62
- const expected = makeFullDashboardState(now);
63
- if (state.timeZone !== expected.timeZone) return true;
64
- if (state.localDate !== expected.localDate) return true;
65
- if (state.localDate > expected.localDate) return true;
66
- if (Date.parse(state.generatedAt) > now.getTime()) return true;
67
- return false;
68
- }
69
- function writeFullDashboardStateAtomic(state) {
70
- const file = paths().dashboardFullStateFile;
71
- const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
72
- try {
73
- writeFileSync(tmp, `${JSON.stringify(state)}
74
- `, "utf8");
75
- renameSync(tmp, file);
76
- } finally {
77
- rmSync(tmp, { force: true });
78
- }
79
- }
80
- function invalidateCanonicalDashboards() {
81
- const p = paths();
82
- for (const file of [p.recentDashboardFile, p.fullDashboardFile, p.dashboardFullStateFile]) {
83
- rmSync(file, { force: true });
84
- }
85
- }
86
-
87
- // src/data-lock.ts
88
- import { randomUUID as randomUUID2 } from "crypto";
89
- import { hostname } from "os";
90
- import {
91
- existsSync as existsSync2,
92
- mkdirSync,
93
- readFileSync as readFileSync2,
94
- renameSync as renameSync2,
95
- rmSync as rmSync2,
96
- writeFileSync as writeFileSync2
97
- } from "fs";
98
17
  import { join } from "path";
99
18
  var DATA_LOCK_LEASE_MS = 3e4;
100
19
  var HEARTBEAT_MS = 5e3;
@@ -103,7 +22,7 @@ function ownerFile(dir) {
103
22
  }
104
23
  function readOwner(dir) {
105
24
  try {
106
- const v = JSON.parse(readFileSync2(ownerFile(dir), "utf8"));
25
+ const v = JSON.parse(readFileSync(ownerFile(dir), "utf8"));
107
26
  if (typeof v.token !== "string" || typeof v.pid !== "number" || typeof v.hostname !== "string" || typeof v.acquiredAt !== "string" || typeof v.heartbeatAt !== "string") return null;
108
27
  return v;
109
28
  } catch {
@@ -111,21 +30,21 @@ function readOwner(dir) {
111
30
  }
112
31
  }
113
32
  function writeOwner(dir, owner) {
114
- const tmp = join(dir, `owner.${owner.token}.${randomUUID2()}.tmp`);
33
+ const tmp = join(dir, `owner.${owner.token}.${randomUUID()}.tmp`);
115
34
  try {
116
- writeFileSync2(tmp, `${JSON.stringify(owner)}
35
+ writeFileSync(tmp, `${JSON.stringify(owner)}
117
36
  `, "utf8");
118
- renameSync2(tmp, ownerFile(dir));
37
+ renameSync(tmp, ownerFile(dir));
119
38
  } finally {
120
- rmSync2(tmp, { force: true });
39
+ rmSync(tmp, { force: true });
121
40
  }
122
41
  }
123
42
  function quarantineOwned(dir, token, label) {
124
43
  const before = readOwner(dir);
125
44
  if (before?.token !== token) return false;
126
- const quarantine = `${dir}.${label}-${token}-${randomUUID2()}`;
45
+ const quarantine = `${dir}.${label}-${token}-${randomUUID()}`;
127
46
  try {
128
- renameSync2(dir, quarantine);
47
+ renameSync(dir, quarantine);
129
48
  } catch {
130
49
  return false;
131
50
  }
@@ -133,21 +52,21 @@ function quarantineOwned(dir, token, label) {
133
52
  if (moved?.token !== token) {
134
53
  return false;
135
54
  }
136
- rmSync2(quarantine, { recursive: true, force: true });
55
+ rmSync(quarantine, { recursive: true, force: true });
137
56
  return true;
138
57
  }
139
58
  function claimDir(fixed, label, now, metadataWriter = writeOwner) {
140
- const token = randomUUID2();
59
+ const token = randomUUID();
141
60
  const staging = `${fixed}.${label}-${token}`;
142
61
  const iso = now.toISOString();
143
62
  const owner = { token, pid: process.pid, hostname: hostname(), acquiredAt: iso, heartbeatAt: iso };
144
63
  try {
145
64
  mkdirSync(staging);
146
65
  metadataWriter(staging, owner);
147
- renameSync2(staging, fixed);
66
+ renameSync(staging, fixed);
148
67
  return { token, owner };
149
68
  } catch {
150
- rmSync2(staging, { recursive: true, force: true });
69
+ rmSync(staging, { recursive: true, force: true });
151
70
  return null;
152
71
  }
153
72
  }
@@ -180,15 +99,15 @@ function tryReclaim(now, leaseMs) {
180
99
  if (!processDefinitelyDead(first.pid)) return false;
181
100
  const second = readOwner(p.dataLockDir);
182
101
  if (second === null || second.token !== first.token || second.heartbeatAt !== first.heartbeatAt) return false;
183
- const orphan = `${p.dataLockDir}.orphan-${first.token}-${randomUUID2()}`;
102
+ const orphan = `${p.dataLockDir}.orphan-${first.token}-${randomUUID()}`;
184
103
  try {
185
- renameSync2(p.dataLockDir, orphan);
104
+ renameSync(p.dataLockDir, orphan);
186
105
  } catch {
187
106
  return false;
188
107
  }
189
108
  const moved = readOwner(orphan);
190
109
  if (moved?.token !== first.token || moved.heartbeatAt !== first.heartbeatAt) return false;
191
- rmSync2(orphan, { recursive: true, force: true });
110
+ rmSync(orphan, { recursive: true, force: true });
192
111
  return true;
193
112
  } finally {
194
113
  quarantineOwned(p.dataReclaimDir, guard.token, "released");
@@ -198,15 +117,15 @@ function acquireDataLock(opts = {}) {
198
117
  const now = opts.now ?? /* @__PURE__ */ new Date();
199
118
  const leaseMs = opts.leaseMs ?? DATA_LOCK_LEASE_MS;
200
119
  const p = paths();
201
- if (existsSync2(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
120
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
202
121
  let claim = claimDir(p.dataLockDir, "acquire", now, opts.metadataWriter);
203
122
  if (claim === null) {
204
123
  if (!tryReclaim(now, leaseMs)) return null;
205
- if (existsSync2(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
124
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
206
125
  claim = claimDir(p.dataLockDir, "acquire", now, opts.metadataWriter);
207
126
  if (claim === null) return null;
208
127
  }
209
- if (existsSync2(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) {
128
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) {
210
129
  quarantineOwned(p.dataLockDir, claim.token, "yielded");
211
130
  return null;
212
131
  }
@@ -249,9 +168,5 @@ async function waitForDataLock(timeoutMs, pollMs = 25) {
249
168
  }
250
169
 
251
170
  export {
252
- makeFullDashboardState,
253
- isFullDashboardDue,
254
- writeFullDashboardStateAtomic,
255
- invalidateCanonicalDashboards,
256
171
  waitForDataLock
257
172
  };
@@ -220,7 +220,24 @@ async function loadPriceTable(cacheDir, opts) {
220
220
  }
221
221
  }
222
222
 
223
+ // src/codex/env.ts
224
+ import { statSync } from "fs";
225
+ import { homedir } from "os";
226
+ import { join } from "path";
227
+ function codexHome() {
228
+ return process.env.CCCN_CODEX_HOME || join(homedir(), ".codex");
229
+ }
230
+ function detectCodex() {
231
+ try {
232
+ return statSync(codexHome()).isDirectory();
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+
223
238
  export {
224
239
  computeCost,
225
- loadPriceTable
240
+ loadPriceTable,
241
+ codexHome,
242
+ detectCodex
226
243
  };
@@ -2,19 +2,17 @@
2
2
  import {
3
3
  notifyOS,
4
4
  notifySlack
5
- } from "./chunk-NV5UOHJA.js";
5
+ } from "./chunk-TTD3BGOL.js";
6
6
  import {
7
7
  codexHome,
8
- detectCodex
9
- } from "./chunk-HTYUYKFW.js";
10
- import {
8
+ detectCodex,
11
9
  loadPriceTable
12
- } from "./chunk-6HTETN26.js";
10
+ } from "./chunk-Q45AAPJY.js";
13
11
  import {
14
12
  configFilePath,
15
13
  paths,
16
14
  readConfig
17
- } from "./chunk-OOAC5ULQ.js";
15
+ } from "./chunk-WONMZ466.js";
18
16
 
19
17
  // src/setup.ts
20
18
  import {
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-DGXUSPS4.js";
5
5
  import {
6
6
  formatSummary
7
- } from "./chunk-J5QAYTFE.js";
7
+ } from "./chunk-XOGYLFX3.js";
8
8
 
9
9
  // src/notify/os.ts
10
10
  import { spawn } from "child_process";
@@ -181,10 +181,10 @@ function spawnNotifyChild(title, body) {
181
181
  }
182
182
  return { child: spawnLinuxNotify(title, body), swallowError: backend.swallowError };
183
183
  }
184
- async function notifyOS(record, cfg, todayUSD) {
184
+ async function notifyOS(record, cfg, todayUSD, summaryOverride) {
185
185
  try {
186
186
  if (!cfg?.notify?.os) return;
187
- const { title, body } = formatSummary(record, cfg, todayUSD);
187
+ const { title, body } = summaryOverride ?? formatSummary(record, cfg, todayUSD);
188
188
  if (process.env.CCCN_DRY_RUN === "1") {
189
189
  writeDryRun("os", { title, body });
190
190
  return;
@@ -221,12 +221,12 @@ async function notifyOS(record, cfg, todayUSD) {
221
221
 
222
222
  // src/notify/slack.ts
223
223
  var SEND_TIMEOUT_MS = 3e3;
224
- async function notifySlack(record, cfg, todayUSD) {
224
+ async function notifySlack(record, cfg, todayUSD, summaryOverride) {
225
225
  try {
226
226
  const slackCfg = cfg?.notify?.slack;
227
227
  const webhookUrl = slackCfg?.webhookUrl;
228
228
  if (!webhookUrl) return;
229
- const { title, body } = formatSummary(record, cfg, todayUSD);
229
+ const { title, body } = summaryOverride ?? formatSummary(record, cfg, todayUSD);
230
230
  const line1 = body.split("\n")[0] ?? "";
231
231
  const rawPrompt = record.prompt ?? "";
232
232
  let promptText;
@@ -28,6 +28,7 @@ import {
28
28
  rmSync,
29
29
  statSync
30
30
  } from "fs";
31
+ import { randomUUID } from "crypto";
31
32
  import { join } from "path";
32
33
  import { homedir } from "os";
33
34
 
@@ -216,9 +217,21 @@ function loadCursor(transcriptPath) {
216
217
  const cursor = parsed[transcriptPath];
217
218
  return cursor ?? null;
218
219
  }
220
+ function cursorPaths() {
221
+ const p = paths();
222
+ if (!existsSync(p.cursorsFile)) return /* @__PURE__ */ new Set();
223
+ try {
224
+ const parsed = JSON.parse(readFileSync(p.cursorsFile, "utf8"));
225
+ if (!isPlainObject(parsed)) return /* @__PURE__ */ new Set();
226
+ return new Set(Object.keys(parsed));
227
+ } catch (err) {
228
+ logError("cursorPaths", err);
229
+ return /* @__PURE__ */ new Set();
230
+ }
231
+ }
219
232
  function sanitizeCursor(raw) {
220
233
  if (!isPlainObject(raw)) return null;
221
- const { offset, lastUuid, lastTs, seenMessageKeys, codexTotals } = raw;
234
+ const { offset, lastUuid, lastTs, seenMessageKeys, codexTotals, codexOriginator, codexModel } = raw;
222
235
  if (typeof offset !== "number" || !Number.isFinite(offset)) return null;
223
236
  if (lastUuid !== null && typeof lastUuid !== "string") return null;
224
237
  if (lastTs !== null && typeof lastTs !== "string") return null;
@@ -235,8 +248,44 @@ function sanitizeCursor(raw) {
235
248
  cursor.codexTotals = { input, cached, output };
236
249
  }
237
250
  }
251
+ if (Object.hasOwn(raw, "codexOriginator")) {
252
+ if (codexOriginator === null || typeof codexOriginator === "string") {
253
+ cursor.codexOriginator = codexOriginator;
254
+ }
255
+ }
256
+ if (Object.hasOwn(raw, "codexModel")) {
257
+ if (codexModel === null || typeof codexModel === "string") {
258
+ cursor.codexModel = codexModel;
259
+ }
260
+ }
238
261
  return cursor;
239
262
  }
263
+ function loadAllCursors() {
264
+ const p = paths();
265
+ if (!existsSync(p.cursorsFile)) return {};
266
+ try {
267
+ const raw = readFileSync(p.cursorsFile, "utf8");
268
+ const parsed = JSON.parse(raw);
269
+ if (!isPlainObject(parsed)) {
270
+ logError("loadAllCursors", new Error("cursors.json root is not an object"));
271
+ return {};
272
+ }
273
+ return parsed;
274
+ } catch (err) {
275
+ logError("loadAllCursors", err);
276
+ return {};
277
+ }
278
+ }
279
+ function saveAllCursors(dict) {
280
+ const p = paths();
281
+ const tmpFile = `${p.cursorsFile}.${randomUUID()}.tmp`;
282
+ try {
283
+ writeFileSync(tmpFile, JSON.stringify(dict), "utf8");
284
+ renameSync(tmpFile, p.cursorsFile);
285
+ } finally {
286
+ rmSync(tmpFile, { force: true });
287
+ }
288
+ }
240
289
  function saveCursor(transcriptPath, c) {
241
290
  const p = paths();
242
291
  let dict = {};
@@ -252,9 +301,83 @@ function saveCursor(transcriptPath, c) {
252
301
  }
253
302
  }
254
303
  dict[transcriptPath] = c;
255
- const tmpFile = `${p.cursorsFile}.tmp`;
256
- writeFileSync(tmpFile, JSON.stringify(dict), "utf8");
257
- renameSync(tmpFile, p.cursorsFile);
304
+ const tmpFile = `${p.cursorsFile}.${randomUUID()}.tmp`;
305
+ try {
306
+ writeFileSync(tmpFile, JSON.stringify(dict), "utf8");
307
+ renameSync(tmpFile, p.cursorsFile);
308
+ } finally {
309
+ rmSync(tmpFile, { force: true });
310
+ }
311
+ }
312
+ function pendingAppendPath() {
313
+ return join(paths().cacheDir, "pending-append.json");
314
+ }
315
+ function readPendingAppends() {
316
+ let raw;
317
+ try {
318
+ raw = readFileSync(pendingAppendPath(), "utf8");
319
+ } catch (err) {
320
+ if (err?.code === "ENOENT") return { dict: {}, trusted: true };
321
+ logError("readPendingAppends", err);
322
+ return { dict: {}, trusted: false };
323
+ }
324
+ try {
325
+ const parsed = JSON.parse(raw);
326
+ if (!isPlainObject(parsed)) {
327
+ logError("readPendingAppends", new Error("pending-append.json root is not an object"));
328
+ return { dict: {}, trusted: false };
329
+ }
330
+ return { dict: parsed, trusted: true };
331
+ } catch (err) {
332
+ logError("readPendingAppends", err);
333
+ return { dict: {}, trusted: false };
334
+ }
335
+ }
336
+ function writePendingAppends(dict) {
337
+ const file = pendingAppendPath();
338
+ const tmp = `${file}.${randomUUID()}.tmp`;
339
+ try {
340
+ writeFileSync(tmp, JSON.stringify(dict), "utf8");
341
+ renameSync(tmp, file);
342
+ } finally {
343
+ rmSync(tmp, { force: true });
344
+ }
345
+ }
346
+ var PENDING_ALL = "*";
347
+ function hasPendingAppend(transcriptPath) {
348
+ try {
349
+ const { dict, trusted } = readPendingAppends();
350
+ if (!trusted) return true;
351
+ return Object.hasOwn(dict, PENDING_ALL) || Object.hasOwn(dict, transcriptPath);
352
+ } catch (err) {
353
+ logError("hasPendingAppend", err);
354
+ return true;
355
+ }
356
+ }
357
+ function markPendingAppend(transcriptPath, ingestKey) {
358
+ const { dict, trusted } = readPendingAppends();
359
+ if (!trusted) dict[PENDING_ALL] = "unreadable-marker";
360
+ dict[transcriptPath] = ingestKey;
361
+ writePendingAppends(dict);
362
+ }
363
+ function clearPendingAppend(transcriptPath) {
364
+ try {
365
+ const { dict, trusted } = readPendingAppends();
366
+ if (!trusted) return;
367
+ if (!Object.hasOwn(dict, transcriptPath)) return;
368
+ delete dict[transcriptPath];
369
+ writePendingAppends(dict);
370
+ } catch (err) {
371
+ logError("clearPendingAppend", err);
372
+ }
373
+ }
374
+ function hasUnresolvedPendingMarker() {
375
+ try {
376
+ const { dict, trusted } = readPendingAppends();
377
+ return !trusted || Object.hasOwn(dict, PENDING_ALL);
378
+ } catch {
379
+ return true;
380
+ }
258
381
  }
259
382
  function appendTurn(record) {
260
383
  const p = paths();
@@ -309,6 +432,53 @@ function readTurns(days) {
309
432
  }
310
433
  return result;
311
434
  }
435
+ function floorKey(scope, sessionId) {
436
+ return `${scope}\0${sessionId}`;
437
+ }
438
+ function loadHistoryIndex() {
439
+ const index = { countedCalls: /* @__PURE__ */ new Set(), ingestKeys: /* @__PURE__ */ new Set(), legacyFloors: /* @__PURE__ */ new Map() };
440
+ let raw;
441
+ try {
442
+ raw = readFileSync(paths().historyFile, "utf8");
443
+ } catch {
444
+ return index;
445
+ }
446
+ for (const line of raw.split("\n")) {
447
+ if (line.length === 0) continue;
448
+ let rec;
449
+ try {
450
+ rec = JSON.parse(line);
451
+ } catch {
452
+ continue;
453
+ }
454
+ let hasFingerprints = false;
455
+ if (Array.isArray(rec.countedCalls)) {
456
+ for (const fp of rec.countedCalls) {
457
+ if (typeof fp === "string" && fp.length > 0) {
458
+ index.countedCalls.add(fp);
459
+ hasFingerprints = true;
460
+ }
461
+ }
462
+ }
463
+ if (typeof rec.ingestKey === "string" && rec.ingestKey.length > 0) index.ingestKeys.add(rec.ingestKey);
464
+ if (hasFingerprints) continue;
465
+ const { sessionId, ts } = rec;
466
+ if (typeof sessionId !== "string" || sessionId.length === 0) continue;
467
+ if (typeof ts !== "string") continue;
468
+ const ms = Date.parse(ts);
469
+ if (!Number.isFinite(ms)) continue;
470
+ const iso = new Date(ms).toISOString();
471
+ const isCodex = rec.source === "codex";
472
+ const scopes = isCodex ? ["codex"] : ["claude"];
473
+ if (!isCodex && rec.subagents !== void 0 && rec.subagents !== null) scopes.push("claude-sa");
474
+ for (const scope of scopes) {
475
+ const key = floorKey(scope, sessionId);
476
+ const cur = index.legacyFloors.get(key);
477
+ if (cur === void 0 || cur < iso) index.legacyFloors.set(key, iso);
478
+ }
479
+ }
480
+ return index;
481
+ }
312
482
  function todayTotalUSD() {
313
483
  const now = /* @__PURE__ */ new Date();
314
484
  const y = now.getFullYear();
@@ -1090,11 +1260,21 @@ export {
1090
1260
  writeMuteState,
1091
1261
  clearMuteState,
1092
1262
  loadCursor,
1263
+ cursorPaths,
1093
1264
  sanitizeCursor,
1265
+ loadAllCursors,
1266
+ saveAllCursors,
1094
1267
  saveCursor,
1268
+ pendingAppendPath,
1269
+ hasPendingAppend,
1270
+ markPendingAppend,
1271
+ clearPendingAppend,
1272
+ hasUnresolvedPendingMarker,
1095
1273
  appendTurn,
1096
1274
  resetHistoryAndCursors,
1097
1275
  readTurns,
1276
+ floorKey,
1277
+ loadHistoryIndex,
1098
1278
  todayTotalUSD,
1099
1279
  currentMonthTotals,
1100
1280
  logError
@@ -80,11 +80,22 @@ function formatSummary(record, cfg, todayUSD) {
80
80
  return { title, body: `${line1}
81
81
  ${line2}` };
82
82
  }
83
+ function formatIngestSummary(input, cfg) {
84
+ const label = cfg.costLabel === "api_equivalent" ? "API\u63DB\u7B97 " : "";
85
+ const title = `\u{1F4B0} ${label}${formatUSD(input.totalUSD)}(${formatJPY(input.totalJPY)})| hook\u975E\u4F9D\u5B58\u306E\u53D6\u308A\u8FBC\u307F ${input.recordCount}\u4EF6`;
86
+ const bySurfaceLine = Object.entries(input.bySurface).sort((a, b) => b[1].usd - a[1].usd).map(([surface, v]) => `${surface}: ${v.turns}\u4EF6 ${formatUSD(v.usd)}`).join(" / ");
87
+ const cachePct = input.totalTokens > 0 ? Math.round(input.cacheTokens / input.totalTokens * 100) : 0;
88
+ const tokensPart = input.totalTokens > 0 ? ` \xB7 \u8A08 ${formatTokens(input.totalTokens)} tokens(cache ${cachePct}%)` : "";
89
+ const body = `${bySurfaceLine}${tokensPart}
90
+ hook \u975E\u4F9D\u5B58\u306E\u5897\u5206\u53D6\u308A\u8FBC\u307F(scan)\u306B\u3088\u308B\u65B0\u898F\u5206\u3067\u3059`;
91
+ return { title, body };
92
+ }
83
93
 
84
94
  export {
85
95
  formatUSD,
86
96
  formatJPY,
87
97
  formatTokens,
88
98
  modelDisplayName,
89
- formatSummary
99
+ formatSummary,
100
+ formatIngestSummary
90
101
  };