ccc-notifier 0.2.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.
@@ -1,19 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- collectSubagentUsage
4
- } from "./chunk-TB5A7U7G.js";
3
+ codexHome,
4
+ detectCodex
5
+ } from "./chunk-HTYUYKFW.js";
6
+ import {
7
+ collectSubagentUsage,
8
+ splitIntoCodexTurnDrafts
9
+ } from "./chunk-CKVK3UCA.js";
5
10
  import {
6
11
  computeCost,
7
12
  extractBucket,
8
13
  getUsdJpy,
9
14
  loadPriceTable,
10
15
  promptCandidate
11
- } from "./chunk-OEG3AVU6.js";
16
+ } from "./chunk-LHKBGA5K.js";
17
+ import {
18
+ waitForDataLock
19
+ } from "./chunk-J6Y3RMMC.js";
12
20
  import {
13
21
  formatJPY,
14
22
  formatUSD,
15
23
  modelDisplayName
16
- } from "./chunk-TBFKGFZX.js";
24
+ } from "./chunk-J5QAYTFE.js";
17
25
  import {
18
26
  appendTurn,
19
27
  loadCursor,
@@ -22,7 +30,7 @@ import {
22
30
  readConfig,
23
31
  sanitizeCursor,
24
32
  saveCursor
25
- } from "./chunk-IIYMGLV4.js";
33
+ } from "./chunk-26CISNOE.js";
26
34
 
27
35
  // src/sweep.ts
28
36
  import { readFile } from "fs/promises";
@@ -253,79 +261,195 @@ function mergeUnknownModels(rec, extra) {
253
261
  for (const m of extra) if (!merged.includes(m)) merged.push(m);
254
262
  rec.unknownModels = merged;
255
263
  }
256
- async function processTranscript(mainPath, table, fx, daysCutoff, dryRun, summary) {
257
- const cursor = sanitizeCursor(loadCursor(mainPath));
258
- const { drafts, newCursor } = await splitIntoTurnDrafts(mainPath, cursor);
259
- const records = [];
260
- for (const draft of drafts) {
261
- const ts = draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString();
262
- if (daysCutoff !== null) {
263
- const tsMs = Date.parse(ts);
264
- if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
265
- }
266
- 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;
267
270
  }
268
- let sa = null;
269
271
  try {
270
- sa = await collectSubagentUsage(mainPath);
271
- } catch (err) {
272
- logError("sweep:subagents", err);
273
- sa = null;
274
- }
275
- const saHasUsage = sa !== null && sa.apiCalls > 0;
276
- if (saHasUsage) {
277
- const saBreakdown = computeCost(sa.perModel, {}, table);
278
- summary.subagentsUSD += saBreakdown.usd;
279
- const saBlock = {
280
- costUSD: saBreakdown.usd,
281
- costByModel: saBreakdown.byModel,
282
- tokens: sumBuckets(sa.perModel),
283
- apiCalls: sa.apiCalls,
284
- agentFiles: sa.agentFiles
285
- };
286
- if (records.length > 0) {
287
- const last = records[records.length - 1];
288
- last.subagents = saBlock;
289
- mergeUnknownModels(last, saBreakdown.unknownModels);
290
- } else {
291
- const rec = {
292
- schemaVersion: 1,
293
- ts: maxCursorTs(sa.newCursors) ?? (/* @__PURE__ */ new Date()).toISOString(),
294
- sessionId: "",
295
- project: "",
296
- gitBranch: null,
297
- models: collectModels(sa.perModel, {}),
298
- tokens: emptyBuckets(),
299
- sidechainTokens: null,
300
- apiCalls: 0,
301
- costUSD: 0,
302
- costByModel: {},
303
- costJPY: 0,
304
- fxRate: fx.rate,
305
- fxSource: fx.source,
306
- prompt: "",
307
- subagents: saBlock,
308
- 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
309
300
  };
310
- mergeUnknownModels(rec, saBreakdown.unknownModels);
311
- 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;
329
+ }
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
+ }
337
+ }
312
338
  }
