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
@@ -1,4 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ makeFullDashboardState,
4
+ writeFullDashboardStateAtomic
5
+ } from "./chunk-3M5UQK76.js";
6
+ import {
7
+ waitForDataLock
8
+ } from "./chunk-PXMXEFM7.js";
9
+ import {
10
+ effectiveSurface,
11
+ surfaceLabel
12
+ } from "./chunk-6IVSZ3S4.js";
2
13
  import {
3
14
  isWSL
4
15
  } from "./chunk-DGXUSPS4.js";
@@ -7,17 +18,12 @@ import {
7
18
  formatTokens,
8
19
  formatUSD,
9
20
  modelDisplayName
10
- } from "./chunk-J5QAYTFE.js";
11
- import {
12
- makeFullDashboardState,
13
- waitForDataLock,
14
- writeFullDashboardStateAtomic
15
- } from "./chunk-27SJELD2.js";
21
+ } from "./chunk-XG73C6CC.js";
16
22
  import {
17
23
  paths,
18
24
  readConfig,
19
25
  readTurns
20
- } from "./chunk-OOAC5ULQ.js";
26
+ } from "./chunk-WONMZ466.js";
21
27
 
22
28
  // src/dashboard.ts
23
29
  import { spawn, spawnSync } from "child_process";
@@ -1070,6 +1076,27 @@ var APP_JS = `<script>
1070
1076
  function statCard(label, sub, totals) {
1071
1077
  return `<div class="stat"><div class="stat-label">${esc(label)}<span class="stat-sub">${esc(sub)}</span></div><div class="stat-value">${esc(formatUSD(totals.usd))}</div><div class="stat-meta">${esc(formatJPY(totals.jpy))} \xB7 ${totals.turns} \u30BF\u30FC\u30F3</div></div>`;
1072
1078
  }
1079
+ function computeSurfaceBreakdown(turns) {
1080
+ const map = /* @__PURE__ */ new Map();
1081
+ for (const rec of turns) {
1082
+ const surface = effectiveSurface(rec);
1083
+ const agg = map.get(surface) ?? { surface, turns: 0, usd: 0, jpy: 0 };
1084
+ agg.turns += 1;
1085
+ agg.usd += turnTotalUSD(rec);
1086
+ agg.jpy += turnTotalJPY(rec);
1087
+ map.set(surface, agg);
1088
+ }
1089
+ return [...map.values()].sort((a, b) => b.usd - a.usd);
1090
+ }
1091
+ function surfaceSection(turns) {
1092
+ const rows = computeSurfaceBreakdown(turns);
1093
+ if (rows.length === 0) return "";
1094
+ if (rows.length === 1 && rows[0].surface === "cli") return "";
1095
+ const trs = rows.map(
1096
+ (r) => `<tr><td>${esc(surfaceLabel(r.surface))}</td><td class="c-num">${r.turns}</td><td class="c-num">${esc(formatUSD(r.usd))}</td><td class="c-num">${esc(formatJPY(r.jpy))}</td></tr>`
1097
+ ).join("");
1098
+ return `<section class="card"><h2>\u30B5\u30FC\u30D5\u30A7\u30B9\u5225\u5185\u8A33 / By surface</h2><p class="note">CLI \u4EE5\u5916(\u30C7\u30B9\u30AF\u30C8\u30C3\u30D7\u30A2\u30D7\u30EA\u7B49)\u304B\u3089\u306E\u5229\u7528\u5206\u306E\u5185\u8A33(\u57CB\u3081\u8FBC\u307F\u5BFE\u8C61\u671F\u9593\u30FBSA \u8FBC\u307F\u7DCF\u984D)\u3002</p><div class="table-wrap"><table class="turns"><thead><tr><th>\u30B5\u30FC\u30D5\u30A7\u30B9</th><th class="c-num">\u30BF\u30FC\u30F3</th><th class="c-num">$</th><th class="c-num">\xA5</th></tr></thead><tbody>${trs}</tbody></table></div></section>`;
1099
+ }
1073
1100
  function renderLegend(slots) {
1074
1101
  if (slots.length < 2) return "";
1075
1102
  const items = slots.map((m) => `<span class="legend-item"><span class="swatch k${m.slot}"></span>${esc(m.name)}</span>`).join("");
@@ -1143,7 +1170,7 @@ function renderDashboard(turns, opts, fullHistory) {
1143
1170
  const refreshMeta = reloadSec > 0 ? `<meta http-equiv="refresh" content="${reloadSec}">` : "";
1144
1171
  const updateTrigger = anyCodex ? "Claude Code / Codex \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642" : "Claude Code \u306E\u5FDC\u7B54\u5B8C\u4E86\u6642";
1145
1172
  const autoUpdateFoot = reloadSec > 0 ? variant === "full" ? `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u30D5\u30A1\u30A4\u30EB\u3092\u518D\u8AAD\u8FBC(\u5168\u5C65\u6B74\u7248\u306E\u751F\u6210\u306F\u6B63\u5E38\u306A sweep \u5B8C\u4E86\u6642\u3001\u30ED\u30FC\u30AB\u30EB\u65E5\u3054\u3068\u3001\u307E\u305F\u306F\u624B\u52D5 dashboard --all)</div>` : `<div class="foot">\u7D04 ${reloadSec} \u79D2\u3054\u3068\u306B\u81EA\u52D5\u66F4\u65B0(\u6700\u65B0\u5316\u306F ${updateTrigger}\u3001\u307E\u305F\u306F\u6B63\u5E38\u306A sweep \u5B8C\u4E86\u6642)</div>` : "";
1146
- const variantNav = variant === "recent" ? `<div class="sub"><strong>${recentVariantLabel}</strong> \xB7 ${opts.peerAvailable ? `<a href="report-all.html">\u5168\u5C65\u6B74\u7248\u3078 / Full history</a>` : `<span aria-disabled="true">\u5168\u5C65\u6B74\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard --all \u3067\u751F\u6210\uFF09 / Full history not generated</span>`}</div>` : variant === "full" ? `<div class="sub"><strong>\u5168\u5C65\u6B74\u7248 / Full history</strong> \xB7 ${opts.peerAvailable ? `<a href="report.html">\u76F4\u8FD1\u7248\u3078 / Recent</a>` : `<span aria-disabled="true">\u76F4\u8FD1\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard \u3067\u751F\u6210\uFF09 / Recent not generated</span>`}</div><div class="sub muted">\u6700\u7D42\u751F\u6210 ${esc(generatedAt)}\u3002\u6B63\u5E38\u306A sweep \u5B8C\u4E86\u6642\u3001\u30ED\u30FC\u30AB\u30EB\u65E5\u306E\u6700\u521D\u306E\u6B63\u5E38\u306A\u30BF\u30FC\u30F3\u6642\u3001\u307E\u305F\u306F\u624B\u52D5\u306E dashboard --all \u3067\u66F4\u65B0\u3055\u308C\u307E\u3059\u3002</div>` : "";
1173
+ const variantNav = variant === "recent" ? `<div class="sub"><strong>${recentVariantLabel}</strong> \xB7 ${opts.peerAvailable ? `<a href="report-all.html">\u5168\u5C65\u6B74\u7248\u3078 / Full history</a>` : `<span aria-disabled="true">\u5168\u5C65\u6B74\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard --all \u3067\u751F\u6210\uFF09 / Full history not generated</span>`}</div>` : variant === "full" ? `<div class="sub"><strong>\u5168\u5C65\u6B74\u7248 / Full history</strong> \xB7 ${opts.peerAvailable ? `<a href="report.html">\u76F4\u8FD1\u7248\u3078 / Recent</a>` : `<span aria-disabled="true">\u76F4\u8FD1\u7248\u306F\u672A\u751F\u6210\u3067\u3059\uFF08dashboard \u3067\u751F\u6210\uFF09 / Recent not generated</span>`}</div><div class="sub muted">\u6700\u7D42\u751F\u6210 ${esc(generatedAt)}</div>` : "";
1147
1174
  const head = `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark">` + refreshMeta + `<title>ccc-notifier \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9</title>` + STYLE + `</head><body><div class="wrap">`;
