ccc-notifier 0.2.0 → 0.3.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,24 @@
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-DSV75EF7.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";
12
17
  import {
13
18
  formatJPY,
14
19
  formatUSD,
15
20
  modelDisplayName
16
- } from "./chunk-TBFKGFZX.js";
21
+ } from "./chunk-J5QAYTFE.js";
17
22
  import {
18
23
  appendTurn,
19
24
  loadCursor,
@@ -22,7 +27,7 @@ import {
22
27
  readConfig,
23
28
  sanitizeCursor,
24
29
  saveCursor
25
- } from "./chunk-IIYMGLV4.js";
30
+ } from "./chunk-ECADO26T.js";
26
31
 
27
32
  // src/sweep.ts
28
33
  import { readFile } from "fs/promises";
@@ -329,6 +334,102 @@ async function processTranscript(mainPath, table, fx, daysCutoff, dryRun, summar
329
334
  }
330
335
  }
331
336
  }
337
+ var CODEX_MAX_DEPTH = 4;
338
+ async function listCodexRollouts(sessionsRoot) {
339
+ const found = [];
340
+ const walk = async (dir, depth) => {
341
+ const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
342
+ if (entries === null) return;
343
+ for (const e of entries) {
344
+ const full = join(dir, e.name);
345
+ if (e.isFile()) {
346
+ if (e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) found.push(full);
347
+ } else if (e.isDirectory() && depth < CODEX_MAX_DEPTH) {
348
+ await walk(full, depth + 1);
349
+ }
350
+ }
351
+ };
352
+ await walk(sessionsRoot, 1);
353
+ return found;
354
+ }
355
+ function codexDraftToRecord(draft, ts, table, fx) {
356
+ const main = draft.agg.main;
357
+ const breakdown = computeCost(main, {}, table);
358
+ const rec = {
359
+ schemaVersion: 1,
360
+ ts,
361
+ sessionId: draft.agg.sessionId,
362
+ project: draft.agg.cwd ?? "",
363
+ gitBranch: null,
364
+ // rollout に git 情報は無い
365
+ models: collectModels(main, {}),
366
+ tokens: sumBuckets(main),
367
+ sidechainTokens: null,
368
+ apiCalls: draft.agg.apiCalls,
369
+ costUSD: breakdown.usd,
370
+ costByModel: breakdown.byModel,
371
+ costJPY: breakdown.usd * fx.rate,
372
+ // 円換算は sweep 実行時レート(Claude 側と同じ)
373
+ fxRate: fx.rate,
374
+ fxSource: fx.source,
375
+ prompt: draft.agg.prompt ?? "",
376
+ ingest: "sweep",
377
+ source: "codex"
378
+ };
379
+ if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
380
+ return rec;
381
+ }
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));
394
+ }
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;
403
+ }
404
+ }
405
+ }
406
+ if (!dryRun) {
407
+ for (const rec of records) appendTurn(rec);
408
+ saveCursor(rolloutPath, drafts[drafts.length - 1].agg.newCursor);
409
+ }
410
+ }
411
+ async function codexSessionsRoot() {
412
+ if (!detectCodex()) return null;
413
+ const sessionsRoot = join(codexHome(), "sessions");
414
+ const isDir = await fsp.stat(sessionsRoot).then((st) => st.isDirectory()).catch(() => false);
415
+ return isDir ? sessionsRoot : null;
416
+ }
417
+ async function sweepCodex(summary, table, fx, daysCutoff, flags) {
418
+ const sessionsRoot = await codexSessionsRoot();
419
+ if (sessionsRoot === null) return;
420
+ const rollouts = await listCodexRollouts(sessionsRoot);
421
+ for (const rolloutPath of rollouts) {
422
+ if (!flags.includeActive && await isRecentlyModified(rolloutPath)) {
423
+ summary.skippedActive += 1;
424
+ continue;
425
+ }
426
+ try {
427
+ await processCodexRollout(rolloutPath, table, fx, daysCutoff, flags.dryRun, summary);
428
+ } catch (err) {
429
+ logError("sweep:codex", err);
430
+ }
431
+ }
432
+ }
332
433
  function projectsRoot(override) {
333
434
  if (override) return override;
334
435
  return process.env.CCCN_CLAUDE_PROJECTS || join(homedir(), ".claude", "projects");
@@ -399,6 +500,11 @@ function printSweepSummary(summary, fx) {
399
500
  ` \u3046\u3061\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ${formatUSD(summary.subagentsUSD)}(${formatJPY(summary.subagentsUSD * fx.rate)})`
400
501
  );
401
502
  }
