ccc-notifier 0.5.0 → 0.6.1

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.
@@ -3,29 +3,29 @@ import {
3
3
  notifyOS,
4
4
  notifySlack
5
5
  } from "./chunk-NV5UOHJA.js";
6
- import {
7
- writeDashboardHtml
8
- } from "./chunk-QWBQN4XY.js";
9
- import "./chunk-DGXUSPS4.js";
10
6
  import {
11
7
  collectSubagentUsage
12
- } from "./chunk-O2AR4PXU.js";
8
+ } from "./chunk-OYD6H3ZZ.js";
13
9
  import {
14
10
  aggregateCodexTurn,
15
11
  aggregateNewTurn,
12
+ getUsdJpy
13
+ } from "./chunk-D4D76RGQ.js";
14
+ import {
16
15
  computeCost,
17
- getUsdJpy,
18
16
  loadPriceTable
19
- } from "./chunk-LVMKY6JB.js";
17
+ } from "./chunk-6HTETN26.js";
18
+ import {
19
+ writeDashboardHtml
20
+ } from "./chunk-6UZXXO5J.js";
21
+ import "./chunk-DGXUSPS4.js";
20
22
  import "./chunk-J5QAYTFE.js";
21
23
  import {
22
24
  isFullDashboardDue,
23
25
  makeFullDashboardState,
26
+ waitForDataLock,
24
27
  writeFullDashboardStateAtomic
25
- } from "./chunk-ENGUOLTD.js";
26
- import {
27
- waitForDataLock
28
- } from "./chunk-O34L3NSI.js";
28
+ } from "./chunk-27SJELD2.js";
29
29
  import {
30
30
  appendTurn,
31
31
  closeCodexRootContext,
@@ -38,7 +38,7 @@ import {
38
38
  sanitizeCursor,
39
39
  saveCursor,
40
40
  todayTotalUSD
41
- } from "./chunk-5PH7PPD6.js";
41
+ } from "./chunk-OOAC5ULQ.js";
42
42
 
43
43
  // src/track.ts
