ccc-notifier 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,717 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ fmtMuteUntil
4
+ } from "./chunk-KUJZYZSG.js";
5
+ import {
6
+ matchesMarker
7
+ } from "./chunk-L5MLVTA2.js";
8
+ import {
9
+ notifyOS,
10
+ notifySlack,
11
+ selectNotifyBackend
12
+ } from "./chunk-4JNZ7BHO.js";
13
+ import {
14
+ isWSL
15
+ } from "./chunk-DGXUSPS4.js";
16
+ import {
17
+ aggregateNewTurn,
18
+ computeCost,
19
+ getUsdJpy,
20
+ loadPriceTable
21
+ } from "./chunk-OEG3AVU6.js";
22
+ import {
23
+ formatJPY,
24
+ formatTokens,
25
+ formatUSD
26
+ } from "./chunk-TBFKGFZX.js";
27
+ import {
28
+ isMuted,
29
+ paths,
30
+ readConfig,
31
+ readMuteState,
32
+ readTurns
33
+ } from "./chunk-IIYMGLV4.js";
34
+
35
+ // src/cli.ts
36
+ import { realpathSync } from "fs";
37
+ import { createRequire } from "module";
38
+ import { fileURLToPath, pathToFileURL } from "url";
39
+
40
+ // src/doctor.ts
41
+ import { existsSync, readdirSync, statSync } from "fs";
42
+ import { readFile } from "fs/promises";
43
+ import { homedir } from "os";
44
+ import { join } from "path";
45
+ function icon(status) {
46
+ if (status === "ok") return "\u2705";
47
+ if (status === "warn") return "\u26A0\uFE0F";
48
+ return "\u274C";
49
+ }
50
+ function log(status, message) {
51
+ console.log(`${icon(status)} ${message}`);
52
+ }
53
+ function errMessage(err) {
54
+ return err instanceof Error ? err.message : String(err);
55
+ }
56
+ function isRecord(v) {
57
+ return typeof v === "object" && v !== null && !Array.isArray(v);
58
+ }
59
+ function settingsPath() {
60
+ return process.env.CCCN_CLAUDE_SETTINGS || join(homedir(), ".claude", "settings.json");
61
+ }
62
+ function projectsDir() {
63
+ return process.env.CCCN_CLAUDE_PROJECTS || join(homedir(), ".claude", "projects");
64
+ }
65
+ function tokenizeCommand(command) {
66
+ const tokens = [];
67
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
68
+ let match;
69
+ while ((match = re.exec(command)) !== null) {
70
+ tokens.push(match[1] ?? match[2] ?? match[3] ?? "");
71
+ }
72
+ return tokens;
73
+ }
74
+ function extractScriptPath(command) {
75
+ const tokens = tokenizeCommand(command).filter((t) => t.length > 0);
76
+ const markerJs = tokens.find((t) => t.endsWith(".js") && matchesMarker(t));
77
+ if (markerJs) return markerJs;
78
+ const anyJs = tokens.find((t) => t.endsWith(".js"));
79
+ if (anyJs) return anyJs;
80
+ const marker = tokens.find((t) => matchesMarker(t));
81
+ return marker ?? null;
82
+ }
83
+ async function safeRun(name, fn) {
84
+ try {
85
+ return await fn();
86
+ } catch (err) {
87
+ log("fail", `[${name}] \u30C1\u30A7\u30C3\u30AF\u4E2D\u306B\u4E88\u671F\u3057\u306A\u3044\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
88
+ return false;
89
+ }
90
+ }
91
+ async function checkHookRegistration() {
92
+ const file = settingsPath();
93
+ if (!existsSync(file)) {
94
+ log("fail", `settings.json \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}(init \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044)`);
95
+ return false;
96
+ }
97
+ let parsed;
98
+ try {
99
+ const raw = await readFile(file, "utf8");
100
+ parsed = JSON.parse(raw);
101
+ } catch (err) {
102
+ log("fail", `settings.json \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093: ${file}(${errMessage(err)})`);
103
+ return false;
104
+ }
105
+ if (!isRecord(parsed)) {
106
+ log("fail", `settings.json \u306E\u5185\u5BB9\u304C\u4E0D\u6B63\u3067\u3059: ${file}`);
107
+ return false;
108
+ }
109
+ const hooks = parsed.hooks;
110
+ const stopEntries = isRecord(hooks) ? hooks.Stop : void 0;
111
+ const matchedCommands = [];
112
+ if (Array.isArray(stopEntries)) {
113
+ for (const entry of stopEntries) {
114
+ if (!isRecord(entry)) continue;
115
+ const innerHooks = entry.hooks;
116
+ if (!Array.isArray(innerHooks)) continue;
117
+ for (const h of innerHooks) {
118
+ if (isRecord(h) && typeof h.command === "string" && matchesMarker(h.command)) {
119
+ matchedCommands.push(h.command);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if (matchedCommands.length === 0) {
125
+ log("fail", "hooks.Stop \u306B ccc-notifier \u306E\u30A8\u30F3\u30C8\u30EA\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093(init \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044)");
126
+ return false;
127
+ }
128
+ log(
129
+ "ok",
130
+ `hooks.Stop \u306B ccc-notifier \u306E\u30A8\u30F3\u30C8\u30EA\u304C\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u3059(${matchedCommands.length}\u4EF6): ${matchedCommands.join(" / ")}`
131
+ );
132
+ let allScriptsExist = true;
133
+ for (const command of matchedCommands) {
134
+ const scriptPath = extractScriptPath(command);
135
+ if (scriptPath === null || !existsSync(scriptPath)) {
136
+ allScriptsExist = false;
137
+ }
138
+ }
139
+ if (!allScriptsExist) {
140
+ log(
141
+ "warn",
142
+ "\u767B\u9332\u6E08\u307F\u30B3\u30DE\u30F3\u30C9\u306E\u30B9\u30AF\u30EA\u30D7\u30C8\u30D1\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093(\u79FB\u52D5\u30FB\u524A\u9664\u3055\u308C\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002init \u306E\u518D\u5B9F\u884C\u3092\u691C\u8A0E\u3057\u3066\u304F\u3060\u3055\u3044)"
143
+ );
144
+ }
145
+ for (const command of matchedCommands) {
146
+ const first = tokenizeCommand(command).filter((t) => t.length > 0)[0];
147
+ if (first === void 0) continue;
148
+ const looksAbsolute = first.includes("/") || first.includes(":\\");
149
+ if (!looksAbsolute) continue;
150
+ if (!existsSync(first)) {
151
+ log(
152
+ "warn",
153
+ `hook \u306E Node \u5B9F\u884C\u30D1\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093(mise \u7B49\u3067\u306E\u66F4\u65B0\u304C\u539F\u56E0\u306E\u53EF\u80FD\u6027)\u3002init \u3092\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044: ${first}`
154
+ );
155
+ }
156
+ }
157
+ return true;
158
+ }
159
+ function readDirSafe(dir) {
160
+ try {
161
+ return readdirSync(dir, { withFileTypes: true });
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+ function findLatestTranscript(dir) {
167
+ let latestPath = null;
168
+ let latestMtime = -Infinity;
169
+ const walk = (current) => {
170
+ const entries = readDirSafe(current);
171
+ if (entries === null) return;
172
+ for (const entry of entries) {
173
+ const full = join(current, entry.name);
174
+ if (entry.isDirectory()) {
175
+ walk(full);
176
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
177
+ try {
178
+ const mtime = statSync(full).mtimeMs;
179
+ if (mtime > latestMtime) {
180
+ latestMtime = mtime;
181
+ latestPath = full;
182
+ }
183
+ } catch {
184
+ }
185
+ }
186
+ }
187
+ };
188
+ walk(dir);
189
+ return latestPath;
190
+ }
191
+ async function checkProjectsAndTranscript() {
192
+ const dir = projectsDir();
193
+ try {
194
+ readdirSync(dir);
195
+ } catch (err) {
196
+ log("fail", `Claude projects \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093: ${dir}(${errMessage(err)})`);
197
+ return { ok: false, latestTranscript: null };
198
+ }
199
+ log("ok", `Claude projects \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u8AAD\u307F\u8FBC\u3081\u307E\u3057\u305F: ${dir}`);
200
+ const latest = findLatestTranscript(dir);
201
+ if (latest === null) {
202
+ log("warn", "*.jsonl \u306E transcript \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093(\u307E\u3060\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u8A18\u9332\u3055\u308C\u3066\u3044\u306A\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059)");
203
+ return { ok: true, latestTranscript: null };
204
+ }
205
+ try {
206
+ const result = await aggregateNewTurn(latest, null);
207
+ if (result === null) {
208
+ log("warn", `\u6700\u65B0\u306E transcript \u304B\u3089\u65B0\u898F usage \u3092\u691C\u51FA\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: ${latest}`);
209
+ } else {
210
+ log("ok", `\u6700\u65B0\u306E transcript \u3092\u89E3\u6790\u3067\u304D\u307E\u3057\u305F(apiCalls=${result.apiCalls}): ${latest}`);
211
+ }
212
+ } catch (err) {
213
+ log("warn", `transcript \u306E\u89E3\u6790\u4E2D\u306B\u4F8B\u5916\u304C\u767A\u751F\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
214
+ }
215
+ return { ok: true, latestTranscript: latest };
216
+ }
217
+ function checkConfig() {
218
+ const cfg = readConfig();
219
+ const slackState = cfg.notify.slack ? "\u6709\u52B9" : "\u7121\u52B9";
220
+ log(
221
+ "ok",
222
+ `config.json \u3092\u8AAD\u307F\u8FBC\u307F\u307E\u3057\u305F(notify.os=${cfg.notify.os}, slack=${slackState}, costLabel=${cfg.costLabel}, minNotifyUSD=${cfg.minNotifyUSD}, fx.fallbackRate=${cfg.fx.fallbackRate})`
223
+ );
224
+ return true;
225
+ }
226
+ async function checkPricing() {
227
+ try {
228
+ const table = await loadPriceTable(paths().cacheDir, { offline: false });
229
+ const entries = Object.entries(table);
230
+ const litellmCount = entries.filter(([, price]) => price.source === "litellm").length;
231
+ if (litellmCount > 0) {
232
+ log("ok", `\u5358\u4FA1\u8868\u3092\u53D6\u5F97\u3057\u307E\u3057\u305F(${entries.length}\u4EF6\u3001litellm\u7531\u6765 ${litellmCount}\u4EF6)`);
233
+ } else {
234
+ log(
235
+ "warn",
236
+ `\u5358\u4FA1\u8868\u306F\u5185\u8535\u30C7\u30FC\u30BF\u306E\u307F\u3067\u3059(${entries.length}\u4EF6\u3001litellm\u7531\u6765 0\u4EF6\u3002\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u53D6\u5F97\u306B\u5931\u6557\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059)`
237
+ );
238
+ }
239
+ return true;
240
+ } catch (err) {
241
+ log("warn", `\u5358\u4FA1\u8868\u306E\u53D6\u5F97\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F(\u5185\u8535\u30C7\u30FC\u30BF\u3067\u52D5\u4F5C\u3057\u307E\u3059): ${errMessage(err)}`);
242
+ return true;
243
+ }
244
+ }
245
+ async function checkFx(cfg) {
246
+ try {
247
+ const result = await getUsdJpy(cfg, paths().cacheDir);
248
+ if (result.source === "fixed") {
249
+ log("warn", `\u70BA\u66FF\u30EC\u30FC\u30C8\u306F\u56FA\u5B9A\u5024\u306B\u30D5\u30A9\u30FC\u30EB\u30D0\u30C3\u30AF\u3057\u3066\u3044\u307E\u3059: 1USD = ${result.rate}JPY(source=fixed)`);
250
+ } else {
251
+ log("ok", `\u70BA\u66FF\u30EC\u30FC\u30C8\u3092\u53D6\u5F97\u3057\u307E\u3057\u305F: 1USD = ${result.rate}JPY(source=${result.source})`);
252
+ }
253
+ return true;
254
+ } catch (err) {
255
+ log("warn", `\u70BA\u66FF\u30EC\u30FC\u30C8\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
256
+ return true;
257
+ }
258
+ }
259
+ async function checkNotification(cfg) {
260
+ try {
261
+ if (isMuted()) {
262
+ const until = readMuteState()?.until;
263
+ log(
264
+ "warn",
265
+ until ? `\u901A\u77E5\u306F\u30DF\u30E5\u30FC\u30C8\u4E2D\u3067\u3059(${fmtMuteUntil(until)} \u307E\u3067)\u3002\u518D\u958B\u306F ccc-notifier unmute` : "\u901A\u77E5\u306F\u30DF\u30E5\u30FC\u30C8\u4E2D\u3067\u3059(\u7121\u671F\u9650)\u3002\u518D\u958B\u306F ccc-notifier unmute"
266
+ );
267
+ }
268
+ if (process.platform === "linux" && isWSL()) {
269
+ log("ok", "WSL2 \u74B0\u5883\u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002\u901A\u77E5\u306F Windows \u306E\u30C8\u30FC\u30B9\u30C8(powershell.exe)\u7D4C\u7531\u3067\u9001\u4FE1\u3057\u307E\u3059");
270
+ } else {
271
+ const backend = selectNotifyBackend();
272
+ log("ok", `\u901A\u77E5\u7D4C\u8DEF: ${backend.kind}(platform=${process.platform})`);
273
+ }
274
+ const dummy = {
275
+ schemaVersion: 1,
276
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
277
+ sessionId: "doctor-test",
278
+ project: process.cwd(),
279
+ gitBranch: null,
280
+ models: ["claude-fable-5"],
281
+ tokens: { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 },
282
+ sidechainTokens: null,
283
+ apiCalls: 0,
284
+ costUSD: 0,
285
+ costJPY: 0,
286
+ fxRate: cfg.fx.fallbackRate,
287
+ fxSource: "fixed",
288
+ prompt: "doctor \u306B\u3088\u308B\u30C6\u30B9\u30C8\u901A\u77E5\u3067\u3059"
289
+ };
290
+ const dryRun = process.env.CCCN_DRY_RUN === "1";
291
+ const dryHint = `(CCCN_DRY_RUN=1 \u306E\u305F\u3081 ${paths().lastNotifyFile} \u306E\u5185\u5BB9\u3067\u78BA\u8A8D\u3067\u304D\u307E\u3059)`;
292
+ if (cfg.notify.os) {
293
+ await notifyOS(dummy, cfg);
294
+ log("ok", `OS \u306E\u30C6\u30B9\u30C8\u901A\u77E5\u3092\u9001\u4FE1\u3057\u307E\u3057\u305F${dryRun ? dryHint : "(OS\u901A\u77E5\u304C\u8868\u793A\u3055\u308C\u305F\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044)"}`);
295
+ } else {
296
+ log("warn", "notify.os \u304C\u7121\u52B9\u306A\u305F\u3081\u3001OS \u306E\u30C6\u30B9\u30C8\u901A\u77E5\u306F\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F");
297
+ }
298
+ if (cfg.notify.slack) {
299
+ await notifySlack(dummy, cfg);
300
+ log(
301
+ "ok",
302
+ `Slack \u306E\u30C6\u30B9\u30C8\u901A\u77E5\u3092\u9001\u4FE1\u3057\u307E\u3057\u305F${dryRun ? dryHint : "(Slack \u30C1\u30E3\u30F3\u30CD\u30EB\u306B\u5C4A\u3044\u305F\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u5C4A\u304B\u306A\u3044\u5834\u5408\u306F error.log \u3092\u53C2\u7167)"}`
303
+ );
304
+ }
305
+ if (!cfg.notify.os && !cfg.notify.slack) {
306
+ log("warn", "OS\u30FBSlack \u3068\u3082\u7121\u52B9\u306A\u305F\u3081\u3001\u30C6\u30B9\u30C8\u901A\u77E5\u306F\u9001\u4FE1\u3057\u3066\u3044\u307E\u305B\u3093");
307
+ }
308
+ return true;
309
+ } catch (err) {
310
+ log("warn", `\u30C6\u30B9\u30C8\u901A\u77E5\u306E\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
311
+ return true;
312
+ }
313
+ }
314
+ async function checkRecentSessionTotal(latestTranscript) {
315
+ if (latestTranscript === null) {
316
+ log("warn", "\u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: transcript \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081\u8A08\u7B97\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F");
317
+ return true;
318
+ }
319
+ try {
320
+ const aggregate2 = await aggregateNewTurn(latestTranscript, null);
321
+ if (aggregate2 === null) {
322
+ log("warn", "\u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: \u65B0\u898F usage \u304C\u7121\u3044\u305F\u3081\u8A08\u7B97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F");
323
+ return true;
324
+ }
325
+ const table = await loadPriceTable(paths().cacheDir, { offline: true });
326
+ const breakdown = computeCost(aggregate2.main, aggregate2.sidechain, table);
327
+ log(
328
+ "ok",
329
+ `\u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: ${formatUSD(breakdown.usd)}(Claude Code \u306E /cost \u306E Total cost \u3068\u898B\u6BD4\u3079\u3066\u304F\u3060\u3055\u3044)`
330
+ );
331
+ return true;
332
+ } catch (err) {
333
+ log("warn", `\u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08\u306E\u8A08\u7B97\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
334
+ return true;
335
+ }
336
+ }
337
+ async function runDoctor() {
338
+ const results = [];
339
+ results.push(await safeRun("settings.json", () => checkHookRegistration()));
340
+ let latestTranscript = null;
341
+ results.push(
342
+ await safeRun("projects", async () => {
343
+ const r = await checkProjectsAndTranscript();
344
+ latestTranscript = r.latestTranscript;
345
+ return r.ok;
346
+ })
347
+ );
348
+ const cfg = readConfig();
349
+ results.push(await safeRun("config", () => Promise.resolve(checkConfig())));
350
+ results.push(await safeRun("pricing", () => checkPricing()));
351
+ results.push(await safeRun("fx", () => checkFx(cfg)));
352
+ results.push(await safeRun("notify", () => checkNotification(cfg)));
353
+ results.push(await safeRun("recent-session", () => checkRecentSessionTotal(latestTranscript)));
354
+ const hasFailure = results.some((ok) => ok === false);
355
+ return hasFailure ? 1 : 0;
356
+ }
357
+
358
+ // src/report.ts
359
+ var DEFAULT_DAYS = 30;
360
+ function parseArgs(argv) {
361
+ let days = DEFAULT_DAYS;
362
+ let json = false;
363
+ for (let i = 0; i < argv.length; i++) {
364
+ const arg = argv[i];
365
+ if (arg === "--json") {
366
+ json = true;
367
+ continue;
368
+ }
369
+ if (arg === "--days") {
370
+ const value = argv[i + 1];
371
+ days = parseDays(value);
372
+ i++;
373
+ continue;
374
+ }
375
+ if (arg.startsWith("--days=")) {
376
+ days = parseDays(arg.slice("--days=".length));
377
+ }
378
+ }
379
+ return { days, json };
380
+ }
381
+ function parseDays(value) {
382
+ if (value === void 0) return DEFAULT_DAYS;
383
+ const parsed = Number.parseInt(value, 10);
384
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAYS;
385
+ }
386
+ function localDateKey(iso) {
387
+ const d = new Date(iso);
388
+ if (Number.isNaN(d.getTime())) return null;
389
+ const y = d.getFullYear();
390
+ const m = String(d.getMonth() + 1).padStart(2, "0");
391
+ const day = String(d.getDate()).padStart(2, "0");
392
+ return `${y}-${m}-${day}`;
393
+ }
394
+ function effectiveInputTokens(rec) {
395
+ const main2 = rec.tokens;
396
+ const side = rec.sidechainTokens;
397
+ const mainIn = main2.input + main2.cacheRead + main2.cacheWrite5m + main2.cacheWrite1h;
398
+ const sideIn = side ? side.input + side.cacheRead + side.cacheWrite5m + side.cacheWrite1h : 0;
399
+ return mainIn + sideIn;
400
+ }
401
+ function outputTokensOf(rec) {
402
+ return rec.tokens.output + (rec.sidechainTokens ? rec.sidechainTokens.output : 0);
403
+ }
404
+ function turnCostByModel(rec) {
405
+ if (rec.costByModel && Object.keys(rec.costByModel).length > 0) return rec.costByModel;
406
+ return { [rec.models[0] ?? "unknown"]: rec.costUSD };
407
+ }
408
+ function turnTotalUSD(rec) {
409
+ return rec.costUSD + (rec.subagents?.costUSD ?? 0);
410
+ }
411
+ function turnTotalJPY(rec) {
412
+ const sa = rec.subagents?.costUSD ?? 0;
413
+ return rec.costJPY + sa * rec.fxRate;
414
+ }
415
+ function turnCostByModelWithSA(rec) {
416
+ const base = { ...turnCostByModel(rec) };
417
+ const sa = rec.subagents?.costByModel;
418
+ if (sa) {
419
+ for (const [model, usd] of Object.entries(sa)) {
420
+ base[model] = (base[model] ?? 0) + usd;
421
+ }
422
+ }
423
+ return base;
424
+ }
425
+ function aggregate(turns) {
426
+ const dailyMap = /* @__PURE__ */ new Map();
427
+ const modelMap = /* @__PURE__ */ new Map();
428
+ const total = {
429
+ turns: 0,
430
+ inputTokens: 0,
431
+ outputTokens: 0,
432
+ costUSD: 0,
433
+ costJPY: 0,
434
+ subagentsUSD: 0
435
+ };
436
+ for (const rec of turns) {
437
+ const dateKey = localDateKey(rec.ts) ?? "unknown";
438
+ const inTok = effectiveInputTokens(rec);
439
+ const outTok = outputTokensOf(rec);
440
+ const totalUsd = turnTotalUSD(rec);
441
+ const totalJpy = turnTotalJPY(rec);
442
+ const saUsd = rec.subagents?.costUSD ?? 0;
443
+ const d = dailyMap.get(dateKey) ?? {
444
+ date: dateKey,
445
+ turns: 0,
446
+ inputTokens: 0,
447
+ outputTokens: 0,
448
+ costUSD: 0,
449
+ costJPY: 0
450
+ };
451
+ d.turns += 1;
452
+ d.inputTokens += inTok;
453
+ d.outputTokens += outTok;
454
+ d.costUSD += totalUsd;
455
+ d.costJPY += totalJpy;
456
+ dailyMap.set(dateKey, d);
457
+ for (const [model, modelUsd] of Object.entries(turnCostByModelWithSA(rec))) {
458
+ const m = modelMap.get(model) ?? { turns: 0, costUSD: 0, costJPY: 0 };
459
+ m.turns += 1;
460
+ m.costUSD += modelUsd;
461
+ m.costJPY += modelUsd * rec.fxRate;
462
+ modelMap.set(model, m);
463
+ }
464
+ total.turns += 1;
465
+ total.inputTokens += inTok;
466
+ total.outputTokens += outTok;
467
+ total.costUSD += totalUsd;
468
+ total.costJPY += totalJpy;
469
+ total.subagentsUSD += saUsd;
470
+ }
471
+ const daily = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
472
+ const byModel = {};
473
+ for (const [model, agg] of modelMap) byModel[model] = agg;
474
+ return { daily, byModel, total };
475
+ }
476
+ function printTable(result, days) {
477
+ console.log(`\u76F4\u8FD1 ${days} \u65E5\u9593\u306E\u96C6\u8A08 (last ${days} days):`);
478
+ console.log("");
479
+ console.log("\u65E5\u5225 (Daily):");
480
+ console.log(
481
+ "\u65E5\u4ED8".padEnd(12) + "\u30BF\u30FC\u30F3".padStart(8) + "\u5165\u529B".padStart(10) + "\u51FA\u529B".padStart(10) + "USD".padStart(10) + "JPY".padStart(12)
482
+ );
483
+ for (const d of result.daily) {
484
+ console.log(
485
+ d.date.padEnd(12) + String(d.turns).padStart(8) + formatTokens(d.inputTokens).padStart(10) + formatTokens(d.outputTokens).padStart(10) + formatUSD(d.costUSD).padStart(10) + formatJPY(d.costJPY).padStart(12)
486
+ );
487
+ }
488
+ console.log(
489
+ "\u5408\u8A08".padEnd(12) + String(result.total.turns).padStart(8) + formatTokens(result.total.inputTokens).padStart(10) + formatTokens(result.total.outputTokens).padStart(10) + formatUSD(result.total.costUSD).padStart(10) + formatJPY(result.total.costJPY).padStart(12)
490
+ );
491
+ if (result.total.subagentsUSD > 0) {
492
+ console.log(`(\u3046\u3061\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8 ${formatUSD(result.total.subagentsUSD)})`);
493
+ }
494
+ console.log("");
495
+ console.log("\u30E2\u30C7\u30EB\u5225 (By model):");
496
+ console.log("\u30E2\u30C7\u30EB".padEnd(28) + "\u30BF\u30FC\u30F3".padStart(8) + "USD".padStart(10) + "JPY".padStart(12));
497
+ for (const [model, m] of Object.entries(result.byModel)) {
498
+ console.log(
499
+ model.padEnd(28) + String(m.turns).padStart(8) + formatUSD(m.costUSD).padStart(10) + formatJPY(m.costJPY).padStart(12)
500
+ );
501
+ }
502
+ console.log("\u203B \u8907\u6570\u30E2\u30C7\u30EB\u3092\u4F7F\u3063\u305F\u30BF\u30FC\u30F3\u306F\u5404\u30E2\u30C7\u30EB\u306E\u884C\u306B1\u305A\u3064\u8A08\u4E0A\u3057\u307E\u3059(\u30BF\u30FC\u30F3\u6570\u306E\u5408\u8A08\u306F\u7DCF\u30BF\u30FC\u30F3\u6570\u3092\u8D85\u3048\u308B\u3053\u3068\u304C\u3042\u308A\u307E\u3059)\u3002");
503
+ console.log("");
504
+ console.log(`\u5408\u8A08 (Total): ${formatUSD(result.total.costUSD)} (${formatJPY(result.total.costJPY)}) / ${result.total.turns} turns`);
505
+ }
506
+ async function runReport(argv) {
507
+ const { days, json } = parseArgs(argv);
508
+ const turns = readTurns(days);
509
+ if (turns.length === 0) {
510
+ console.log("\u5C65\u6B74\u304C\u3042\u308A\u307E\u305B\u3093(\u307E\u30601\u30BF\u30FC\u30F3\u3082\u8A18\u9332\u3055\u308C\u3066\u3044\u307E\u305B\u3093)");
511
+ return 0;
512
+ }
513
+ const result = aggregate(turns);
514
+ if (json) {
515
+ console.log(JSON.stringify({ days, daily: result.daily, byModel: result.byModel, total: result.total }, null, 2));
516
+ return 0;
517
+ }
518
+ printTable(result, days);
519
+ return 0;
520
+ }
521
+
522
+ // src/cli.ts
523
+ var COMMANDS = [
524
+ { cmd: "init", ja: "Stop hook \u3092\u5BFE\u8A71\u5F62\u5F0F\u3067\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7", en: "Interactively set up the Stop hook" },
525
+ {
526
+ cmd: "uninstall [--purge]",
527
+ ja: "Stop hook \u3092\u524A\u9664(--purge \u3067\u30C7\u30FC\u30BF\u3082\u524A\u9664)",
528
+ en: "Remove the Stop hook (--purge also deletes data)"
529
+ },
530
+ { cmd: "doctor", ja: "\u8A2D\u5B9A\u30FB\u901A\u77E5\u30FB\u5358\u4FA1\u30FB\u70BA\u66FF\u306E\u52D5\u4F5C\u8A3A\u65AD", en: "Diagnose hook, notifications, pricing and fx" },
531
+ {
532
+ cmd: "report [--days N] [--json]",
533
+ ja: "\u96C6\u8A08\u30EC\u30DD\u30FC\u30C8\u3092\u30BF\u30FC\u30DF\u30CA\u30EB\u306B\u8868\u793A",
534
+ en: "Print an aggregated cost report"
535
+ },
536
+ {
537
+ cmd: "dashboard [--days N] [--no-open] [--out <path>] [--refresh <sec>|--no-refresh]",
538
+ ja: "HTML\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3092\u751F\u6210\u3057\u3066\u30D6\u30E9\u30A6\u30B6\u3067\u958B\u304F",
539
+ en: "Generate and open the HTML dashboard"
540
+ },
541
+ {
542
+ cmd: "sweep [--dry-run] [--days N] [--include-active]",
543
+ ja: "\u904E\u53BB\u306E\u672A\u8A08\u4E0A\u5206\u3092\u4E00\u62EC\u3067\u5C65\u6B74\u306B\u53D6\u308A\u8FBC\u3080(\u9032\u884C\u4E2D\u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u81EA\u52D5\u30B9\u30AD\u30C3\u30D7)",
544
+ en: "Backfill uncounted history (active sessions are skipped)"
545
+ },
546
+ {
547
+ cmd: "history <clear|redact> [--days N] [--yes]",
548
+ ja: "\u5C65\u6B74\u3092\u524A\u9664(clear=\u30EC\u30B3\u30FC\u30C9\u3054\u3068 / redact=\u30D7\u30ED\u30F3\u30D7\u30C8\u306E\u307F\u3002--days \u3067\u53E4\u3044\u5206\u3060\u3051)",
549
+ en: "Delete history (clear=records / redact=prompts only; --days limits to older)"
550
+ },
551
+ {
552
+ cmd: "budget [<USD>]",
553
+ ja: "\u6708\u4E88\u7B97\u306E\u8868\u793A/\u8A2D\u5B9A(\u91D1\u984D\u7701\u7565\u3067\u8868\u793A\u30010 \u3067\u89E3\u9664)\u3002\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u306B\u5F53\u6708\u306E\u4F7F\u7528\u7387\u3092\u8868\u793A",
554
+ en: "Show/set the monthly budget (omit to show, 0 to clear)"
555
+ },
556
+ {
557
+ cmd: "mute [30m|2h|1d]",
558
+ ja: "\u901A\u77E5\u3092\u4E00\u6642\u505C\u6B62(\u671F\u9593\u7701\u7565\u3067\u7121\u671F\u9650\u3002\u8A18\u9332\u306F\u7D9A\u304F)",
559
+ en: "Pause notifications (indefinitely if no duration; tracking continues)"
560
+ },
561
+ { cmd: "unmute", ja: "\u901A\u77E5\u3092\u518D\u958B", en: "Resume notifications" },
562
+ {
563
+ cmd: "track",
564
+ ja: "Stop hook \u304B\u3089\u547C\u3070\u308C\u308B\u5185\u90E8\u30B3\u30DE\u30F3\u30C9(\u624B\u52D5\u5B9F\u884C\u306F\u4E0D\u8981)",
565
+ en: "Internal command invoked by the Stop hook (not for manual use)"
566
+ },
567
+ { cmd: "--version, -v", ja: "\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u8868\u793A", en: "Show the version" },
568
+ { cmd: "--help, -h", ja: "\u3053\u306E\u30D8\u30EB\u30D7\u3092\u8868\u793A", en: "Show this help" }
569
+ ];
570
+ var CMD_COLUMN_WIDTH = Math.max(...COMMANDS.map((c) => c.cmd.length)) + 2;
571
+ var HELP_TEXT = [
572
+ "ccc-notifier(Claude Code Cost notifier)\u2014 Claude Code \u306E\u30D7\u30ED\u30F3\u30D7\u30C8\u3054\u3068\u306E\u30B3\u30B9\u30C8\u901A\u77E5 / per-prompt cost notifier for Claude Code",
573
+ "",
574
+ "\u4F7F\u3044\u65B9 / Usage: ccc-notifier <command> [options]",
575
+ "",
576
+ ...COMMANDS.map((c) => ` ${c.cmd.padEnd(CMD_COLUMN_WIDTH)}${c.ja} / ${c.en}`)
577
+ ].join("\n");
578
+ function printHelp() {
579
+ console.log(HELP_TEXT);
580
+ }
581
+ function readVersion() {
582
+ try {
583
+ const require2 = createRequire(import.meta.url);
584
+ const pkg = require2("../package.json");
585
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
586
+ } catch {
587
+ return "unknown";
588
+ }
589
+ }
590
+ function readStdin(timeoutMs = 500) {
591
+ return new Promise((resolve) => {
592
+ if (process.stdin.isTTY) {
593
+ resolve("");
594
+ return;
595
+ }
596
+ let data = "";
597
+ let settled = false;
598
+ const finish = (result) => {
599
+ if (settled) return;
600
+ settled = true;
601
+ clearTimeout(timer);
602
+ process.stdin.removeListener("data", onData);
603
+ process.stdin.removeListener("end", onEnd);
604
+ process.stdin.removeListener("error", onError);
605
+ try {
606
+ process.stdin.pause();
607
+ } catch {
608
+ }
609
+ try {
610
+ process.stdin.unref();
611
+ } catch {
612
+ }
613
+ resolve(result);
614
+ };
615
+ const onData = (chunk) => {
616
+ data += typeof chunk === "string" ? chunk : chunk.toString("utf8");
617
+ };
618
+ const onEnd = () => finish(data);
619
+ const onError = () => finish(data);
620
+ const timer = setTimeout(() => finish(data), timeoutMs);
621
+ process.stdin.on("data", onData);
622
+ process.stdin.once("end", onEnd);
623
+ process.stdin.once("error", onError);
624
+ });
625
+ }
626
+ async function main(argv) {
627
+ const [cmd, ...rest] = argv;
628
+ try {
629
+ switch (cmd) {
630
+ case "track": {
631
+ const text = await readStdin();
632
+ try {
633
+ const trackMod = await import("./track-PBHTVZ3L.js");
634
+ await trackMod.runTrack(text);
635
+ } catch {
636
+ }
637
+ return 0;
638
+ }
639
+ case "init": {
640
+ const { runInit } = await import("./setup-RT62VO37.js");
641
+ return await runInit(rest);
642
+ }
643
+ case "uninstall": {
644
+ const { runUninstall } = await import("./setup-RT62VO37.js");
645
+ return await runUninstall(rest);
646
+ }
647
+ case "doctor":
648
+ return await runDoctor();
649
+ case "report":
650
+ return await runReport(rest);
651
+ case "dashboard": {
652
+ const { runDashboard } = await import("./dashboard-LKK6NOBN.js");
653
+ return await runDashboard(rest);
654
+ }
655
+ case "sweep": {
656
+ const { runSweep } = await import("./sweep-XKKODGMA.js");
657
+ return await runSweep(rest);
658
+ }
659
+ case "history": {
660
+ const { runHistory } = await import("./history-MWFHO5VR.js");
661
+ return await runHistory(rest);
662
+ }
663
+ case "budget": {
664
+ const { runBudget } = await import("./budget-5H2GQRXH.js");
665
+ return runBudget(rest);
666
+ }
667
+ case "mute": {
668
+ const { runMute } = await import("./mute-2JW3NQXN.js");
669
+ return runMute(rest);
670
+ }
671
+ case "unmute": {
672
+ const { runUnmute } = await import("./mute-2JW3NQXN.js");
673
+ return runUnmute();
674
+ }
675
+ case "--version":
676
+ case "-v":
677
+ console.log(readVersion());
678
+ return 0;
679
+ case "--help":
680
+ case "-h":
681
+ case void 0:
682
+ printHelp();
683
+ return 0;
684
+ default:
685
+ printHelp();
686
+ return 1;
687
+ }
688
+ } catch (err) {
689
+ console.error(
690
+ `ccc-notifier: unexpected error: ${err instanceof Error ? err.message : String(err)}`
691
+ );
692
+ return 1;
693
+ }
694
+ }
695
+ function isEntryPoint() {
696
+ const invoked = process.argv[1];
697
+ if (!invoked) return false;
698
+ try {
699
+ const invokedUrl = pathToFileURL(realpathSync(invoked)).href;
700
+ const selfUrl = pathToFileURL(realpathSync(fileURLToPath(import.meta.url))).href;
701
+ return invokedUrl === selfUrl;
702
+ } catch {
703
+ try {
704
+ return pathToFileURL(invoked).href === import.meta.url;
705
+ } catch {
706
+ return false;
707
+ }
708
+ }
709
+ }
710
+ if (isEntryPoint()) {
711
+ main(process.argv.slice(2)).then((code) => {
712
+ process.exitCode = code;
713
+ });
714
+ }
715
+ export {
716
+ main
717
+ };