503
+ if (summary.codexRecords > 0) {
504
+ console.log(
505
+ ` Codex: ${summary.codexRecords} \u30BF\u30FC\u30F3 ${formatUSD(summary.codexUSD)}(${formatJPY(summary.codexUSD * fx.rate)})`
506
+ );
507
+ }
402
508
  const top = Object.entries(summary.byModel).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]).slice(0, 5);
403
509
  if (top.length > 0) {
404
510
  console.log(" \u30E2\u30C7\u30EB\u5225(\u4E0A\u4F4D):");
@@ -415,15 +521,18 @@ async function runSweep(argv) {
415
521
  const root = projectsRoot(flags.projects);
416
522
  const projectDirs = await listProjectDirs(root);
417
523
  if (projectDirs === null) {
418
- console.log(`\u8D70\u67FB\u30EB\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${root}`);
419
- return 1;
524
+ if (await codexSessionsRoot() === null) {
525
+ console.log(`\u8D70\u67FB\u30EB\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${root}`);
526
+ return 1;
527
+ }
528
+ 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
529
  }
421
530
  const cfg = readConfig();
422
531
  const cacheDir = paths().cacheDir;
423
532
  const table = await loadPriceTable(cacheDir, { offline: false });
424
533
  const fx = await getUsdJpy(cfg, cacheDir);
425
534
  const summary = {
426
- projects: projectDirs.length,
535
+ projects: projectDirs === null ? 0 : projectDirs.length,
427
536
  transcripts: 0,
428
537
  agentFiles: 0,
429
538
  newRecords: 0,
@@ -432,10 +541,12 @@ async function runSweep(argv) {
432
541
  subagentsUSD: 0,
433
542
  byModel: {},
434
543
  skippedActive: 0,
544
+ codexRecords: 0,
545
+ codexUSD: 0,
435
546
  dryRun: flags.dryRun
436
547
  };
437
548
  const daysCutoff = flags.days !== null ? Date.now() - flags.days * DAY_MS : null;
438
- for (const projectDir of projectDirs) {
549
+ for (const projectDir of projectDirs ?? []) {
439
550
  const transcripts = await listTranscripts(projectDir);
440
551
  for (const mainPath of transcripts) {
441
552
  summary.transcripts += 1;
@@ -450,6 +561,7 @@ async function runSweep(argv) {
450
561
  }
451
562
  }
452
563
  }
564
+ await sweepCodex(summary, table, fx, daysCutoff, flags);
453
565
  summary.totalJPY = summary.totalUSD * fx.rate;
454
566
  printSweepSummary(summary, fx);
455
567
  return 0;
@@ -2,21 +2,22 @@
2
2
  import {
3
3
  notifyOS,
4
4
  notifySlack
5
- } from "./chunk-4JNZ7BHO.js";
5
+ } from "./chunk-NV5UOHJA.js";
6
6
  import {
7
7
  writeDashboardHtml
8
- } from "./chunk-64N5SGTT.js";
8
+ } from "./chunk-QX5KIRSU.js";
9
9
  import "./chunk-DGXUSPS4.js";
10
10
  import {
11
+ aggregateCodexTurn,
11
12
  collectSubagentUsage
12
- } from "./chunk-TB5A7U7G.js";
13
+ } from "./chunk-DSV75EF7.js";
13
14
  import {
14
15
  aggregateNewTurn,
15
16
  computeCost,
16
17
  getUsdJpy,
17
18
  loadPriceTable
18
- } from "./chunk-OEG3AVU6.js";
19
- import "./chunk-TBFKGFZX.js";
19
+ } from "./chunk-LHKBGA5K.js";
20
+ import "./chunk-J5QAYTFE.js";
20
21
  import {
21
22
  appendTurn,
22
23
  isMuted,
@@ -27,7 +28,7 @@ import {
27
28
  sanitizeCursor,
28
29
  saveCursor,
29
30
  todayTotalUSD
30
- } from "./chunk-IIYMGLV4.js";
31
+ } from "./chunk-ECADO26T.js";
31
32
 
32
33
  // src/track.ts
33
34
  import { join } from "path";
@@ -58,7 +59,13 @@ function collectModels(main, sidechain) {
58
59
  }
59
60
  return models;
60
61
  }