1148
1175
  const foot = autoUpdateFoot + `<div class="foot">ccc-notifier v${esc(version)} \xB7 \u30C7\u30FC\u30BF\u306F\u30ED\u30FC\u30AB\u30EB\u306E\u307F / all data stays local</div></div><div id="cccn-tip" class="cccn-tip" hidden></div><script id="cccn-data" type="application/json">${dataJson}</script>` + APP_JS + `</body></html>`;
1149
1176
  const budgetSection = budgetCard(budgetUSD, budgetMonth, cfg.fx.fallbackRate, anyCodex, limited);
@@ -1161,7 +1188,7 @@ function renderDashboard(turns, opts, fullHistory) {
1161
1188
  const capNote = "\u4EF6\u6570\u304C\u591A\u3044\u5834\u5408\u306F\u65B0\u3057\u3044\u9806\u306B\u5148\u982D200\u4EF6\u306E\u307F\u63CF\u753B\u3057\u307E\u3059(\u691C\u7D22\u3084\u3001\u4E0A\u306E\u30B0\u30E9\u30D5\u3067\u671F\u9593\u3092\u9078\u3076\u3068\u7D5E\u308A\u8FBC\u3081\u307E\u3059)\u3002";
1162
1189
  const truncNote = anyTruncated ? `<p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3\u3092\u65B0\u3057\u3044\u9806\u306B\u8868\u793A\u3057\u307E\u3059\u3002${capNote}\u30D7\u30ED\u30F3\u30D7\u30C8\u306F1\u4EF6\u3042\u305F\u308A\u6700\u5927 ${PROMPT_MAX.toLocaleString("en-US")} \u5B57\u307E\u3067(\u8D85\u904E\u5206\u306F\u300C${esc(PROMPT_TRUNC_MARK)}\u300D)\u3002\u884C\u30AF\u30EA\u30C3\u30AF\u3067\u5168\u6587\u5C55\u958B\u3002</p>` : `<p class="note">\u9078\u629E\u4E2D\u306E\u671F\u9593\u306E\u30BF\u30FC\u30F3\u3092\u65B0\u3057\u3044\u9806\u306B\u8868\u793A\u3057\u307E\u3059\u3002${capNote}\u884C\u30AF\u30EA\u30C3\u30AF\u3067\u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3092\u5C55\u958B\u3057\u307E\u3059\u3002</p>`;
1163
1190
  const historySection = `<section class="card"><h2>\u30BF\u30FC\u30F3\u5C65\u6B74 / History</h2>` + truncNote + `<div class="toolbar"><input id="turn-search" class="search" type="search" placeholder="\u30D7\u30ED\u30F3\u30D7\u30C8\u30FB\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30FB\u30E2\u30C7\u30EB\u3067\u691C\u7D22 / search\u2026" autocomplete="off"><span class="count"><span id="turn-count">0</span></span></div><div class="table-wrap"><table class="turns"><thead><tr><th>\u6642\u523B</th><th>\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8</th><th>\u30E2\u30C7\u30EB</th><th class="c-num">in</th><th class="c-num">out</th><th class="c-num">$</th><th class="c-num">\xA5</th><th>\u30D7\u30ED\u30F3\u30D7\u30C8</th></tr></thead><tbody id="turn-body"></tbody></table></div></section>`;
1164
- return head + header + kpiSection + budgetSection + chartSection + breakdownSection + historySection + foot;
1191
+ return head + header + kpiSection + budgetSection + chartSection + breakdownSection + surfaceSection(turns) + historySection + foot;
1165
1192
  }
