ccc-notifier 0.6.2 → 0.7.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-O4VAP6RB.js} +2 -2
  3. package/dist/chunk-3M5UQK76.js +92 -0
  4. package/dist/{sweep-65D57PXU.js → chunk-6BXD6HU5.js} +275 -113
  5. package/dist/chunk-6IVSZ3S4.js +22 -0
  6. package/dist/{chunk-NV5UOHJA.js → chunk-JLDI4D5E.js} +5 -5
  7. package/dist/{chunk-T6Z2ZAN4.js → chunk-ML5H43NH.js} +1 -1
  8. package/dist/{chunk-6UZXXO5J.js → chunk-O2E4VTDR.js} +36 -9
  9. package/dist/{chunk-D4D76RGQ.js → chunk-OCKZZVOG.js} +353 -20
  10. package/dist/{chunk-27SJELD2.js → chunk-PXMXEFM7.js} +22 -107
  11. package/dist/{chunk-6HTETN26.js → chunk-Q45AAPJY.js} +18 -1
  12. package/dist/chunk-V4IS4FXH.js +454 -0
  13. package/dist/{chunk-OOAC5ULQ.js → chunk-WONMZ466.js} +184 -4
  14. package/dist/{chunk-OXXDZZPJ.js → chunk-X7U63BZG.js} +4 -6
  15. package/dist/{chunk-J5QAYTFE.js → chunk-XG73C6CC.js} +10 -1
  16. package/dist/cli.js +286 -29
  17. package/dist/{dashboard-VEKQ4TI3.js → dashboard-64XHDUVT.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-5CCM6MQL.js +41 -0
  21. package/dist/scan-UB6ORRZK.js +87 -0
  22. package/dist/{setup-I3JNGWZG.js → setup-VLMW433R.js} +5 -6
  23. package/dist/{subagent-store-US4TJJQR.js → subagent-store-USG3WJEB.js} +1 -1
  24. package/dist/sweep-DVOV4XIZ.js +20 -0
  25. package/dist/track-P6A5WDJZ.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-WCP7446C.js +0 -257
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ attachSubagentGroups,
4
+ runSweep,
5
+ splitIntoTurnDrafts
6
+ } from "./chunk-6BXD6HU5.js";
7
+ import "./chunk-OCKZZVOG.js";
8
+ import "./chunk-Q45AAPJY.js";
9
+ import "./chunk-O2E4VTDR.js";
10
+ import "./chunk-3M5UQK76.js";
11
+ import "./chunk-PXMXEFM7.js";
12
+ import "./chunk-6IVSZ3S4.js";
13
+ import "./chunk-DGXUSPS4.js";
14
+ import "./chunk-XG73C6CC.js";
15
+ import "./chunk-WONMZ466.js";
16
+ export {
17
+ attachSubagentGroups,
18
+ runSweep,
19
+ splitIntoTurnDrafts
20
+ };
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ notifyIngestSummary,
4
+ runIngest
5
+ } from "./chunk-V4IS4FXH.js";
6
+ import {
7
+ notifyOS,
8
+ notifySlack
9
+ } from "./chunk-JLDI4D5E.js";
10
+ import {
11
+ collectSubagentUsage,
12
+ normalizeCodexOriginator
13
+ } from "./chunk-6BXD6HU5.js";
14
+ import {
15
+ aggregateCodexTurn,
16
+ aggregateNewTurn,
17
+ anyOf,
18
+ callFingerprints,
19
+ codexConsumedCursor,
20
+ codexResumePointAtTs,
21
+ determineClaudeSurface,
22
+ getUsdJpy,
23
+ messageKeyFilterOf,
24
+ setCountedCalls
25
+ } from "./chunk-OCKZZVOG.js";
26
+ import {
27
+ computeCost,
28
+ loadPriceTable
29
+ } from "./chunk-Q45AAPJY.js";
30
+ import {
31
+ writeDashboardHtml
32
+ } from "./chunk-O2E4VTDR.js";
33
+ import {
34
+ isFullDashboardDue,
35
+ makeFullDashboardState,
36
+ writeFullDashboardStateAtomic
37
+ } from "./chunk-3M5UQK76.js";
38
+ import {
39
+ waitForDataLock
40
+ } from "./chunk-PXMXEFM7.js";
41
+ import "./chunk-6IVSZ3S4.js";
42
+ import "./chunk-DGXUSPS4.js";
43
+ import "./chunk-XG73C6CC.js";
44
+ import {
45
+ appendTurn,
46
+ clearPendingAppend,
47
+ closeCodexRootContext,
48
+ floorKey,
49
+ hasPendingAppend,
50
+ isMuted,
51
+ loadCursor,
52
+ loadHistoryIndex,
53
+ logError,
54
+ markPendingAppend,
55
+ paths,
56
+ readConfig,
57
+ readTurns,
58
+ sanitizeCursor,
59
+ saveCursor,
60
+ todayTotalUSD
61
+ } from "./chunk-WONMZ466.js";
62
+
63
+ // src/track.ts
64
+ import { basename } from "path";
65
+ function isRecord(v) {
66
+ return typeof v === "object" && v !== null && !Array.isArray(v);
67
+ }
68
+ function emptyBuckets() {
69
+ return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
70
+ }
71
+ function sumBuckets(usage) {
72
+ const total = emptyBuckets();
73
+ for (const b of Object.values(usage)) {
74
+ total.input += b.input;
75
+ total.output += b.output;
76
+ total.cacheWrite5m += b.cacheWrite5m;
77
+ total.cacheWrite1h += b.cacheWrite1h;
78
+ total.cacheRead += b.cacheRead;
79
+ }
80
+ return total;
81
+ }
82
+ function collectModels(main, sidechain) {
83
+ const models = [];
84
+ for (const m of Object.keys(main)) {
85
+ if (!models.includes(m)) models.push(m);
86
+ }
87
+ for (const m of Object.keys(sidechain)) {
88
+ if (!models.includes(m)) models.push(m);
89
+ }
90
+ return models;
91
+ }
92
+ function withCodexModel(agg, payloadModel) {
93
+ const model = typeof payloadModel === "string" && payloadModel.length > 0 ? payloadModel : null;
94
+ if (model === null) return agg;
95
+ const buckets = Object.values(agg.main)[0] ?? emptyBuckets();
96
+ return { ...agg, main: { [model]: buckets } };
97
+ }
98
+ async function runTrack(stdinText, opts) {
99
+ try {
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(stdinText);
103
+ } catch {
104
+ return;
105
+ }
106
+ if (!isRecord(parsed)) return;
107
+ const input = parsed;
108
+ const isCodex = opts?.codex === true;
109
+ let activityProjectionKey = null;
110
+ if (isCodex) {
111
+ try {
112
+ activityProjectionKey = closeCodexRootContext(parsed);
113
+ } catch {
114
+ logError("track:codex-subagent-projection", new Error("activity projection was not attached"));
115
+ }
116
+ }
117
+ const transcriptPath = input.transcript_path;
118
+ if (typeof transcriptPath !== "string") return;
119
+ const cfg = readConfig();
120
+ const cacheDir = paths().cacheDir;
121
+ const table = await loadPriceTable(cacheDir, { offline: true });
122
+ const fx = await getUsdJpy(cfg, cacheDir);
123
+ let record;
124
+ let hasMainUsage = false;
125
+ const commitLock = await waitForDataLock(1e3);
126
+ if (commitLock === null) {
127
+ logError("track:data-lock", new Error("data lock timeout; turn was not consumed"));
128
+ return;
129
+ }
130
+ try {
131
+ const cursor = sanitizeCursor(loadCursor(transcriptPath));
132
+ const cursorMayBeStale = cursor === null || hasPendingAppend(transcriptPath);
133
+ let indexCache = null;
134
+ const history = () => indexCache ??= loadHistoryIndex();
135
+ const counted = () => history().countedCalls;
136
+ const countedFilter = () => cursorMayBeStale ? messageKeyFilterOf(counted()) : void 0;
137
+ const sessionKey = typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : basename(transcriptPath).replace(/\.jsonl$/, "");
138
+ const legacyFloorMs = (scope) => {
139
+ const iso = history().legacyFloors.get(floorKey(scope, sessionKey));
140
+ if (iso === void 0) return null;
141
+ const ms = Date.parse(iso);
142
+ return Number.isFinite(ms) ? ms + 1 : null;
143
+ };
144
+ let recoveredCursor = null;
145
+ let agg;
146
+ if (isCodex) {
147
+ let readFrom = cursor;
148
+ const scanOpts = cursorMayBeStale ? { excludeEvents: counted() } : {};
149
+ if (cursor === null) {
150
+ const floorIso = history().legacyFloors.get(floorKey("codex", sessionKey));
151
+ if (floorIso !== void 0) readFrom = await codexResumePointAtTs(transcriptPath, floorIso);
152
+ }
153
+ agg = await aggregateCodexTurn(transcriptPath, readFrom, scanOpts);
154
+ if (agg === null) {
155
+ if (cursorMayBeStale) {
156
+ const consumed = await codexConsumedCursor(transcriptPath, readFrom, scanOpts);
157
+ if (consumed !== null) saveCursor(transcriptPath, consumed);
158
+ clearPendingAppend(transcriptPath);
159
+ }
160
+ return;
161
+ }
162
+ } else if (cursorMayBeStale) {
163
+ const recovered = await aggregateNewTurn(transcriptPath, cursor, {
164
+ excludeMessageKeys: messageKeyFilterOf(counted()),
165
+ // 指紋を持たない旧レコードぶんのフォールバックは、先頭から読み直すときだけ効かせる。
166
+ minTimestampMs: cursor === null ? legacyFloorMs("claude") : null,
167
+ returnEmpty: true
168
+ });
169
+ if (recovered !== null && recovered.apiCalls === 0) {
170
+ recoveredCursor = recovered.newCursor;
171
+ agg = null;
172
+ } else {
173
+ agg = recovered;
174
+ }
175
+ } else {
176
+ agg = await aggregateNewTurn(transcriptPath, cursor);
177
+ }
178
+ hasMainUsage = agg !== null;
179
+ if (isCodex && agg !== null) {
180
+ agg = withCodexModel(agg, input.model);
181
+ }
182
+ let sa = null;
183
+ if (!isCodex) {
184
+ try {
185
+ const excluded = new Set(cursor?.seenMessageKeys ?? []);
186
+ if (agg !== null && "messageKeys" in agg) {
187
+ for (const key of agg.messageKeys) excluded.add(key);
188
+ }
189
+ const staleFilter = countedFilter();
190
+ sa = await collectSubagentUsage(transcriptPath, {
191
+ excludeMessageKeys: staleFilter === void 0 ? excluded : anyOf([excluded, staleFilter]),
192
+ // agent ファイル側のカーソルだけを失っている場合もあるので、そのファイルに限って
193
+ // history 由来の指紋と旧レコード向けの下限で弾く
194
+ // (すべてのカーソルが健全なら一度も呼ばれない = history を読まない)。
195
+ recovery: () => ({
196
+ excludeMessageKeys: messageKeyFilterOf(counted()),
197
+ minTimestampMs: legacyFloorMs("claude-sa")
198
+ })
199
+ });
200
+ } catch (err) {
201
+ logError("track:subagents", err);
202
+ sa = null;
203
+ }
204
+ }
205
+ if (agg === null && (sa === null || sa.apiCalls === 0)) {
206
+ if (recoveredCursor !== null) saveCursor(transcriptPath, recoveredCursor);
207
+ for (const nc of sa?.newCursors ?? []) saveCursor(nc.path, nc.cursor);
208
+ if (cursorMayBeStale) clearPendingAppend(transcriptPath);
209
+ return;
210
+ }
211
+ const main = agg?.main ?? {};
212
+ const sidechain = agg?.sidechain ?? {};
213
+ const breakdown = computeCost(main, sidechain, table);
214
+ const sessionId = agg?.sessionId || sa?.sessionId || (typeof input.session_id === "string" ? input.session_id : "") || "";
215
+ const project = agg?.cwd ?? sa?.cwd ?? (typeof input.cwd === "string" ? input.cwd : void 0) ?? "";
216
+ const sidechainHasModels = Object.keys(sidechain).length > 0;
217
+ record = {
218
+ schemaVersion: 1,
219
+ ts: agg?.lastTs ?? sa?.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(),
220
+ sessionId,
221
+ project,
222
+ gitBranch: agg?.gitBranch ?? sa?.gitBranch ?? null,
223
+ models: hasMainUsage ? collectModels(main, sidechain) : collectModels(sa?.perModel ?? {}, {}),
224
+ tokens: sumBuckets(main),
225
+ sidechainTokens: sidechainHasModels ? sumBuckets(sidechain) : null,
226
+ apiCalls: agg?.apiCalls ?? 0,
227
+ costUSD: breakdown.usd,
228
+ costByModel: breakdown.byModel,
229
+ // モデル別 USD(main+sidechain 合算、丸めない)
230
+ costJPY: breakdown.usd * fx.rate,
231
+ // 丸めない(表示時に丸める)
232
+ fxRate: fx.rate,
233
+ fxSource: fx.source,
234
+ prompt: agg?.prompt ?? ""
235
+ };
236
+ if (isCodex) {
237
+ record.source = "codex";
238
+ if (activityProjectionKey !== null) record.activityProjectionKey = activityProjectionKey;
239
+ const rawOriginator = agg?.originator;
240
+ record.surface = normalizeCodexOriginator(rawOriginator);
241
+ if (typeof rawOriginator === "string") record.originator = rawOriginator;
242
+ } else {
243
+ try {
244
+ record.surface = await determineClaudeSurface(transcriptPath);
245
+ } catch (err) {
246
+ logError("track:surface", err);
247
+ }
248
+ }
249
+ if (breakdown.unknownModels.length > 0) {
250
+ record.unknownModels = breakdown.unknownModels;
251
+ }
252
+ if (sa !== null && sa.apiCalls > 0) {
253
+ const saBreakdown = computeCost(sa.perModel, {}, table);
254
+ record.subagents = {
255
+ costUSD: saBreakdown.usd,
256
+ costByModel: saBreakdown.byModel,
257
+ tokens: sumBuckets(sa.perModel),
258
+ apiCalls: sa.apiCalls,
259
+ agentFiles: sa.agentFiles
260
+ };
261
+ if (saBreakdown.unknownModels.length > 0) {
262
+ const merged = record.unknownModels ? [...record.unknownModels] : [];
263
+ for (const m of saBreakdown.unknownModels) {
264
+ if (!merged.includes(m)) merged.push(m);
265
+ }
266
+ record.unknownModels = merged;
267
+ }
268
+ }
269
+ if (isCodex) {
270
+ setCountedCalls(record, agg?.codexEventKeys ?? []);
271
+ } else {
272
+ const countedKeys = [];
273
+ if (agg !== null && "messageKeys" in agg) {
274
+ countedKeys.push(...agg.messageKeys);
275
+ }
276
+ for (const group of sa?.groups ?? []) countedKeys.push(...group.messageKeys);
277
+ setCountedCalls(record, callFingerprints(countedKeys));
278
+ }
279
+ try {
280
+ markPendingAppend(transcriptPath, record.ingestKey ?? "");
281
+ } catch (err) {
282
+ logError("track:pending-marker", err);
283
+ return;
284
+ }
285
+ appendTurn(record);
286
+ const mainCursor = agg?.newCursor ?? recoveredCursor;
287
+ if (mainCursor !== null) saveCursor(transcriptPath, mainCursor);
288
+ if (sa !== null) {
289
+ for (const nc of sa.newCursors) {
290
+ saveCursor(nc.path, nc.cursor);
291
+ }
292
+ }
293
+ clearPendingAppend(transcriptPath);
294
+ } finally {
295
+ commitLock.release();
296
+ }
297
+ const tasks = [];
298
+ if (hasMainUsage && (cfg.notify.os || cfg.notify.slack !== null) && record.costUSD >= cfg.minNotifyUSD && !isMuted()) {
299
+ const todayUSD = cfg.includeDailyTotal ? todayTotalUSD() : void 0;
300
+ tasks.push(notifyOS(record, cfg, todayUSD));
301
+ tasks.push(notifySlack(record, cfg, todayUSD));
302
+ }
303
+ tasks.push(
304
+ (async () => {
305
+ try {
306
+ const result = await runIngest({ dryRun: false, offlinePricing: true });
307
+ if (result.records.length > 0) {
308
+ await notifyIngestSummary(result, cfg);
309
+ }
310
+ } catch (err) {
311
+ logError("track:ingest", err);
312
+ }
313
+ if (!cfg.dashboard.autoRegenerate) return;
314
+ const now = /* @__PURE__ */ new Date();
315
+ const dashboardLock = await waitForDataLock(1e3);
316
+ if (dashboardLock === null) {
317
+ logError("track:dashboard-lock", new Error("data lock timeout; dashboard skipped"));
318
+ return;
319
+ }
320
+ try {
321
+ let allTurns;
322
+ try {
323
+ allTurns = readTurns();
324
+ } catch (err) {
325
+ logError("track:dashboard-read", err);
326
+ return;
327
+ }
328
+ try {
329
+ writeDashboardHtml({
330
+ days: cfg.dashboard.days,
331
+ outPath: paths().recentDashboardFile,
332
+ autoReloadSec: cfg.dashboard.autoReloadSec,
333
+ allTurns,
334
+ variant: "recent"
335
+ });
336
+ } catch (err) {
337
+ logError("track:dashboard-recent", err);
338
+ }
339
+ if (isFullDashboardDue(now)) {
340
+ try {
341
+ writeDashboardHtml({
342
+ days: null,
343
+ outPath: paths().fullDashboardFile,
344
+ autoReloadSec: cfg.dashboard.autoReloadSec,
345
+ allTurns,
346
+ variant: "full",
347
+ generatedAt: now.toISOString()
348
+ });
349
+ writeFullDashboardStateAtomic(makeFullDashboardState(now));
350
+ } catch (err) {
351
+ logError("track:dashboard-full", err);
352
+ }
353
+ }
354
+ } finally {
355
+ dashboardLock.release();
356
+ }
357
+ })()
358
+ );
359
+ if (tasks.length > 0) {
360
+ await Promise.allSettled(tasks);
361
+ }
362
+ } catch (err) {
363
+ logError("track", err);
364
+ }
365
+ }
366
+ export {
367
+ runTrack
368
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccc-notifier",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
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,55 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/codex/sessions.ts
4
- import { promises as fsp } from "fs";
5
- import { join, resolve } from "path";
6
- var CODEX_MAX_DEPTH = 4;
7
- async function listCodexRollouts(sessionsRoot) {
8
- const rollouts = [];
9
- let unreadableDirs = 0;
10
- const root = resolve(sessionsRoot);
11
- const rootStat = await fsp.lstat(root).catch(() => null);
12
- if (rootStat === null || !rootStat.isDirectory()) {
13
- return { rollouts, unreadableDirs: 1 };
14
- }
15
- const walk = async (dir, depth) => {
16
- const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
17
- if (entries === null) {
18
- unreadableDirs += 1;
19
- return;
20
- }
21
- for (const entry of entries) {
22
- const full = resolve(join(dir, entry.name));
23
- if (entry.isFile()) {
24
- if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) rollouts.push(full);
25
- } else if (entry.isDirectory() && depth < CODEX_MAX_DEPTH) {
26
- await walk(full, depth + 1);
27
- }
28
- }
29
- };
30
- await walk(root, 1);
31
- return { rollouts, unreadableDirs };
32
- }
33
- async function findLatestCodexRollout(sessionsRoot) {
34
- const discovery = await listCodexRollouts(sessionsRoot);
35
- let latest = null;
36
- let latestMtime = -Infinity;
37
- let unreadableFiles = 0;
38
- for (const path of discovery.rollouts) {
39
- const stat = await fsp.lstat(path).catch(() => null);
40
- if (stat === null || !stat.isFile()) {
41
- unreadableFiles += 1;
42
- continue;
43
- }
44
- if (stat.mtimeMs > latestMtime || stat.mtimeMs === latestMtime && (latest === null || path < latest)) {
45
- latest = path;
46
- latestMtime = stat.mtimeMs;
47
- }
48
- }
49
- return { latest, unreadableDirs: discovery.unreadableDirs, unreadableFiles };
50
- }
51
-
52
- export {
53
- listCodexRollouts,
54
- findLatestCodexRollout
55
- };
@@ -1,21 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/codex/env.ts
4
- import { statSync } from "fs";
5
- import { homedir } from "os";
6
- import { join } from "path";
7
- function codexHome() {
8
- return process.env.CCCN_CODEX_HOME || join(homedir(), ".codex");
9
- }
10
- function detectCodex() {
11
- try {
12
- return statSync(codexHome()).isDirectory();
13
- } catch {
14
- return false;
15
- }
16
- }
17
-
18
- export {
19
- codexHome,
20
- detectCodex
21
- };
@@ -1,124 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- aggregateNewTurn
4
- } from "./chunk-D4D76RGQ.js";
5
- import {
6
- loadCursor,
7
- logError,
8
- sanitizeCursor
9
- } from "./chunk-OOAC5ULQ.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 sameCursor(a, b) {
31
- return a !== null && a.offset === b.offset && a.lastUuid === b.lastUuid && a.lastTs === b.lastTs && a.seenMessageKeys.length === b.seenMessageKeys.length && a.seenMessageKeys.every((key, i) => key === b.seenMessageKeys[i]);
32
- }
33
- function subagentsDirOf(mainTranscriptPath) {
34
- const base = mainTranscriptPath.endsWith(".jsonl") ? mainTranscriptPath.slice(0, -".jsonl".length) : mainTranscriptPath;
35
- return join(base, "subagents");
36
- }
37
- async function listAgentFiles(dir, entries, includeAllFiles) {
38
- const files = entries.filter((e) => e.isFile() && e.name.startsWith("agent-") && e.name.endsWith(".jsonl")).map((e) => join(dir, e.name));
39
- if (includeAllFiles || files.length <= MAX_AGENT_FILES) return files;
40
- const withMtime = [];
41
- for (const p of files) {
42
- let mtime = 0;
43
- try {
44
- mtime = (await fs.stat(p)).mtimeMs;
45
- } catch {
46
- mtime = 0;
47
- }
48
- withMtime.push({ path: p, mtime });
49
- }
50
- withMtime.sort((a, b) => b.mtime - a.mtime);
51
- return withMtime.slice(0, MAX_AGENT_FILES).map((x) => x.path);
52
- }
53
- async function collectSubagentUsage(mainTranscriptPath, opts = {}) {
54
- const dir = subagentsDirOf(mainTranscriptPath);
55
- let entries;
56
- try {
57
- entries = await fs.readdir(dir, { withFileTypes: true });
58
- } catch (err) {
59
- if (opts.strictRead && err.code !== "ENOENT") throw err;
60
- return null;
61
- }
62
- const files = await listAgentFiles(dir, entries, opts.includeAllFiles === true);
63
- const perModel = {};
64
- let apiCalls = 0;
65
- let agentFiles = 0;
66
- const newCursors = [];
67
- const groups = [];
68
- let sessionId = "";
69
- let cwd = null;
70
- let gitBranch = null;
71
- let lastTs = null;
72
- const excluded = new Set(opts.excludeMessageKeys ?? []);
73
- if (!opts.ignoreCursors) {
74
- for (const filePath of files) {
75
- const prior = sanitizeCursor(loadCursor(filePath));
76
- for (const key of prior?.seenMessageKeys ?? []) excluded.add(key);
77
- }
78
- }
79
- for (const filePath of files) {
80
- try {
81
- if (opts.strictRead) {
82
- const file = await fs.open(filePath, "r");
83
- await file.close();
84
- }
85
- const cursor = opts.ignoreCursors ? null : sanitizeCursor(loadCursor(filePath));
86
- const agg = await aggregateNewTurn(filePath, cursor, {
87
- excludeMessageKeys: excluded,
88
- minTimestampMs: opts.minTimestampMs,
89
- returnEmpty: true
90
- });
91
- if (agg === null) continue;
92
- if (!sameCursor(cursor, agg.newCursor)) {
93
- newCursors.push({ path: filePath, cursor: agg.newCursor });
94
- }
95
- for (const key of agg.messageKeys) excluded.add(key);
96
- if (agg.apiCalls === 0) continue;
97
- mergeUsage(perModel, agg.main);
98
- mergeUsage(perModel, agg.sidechain);
99
- apiCalls += agg.apiCalls;
100
- agentFiles += 1;
101
- const groupUsage = {};
102
- mergeUsage(groupUsage, agg.main);
103
- mergeUsage(groupUsage, agg.sidechain);
104
- groups.push({
105
- perModel: groupUsage,
106
- apiCalls: agg.apiCalls,
107
- firstTs: agg.firstTs,
108
- lastTs: agg.lastTs
109
- });
110
- if (agg.sessionId) sessionId = agg.sessionId;
111
- if (agg.cwd !== null) cwd = agg.cwd;
112
- if (agg.gitBranch !== null) gitBranch = agg.gitBranch;
113
- if (agg.lastTs !== null && (lastTs === null || agg.lastTs > lastTs)) lastTs = agg.lastTs;
114
- } catch (err) {
115
- if (opts.strictRead) throw err;
116
- logError("subagents:file", err);
117
- }
118
- }
119
- return { perModel, apiCalls, agentFiles, newCursors, groups, sessionId, cwd, gitBranch, lastTs };
120
- }
121
-
122
- export {
123
- collectSubagentUsage
124
- };