61
- async function runTrack(stdinText) {
62
+ function withCodexModel(agg, payloadModel) {
63
+ const model = typeof payloadModel === "string" && payloadModel.length > 0 ? payloadModel : null;
64
+ if (model === null) return agg;
65
+ const buckets = Object.values(agg.main)[0] ?? emptyBuckets();
66
+ return { ...agg, main: { [model]: buckets } };
67
+ }
68
+ async function runTrack(stdinText, opts) {
62
69
  try {
63
70
  let parsed;
64
71
  try {
@@ -72,14 +79,20 @@ async function runTrack(stdinText) {
72
79
  if (typeof transcriptPath !== "string") return;
73
80
  const cfg = readConfig();
74
81
  const cursor = sanitizeCursor(loadCursor(transcriptPath));
75
- const agg = await aggregateNewTurn(transcriptPath, cursor);
82
+ const isCodex = opts?.codex === true;
83
+ let agg = isCodex ? await aggregateCodexTurn(transcriptPath, cursor) : await aggregateNewTurn(transcriptPath, cursor);
76
84
  if (agg === null) return;
85
+ if (isCodex) {
86
+ agg = withCodexModel(agg, input.model);
87
+ }
77
88
  let sa = null;
78
- try {
79
- sa = await collectSubagentUsage(transcriptPath);
80
- } catch (err) {
81
- logError("track:subagents", err);
82
- sa = null;
89
+ if (!isCodex) {
90
+ try {
91
+ sa = await collectSubagentUsage(transcriptPath);
92
+ } catch (err) {
93
+ logError("track:subagents", err);
94
+ sa = null;
95
+ }
83
96
  }
84
97
  const cacheDir = paths().cacheDir;
85
98
  const table = await loadPriceTable(cacheDir, { offline: true });
@@ -107,6 +120,9 @@ async function runTrack(stdinText) {
107
120
  fxSource: fx.source,
108
121
  prompt: agg.prompt ?? ""
109
122
  };
123
+ if (isCodex) {
124
+ record.source = "codex";
125
+ }
110
126
  if (breakdown.unknownModels.length > 0) {
111
127
  record.unknownModels = breakdown.unknownModels;
112
128
  }
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.3.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",
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- aggregateNewTurn
4
- } from "./chunk-OEG3AVU6.js";
5
- import {
6
- loadCursor,
7
- logError,
8
- sanitizeCursor
9
- } from "./chunk-IIYMGLV4.js";
10
-
11
- // src/subagents.ts
12
- import { promises as fs } from "fs";
13
- import { join } from "path";
14
- var MAX_AGENT_FILES = 200;
15
- function emptyBuckets() {
16
- return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
17
- }
18
- function addToModel(target, model, b) {
19
- const cur = target[model] ?? emptyBuckets();
20
- cur.input += b.input;
21
- cur.output += b.output;
22
- cur.cacheWrite5m += b.cacheWrite5m;
23
- cur.cacheWrite1h += b.cacheWrite1h;
24
- cur.cacheRead += b.cacheRead;
25
- target[model] = cur;
26
- }
27
- function mergeUsage(target, src) {
28
- for (const [model, b] of Object.entries(src)) addToModel(target, model, b);
29
- }
30
- function subagentsDirOf(mainTranscriptPath) {
31
- const base = mainTranscriptPath.endsWith(".jsonl") ? mainTranscriptPath.slice(0, -".jsonl".length) : mainTranscriptPath;
32
- return join(base, "subagents");
33
- }
34
- async function listAgentFiles(dir, entries) {
35
- const files = entries.filter((e) => e.isFile() && e.name.startsWith("agent-") && e.name.endsWith(".jsonl")).map((e) => join(dir, e.name));
36
- if (files.length <= MAX_AGENT_FILES) return files;
37
- const withMtime = [];
38
- for (const p of files) {
39
- let mtime = 0;
40
- try {
41
- mtime = (await fs.stat(p)).mtimeMs;
42
- } catch {
43
- mtime = 0;
44
- }
45
- withMtime.push({ path: p, mtime });
46
- }
47
- withMtime.sort((a, b) => b.mtime - a.mtime);
48
- return withMtime.slice(0, MAX_AGENT_FILES).map((x) => x.path);
49
- }
50
- async function collectSubagentUsage(mainTranscriptPath) {
51
- const dir = subagentsDirOf(mainTranscriptPath);
52
- let entries;
53
- try {
54
- entries = await fs.readdir(dir, { withFileTypes: true });
55
- } catch {
56
- return null;
57
- }
58
- const files = await listAgentFiles(dir, entries);
59
- const perModel = {};
60
- let apiCalls = 0;
61
- let agentFiles = 0;
62
- const newCursors = [];
63
- for (const filePath of files) {
64
- try {
65
- const cursor = sanitizeCursor(loadCursor(filePath));
66
- const agg = await aggregateNewTurn(filePath, cursor);
67
- if (agg === null) continue;
68
- mergeUsage(perModel, agg.main);
69
- mergeUsage(perModel, agg.sidechain);
70
- apiCalls += agg.apiCalls;
71
- agentFiles += 1;
72
- newCursors.push({ path: filePath, cursor: agg.newCursor });
73
- } catch (err) {
74
- logError("subagents:file", err);
75
- }
76
- }
77
- return { perModel, apiCalls, agentFiles, newCursors };
78
- }
79
-
80
- export {
81
- collectSubagentUsage
82
- };