1166
1193
  function toWindowsPath(path) {
1167
1194
  try {
@@ -82,6 +82,205 @@ async function getUsdJpy(cfg, cacheDir) {
82
82
  return { rate: cfg.fx.fallbackRate, source: "fixed", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
83
83
  }
84
84
 
85
+ // src/claude-roots.ts
86
+ import { promises as fsp } from "fs";
87
+ import { homedir } from "os";
88
+ import * as nodePath from "path";
89
+ import { delimiter, join as join2 } from "path";
90
+ var DESKTOP_DISCOVERY_MAX_DEPTH = 8;
91
+ function defaultClaudeProjectsRoot(override) {
92
+ if (override) return override;
93
+ return process.env.CCCN_CLAUDE_PROJECTS || join2(homedir(), ".claude", "projects");
94
+ }
95
+ function defaultClaudeDesktopSessionsRoot() {
96
+ return join2(homedir(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
97
+ }
98
+ async function isDirectory(path) {
99
+ try {
100
+ return (await fsp.stat(path)).isDirectory();
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+ async function discoverDesktopProjectRoots(sessionsRoot) {
106
+ const found = [];
107
+ const walk = async (dir, depth) => {
108
+ if (depth > DESKTOP_DISCOVERY_MAX_DEPTH) return;
109
+ let entries;
110
+ try {
111
+ entries = await fsp.readdir(dir, { withFileTypes: true });
112
+ } catch {
113
+ return;
114
+ }
115
+ for (const entry of entries) {
116
+ if (!entry.isDirectory()) continue;
117
+ const full = join2(dir, entry.name);
118
+ if (entry.name === ".claude") {
119
+ const projects = join2(full, "projects");
120
+ if (await isDirectory(projects)) found.push(projects);
121
+ continue;
122
+ }
123
+ await walk(full, depth + 1);
124
+ }
125
+ };
126
+ await walk(sessionsRoot, 0);
127
+ return found;
128
+ }
129
+ async function claudeTranscriptRoots(opts = {}) {
130
+ const roots = [
131
+ { path: defaultClaudeProjectsRoot(opts.projectsOverride), surface: "cli" }
132
+ ];
133
+ const override = process.env.CCCN_CLAUDE_DESKTOP_ROOTS;
134
+ if (typeof override === "string" && override.length > 0) {
135
+ for (const raw of override.split(delimiter)) {
136
+ const p = raw.trim();
137
+ if (p.length > 0) roots.push({ path: p, surface: "desktop" });
138
+ }
139
+ return roots;
140
+ }
141
+ const explicitSessionsRoot = opts.desktopSessionsRoot;
142
+ const sessionsRoot = explicitSessionsRoot ?? (process.platform === "darwin" ? defaultClaudeDesktopSessionsRoot() : null);
143
+ if (sessionsRoot !== null && await isDirectory(sessionsRoot)) {
144
+ for (const p of await discoverDesktopProjectRoots(sessionsRoot)) {
145
+ roots.push({ path: p, surface: "desktop" });
146
+ }
147
+ }
148
+ return roots;
149
+ }
150
+ function surfaceForClaudePath(transcriptPath, roots) {
151
+ return rootForClaudePath(transcriptPath, roots)?.surface ?? "cli";
152
+ }
153
+ function rootForClaudePath(transcriptPath, roots) {
154
+ let best = null;
155
+ for (const root of roots) {
156
+ if (!isUnderRoot(transcriptPath, root.path)) continue;
157
+ if (best === null || root.path.length > best.path.length) best = root;
158
+ }
159
+ return best;
160
+ }
161
+ function isUnderRoot(target, root, flavor = nodePath) {
162
+ let rel;
163
+ try {
164
+ rel = flavor.relative(root, target);
165
+ } catch {
166
+ return false;
167
+ }
168
+ if (rel.length === 0) return true;
169
+ if (flavor.isAbsolute(rel)) return false;
170
+ return rel !== ".." && !rel.startsWith(`..${flavor.sep}`);
171
+ }
172
+ async function determineClaudeSurface(transcriptPath) {
173
+ if (process.env.CCCN_CLAUDE_DESKTOP_ROOTS === void 0) {
174
+ if (process.platform !== "darwin") return "cli";
175
+ const sessionsRoot = defaultClaudeDesktopSessionsRoot();
176
+ const withSep = sessionsRoot.endsWith("/") ? sessionsRoot : `${sessionsRoot}/`;
177
+ if (transcriptPath !== sessionsRoot && !transcriptPath.startsWith(withSep)) return "cli";
178
+ }
179
+ const roots = await claudeTranscriptRoots();
180
+ return surfaceForClaudePath(transcriptPath, roots);
181
+ }
182
+
183
+ // src/codex/sessions.ts
184
+ import { promises as fsp2 } from "fs";
185
+ import { join as join3, resolve } from "path";
186
+ var CODEX_MAX_DEPTH = 4;
187
+ async function listCodexRollouts(sessionsRoot) {
188
+ const rollouts = [];
189
+ let unreadableDirs = 0;
190
+ const root = resolve(sessionsRoot);
191
+ const rootStat = await fsp2.lstat(root).catch(() => null);
192
+ if (rootStat === null || !rootStat.isDirectory()) {
193
+ return { rollouts, unreadableDirs: 1 };
194
+ }
195
+ const walk = async (dir, depth) => {
196
+ const entries = await fsp2.readdir(dir, { withFileTypes: true }).catch(() => null);
197
+ if (entries === null) {
198
+ unreadableDirs += 1;
199
+ return;
200
+ }
201
+ for (const entry of entries) {
202
+ const full = resolve(join3(dir, entry.name));
203
+ if (entry.isFile()) {
204
+ if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) rollouts.push(full);
205
+ } else if (entry.isDirectory() && depth < CODEX_MAX_DEPTH) {
206
+ await walk(full, depth + 1);
207
+ }
208
+ }
209
+ };
210
+ await walk(root, 1);
211
+ return { rollouts, unreadableDirs };
212
+ }
213
+ async function findLatestCodexRollout(sessionsRoot) {
214
+ const discovery = await listCodexRollouts(sessionsRoot);
215
+ let latest = null;
216
+ let latestMtime = -Infinity;
217
+ let unreadableFiles = 0;
218
+ for (const path of discovery.rollouts) {
219
+ const stat = await fsp2.lstat(path).catch(() => null);
220
+ if (stat === null || !stat.isFile()) {
221
+ unreadableFiles += 1;
222
+ continue;
223
+ }
224
+ if (stat.mtimeMs > latestMtime || stat.mtimeMs === latestMtime && (latest === null || path < latest)) {
225
+ latest = path;
226
+ latestMtime = stat.mtimeMs;
227
+ }
228
+ }
229
+ return { latest, unreadableDirs: discovery.unreadableDirs, unreadableFiles };
230
+ }
231
+
232
+ // src/counted-calls.ts
233
+ import { createHash } from "crypto";
234
+ var FINGERPRINT_HEX = 16;
235
+ function sha256Hex(material) {
236
+ return createHash("sha256").update(material).digest("hex");
237
+ }
238
+ function callFingerprint(messageKey) {
239
+ return sha256Hex(`claude-call|${messageKey}`).slice(0, FINGERPRINT_HEX);
240
+ }
241
+ function callFingerprints(messageKeys) {
242
+ const out = [];
243
+ for (const key of messageKeys) out.push(callFingerprint(key));
244
+ return out;
245
+ }
246
+ function codexEventFingerprint(rolloutFile, sessionId, byteOffset, totals) {
247
+ const material = [
248
+ "codex-event",
249
+ rolloutFile,
250
+ sessionId,
251
+ byteOffset,
252
+ totals.input,
253
+ totals.cached,
254
+ totals.output
255
+ ].join("|");
256
+ return sha256Hex(material).slice(0, FINGERPRINT_HEX);
257
+ }
258
+ function ingestKeyOf(fingerprints) {
259
+ if (fingerprints.length === 0) return void 0;
260
+ return sha256Hex([...fingerprints].sort().join(","));
261
+ }
262
+ function setCountedCalls(rec, fingerprints) {
263
+ const unique = [...new Set(fingerprints)].sort();
264
+ if (unique.length === 0) {
265
+ delete rec.countedCalls;
266
+ delete rec.ingestKey;
267
+ return rec;
268
+ }
269
+ rec.countedCalls = unique;
270
+ rec.ingestKey = ingestKeyOf(unique);
271
+ return rec;
272
+ }
273
+ function addCountedCalls(rec, fingerprints) {
274
+ return setCountedCalls(rec, [...rec.countedCalls ?? [], ...fingerprints]);
275
+ }
276
+ function messageKeyFilterOf(counted) {
277
+ return { has: (messageKey) => counted.has(callFingerprint(messageKey)) };
278
+ }
279
+ function anyOf(filters) {
280
+ const active = filters.filter((f) => f !== void 0);
281
+ return { has: (messageKey) => active.some((f) => f.has(messageKey)) };
282
+ }
283
+
85
284
  // src/codex/transcript.ts
86
285
  import { readFile as readFile2 } from "fs/promises";
87
286
  import { basename } from "path";
@@ -108,11 +307,14 @@ function addTotals(target, d) {
108
307
  }
109
308
  function readTotals(v) {
110
309
  if (!isRecord(v)) return null;
111
- return {
112
- input: numOf(v.input_tokens),
113
- cached: numOf(v.cached_input_tokens),
114
- output: numOf(v.output_tokens)
115
- };
310
+ const input = numOf(v.input_tokens);
311
+ const cached = numOf(v.cached_input_tokens);
312
+ const output = numOf(v.output_tokens);
313
+ if (input === 0 && cached === 0 && output === 0) {
314
+ const total = numOf(v.total_tokens);
315
+ if (total > 0) return { input: total, cached: total, output: 0 };
316
+ }
317
+ return { input, cached, output };
116
318
  }
117
319
  function totalsToBuckets(acc) {
118
320
  return {
@@ -136,6 +338,27 @@ async function readAll(path) {
136
338
  return null;
137
339
  }
138
340
  }
341
+ function readSessionMetaSeed(buffer) {
342
+ const nl = buffer.indexOf(NEWLINE);
343
+ if (nl <= 0) return null;
344
+ let obj;
345
+ try {
346
+ obj = JSON.parse(buffer.toString("utf8", 0, nl));
347
+ } catch {
348
+ return null;
349
+ }
350
+ if (!isRecord(obj) || obj.type !== "session_meta") return null;
351
+ const payload = isRecord(obj.payload) ? obj.payload : null;
352
+ if (payload === null) return null;
353
+ const source = payload.source;
354
+ return {
355
+ // session_meta.payload のキーは id。将来 session_id へ戻る可能性に備えて両方見る。
356
+ sessionId: strOrNull(payload.id) ?? strOrNull(payload.session_id),
357
+ cwd: strOrNull(payload.cwd),
358
+ originator: strOrNull(payload.originator),
359
+ isSubagentRollout: isRecord(source) && Object.hasOwn(source, "subagent")
360
+ };
361
+ }
139
362
  function newSegmentBuf() {
140
363
  return {
141
364
  acc: zeroTotals(),
@@ -144,10 +367,11 @@ function newSegmentBuf() {
144
367
  turnCtxCwd: null,
145
368
  firstTs: null,
146
369
  endTs: null,
147
- hasLines: false
370
+ hasLines: false,
371
+ eventKeys: []
148
372
  };
149
373
  }
150
- async function scanWindow(rolloutPath, cursor) {
374
+ async function scanWindow(rolloutPath, cursor, opts = {}) {
151
375
  const buffer = await readAll(rolloutPath);
152
376
  if (buffer === null) return null;
153
377
  const fileSize = buffer.length;
@@ -165,16 +389,30 @@ async function scanWindow(rolloutPath, cursor) {
165
389
  let prev = initTotals !== void 0 ? { ...initTotals } : zeroTotals();
166
390
  const acc = zeroTotals();
167
391
  let apiCalls = 0;
168
- let lastModel = null;
392
+ let lastModel = cursor?.codexModel ?? null;
169
393
  let windowPrompt = null;
170
394
  let windowTurnCtxCwd = null;
171
395
  let sessionMetaCwd = null;
172
396
  let sessionMetaSid = null;
173
397
  let isSubagentRollout = false;
398
+ let originator = cursor?.codexOriginator ?? null;
399
+ if (startOffset > 0) {
400
+ const seed = readSessionMetaSeed(buffer);
401
+ if (seed !== null) {
402
+ sessionMetaSid = seed.sessionId;
403
+ sessionMetaCwd = seed.cwd;
404
+ if (seed.originator !== null) originator = seed.originator;
405
+ isSubagentRollout = seed.isSubagentRollout;
406
+ }
407
+ }
174
408
  let firstTs = null;
175
409
  let lastTs = null;
176
410
  const segments = [];
411
+ const windowEventKeys = [];
177
412
  let seg = newSegmentBuf();
413
+ const rolloutFile = basename(rolloutPath);
414
+ const filenameId = sessionIdFromFilename(rolloutPath);
415
+ const rolloutId = () => sessionMetaSid ?? filenameId;
178
416
  const snapshotSegment = (endOffset) => ({
179
417
  acc: seg.acc,
180
418
  apiCalls: seg.apiCalls,
@@ -185,9 +423,10 @@ async function scanWindow(rolloutPath, cursor) {
185
423
  endTs: seg.endTs,
186
424
  endOffset,
187
425
  prevAtEnd: { ...prev },
188
- lastTsAtEnd: lastTs
426
+ lastTsAtEnd: lastTs,
427
+ eventKeys: seg.eventKeys
189
428
  });
190
- const handleLine = (raw, endOffset) => {
429
+ const handleLine = (raw, lineOffset, endOffset) => {
191
430
  if (raw.trim().length === 0) return;
192
431
  let obj;
193
432
  try {
@@ -209,12 +448,14 @@ async function scanWindow(rolloutPath, cursor) {
209
448
  if (payload === null) return;
210
449
  const type = obj.type;
211
450
  if (type === "session_meta") {
212
- const sid = strOrNull(payload.session_id);
451
+ const sid = strOrNull(payload.id) ?? strOrNull(payload.session_id);
213
452
  if (sid !== null) sessionMetaSid = sid;
214
453
  const c = strOrNull(payload.cwd);
215
454
  if (c !== null) sessionMetaCwd = c;
216
455
  const source = payload.source;
217
456
  if (isRecord(source) && Object.hasOwn(source, "subagent")) isSubagentRollout = true;
457
+ const orig = strOrNull(payload.originator);
458
+ if (orig !== null) originator = orig;
218
459
  return;
219
460
  }
220
461
  if (type === "turn_context") {
@@ -242,6 +483,11 @@ async function scanWindow(rolloutPath, cursor) {
242
483
  if (info === null) return;
243
484
  const total = readTotals(info.total_token_usage);
244
485
  if (total === null) return;
486
+ const eventKey = codexEventFingerprint(rolloutFile, rolloutId(), lineOffset, total);
487
+ if (opts.excludeEvents?.has(eventKey) === true) {
488
+ prev = total;
489
+ return;
490
+ }
245
491
  let step = {
246
492
  input: total.input - prev.input,
247
493
  cached: total.cached - prev.cached,
@@ -252,6 +498,8 @@ async function scanWindow(rolloutPath, cursor) {
252
498
  }
253
499
  addTotals(acc, step);
254
500
  addTotals(seg.acc, step);
501
+ seg.eventKeys.push(eventKey);
502
+ windowEventKeys.push(eventKey);
255
503
  prev = total;
256
504
  if (!isZeroTotals(step)) {
257
505
  apiCalls++;
@@ -267,7 +515,7 @@ async function scanWindow(rolloutPath, cursor) {
267
515
  let lineStart = startOffset;
268
516
  for (let pos = startOffset; pos < fileSize; pos++) {
269
517
  if (buffer[pos] !== NEWLINE) continue;
270
- handleLine(buffer.toString("utf8", lineStart, pos), pos + 1);
518
+ handleLine(buffer.toString("utf8", lineStart, pos), lineStart, pos + 1);
271
519
  lineStart = pos + 1;
272
520
  }
273
521
  const newOffset = lineStart;
@@ -281,8 +529,10 @@ async function scanWindow(rolloutPath, cursor) {
281
529
  model: lastModel,
282
530
  prompt: windowPrompt,
283
531
  cwd: windowTurnCtxCwd ?? sessionMetaCwd,
284
- sessionId: sessionMetaSid ?? sessionIdFromFilename(rolloutPath),
532
+ sessionId: rolloutId(),
533
+ eventKeys: windowEventKeys,
285
534
  isSubagentRollout,
535
+ originator,
286
536
  firstTs,
287
537
  lastTs,
288
538
  newOffset
@@ -296,11 +546,13 @@ function windowCursor(scan) {
296
546
  lastTs: scan.lastTs,
297
547
  seenMessageKeys: [],
298
548
  // 去重は codexTotals の差分方式が担う
299
- codexTotals: { ...scan.prev }
549
+ codexTotals: { ...scan.prev },
550
+ codexOriginator: scan.originator,
551
+ codexModel: scan.model
300
552
  };
301
553
  }
302
- async function aggregateCodexTurn(rolloutPath, cursor) {
303
- const scan = await scanWindow(rolloutPath, cursor);
554
+ async function aggregateCodexTurn(rolloutPath, cursor, opts = {}) {
555
+ const scan = await scanWindow(rolloutPath, cursor, opts);
304
556
  if (scan === null || isZeroTotals(scan.acc)) return null;
305
557
  return {
306
558
  sessionId: scan.sessionId,
@@ -314,12 +566,73 @@ async function aggregateCodexTurn(rolloutPath, cursor) {
314
566
  // rollout に無い
315
567
  firstTs: scan.firstTs,
316
568
  lastTs: scan.lastTs,
317
- newCursor: windowCursor(scan)
569
+ newCursor: windowCursor(scan),
570
+ originator: scan.originator,
571
+ isSubagentRollout: scan.isSubagentRollout,
572
+ codexEventKeys: scan.eventKeys
573
+ };
574
+ }
575
+ async function codexConsumedCursor(rolloutPath, cursor, opts = {}) {
576
+ const scan = await scanWindow(rolloutPath, cursor, opts);
577
+ return scan === null ? null : windowCursor(scan);
578
+ }
579
+ async function scanCodexTurns(rolloutPath, cursor, opts = {}) {
580
+ const scan = await scanWindow(rolloutPath, cursor, opts);
581
+ if (scan === null) return null;
582
+ const newCursor = windowCursor(scan);
583
+ if (isZeroTotals(scan.acc)) return { drafts: [], newCursor };
584
+ return { drafts: draftsFromScan(scan), newCursor };
585
+ }
586
+ async function codexResumePointAtTs(rolloutPath, floorTs) {
587
+ const buffer = await readAll(rolloutPath);
588
+ if (buffer === null) return null;
589
+ const seed = readSessionMetaSeed(buffer);
590
+ let prev = zeroTotals();
591
+ let lastModel = null;
592
+ let consumedEnd = 0;
593
+ let lineStart = 0;
594
+ for (let pos = 0; pos < buffer.length; pos++) {
595
+ if (buffer[pos] !== NEWLINE) continue;
596
+ const raw = buffer.toString("utf8", lineStart, pos);
597
+ let obj = null;
598
+ try {
599
+ obj = JSON.parse(raw);
600
+ } catch {
601
+ obj = null;
602
+ }
603
+ if (isRecord(obj)) {
604
+ const ts = strOrNull(obj.timestamp);
605
+ if (ts !== null && ts > floorTs) break;
606
+ const payload = isRecord(obj.payload) ? obj.payload : null;
607
+ if (obj.type === "turn_context" && payload !== null) {
608
+ const m = strOrNull(payload.model);
609
+ if (m !== null) lastModel = m;
610
+ }
611
+ if (obj.type === "event_msg" && payload !== null && payload.type === "token_count") {
612
+ const info = isRecord(payload.info) ? payload.info : null;
613
+ const total = info === null ? null : readTotals(info.total_token_usage);
614
+ if (total !== null) prev = total;
615
+ }
616
+ }
617
+ consumedEnd = pos + 1;
618
+ lineStart = pos + 1;
619
+ }
620
+ return {
621
+ offset: consumedEnd,
622
+ lastUuid: null,
623
+ lastTs: floorTs,
624
+ seenMessageKeys: [],
625
+ codexTotals: prev,
626
+ codexOriginator: seed?.originator ?? null,
627
+ codexModel: lastModel
318
628
  };
319
629
  }
320
- async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
321
- const scan = await scanWindow(rolloutPath, cursor);
630
+ async function splitIntoCodexTurnDrafts(rolloutPath, cursor, opts = {}) {
631
+ const scan = await scanWindow(rolloutPath, cursor, opts);
322
632
  if (scan === null || isZeroTotals(scan.acc)) return null;
633
+ return draftsFromScan(scan);
634
+ }
635
+ function draftsFromScan(scan) {
323
636
  const picked = scan.segments.filter((s) => !isZeroTotals(s.acc));
324
637
  if (scan.open !== null && !isZeroTotals(scan.open.acc)) {
325
638
  picked.push(scan.open);
@@ -336,6 +649,10 @@ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
336
649
  prompt: s.prompt,
337
650
  cwd: s.cwd,
338
651
  gitBranch: null,
652
+ originator: scan.originator,
653
+ // session_meta はファイル先頭にしか無いので全ドラフト共通
654
+ isSubagentRollout: scan.isSubagentRollout,
655
+ codexEventKeys: s.eventKeys,
339
656
  firstTs: s.firstTs,
340
657
  lastTs: s.endTs,
341
658
  // 最後のドラフトはウィンドウ全体を消費した状態(= aggregateCodexTurn の newCursor と同一。
@@ -346,7 +663,9 @@ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
346
663
  lastUuid: null,
347
664
  lastTs: s.lastTsAtEnd,
348
665
  seenMessageKeys: [],
349
- codexTotals: { ...s.prevAtEnd }
666
+ codexTotals: { ...s.prevAtEnd },
667
+ codexOriginator: scan.originator,
668
+ codexModel: s.model
350
669
  }
351
670
  },
352
671
  endTs: s.endTs
@@ -539,7 +858,21 @@ async function aggregateNewTurn(transcriptPath, cursor, opts = {}) {
539
858
 
540
859
  export {
541
860
  getUsdJpy,
861
+ claudeTranscriptRoots,
862
+ surfaceForClaudePath,
863
+ rootForClaudePath,
864
+ determineClaudeSurface,
865
+ listCodexRollouts,
866
+ findLatestCodexRollout,
867
+ callFingerprints,
868
+ setCountedCalls,
869
+ addCountedCalls,
870
+ messageKeyFilterOf,
871
+ anyOf,
542
872
  aggregateCodexTurn,
873
+ codexConsumedCursor,
874
+ scanCodexTurns,
875
+ codexResumePointAtTs,
543
876
  splitIntoCodexTurnDrafts,
544
877
  extractBucket,
545
878
  promptCandidate,