ccc-notifier 0.3.0 → 0.4.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.
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  collectSubagentUsage,
8
8
  splitIntoCodexTurnDrafts
9
- } from "./chunk-DSV75EF7.js";
9
+ } from "./chunk-CKVK3UCA.js";
10
10
  import {
11
11
  computeCost,
12
12
  extractBucket,
@@ -14,6 +14,9 @@ import {
14
14
  loadPriceTable,
15
15
  promptCandidate
16
16
  } from "./chunk-LHKBGA5K.js";
17
+ import {
18
+ waitForDataLock
19
+ } from "./chunk-J6Y3RMMC.js";
17
20
  import {
18
21
  formatJPY,
19
22
  formatUSD,
@@ -27,7 +30,7 @@ import {
27
30
  readConfig,
28
31
  sanitizeCursor,
29
32
  saveCursor
30
- } from "./chunk-ECADO26T.js";
33
+ } from "./chunk-26CISNOE.js";
31
34
 
32
35
  // src/sweep.ts
33
36
  import { readFile } from "fs/promises";
@@ -258,80 +261,90 @@ function mergeUnknownModels(rec, extra) {
258
261
  for (const m of extra) if (!merged.includes(m)) merged.push(m);
259
262
  rec.unknownModels = merged;
260
263
  }
261
- async function processTranscript(mainPath, table, fx, daysCutoff, dryRun, summary) {
262
- const cursor = sanitizeCursor(loadCursor(mainPath));
263
- const { drafts, newCursor } = await splitIntoTurnDrafts(mainPath, cursor);
264
- const records = [];
265
- for (const draft of drafts) {
266
- const ts = draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString();
267
- if (daysCutoff !== null) {
268
- const tsMs = Date.parse(ts);
269
- if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
270
- }
271
- records.push(draftToRecord(draft, ts, table, fx));
264
+ async function processTranscript(mainPath, table, fx, daysCutoff, dryRun, summary, lockProvider) {
265
+ const lock = dryRun ? null : await lockProvider();
266
+ if (!dryRun && lock === null) {
267
+ logError("sweep:data-lock", new Error(`data lock timeout: ${mainPath}`));
268
+ summary.lockTimeouts += 1;
269
+ return;
272
270
  }
273
- let sa = null;
274
271
  try {
275
- sa = await collectSubagentUsage(mainPath);
276
- } catch (err) {
277
- logError("sweep:subagents", err);
278
- sa = null;
279
- }
280
- const saHasUsage = sa !== null && sa.apiCalls > 0;
281
- if (saHasUsage) {
282
- const saBreakdown = computeCost(sa.perModel, {}, table);
283
- summary.subagentsUSD += saBreakdown.usd;
284
- const saBlock = {
285
- costUSD: saBreakdown.usd,
286
- costByModel: saBreakdown.byModel,
287
- tokens: sumBuckets(sa.perModel),
288
- apiCalls: sa.apiCalls,
289
- agentFiles: sa.agentFiles
290
- };
291
- if (records.length > 0) {
292
- const last = records[records.length - 1];
293
- last.subagents = saBlock;
294
- mergeUnknownModels(last, saBreakdown.unknownModels);
295
- } else {
296
- const rec = {
297
- schemaVersion: 1,
298
- ts: maxCursorTs(sa.newCursors) ?? (/* @__PURE__ */ new Date()).toISOString(),
299
- sessionId: "",
300
- project: "",
301
- gitBranch: null,
302
- models: collectModels(sa.perModel, {}),
303
- tokens: emptyBuckets(),
304
- sidechainTokens: null,
305
- apiCalls: 0,
306
- costUSD: 0,
307
- costByModel: {},
308
- costJPY: 0,
309
- fxRate: fx.rate,
310
- fxSource: fx.source,
311
- prompt: "",
312
- subagents: saBlock,
313
- ingest: "sweep"
272
+ const cursor = sanitizeCursor(loadCursor(mainPath));
273
+ const { drafts, newCursor } = await splitIntoTurnDrafts(mainPath, cursor);
274
+ const records = [];
275
+ for (const draft of drafts) {
276
+ const ts = draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString();
277
+ if (daysCutoff !== null) {
278
+ const tsMs = Date.parse(ts);
279
+ if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
280
+ }
281
+ records.push(draftToRecord(draft, ts, table, fx));
282
+ }
283
+ let sa = null;
284
+ try {
285
+ sa = await collectSubagentUsage(mainPath);
286
+ } catch (err) {
287
+ logError("sweep:subagents", err);
288
+ sa = null;
289
+ }
290
+ const saHasUsage = sa !== null && sa.apiCalls > 0;
291
+ if (saHasUsage) {
292
+ const saBreakdown = computeCost(sa.perModel, {}, table);
293
+ summary.subagentsUSD += saBreakdown.usd;
294
+ const saBlock = {
295
+ costUSD: saBreakdown.usd,
296
+ costByModel: saBreakdown.byModel,
297
+ tokens: sumBuckets(sa.perModel),
298
+ apiCalls: sa.apiCalls,
299
+ agentFiles: sa.agentFiles
314
300
  };
315
- mergeUnknownModels(rec, saBreakdown.unknownModels);
316
- records.push(rec);
301
+ if (records.length > 0) {
302
+ const last = records[records.length - 1];
303
+ last.subagents = saBlock;
304
+ mergeUnknownModels(last, saBreakdown.unknownModels);
305
+ } else {
306
+ const rec = {
307
+ schemaVersion: 1,
308
+ ts: maxCursorTs(sa.newCursors) ?? (/* @__PURE__ */ new Date()).toISOString(),
309
+ sessionId: "",
310
+ project: "",
311
+ gitBranch: null,
312
+ models: collectModels(sa.perModel, {}),
313
+ tokens: emptyBuckets(),
314
+ sidechainTokens: null,
315
+ apiCalls: 0,
316
+ costUSD: 0,
317
+ costByModel: {},
318
+ costJPY: 0,
319
+ fxRate: fx.rate,
320
+ fxSource: fx.source,
321
+ prompt: "",
322
+ subagents: saBlock,
323
+ ingest: "sweep"
324
+ };
325
+ mergeUnknownModels(rec, saBreakdown.unknownModels);
326
+ records.push(rec);
327
+ }
328
+ summary.agentFiles += sa.agentFiles;
317
329
  }
318
- summary.agentFiles += sa.agentFiles;
319
- }
320
- for (const rec of records) {
321
- summary.newRecords += 1;
322
- summary.totalUSD += rec.costUSD;
323
- if (rec.costByModel) {
324
- for (const [m, c] of Object.entries(rec.costByModel)) {
325
- summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
330
+ for (const rec of records) {
331
+ summary.newRecords += 1;
332
+ summary.totalUSD += rec.costUSD;
333
+ if (rec.costByModel) {
334
+ for (const [m, c] of Object.entries(rec.costByModel)) {
335
+ summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
336
+ }
326
337
  }
327
338
  }
328
- }
329
- if (!dryRun) {
330
- for (const rec of records) appendTurn(rec);
331
- if (drafts.length > 0) saveCursor(mainPath, newCursor);
332
- if (saHasUsage) {
333
- for (const nc of sa.newCursors) saveCursor(nc.path, nc.cursor);
339
+ if (!dryRun) {
340
+ for (const rec of records) appendTurn(rec);
341
+ if (drafts.length > 0) saveCursor(mainPath, newCursor);
342
+ if (saHasUsage) {
343
+ for (const nc of sa.newCursors) saveCursor(nc.path, nc.cursor);
344
+ }
334
345
  }
346
+ } finally {
347
+ lock?.release();
335
348
  }
336
349
  }
337
350
  var CODEX_MAX_DEPTH = 4;
@@ -379,33 +392,43 @@ function codexDraftToRecord(draft, ts, table, fx) {
379
392
  if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
380
393
  return rec;
381
394
  }
382
- async function processCodexRollout(rolloutPath, table, fx, daysCutoff, dryRun, summary) {
383
- const cursor = sanitizeCursor(loadCursor(rolloutPath));
384
- const drafts = await splitIntoCodexTurnDrafts(rolloutPath, cursor);
385
- if (drafts === null || drafts.length === 0) return;
386
- const records = [];
387
- for (const draft of drafts) {
388
- const ts = draft.endTs ?? (/* @__PURE__ */ new Date()).toISOString();
389
- if (daysCutoff !== null) {
390
- const tsMs = Date.parse(ts);
391
- if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
392
- }
393
- records.push(codexDraftToRecord(draft, ts, table, fx));
395
+ async function processCodexRollout(rolloutPath, table, fx, daysCutoff, dryRun, summary, lockProvider) {
396
+ const lock = dryRun ? null : await lockProvider();
397
+ if (!dryRun && lock === null) {
398
+ logError("sweep:data-lock", new Error(`data lock timeout: ${rolloutPath}`));
399
+ summary.lockTimeouts += 1;
400
+ return;
394
401
  }
395
- for (const rec of records) {
396
- summary.newRecords += 1;
397
- summary.totalUSD += rec.costUSD;
398
- summary.codexRecords += 1;
399
- summary.codexUSD += rec.costUSD;
400
- if (rec.costByModel) {
401
- for (const [m, c] of Object.entries(rec.costByModel)) {
402
- summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
402
+ try {
403
+ const cursor = sanitizeCursor(loadCursor(rolloutPath));
404
+ const drafts = await splitIntoCodexTurnDrafts(rolloutPath, cursor);
405
+ if (drafts === null || drafts.length === 0) return;
406
+ const records = [];
407
+ for (const draft of drafts) {
408
+ const ts = draft.endTs ?? (/* @__PURE__ */ new Date()).toISOString();
409
+ if (daysCutoff !== null) {
410
+ const tsMs = Date.parse(ts);
411
+ if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
403
412
  }
413
+ records.push(codexDraftToRecord(draft, ts, table, fx));
404
414
  }
405
- }
406
- if (!dryRun) {
407
- for (const rec of records) appendTurn(rec);
408
- saveCursor(rolloutPath, drafts[drafts.length - 1].agg.newCursor);
415
+ for (const rec of records) {
416
+ summary.newRecords += 1;
417
+ summary.totalUSD += rec.costUSD;
418
+ summary.codexRecords += 1;
419
+ summary.codexUSD += rec.costUSD;
420
+ if (rec.costByModel) {
421
+ for (const [m, c] of Object.entries(rec.costByModel)) {
422
+ summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
423
+ }
424
+ }
425
+ }
426
+ if (!dryRun) {
427
+ for (const rec of records) appendTurn(rec);
428
+ saveCursor(rolloutPath, drafts[drafts.length - 1].agg.newCursor);
429
+ }
430
+ } finally {
431
+ lock?.release();
409
432
  }
410
433
  }
411
434
  async function codexSessionsRoot() {
@@ -414,7 +437,7 @@ async function codexSessionsRoot() {
414
437
  const isDir = await fsp.stat(sessionsRoot).then((st) => st.isDirectory()).catch(() => false);
415
438
  return isDir ? sessionsRoot : null;
416
439
  }
417
- async function sweepCodex(summary, table, fx, daysCutoff, flags) {
440
+ async function sweepCodex(summary, table, fx, daysCutoff, flags, lockProvider) {
418
441
  const sessionsRoot = await codexSessionsRoot();
419
442
  if (sessionsRoot === null) return;
420
443
  const rollouts = await listCodexRollouts(sessionsRoot);
@@ -424,7 +447,7 @@ async function sweepCodex(summary, table, fx, daysCutoff, flags) {
424
447
  continue;
425
448
  }
426
449
  try {
427
- await processCodexRollout(rolloutPath, table, fx, daysCutoff, flags.dryRun, summary);
450
+ await processCodexRollout(rolloutPath, table, fx, daysCutoff, flags.dryRun, summary, lockProvider);
428
451
  } catch (err) {
429
452
  logError("sweep:codex", err);
430
453
  }
@@ -489,7 +512,12 @@ function printSweepSummary(summary, fx) {
489
512
  `\u30B9\u30AD\u30C3\u30D7: ${summary.skippedActive} transcript(\u76F4\u8FD1${ACTIVE_GUARD_MIN}\u5206\u4EE5\u5185\u306B\u66F4\u65B0 = \u9032\u884C\u4E2D\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u53EF\u80FD\u6027)\u3002\u30BB\u30C3\u30B7\u30E7\u30F3\u5B8C\u4E86\u5F8C\u306B\u518D\u5B9F\u884C\u3059\u308B\u304B\u3001\u5B8C\u4E86\u6E08\u307F\u3068\u5206\u304B\u3063\u3066\u3044\u308B\u5834\u5408\u306F --include-active \u3067\u53D6\u308A\u8FBC\u3081\u307E\u3059`
490
513
  );
491
514
  }
492
- if (summary.newRecords === 0) {
515
+ if (summary.lockTimeouts > 0) {
516
+ console.log(
517
+ `\u672A\u5B8C\u4E86: ${summary.lockTimeouts} \u4EF6\u306Fdata lock\u3092\u53D6\u5F97\u3067\u304D\u305A\u672A\u51E6\u7406\u3067\u3059\u3002\u30AB\u30FC\u30BD\u30EB\u306F\u9032\u3081\u3066\u3044\u306A\u3044\u305F\u3081\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044`
518
+ );
519
+ }
520
+ if (summary.newRecords === 0 && summary.lockTimeouts === 0) {
493
521
  console.log("\u65B0\u898F\u306F\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F");
494
522
  } else {
495
523
  console.log(
@@ -513,10 +541,12 @@ function printSweepSummary(summary, fx) {
513
541
  }
514
542
  }
515
543
  }
516
- console.log("\u65E2\u306B\u8A08\u4E0A\u6E08\u307F\u306E\u5206\u306F\u30B9\u30AD\u30C3\u30D7\u3055\u308C\u307E\u3057\u305F(\u4E8C\u91CD\u8A08\u4E0A\u306A\u3057)");
544
+ if (summary.lockTimeouts === 0) {
545
+ console.log("\u65E2\u306B\u8A08\u4E0A\u6E08\u307F\u306E\u5206\u306F\u30B9\u30AD\u30C3\u30D7\u3055\u308C\u307E\u3057\u305F(\u4E8C\u91CD\u8A08\u4E0A\u306A\u3057)");
546
+ }
517
547
  console.log(`\u5186\u63DB\u7B97\u30EC\u30FC\u30C8: 1USD = ${fx.rate}JPY(source=${fx.source})`);
518
548
  }
519
- async function runSweep(argv) {
549
+ async function runSweep(argv, deps = {}) {
520
550
  const flags = parseSweepFlags(argv);
521
551
  const root = projectsRoot(flags.projects);
522
552
  const projectDirs = await listProjectDirs(root);
@@ -543,9 +573,11 @@ async function runSweep(argv) {
543
573
  skippedActive: 0,
544
574
  codexRecords: 0,
545
575
  codexUSD: 0,
546
- dryRun: flags.dryRun
576
+ dryRun: flags.dryRun,
577
+ lockTimeouts: 0
547
578
  };
548
579
  const daysCutoff = flags.days !== null ? Date.now() - flags.days * DAY_MS : null;
580
+ const lockProvider = deps.lockProvider ?? (() => waitForDataLock());
549
581
  for (const projectDir of projectDirs ?? []) {
550
582
  const transcripts = await listTranscripts(projectDir);
551
583
  for (const mainPath of transcripts) {
@@ -555,16 +587,16 @@ async function runSweep(argv) {
555
587
  continue;
556
588
  }
557
589
  try {
558
- await processTranscript(mainPath, table, fx, daysCutoff, flags.dryRun, summary);
590
+ await processTranscript(mainPath, table, fx, daysCutoff, flags.dryRun, summary, lockProvider);
559
591
  } catch (err) {
560
592
  logError("sweep:transcript", err);
561
593
  }
562
594
  }
563
595
  }
564
- await sweepCodex(summary, table, fx, daysCutoff, flags);
596
+ await sweepCodex(summary, table, fx, daysCutoff, flags, lockProvider);
565
597
  summary.totalJPY = summary.totalUSD * fx.rate;
566
598
  printSweepSummary(summary, fx);
567
- return 0;
599
+ return summary.lockTimeouts > 0 ? 1 : 0;
568
600
  }
569
601
  export {
570
602
  runSweep,
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ notifyOS,
4
+ notifySlack
5
+ } from "./chunk-NV5UOHJA.js";
6
+ import {
7
+ writeDashboardHtml
8
+ } from "./chunk-2VPBBIDW.js";
9
+ import "./chunk-DGXUSPS4.js";
10
+ import {
11
+ aggregateCodexTurn,
12
+ collectSubagentUsage
13
+ } from "./chunk-CKVK3UCA.js";
14
+ import {
15
+ aggregateNewTurn,
16
+ computeCost,
17
+ getUsdJpy,
18
+ loadPriceTable
19
+ } from "./chunk-LHKBGA5K.js";
20
+ import {
21
+ isFullDashboardDue,
22
+ makeFullDashboardState,
23
+ writeFullDashboardStateAtomic
24
+ } from "./chunk-ZHIZZ6V5.js";
25
+ import {
26
+ waitForDataLock
27
+ } from "./chunk-J6Y3RMMC.js";
28
+ import "./chunk-J5QAYTFE.js";
29
+ import {
30
+ appendTurn,
31
+ isMuted,
32
+ loadCursor,
33
+ logError,
34
+ paths,
35
+ readConfig,
36
+ readTurns,
37
+ sanitizeCursor,
38
+ saveCursor,
39
+ todayTotalUSD
40
+ } from "./chunk-26CISNOE.js";
41
+
42
+ // src/track.ts
43
+ function isRecord(v) {
44
+ return typeof v === "object" && v !== null && !Array.isArray(v);
45
+ }
46
+ function emptyBuckets() {
47
+ return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
48
+ }
49
+ function sumBuckets(usage) {
50
+ const total = emptyBuckets();
51
+ for (const b of Object.values(usage)) {
52
+ total.input += b.input;
53
+ total.output += b.output;
54
+ total.cacheWrite5m += b.cacheWrite5m;
55
+ total.cacheWrite1h += b.cacheWrite1h;
56
+ total.cacheRead += b.cacheRead;
57
+ }
58
+ return total;
59
+ }
60
+ function collectModels(main, sidechain) {
61
+ const models = [];
62
+ for (const m of Object.keys(main)) {
63
+ if (!models.includes(m)) models.push(m);
64
+ }
65
+ for (const m of Object.keys(sidechain)) {
66
+ if (!models.includes(m)) models.push(m);
67
+ }
68
+ return models;
69
+ }
70
+ function withCodexModel(agg, payloadModel) {
71
+ const model = typeof payloadModel === "string" && payloadModel.length > 0 ? payloadModel : null;
72
+ if (model === null) return agg;
73
+ const buckets = Object.values(agg.main)[0] ?? emptyBuckets();
74
+ return { ...agg, main: { [model]: buckets } };
75
+ }
76
+ async function runTrack(stdinText, opts) {
77
+ try {
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(stdinText);
81
+ } catch {
82
+ return;
83
+ }
84
+ if (!isRecord(parsed)) return;
85
+ const input = parsed;
86
+ const transcriptPath = input.transcript_path;
87
+ if (typeof transcriptPath !== "string") return;
88
+ const cfg = readConfig();
89
+ const cacheDir = paths().cacheDir;
90
+ const table = await loadPriceTable(cacheDir, { offline: true });
91
+ const fx = await getUsdJpy(cfg, cacheDir);
92
+ const isCodex = opts?.codex === true;
93
+ let record;
94
+ const commitLock = await waitForDataLock(1e3);
95
+ if (commitLock === null) {
96
+ logError("track:data-lock", new Error("data lock timeout; turn was not consumed"));
97
+ return;
98
+ }
99
+ try {
100
+ const cursor = sanitizeCursor(loadCursor(transcriptPath));
101
+ let agg = isCodex ? await aggregateCodexTurn(transcriptPath, cursor) : await aggregateNewTurn(transcriptPath, cursor);
102
+ if (agg === null) return;
103
+ if (isCodex) {
104
+ agg = withCodexModel(agg, input.model);
105
+ }
106
+ let sa = null;
107
+ if (!isCodex) {
108
+ try {
109
+ sa = await collectSubagentUsage(transcriptPath);
110
+ } catch (err) {
111
+ logError("track:subagents", err);
112
+ sa = null;
113
+ }
114
+ }
115
+ const breakdown = computeCost(agg.main, agg.sidechain, table);
116
+ const sessionId = agg.sessionId || (typeof input.session_id === "string" ? input.session_id : "") || "";
117
+ const project = agg.cwd ?? (typeof input.cwd === "string" ? input.cwd : void 0) ?? "";
118
+ const sidechainHasModels = Object.keys(agg.sidechain).length > 0;
119
+ record = {
120
+ schemaVersion: 1,
121
+ ts: agg.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(),
122
+ sessionId,
123
+ project,
124
+ gitBranch: agg.gitBranch,
125
+ models: collectModels(agg.main, agg.sidechain),
126
+ tokens: sumBuckets(agg.main),
127
+ sidechainTokens: sidechainHasModels ? sumBuckets(agg.sidechain) : null,
128
+ apiCalls: agg.apiCalls,
129
+ costUSD: breakdown.usd,
130
+ costByModel: breakdown.byModel,
131
+ // モデル別 USD(main+sidechain 合算、丸めない)
132
+ costJPY: breakdown.usd * fx.rate,
133
+ // 丸めない(表示時に丸める)
134
+ fxRate: fx.rate,
135
+ fxSource: fx.source,
136
+ prompt: agg.prompt ?? ""
137
+ };
138
+ if (isCodex) {
139
+ record.source = "codex";
140
+ }
141
+ if (breakdown.unknownModels.length > 0) {
142
+ record.unknownModels = breakdown.unknownModels;
143
+ }
144
+ if (sa !== null && sa.apiCalls > 0) {
145
+ const saBreakdown = computeCost(sa.perModel, {}, table);
146
+ record.subagents = {
147
+ costUSD: saBreakdown.usd,
148
+ costByModel: saBreakdown.byModel,
149
+ tokens: sumBuckets(sa.perModel),
150
+ apiCalls: sa.apiCalls,
151
+ agentFiles: sa.agentFiles
152
+ };
153
+ if (saBreakdown.unknownModels.length > 0) {
154
+ const merged = record.unknownModels ? [...record.unknownModels] : [];
155
+ for (const m of saBreakdown.unknownModels) {
156
+ if (!merged.includes(m)) merged.push(m);
157
+ }
158
+ record.unknownModels = merged;
159
+ }
160
+ }
161
+ appendTurn(record);
162
+ saveCursor(transcriptPath, agg.newCursor);
163
+ if (sa !== null) {
164
+ for (const nc of sa.newCursors) {
165
+ saveCursor(nc.path, nc.cursor);
166
+ }
167
+ }
168
+ } finally {
169
+ commitLock.release();
170
+ }
171
+ const tasks = [];
172
+ if ((cfg.notify.os || cfg.notify.slack !== null) && record.costUSD >= cfg.minNotifyUSD && !isMuted()) {
173
+ const todayUSD = cfg.includeDailyTotal ? todayTotalUSD() : void 0;
174
+ tasks.push(notifyOS(record, cfg, todayUSD));
175
+ tasks.push(notifySlack(record, cfg, todayUSD));
176
+ }
177
+ if (cfg.dashboard.autoRegenerate) {
178
+ tasks.push(
179
+ (async () => {
180
+ const now = /* @__PURE__ */ new Date();
181
+ const dashboardLock = await waitForDataLock(1e3);
182
+ if (dashboardLock === null) {
183
+ logError("track:dashboard-lock", new Error("data lock timeout; dashboard skipped"));
184
+ return;
185
+ }
186
+ try {
187
+ let allTurns;
188
+ try {
189
+ allTurns = readTurns();
190
+ } catch (err) {
191
+ logError("track:dashboard-read", err);
192
+ return;
193
+ }
194
+ try {
195
+ writeDashboardHtml({
196
+ days: cfg.dashboard.days,
197
+ outPath: paths().recentDashboardFile,
198
+ autoReloadSec: cfg.dashboard.autoReloadSec,
199
+ allTurns,
200
+ variant: "recent"
201
+ });
202
+ } catch (err) {
203
+ logError("track:dashboard-recent", err);
204
+ }
205
+ if (isFullDashboardDue(now)) {
206
+ try {
207
+ writeDashboardHtml({
208
+ days: null,
209
+ outPath: paths().fullDashboardFile,
210
+ autoReloadSec: cfg.dashboard.autoReloadSec,
211
+ allTurns,
212
+ variant: "full",
213
+ generatedAt: now.toISOString()
214
+ });
215
+ writeFullDashboardStateAtomic(makeFullDashboardState(now));
216
+ } catch (err) {
217
+ logError("track:dashboard-full", err);
218
+ }
219
+ }
220
+ } finally {
221
+ dashboardLock.release();
222
+ }
223
+ })()
224
+ );
225
+ }
226
+ if (tasks.length > 0) {
227
+ await Promise.allSettled(tasks);
228
+ }
229
+ } catch (err) {
230
+ logError("track", err);
231
+ }
232
+ }
233
+ export {
234
+ runTrack
235
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccc-notifier",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Claude Code Cost notifier (now also covers Codex CLI): per-prompt cost notifications (USD/JPY) with local history and HTML dashboard",
5
5
  "keywords": [
6
6
  "claude",