ccc-notifier 0.6.3 → 0.8.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.
Files changed (30) hide show
  1. package/README.md +5 -0
  2. package/dist/{budget-V7CCMJHF.js → budget-U37CCG4Y.js} +2 -2
  3. package/dist/chunk-3M5UQK76.js +92 -0
  4. package/dist/chunk-6IVSZ3S4.js +22 -0
  5. package/dist/{sweep-CONNNBK7.js → chunk-C7QOYVGT.js} +324 -120
  6. package/dist/{chunk-PYU7CAK2.js → chunk-DSTGXYSY.js} +44 -11
  7. package/dist/{chunk-D4D76RGQ.js → chunk-GAOGTALW.js} +361 -20
  8. package/dist/{chunk-T6Z2ZAN4.js → chunk-ML5H43NH.js} +1 -1
  9. package/dist/{chunk-27SJELD2.js → chunk-PXMXEFM7.js} +22 -107
  10. package/dist/{chunk-6HTETN26.js → chunk-Q45AAPJY.js} +18 -1
  11. package/dist/{chunk-OXXDZZPJ.js → chunk-RE72XBXB.js} +4 -6
  12. package/dist/{chunk-NV5UOHJA.js → chunk-TTD3BGOL.js} +5 -5
  13. package/dist/{chunk-OOAC5ULQ.js → chunk-WONMZ466.js} +184 -4
  14. package/dist/{chunk-J5QAYTFE.js → chunk-XOGYLFX3.js} +12 -1
  15. package/dist/chunk-YH45W7TA.js +479 -0
  16. package/dist/cli.js +287 -30
  17. package/dist/{dashboard-O7LJLYM3.js → dashboard-YG4H3GLA.js} +6 -4
  18. package/dist/{history-DL4SG3PG.js → history-W34EJOVV.js} +5 -3
  19. package/dist/{mute-UIR5P5N5.js → mute-23BWQXS3.js} +2 -2
  20. package/dist/reset-cursors-UVQXFN5Y.js +44 -0
  21. package/dist/scan-QB2N2O4Q.js +94 -0
  22. package/dist/{setup-I3JNGWZG.js → setup-2QDA3OWE.js} +5 -6
  23. package/dist/{subagent-store-US4TJJQR.js → subagent-store-USG3WJEB.js} +1 -1
  24. package/dist/sweep-OLI2LIXJ.js +22 -0
  25. package/dist/track-TIINM4ZR.js +368 -0
  26. package/package.json +1 -1
  27. package/dist/chunk-HLJAJS2W.js +0 -55
  28. package/dist/chunk-HTYUYKFW.js +0 -21
  29. package/dist/chunk-OYD6H3ZZ.js +0 -124
  30. package/dist/track-RBLLR25M.js +0 -257
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ notifyOS,
4
+ notifySlack
5
+ } from "./chunk-TTD3BGOL.js";
6
+ import {
7
+ attachSubagentGroups,
8
+ codexChildDraftToRecord,
9
+ collectSubagentUsage,
10
+ normalizeCodexOriginator,
11
+ splitIntoTurnDrafts
12
+ } from "./chunk-C7QOYVGT.js";
13
+ import {
14
+ anyOf,
15
+ callFingerprints,
16
+ claudeTranscriptRoots,
17
+ codexResumePointAtTs,
18
+ getUsdJpy,
19
+ listCodexRollouts,
20
+ messageKeyFilterOf,
21
+ scanCodexTurns,
22
+ setCountedCalls,
23
+ surfaceForClaudePath
24
+ } from "./chunk-GAOGTALW.js";
25
+ import {
26
+ codexHome,
27
+ computeCost,
28
+ loadPriceTable
29
+ } from "./chunk-Q45AAPJY.js";
30
+ import {
31
+ waitForDataLock
32
+ } from "./chunk-PXMXEFM7.js";
33
+ import {
34
+ formatIngestSummary
35
+ } from "./chunk-XOGYLFX3.js";
36
+ import {
37
+ appendTurn,
38
+ floorKey,
39
+ isMuted,
40
+ loadAllCursors,
41
+ loadHistoryIndex,
42
+ logError,
43
+ paths,
44
+ readConfig,
45
+ readConfigReadOnly,
46
+ sanitizeCursor,
47
+ saveAllCursors
48
+ } from "./chunk-WONMZ466.js";
49
+
50
+ // src/ingest.ts
51
+ import { promises as fsp, readFileSync, renameSync, writeFileSync } from "fs";
52
+ import { basename, join } from "path";
53
+ function emptyBuckets() {
54
+ return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
55
+ }
56
+ function sumBuckets(usage) {
57
+ const total = emptyBuckets();
58
+ for (const b of Object.values(usage)) {
59
+ total.input += b.input;
60
+ total.output += b.output;
61
+ total.cacheWrite5m += b.cacheWrite5m;
62
+ total.cacheWrite1h += b.cacheWrite1h;
63
+ total.cacheRead += b.cacheRead;
64
+ }
65
+ return total;
66
+ }
67
+ function collectModels(main, sidechain) {
68
+ const models = [];
69
+ for (const m of Object.keys(main)) if (!models.includes(m)) models.push(m);
70
+ for (const m of Object.keys(sidechain)) if (!models.includes(m)) models.push(m);
71
+ return models;
72
+ }
73
+ async function listProjectDirsBestEffort(root) {
74
+ try {
75
+ const entries = await fsp.readdir(root, { withFileTypes: true });
76
+ return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name));
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+ async function listTranscriptsBestEffort(projectDir) {
82
+ try {
83
+ const entries = await fsp.readdir(projectDir, { withFileTypes: true });
84
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join(projectDir, e.name));
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+ async function discoverClaudeFiles(roots) {
90
+ const files = [];
91
+ for (const root of roots) {
92
+ for (const dir of await listProjectDirsBestEffort(root.path)) {
93
+ files.push(...await listTranscriptsBestEffort(dir));
94
+ }
95
+ }
96
+ return files;
97
+ }
98
+ async function discoverCodexFiles() {
99
+ const sessionsRoot = join(codexHome(), "sessions");
100
+ try {
101
+ const stat = await fsp.lstat(sessionsRoot);
102
+ if (!stat.isDirectory()) return [];
103
+ } catch {
104
+ return [];
105
+ }
106
+ try {
107
+ const discovery = await listCodexRollouts(sessionsRoot);
108
+ return discovery.rollouts;
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+ function ingestMtimeCachePath() {
114
+ return join(paths().cacheDir, "ingest-mtimes.json");
115
+ }
116
+ function loadIngestMtimeCache() {
117
+ try {
118
+ const raw = readFileSync(ingestMtimeCachePath(), "utf8");
119
+ const parsed = JSON.parse(raw);
120
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
121
+ return parsed;
122
+ }
123
+ } catch {
124
+ }
125
+ return {};
126
+ }
127
+ function saveIngestMtimeCache(cache) {
128
+ try {
129
+ const file = ingestMtimeCachePath();
130
+ const tmp = `${file}.tmp`;
131
+ writeFileSync(tmp, JSON.stringify(cache), "utf8");
132
+ renameSync(tmp, file);
133
+ } catch {
134
+ }
135
+ }
136
+ async function fileMtimeMs(filePath) {
137
+ return (await fsp.stat(filePath)).mtimeMs;
138
+ }
139
+ async function claudeMtimeSignature(filePath) {
140
+ let newest = (await fsp.stat(filePath)).mtimeMs;
141
+ const dir = join(
142
+ filePath.endsWith(".jsonl") ? filePath.slice(0, -".jsonl".length) : filePath,
143
+ "subagents"
144
+ );
145
+ let entries;
146
+ try {
147
+ entries = await fsp.readdir(dir, { withFileTypes: true });
148
+ } catch (err) {
149
+ if (err?.code === "ENOENT") return newest;
150
+ throw err;
151
+ }
152
+ for (const entry of entries) {
153
+ if (!entry.isFile() || !entry.name.startsWith("agent-") || !entry.name.endsWith(".jsonl")) continue;
154
+ try {
155
+ const m = (await fsp.stat(join(dir, entry.name))).mtimeMs;
156
+ if (m > newest) newest = m;
157
+ } catch (err) {
158
+ if (err?.code !== "ENOENT") throw err;
159
+ }
160
+ }
161
+ return newest;
162
+ }
163
+ function recoveryCursor(floorTs) {
164
+ return { offset: 0, lastUuid: null, lastTs: floorTs, seenMessageKeys: [] };
165
+ }
166
+ function buildClaudeDraftRecord(draft, ts, table, fx, surface) {
167
+ const main = draft.mainPerModel;
168
+ const sidechain = draft.sidechainPerModel;
169
+ const breakdown = computeCost(main, sidechain, table);
170
+ const sidechainHasModels = Object.keys(sidechain).length > 0;
171
+ const rec = {
172
+ schemaVersion: 1,
173
+ ts,
174
+ sessionId: draft.sessionId,
175
+ project: draft.cwd ?? "",
176
+ gitBranch: draft.gitBranch,
177
+ models: collectModels(main, sidechain),
178
+ tokens: sumBuckets(main),
179
+ sidechainTokens: sidechainHasModels ? sumBuckets(sidechain) : null,
180
+ apiCalls: draft.apiCalls,
181
+ costUSD: breakdown.usd,
182
+ costByModel: breakdown.byModel,
183
+ costJPY: breakdown.usd * fx.rate,
184
+ fxRate: fx.rate,
185
+ fxSource: fx.source,
186
+ prompt: draft.prompt,
187
+ ingest: "scan",
188
+ surface
189
+ };
190
+ if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
191
+ setCountedCalls(rec, callFingerprints(draft.messageKeys));
192
+ return rec;
193
+ }
194
+ function sessionIdOfDrafts(drafts) {
195
+ for (let i = drafts.length - 1; i >= 0; i--) {
196
+ if (drafts[i].sessionId.length > 0) return drafts[i].sessionId;
197
+ }
198
+ return "";
199
+ }
200
+ function sessionIdFromTranscriptPath(filePath) {
201
+ const name = basename(filePath);
202
+ return name.endsWith(".jsonl") ? name.slice(0, -".jsonl".length) : "";
203
+ }
204
+ function buildCodexRecord(agg, table, fx) {
205
+ const main = agg.main;
206
+ const breakdown = computeCost(main, {}, table);
207
+ const surface = normalizeCodexOriginator(agg.originator);
208
+ const rec = {
209
+ schemaVersion: 1,
210
+ ts: agg.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(),
211
+ sessionId: agg.sessionId,
212
+ project: agg.cwd ?? "",
213
+ gitBranch: null,
214
+ models: collectModels(main, {}),
215
+ tokens: sumBuckets(main),
216
+ sidechainTokens: null,
217
+ apiCalls: agg.apiCalls,
218
+ costUSD: breakdown.usd,
219
+ costByModel: breakdown.byModel,
220
+ costJPY: breakdown.usd * fx.rate,
221
+ fxRate: fx.rate,
222
+ fxSource: fx.source,
223
+ prompt: agg.prompt ?? "",
224
+ ingest: "scan",
225
+ source: "codex",
226
+ surface
227
+ };
228
+ if (typeof agg.originator === "string") rec.originator = agg.originator;
229
+ if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
230
+ setCountedCalls(rec, agg.codexEventKeys ?? []);
231
+ return rec;
232
+ }
233
+ function emptyResult(dryRun, fx, lockAcquired) {
234
+ return {
235
+ dryRun,
236
+ lockAcquired,
237
+ records: [],
238
+ scannedFiles: 0,
239
+ skippedByMtime: 0,
240
+ skippedDuplicates: 0,
241
+ failures: 0,
242
+ totalUSD: 0,
243
+ totalJPY: 0,
244
+ totalTokens: 0,
245
+ cacheTokens: 0,
246
+ fxRate: fx.rate,
247
+ fxSource: fx.source,
248
+ bySurface: {}
249
+ };
250
+ }
251
+ function recordTokenTotals(rec) {
252
+ let total = 0;
253
+ let cache = 0;
254
+ for (const b of [rec.tokens, rec.sidechainTokens, rec.subagents?.tokens]) {
255
+ if (!b) continue;
256
+ const cached = b.cacheRead + b.cacheWrite5m + b.cacheWrite1h;
257
+ total += b.input + b.output + cached;
258
+ cache += cached;
259
+ }
260
+ return { total, cache };
261
+ }
262
+ function addRecord(result, rec) {
263
+ const usd = rec.costUSD + (rec.subagents?.costUSD ?? 0);
264
+ const jpy = rec.costJPY + (rec.subagents?.costUSD ?? 0) * rec.fxRate;
265
+ const tokens = recordTokenTotals(rec);
266
+ result.records.push(rec);
267
+ result.totalUSD += usd;
268
+ result.totalJPY += jpy;
269
+ result.totalTokens += tokens.total;
270
+ result.cacheTokens += tokens.cache;
271
+ const surface = rec.surface ?? "cli";
272
+ const cur = result.bySurface[surface] ?? { turns: 0, usd: 0 };
273
+ cur.turns += 1;
274
+ cur.usd += usd;
275
+ result.bySurface[surface] = cur;
276
+ }
277
+ async function runIngest(opts) {
278
+ const dryRun = opts.dryRun;
279
+ const cfg = opts.cfg ?? (dryRun ? readConfigReadOnly() : readConfig());
280
+ const cacheDir = paths().cacheDir;
281
+ const table = await loadPriceTable(cacheDir, { offline: opts.offlinePricing ?? true });
282
+ const fx = await getUsdJpy(cfg, cacheDir);
283
+ const claudeRoots = opts.claudeRoots ?? await claudeTranscriptRoots();
284
+ const includeCodex = opts.includeCodex ?? true;
285
+ const claudeFiles = await discoverClaudeFiles(claudeRoots);
286
+ const codexFiles = includeCodex ? await discoverCodexFiles() : [];
287
+ if (dryRun) {
288
+ return await processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, true);
289
+ }
290
+ const lock = await waitForDataLock(1e3);
291
+ if (lock === null) {
292
+ logError("ingest", new Error("data lock timeout; ingest skipped this cycle"));
293
+ return emptyResult(false, fx, false);
294
+ }
295
+ try {
296
+ return await processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, false);
297
+ } finally {
298
+ lock.release();
299
+ }
300
+ }
301
+ async function processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, dryRun) {
302
+ const mtimeCache = loadIngestMtimeCache();
303
+ const nextMtimeCache = { ...mtimeCache };
304
+ const result = emptyResult(dryRun, fx, true);
305
+ const cursorsDict = loadAllCursors();
306
+ let cursorsChanged = false;
307
+ let index = null;
308
+ const historyIndex = () => index ??= loadHistoryIndex();
309
+ const recordedFloor = (scope, sessionId) => {
310
+ if (sessionId.length === 0) return null;
311
+ return historyIndex().legacyFloors.get(floorKey(scope, sessionId)) ?? null;
312
+ };
313
+ const countedFilter = () => messageKeyFilterOf(historyIndex().countedCalls);
314
+ const commit = (filePath, records, newCursor, subagentCursors = []) => {
315
+ const fresh = [];
316
+ for (const rec of records) {
317
+ const key = rec.ingestKey;
318
+ if (key !== void 0 && historyIndex().ingestKeys.has(key)) {
319
+ result.skippedDuplicates += 1;
320
+ continue;
321
+ }
322
+ if (key !== void 0) historyIndex().ingestKeys.add(key);
323
+ for (const fp of rec.countedCalls ?? []) historyIndex().countedCalls.add(fp);
324
+ fresh.push(rec);
325
+ }
326
+ if (!dryRun) {
327
+ for (const rec of fresh) appendTurn(rec);
328
+ cursorsDict[filePath] = newCursor;
329
+ for (const nc of subagentCursors) cursorsDict[nc.path] = nc.cursor;
330
+ cursorsChanged = true;
331
+ }
332
+ for (const rec of fresh) addRecord(result, rec);
333
+ };
334
+ const carryCursor = (next, prev, floor) => ({
335
+ ...next,
336
+ lastUuid: next.lastUuid ?? prev?.lastUuid ?? null,
337
+ lastTs: next.lastTs ?? prev?.lastTs ?? floor
338
+ });
339
+ const processClaudeFile = async (filePath) => {
340
+ const cursor = sanitizeCursor(cursorsDict[filePath]);
341
+ const counted = countedFilter();
342
+ let split = await splitIntoTurnDrafts(filePath, cursor, { excludeMessageKeys: counted });
343
+ if (split.unreadable === true) throw new Error(`transcript \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F: ${filePath}`);
344
+ const sessionId = sessionIdOfDrafts(split.drafts) || sessionIdFromTranscriptPath(filePath);
345
+ let floor = null;
346
+ if (cursor === null) {
347
+ floor = recordedFloor("claude", sessionId);
348
+ if (floor !== null) {
349
+ split = await splitIntoTurnDrafts(filePath, recoveryCursor(floor), { excludeMessageKeys: counted });
350
+ }
351
+ }
352
+ const newCursor = carryCursor(split.newCursor, cursor, floor);
353
+ const surface = surfaceForClaudePath(filePath, claudeRoots);
354
+ const records = split.drafts.map(
355
+ (draft) => buildClaudeDraftRecord(draft, draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(), table, fx, surface)
356
+ );
357
+ const saFloor = recordedFloor("claude-sa", sessionId);
358
+ let sa = null;
359
+ try {
360
+ sa = await collectSubagentUsage(filePath, {
361
+ excludeMessageKeys: anyOf([new Set(split.messageKeys), counted]),
362
+ recovery: () => ({ minTimestampMs: saFloor === null ? null : Date.parse(saFloor) + 1 }),
363
+ readCursor: (path) => sanitizeCursor(cursorsDict[path])
364
+ });
365
+ } catch (err) {
366
+ logError("ingest:subagents", err);
367
+ sa = null;
368
+ }
369
+ if (sa !== null && sa.apiCalls > 0) {
370
+ attachSubagentGroups(records, sa, table, fx, surface, "scan");
371
+ }
372
+ commit(filePath, records, newCursor, sa?.newCursors ?? []);
373
+ };
374
+ const processCodexFile = async (filePath) => {
375
+ const cursor = sanitizeCursor(cursorsDict[filePath]);
376
+ const scanOpts = { excludeEvents: historyIndex().countedCalls };
377
+ const scan = await scanCodexTurns(filePath, cursor, scanOpts);
378
+ if (scan === null) throw new Error(`rollout \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F: ${filePath}`);
379
+ const drafts = scan.drafts;
380
+ if (drafts.length === 0) {
381
+ commit(filePath, [], scan.newCursor);
382
+ return;
383
+ }
384
+ const windowCursor = scan.newCursor;
385
+ if (drafts[0].isSubagentRollout && drafts[0].isForkedRollout) {
386
+ commit(filePath, [], windowCursor);
387
+ return;
388
+ }
389
+ const build = drafts[0].isSubagentRollout ? (draft) => codexChildDraftToRecord(draft, draft.endTs ?? (/* @__PURE__ */ new Date()).toISOString(), table, fx, "scan") : (draft) => buildCodexRecord(draft.agg, table, fx);
390
+ if (cursor !== null) {
391
+ commit(filePath, drafts.map(build), windowCursor);
392
+ return;
393
+ }
394
+ const floor = recordedFloor("codex", drafts[0].agg.sessionId);
395
+ const target = floor === null ? drafts : (await scanCodexTurns(filePath, await codexResumePointAtTs(filePath, floor), scanOpts))?.drafts ?? [];
396
+ commit(filePath, target.map(build), windowCursor);
397
+ };
398
+ const runOver = async (files, context, handle, signature = fileMtimeMs) => {
399
+ for (const filePath of files) {
400
+ let mtimeMs;
401
+ try {
402
+ mtimeMs = await signature(filePath);
403
+ } catch (err) {
404
+ if (err?.code !== "ENOENT") {
405
+ result.failures += 1;
406
+ logError(context, err);
407
+ }
408
+ continue;
409
+ }
410
+ const cached = mtimeCache[filePath];
411
+ if (cached !== void 0 && cached >= mtimeMs && sanitizeCursor(cursorsDict[filePath]) !== null) {
412
+ result.skippedByMtime += 1;
413
+ continue;
414
+ }
415
+ result.scannedFiles += 1;
416
+ try {
417
+ await handle(filePath);
418
+ nextMtimeCache[filePath] = mtimeMs;
419
+ } catch (err) {
420
+ result.failures += 1;
421
+ logError(context, err);
422
+ }
423
+ }
424
+ };
425
+ await runOver(claudeFiles, "ingest:claude", processClaudeFile, claudeMtimeSignature);
426
+ await runOver(codexFiles, "ingest:codex", processCodexFile);
427
+ if (!dryRun) {
428
+ if (cursorsChanged) saveAllCursors(cursorsDict);
429
+ saveIngestMtimeCache(nextMtimeCache);
430
+ }
431
+ return result;
432
+ }
433
+ async function notifyIngestSummary(result, cfg) {
434
+ if (result.dryRun) return;
435
+ if (result.records.length === 0) return;
436
+ if (!(cfg.notify.os || cfg.notify.slack)) return;
437
+ if (result.totalUSD < cfg.minNotifyUSD) return;
438
+ if (isMuted()) return;
439
+ const bySurface = {};
440
+ for (const [surface, v] of Object.entries(result.bySurface)) {
441
+ if (v) bySurface[surface] = v;
442
+ }
443
+ const summary = formatIngestSummary(
444
+ {
445
+ recordCount: result.records.length,
446
+ totalUSD: result.totalUSD,
447
+ totalJPY: result.totalJPY,
448
+ totalTokens: result.totalTokens,
449
+ cacheTokens: result.cacheTokens,
450
+ bySurface
451
+ },
452
+ cfg
453
+ );
454
+ const record = {
455
+ schemaVersion: 1,
456
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
457
+ sessionId: "ingest-scan",
458
+ project: "",
459
+ gitBranch: null,
460
+ models: [],
461
+ tokens: emptyBuckets(),
462
+ sidechainTokens: null,
463
+ apiCalls: 0,
464
+ costUSD: result.totalUSD,
465
+ costJPY: result.totalJPY,
466
+ fxRate: result.fxRate,
467
+ fxSource: result.fxSource,
468
+ prompt: ""
469
+ };
470
+ await Promise.allSettled([
471
+ notifyOS(record, cfg, void 0, summary),
472
+ notifySlack(record, cfg, void 0, summary)
473
+ ]);
474
+ }
475
+
476
+ export {
477
+ runIngest,
478
+ notifyIngestSummary
479
+ };