44
44
  function isRecord(v) {
@@ -100,6 +100,7 @@ async function runTrack(stdinText, opts) {
100
100
  const table = await loadPriceTable(cacheDir, { offline: true });
101
101
  const fx = await getUsdJpy(cfg, cacheDir);
102
102
  let record;
103
+ let hasMainUsage = false;
103
104
  const commitLock = await waitForDataLock(1e3);
104
105
  if (commitLock === null) {
105
106
  logError("track:data-lock", new Error("data lock timeout; turn was not consumed"));
@@ -108,33 +109,44 @@ async function runTrack(stdinText, opts) {
108
109
  try {
109
110
  const cursor = sanitizeCursor(loadCursor(transcriptPath));
110
111
  let agg = isCodex ? await aggregateCodexTurn(transcriptPath, cursor) : await aggregateNewTurn(transcriptPath, cursor);
111
- if (agg === null) return;
112
- if (isCodex) {
112
+ if (isCodex && agg === null) return;
113
+ hasMainUsage = agg !== null;
114
+ if (isCodex && agg !== null) {
113
115
  agg = withCodexModel(agg, input.model);
114
116
  }
115
117
  let sa = null;
116
118
  if (!isCodex) {
117
119
  try {
118
- sa = await collectSubagentUsage(transcriptPath);
120
+ const excluded = new Set(cursor?.seenMessageKeys ?? []);
121
+ if (agg !== null && "messageKeys" in agg) {
122
+ for (const key of agg.messageKeys) excluded.add(key);
123
+ }
124
+ sa = await collectSubagentUsage(transcriptPath, { excludeMessageKeys: excluded });
119
125
  } catch (err) {
120
126
  logError("track:subagents", err);
121
127
  sa = null;
122
128
  }
123
129
  }
124
- const breakdown = computeCost(agg.main, agg.sidechain, table);
125
- const sessionId = agg.sessionId || (typeof input.session_id === "string" ? input.session_id : "") || "";
126
- const project = agg.cwd ?? (typeof input.cwd === "string" ? input.cwd : void 0) ?? "";
127
- const sidechainHasModels = Object.keys(agg.sidechain).length > 0;
130
+ if (agg === null && (sa === null || sa.apiCalls === 0)) {
131
+ for (const nc of sa?.newCursors ?? []) saveCursor(nc.path, nc.cursor);
132
+ return;
133
+ }
134
+ const main = agg?.main ?? {};
135
+ const sidechain = agg?.sidechain ?? {};
136
+ const breakdown = computeCost(main, sidechain, table);
137
+ const sessionId = agg?.sessionId || sa?.sessionId || (typeof input.session_id === "string" ? input.session_id : "") || "";
138
+ const project = agg?.cwd ?? sa?.cwd ?? (typeof input.cwd === "string" ? input.cwd : void 0) ?? "";
139
+ const sidechainHasModels = Object.keys(sidechain).length > 0;
128
140
  record = {
129
141
  schemaVersion: 1,
130
- ts: agg.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(),
142
+ ts: agg?.lastTs ?? sa?.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(),
131
143
  sessionId,
132
144
  project,
133
- gitBranch: agg.gitBranch,
134
- models: collectModels(agg.main, agg.sidechain),
135
- tokens: sumBuckets(agg.main),
136
- sidechainTokens: sidechainHasModels ? sumBuckets(agg.sidechain) : null,
137
- apiCalls: agg.apiCalls,
145
+ gitBranch: agg?.gitBranch ?? sa?.gitBranch ?? null,
146
+ models: hasMainUsage ? collectModels(main, sidechain) : collectModels(sa?.perModel ?? {}, {}),
147
+ tokens: sumBuckets(main),
148
+ sidechainTokens: sidechainHasModels ? sumBuckets(sidechain) : null,
149
+ apiCalls: agg?.apiCalls ?? 0,
138
150
  costUSD: breakdown.usd,
139
151
  costByModel: breakdown.byModel,
140
152
  // モデル別 USD(main+sidechain 合算、丸めない)
@@ -142,7 +154,7 @@ async function runTrack(stdinText, opts) {
142
154
  // 丸めない(表示時に丸める)
143
155
  fxRate: fx.rate,
144
156
  fxSource: fx.source,
145
- prompt: agg.prompt ?? ""
157
+ prompt: agg?.prompt ?? ""
146
158
  };
147
159
  if (isCodex) {
148
160
  record.source = "codex";
@@ -169,7 +181,7 @@ async function runTrack(stdinText, opts) {
169
181
  }
170
182
  }
171
183
  appendTurn(record);
172
- saveCursor(transcriptPath, agg.newCursor);
184
+ if (agg !== null) saveCursor(transcriptPath, agg.newCursor);
173
185
  if (sa !== null) {
174
186
  for (const nc of sa.newCursors) {
175
187
  saveCursor(nc.path, nc.cursor);
@@ -179,7 +191,7 @@ async function runTrack(stdinText, opts) {
179
191
  commitLock.release();
180
192
  }
181
193
  const tasks = [];
182
- if ((cfg.notify.os || cfg.notify.slack !== null) && record.costUSD >= cfg.minNotifyUSD && !isMuted()) {
194
+ if (hasMainUsage && (cfg.notify.os || cfg.notify.slack !== null) && record.costUSD >= cfg.minNotifyUSD && !isMuted()) {
183
195
  const todayUSD = cfg.includeDailyTotal ? todayTotalUSD() : void 0;
184
196
  tasks.push(notifyOS(record, cfg, todayUSD));
185
197
  tasks.push(notifySlack(record, cfg, todayUSD));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccc-notifier",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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",
@@ -1,92 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- paths
4
- } from "./chunk-5PH7PPD6.js";
5
-
6
- // src/dashboard-state.ts
7
- import {
8
- closeSync,
9
- existsSync,
10
- openSync,
11
- readFileSync,
12
- readSync,
13
- renameSync,
14
- rmSync,
15
- writeFileSync
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
- export {
88
- makeFullDashboardState,
89
- isFullDashboardDue,
90
- writeFullDashboardStateAtomic,
91
- invalidateCanonicalDashboards
92
- };
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- aggregateNewTurn
4
- } from "./chunk-LVMKY6JB.js";
5
- import {
6
- loadCursor,
7
- logError,
8
- sanitizeCursor
9
- } from "./chunk-5PH7PPD6.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
- };