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.
- package/README.md +5 -0
- package/dist/{budget-V7CCMJHF.js → budget-O4VAP6RB.js} +2 -2
- package/dist/chunk-3M5UQK76.js +92 -0
- package/dist/{sweep-65D57PXU.js → chunk-6BXD6HU5.js} +275 -113
- package/dist/chunk-6IVSZ3S4.js +22 -0
- package/dist/{chunk-NV5UOHJA.js → chunk-JLDI4D5E.js} +5 -5
- package/dist/{chunk-T6Z2ZAN4.js → chunk-ML5H43NH.js} +1 -1
- package/dist/{chunk-6UZXXO5J.js → chunk-O2E4VTDR.js} +36 -9
- package/dist/{chunk-D4D76RGQ.js → chunk-OCKZZVOG.js} +353 -20
- package/dist/{chunk-27SJELD2.js → chunk-PXMXEFM7.js} +22 -107
- package/dist/{chunk-6HTETN26.js → chunk-Q45AAPJY.js} +18 -1
- package/dist/chunk-V4IS4FXH.js +454 -0
- package/dist/{chunk-OOAC5ULQ.js → chunk-WONMZ466.js} +184 -4
- package/dist/{chunk-OXXDZZPJ.js → chunk-X7U63BZG.js} +4 -6
- package/dist/{chunk-J5QAYTFE.js → chunk-XG73C6CC.js} +10 -1
- package/dist/cli.js +286 -29
- package/dist/{dashboard-VEKQ4TI3.js → dashboard-64XHDUVT.js} +6 -4
- package/dist/{history-DL4SG3PG.js → history-W34EJOVV.js} +5 -3
- package/dist/{mute-UIR5P5N5.js → mute-23BWQXS3.js} +2 -2
- package/dist/reset-cursors-5CCM6MQL.js +41 -0
- package/dist/scan-UB6ORRZK.js +87 -0
- package/dist/{setup-I3JNGWZG.js → setup-VLMW433R.js} +5 -6
- package/dist/{subagent-store-US4TJJQR.js → subagent-store-USG3WJEB.js} +1 -1
- package/dist/sweep-DVOV4XIZ.js +20 -0
- package/dist/track-P6A5WDJZ.js +368 -0
- package/package.json +1 -1
- package/dist/chunk-HLJAJS2W.js +0 -55
- package/dist/chunk-HTYUYKFW.js +0 -21
- package/dist/chunk-OYD6H3ZZ.js +0 -124
- 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-
|
|
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-
|
|
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)}
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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:
|
|
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,
|