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.
- package/README.md +20 -12
- package/dist/{budget-5H2GQRXH.js → budget-EHWPEYXY.js} +2 -2
- package/dist/{chunk-IIYMGLV4.js → chunk-26CISNOE.js} +19 -4
- package/dist/{chunk-64N5SGTT.js → chunk-2VPBBIDW.js} +267 -57
- package/dist/{chunk-OXMHOVUL.js → chunk-5LHZOZZO.js} +228 -34
- package/dist/chunk-CKVK3UCA.js +357 -0
- package/dist/chunk-HTYUYKFW.js +21 -0
- package/dist/{chunk-TBFKGFZX.js → chunk-J5QAYTFE.js} +3 -0
- package/dist/chunk-J6Y3RMMC.js +172 -0
- package/dist/{chunk-OEG3AVU6.js → chunk-LHKBGA5K.js} +27 -1
- package/dist/{chunk-4JNZ7BHO.js → chunk-NV5UOHJA.js} +1 -1
- package/dist/{chunk-KUJZYZSG.js → chunk-TUZVISLD.js} +1 -1
- package/dist/chunk-ZHIZZ6V5.js +92 -0
- package/dist/cli.js +68 -17
- package/dist/{dashboard-LKK6NOBN.js → dashboard-GYKABTTN.js} +5 -3
- package/dist/history-6OUJLXJT.js +162 -0
- package/dist/{mute-2JW3NQXN.js → mute-GBDNN5PB.js} +2 -2
- package/dist/{setup-IXPUR4CM.js → setup-W3CSTHJC.js} +5 -4
- package/dist/{sweep-XKKODGMA.js → sweep-IBSV4LRD.js} +224 -80
- package/dist/track-QT2U3E2R.js +235 -0
- package/package.json +5 -2
- package/dist/chunk-TB5A7U7G.js +0 -82
- package/dist/history-MWFHO5VR.js +0 -114
- package/dist/track-76Q5CZM5.js +0 -168
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|