313
- summary.agentFiles += sa.agentFiles;
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
+ }
345
+ }
346
+ } finally {
347
+ lock?.release();
348
+ }
349
+ }
350
+ var CODEX_MAX_DEPTH = 4;
351
+ async function listCodexRollouts(sessionsRoot) {
352
+ const found = [];
353
+ const walk = async (dir, depth) => {
354
+ const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
355
+ if (entries === null) return;
356
+ for (const e of entries) {
357
+ const full = join(dir, e.name);
358
+ if (e.isFile()) {
359
+ if (e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) found.push(full);
360
+ } else if (e.isDirectory() && depth < CODEX_MAX_DEPTH) {
361
+ await walk(full, depth + 1);
362
+ }
363
+ }
364
+ };
365
+ await walk(sessionsRoot, 1);
366
+ return found;
367
+ }
368
+ function codexDraftToRecord(draft, ts, table, fx) {
369
+ const main = draft.agg.main;
370
+ const breakdown = computeCost(main, {}, table);
371
+ const rec = {
372
+ schemaVersion: 1,
373
+ ts,
374
+ sessionId: draft.agg.sessionId,
375
+ project: draft.agg.cwd ?? "",
376
+ gitBranch: null,
377
+ // rollout に git 情報は無い
378
+ models: collectModels(main, {}),
379
+ tokens: sumBuckets(main),
380
+ sidechainTokens: null,
381
+ apiCalls: draft.agg.apiCalls,
382
+ costUSD: breakdown.usd,
383
+ costByModel: breakdown.byModel,
384
+ costJPY: breakdown.usd * fx.rate,
385
+ // 円換算は sweep 実行時レート(Claude 側と同じ)
386
+ fxRate: fx.rate,
387
+ fxSource: fx.source,
388
+ prompt: draft.agg.prompt ?? "",
389
+ ingest: "sweep",
390
+ source: "codex"
391
+ };
392
+ if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
393
+ return rec;
394
+ }
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;
314
401
  }
315
- for (const rec of records) {
316
- summary.newRecords += 1;
317
- summary.totalUSD += rec.costUSD;
318
- if (rec.costByModel) {
319
- for (const [m, c] of Object.entries(rec.costByModel)) {
320
- 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;
412
+ }
413
+ records.push(codexDraftToRecord(draft, ts, table, fx));
414
+ }
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
+ }
321
424
  }
322
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();
323
432
  }
324
- if (!dryRun) {
325
- for (const rec of records) appendTurn(rec);
326
- if (drafts.length > 0) saveCursor(mainPath, newCursor);
327
- if (saHasUsage) {
328
- for (const nc of sa.newCursors) saveCursor(nc.path, nc.cursor);
433
+ }
434
+ async function codexSessionsRoot() {
435
+ if (!detectCodex()) return null;
436
+ const sessionsRoot = join(codexHome(), "sessions");
437
+ const isDir = await fsp.stat(sessionsRoot).then((st) => st.isDirectory()).catch(() => false);
438
+ return isDir ? sessionsRoot : null;
439
+ }
440
+ async function sweepCodex(summary, table, fx, daysCutoff, flags, lockProvider) {
441
+ const sessionsRoot = await codexSessionsRoot();
442
+ if (sessionsRoot === null) return;
443
+ const rollouts = await listCodexRollouts(sessionsRoot);
444
+ for (const rolloutPath of rollouts) {
445
+ if (!flags.includeActive && await isRecentlyModified(rolloutPath)) {
446
+ summary.skippedActive += 1;
447
+ continue;
448
+ }
449
+ try {
450
+ await processCodexRollout(rolloutPath, table, fx, daysCutoff, flags.dryRun, summary, lockProvider);
451
+ } catch (err) {
452
+ logError("sweep:codex", err);
329
453
  }
330
454
  }
331
455
  }
@@ -388,7 +512,12 @@ function printSweepSummary(summary, fx) {
388
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`
389
513
  );
390
514
  }
391
- 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) {
392
521
  console.log("\u65B0\u898F\u306F\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F");
393
522
  } else {
394
523
  console.log(
@@ -399,6 +528,11 @@ function printSweepSummary(summary, fx) {
399
528
  ` \u3046\u3061\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ${formatUSD(summary.subagentsUSD)}(${formatJPY(summary.subagentsUSD * fx.rate)})`
400
529
  );
401
530
  }
531
+ if (summary.codexRecords > 0) {
532
+ console.log(
533
+ ` Codex: ${summary.codexRecords} \u30BF\u30FC\u30F3 ${formatUSD(summary.codexUSD)}(${formatJPY(summary.codexUSD * fx.rate)})`
534
+ );
535
+ }
402
536
  const top = Object.entries(summary.byModel).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]).slice(0, 5);
403
537
  if (top.length > 0) {
404
538
  console.log(" \u30E2\u30C7\u30EB\u5225(\u4E0A\u4F4D):");
@@ -407,23 +541,28 @@ function printSweepSummary(summary, fx) {
407
541
  }
408
542
  }
409
543
  }
410
- 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
+ }
411
547
  console.log(`\u5186\u63DB\u7B97\u30EC\u30FC\u30C8: 1USD = ${fx.rate}JPY(source=${fx.source})`);
412
548
  }
413
- async function runSweep(argv) {
549
+ async function runSweep(argv, deps = {}) {
414
550
  const flags = parseSweepFlags(argv);
415
551
  const root = projectsRoot(flags.projects);
416
552
  const projectDirs = await listProjectDirs(root);
417
553
  if (projectDirs === null) {
418
- console.log(`\u8D70\u67FB\u30EB\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${root}`);
419
- return 1;
554
+ if (await codexSessionsRoot() === null) {
555
+ console.log(`\u8D70\u67FB\u30EB\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${root}`);
556
+ return 1;
557
+ }
558
+ console.log(`Claude \u306E\u8D70\u67FB\u30EB\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${root}(Codex \u306E\u307F\u8D70\u67FB\u3057\u307E\u3059)`);
420
559
  }
421
560
  const cfg = readConfig();
422
561
  const cacheDir = paths().cacheDir;
423
562
  const table = await loadPriceTable(cacheDir, { offline: false });
424
563
  const fx = await getUsdJpy(cfg, cacheDir);
425
564
  const summary = {
426
- projects: projectDirs.length,
565
+ projects: projectDirs === null ? 0 : projectDirs.length,
427
566
  transcripts: 0,
428
567
  agentFiles: 0,
429
568
  newRecords: 0,
@@ -432,10 +571,14 @@ async function runSweep(argv) {
432
571
  subagentsUSD: 0,
433
572
  byModel: {},
434
573
  skippedActive: 0,
435
- dryRun: flags.dryRun
574
+ codexRecords: 0,
575
+ codexUSD: 0,
576
+ dryRun: flags.dryRun,
577
+ lockTimeouts: 0
436
578
  };
437
579
  const daysCutoff = flags.days !== null ? Date.now() - flags.days * DAY_MS : null;
438
- for (const projectDir of projectDirs) {
580
+ const lockProvider = deps.lockProvider ?? (() => waitForDataLock());
581
+ for (const projectDir of projectDirs ?? []) {
439
582
  const transcripts = await listTranscripts(projectDir);
440
583
  for (const mainPath of transcripts) {
441
584
  summary.transcripts += 1;
@@ -444,15 +587,16 @@ async function runSweep(argv) {
444
587
  continue;
445
588
  }
446
589
  try {
447
- await processTranscript(mainPath, table, fx, daysCutoff, flags.dryRun, summary);
590
+ await processTranscript(mainPath, table, fx, daysCutoff, flags.dryRun, summary, lockProvider);
448
591
  } catch (err) {
449
592
  logError("sweep:transcript", err);
450
593
  }
451
594
  }
452
595
  }
596
+ await sweepCodex(summary, table, fx, daysCutoff, flags, lockProvider);
453
597
  summary.totalJPY = summary.totalUSD * fx.rate;
454
598
  printSweepSummary(summary, fx);
455
- return 0;
599
+ return summary.lockTimeouts > 0 ? 1 : 0;
456
600
  }
457
601
  export {
458
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,11 +1,14 @@
1
1
  {
2
2
  "name": "ccc-notifier",
3
- "version": "0.2.0",
4
- "description": "Claude Code Cost notifier: per-prompt cost notifications (USD/JPY) with local history and HTML dashboard",
3
+ "version": "0.4.0",
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",
7
7
  "claude-code",
8
8
  "anthropic",
9
+ "codex",
10
+ "codex-cli",
11
+ "openai",
9
12
  "cost",
10
13
  "usage",
11
14
  "notifier",