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.
- package/README.md +5 -0
- package/dist/{budget-V7CCMJHF.js → budget-U37CCG4Y.js} +2 -2
- package/dist/chunk-3M5UQK76.js +92 -0
- package/dist/chunk-6IVSZ3S4.js +22 -0
- package/dist/{sweep-CONNNBK7.js → chunk-C7QOYVGT.js} +324 -120
- package/dist/{chunk-PYU7CAK2.js → chunk-DSTGXYSY.js} +44 -11
- package/dist/{chunk-D4D76RGQ.js → chunk-GAOGTALW.js} +361 -20
- package/dist/{chunk-T6Z2ZAN4.js → chunk-ML5H43NH.js} +1 -1
- package/dist/{chunk-27SJELD2.js → chunk-PXMXEFM7.js} +22 -107
- package/dist/{chunk-6HTETN26.js → chunk-Q45AAPJY.js} +18 -1
- package/dist/{chunk-OXXDZZPJ.js → chunk-RE72XBXB.js} +4 -6
- package/dist/{chunk-NV5UOHJA.js → chunk-TTD3BGOL.js} +5 -5
- package/dist/{chunk-OOAC5ULQ.js → chunk-WONMZ466.js} +184 -4
- package/dist/{chunk-J5QAYTFE.js → chunk-XOGYLFX3.js} +12 -1
- package/dist/chunk-YH45W7TA.js +479 -0
- package/dist/cli.js +287 -30
- package/dist/{dashboard-O7LJLYM3.js → dashboard-YG4H3GLA.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-UVQXFN5Y.js +44 -0
- package/dist/scan-QB2N2O4Q.js +94 -0
- package/dist/{setup-I3JNGWZG.js → setup-2QDA3OWE.js} +5 -6
- package/dist/{subagent-store-US4TJJQR.js → subagent-store-USG3WJEB.js} +1 -1
- package/dist/sweep-OLI2LIXJ.js +22 -0
- package/dist/track-TIINM4ZR.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-RBLLR25M.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-XOGYLFX3.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";
|
|
@@ -219,7 +225,7 @@ function buildTurnEmbed(rec, map) {
|
|
|
219
225
|
const primary = rec.models[0] ?? "unknown";
|
|
220
226
|
const extra = rec.models.length > 1 ? ` +${rec.models.length - 1}` : "";
|
|
221
227
|
const saMark = rec.subagents ? " +SA" : "";
|
|
222
|
-
const codexActivityMark = rec.source === "codex" && rec.subagentActivity ? " +SA(\
|
|
228
|
+
const codexActivityMark = rec.source === "codex" && rec.subagentActivity ? " +SA(\u691C\u51FA)" : "";
|
|
223
229
|
let sa = null;
|
|
224
230
|
if (rec.subagents) {
|
|
225
231
|
const saModels = Object.keys(rec.subagents.costByModel).map((m) => modelDisplayName(m));
|
|
@@ -229,6 +235,12 @@ function buildTurnEmbed(rec, map) {
|
|
|
229
235
|
models: saModels.join(", "),
|
|
230
236
|
apiCalls: rec.subagents.apiCalls
|
|
231
237
|
};
|
|
238
|
+
const b = rec.subagents.tokens;
|
|
239
|
+
const cache = b.cacheRead + b.cacheWrite5m + b.cacheWrite1h;
|
|
240
|
+
const total = b.input + b.output + cache;
|
|
241
|
+
if (total > 0) {
|
|
242
|
+
sa.tok = `\u8A08 ${formatTokens(total)} tokens(cache ${Math.round(cache / total * 100)}%)`;
|
|
243
|
+
}
|
|
232
244
|
}
|
|
233
245
|
const ms = Date.parse(rec.ts);
|
|
234
246
|
const out = {
|
|
@@ -968,12 +980,12 @@ var APP_JS = `<script>
|
|
|
968
980
|
dtd.appendChild(meta);
|
|
969
981
|
if(t.sa){
|
|
970
982
|
var saLine = document.createElement('div'); saLine.className = 'detail-meta';
|
|
971
|
-
saLine.textContent = '\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ' + (t.sa.usd||'') + '(' + (t.sa.jpy||'') + ')\xB7 ' + (t.sa.models||'') + ' \xB7 API\u30B3\u30FC\u30EB ' + (t.sa.apiCalls||0);
|
|
983
|
+
saLine.textContent = '\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ' + (t.sa.usd||'') + '(' + (t.sa.jpy||'') + ')\xB7 ' + (t.sa.models||'') + ' \xB7 API\u30B3\u30FC\u30EB ' + (t.sa.apiCalls||0) + (t.sa.tok ? ' \xB7 ' + t.sa.tok : '');
|
|
972
984
|
dtd.appendChild(saLine);
|
|
973
985
|
}
|
|
974
986
|
if(t.ca){
|
|
975
987
|
var caLine = document.createElement('div'); caLine.className = 'detail-meta';
|
|
976
|
-
caLine.textContent = 'Codex\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: \u5229\u7528\u3042\u308A\
|
|
988
|
+
caLine.textContent = 'Codex\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: \u5229\u7528\u3042\u308A(\u6599\u91D1\u306F\u3053\u306E\u30BF\u30FC\u30F3\u306B\u542B\u3081\u306A\u3044)\xB7 \u958B\u59CB ' +
|
|
977
989
|
(t.ca.started||0) + ' \xB7 \u7D42\u4E86 ' + (t.ca.stopped||0) + ' \xB7 \u7A2E\u5225 ' + ((t.ca.agentTypes||[]).join(', ')||'unknown');
|
|
978
990
|
dtd.appendChild(caLine);
|
|
979
991
|
}
|
|
@@ -1070,6 +1082,27 @@ var APP_JS = `<script>
|
|
|
1070
1082
|
function statCard(label, sub, totals) {
|
|
1071
1083
|
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
1084
|
}
|
|
1085
|
+
function computeSurfaceBreakdown(turns) {
|
|
1086
|
+
const map = /* @__PURE__ */ new Map();
|
|
1087
|
+
for (const rec of turns) {
|
|
1088
|
+
const surface = effectiveSurface(rec);
|
|
1089
|
+
const agg = map.get(surface) ?? { surface, turns: 0, usd: 0, jpy: 0 };
|
|
1090
|
+
agg.turns += 1;
|
|
1091
|
+
agg.usd += turnTotalUSD(rec);
|
|
1092
|
+
agg.jpy += turnTotalJPY(rec);
|
|
1093
|
+
map.set(surface, agg);
|
|
1094
|
+
}
|
|
1095
|
+
return [...map.values()].sort((a, b) => b.usd - a.usd);
|
|
1096
|
+
}
|
|
1097
|
+
function surfaceSection(turns) {
|
|
1098
|
+
const rows = computeSurfaceBreakdown(turns);
|
|
1099
|
+
if (rows.length === 0) return "";
|
|
1100
|
+
if (rows.length === 1 && rows[0].surface === "cli") return "";
|
|
1101
|
+
const trs = rows.map(
|
|
1102
|
+
(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>`
|
|
1103
|
+
).join("");
|
|
1104
|
+
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>`;
|
|
1105
|
+
}
|
|
1073
1106
|
function renderLegend(slots) {
|
|
1074
1107
|
if (slots.length < 2) return "";
|
|
1075
1108
|
const items = slots.map((m) => `<span class="legend-item"><span class="swatch k${m.slot}"></span>${esc(m.name)}</span>`).join("");
|
|
@@ -1161,7 +1194,7 @@ function renderDashboard(turns, opts, fullHistory) {
|
|
|
1161
1194
|
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
1195
|
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
1196
|
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;
|
|
1197
|
+
return head + header + kpiSection + budgetSection + chartSection + breakdownSection + surfaceSection(turns) + historySection + foot;
|
|
1165
1198
|
}
|
|
1166
1199
|
function toWindowsPath(path) {
|
|
1167
1200
|
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,28 @@ 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
|
+
isForkedRollout: strOrNull(payload.forked_from_id) !== null
|
|
361
|
+
};
|
|
362
|
+
}
|
|
139
363
|
function newSegmentBuf() {
|
|
140
364
|
return {
|
|
141
365
|
acc: zeroTotals(),
|
|
@@ -144,10 +368,11 @@ function newSegmentBuf() {
|
|
|
144
368
|
turnCtxCwd: null,
|
|
145
369
|
firstTs: null,
|
|
146
370
|
endTs: null,
|
|
147
|
-
hasLines: false
|
|
371
|
+
hasLines: false,
|
|
372
|
+
eventKeys: []
|
|
148
373
|
};
|
|
149
374
|
}
|
|
150
|
-
async function scanWindow(rolloutPath, cursor) {
|
|
375
|
+
async function scanWindow(rolloutPath, cursor, opts = {}) {
|
|
151
376
|
const buffer = await readAll(rolloutPath);
|
|
152
377
|
if (buffer === null) return null;
|
|
153
378
|
const fileSize = buffer.length;
|
|
@@ -165,16 +390,32 @@ async function scanWindow(rolloutPath, cursor) {
|
|
|
165
390
|
let prev = initTotals !== void 0 ? { ...initTotals } : zeroTotals();
|
|
166
391
|
const acc = zeroTotals();
|
|
167
392
|
let apiCalls = 0;
|
|
168
|
-
let lastModel = null;
|
|
393
|
+
let lastModel = cursor?.codexModel ?? null;
|
|
169
394
|
let windowPrompt = null;
|
|
170
395
|
let windowTurnCtxCwd = null;
|
|
171
396
|
let sessionMetaCwd = null;
|
|
172
397
|
let sessionMetaSid = null;
|
|
173
398
|
let isSubagentRollout = false;
|
|
399
|
+
let isForkedRollout = false;
|
|
400
|
+
let originator = cursor?.codexOriginator ?? null;
|
|
401
|
+
if (startOffset > 0) {
|
|
402
|
+
const seed = readSessionMetaSeed(buffer);
|
|
403
|
+
if (seed !== null) {
|
|
404
|
+
sessionMetaSid = seed.sessionId;
|
|
405
|
+
sessionMetaCwd = seed.cwd;
|
|
406
|
+
if (seed.originator !== null) originator = seed.originator;
|
|
407
|
+
isSubagentRollout = seed.isSubagentRollout;
|
|
408
|
+
isForkedRollout = seed.isForkedRollout;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
174
411
|
let firstTs = null;
|
|
175
412
|
let lastTs = null;
|
|
176
413
|
const segments = [];
|
|
414
|
+
const windowEventKeys = [];
|
|
177
415
|
let seg = newSegmentBuf();
|
|
416
|
+
const rolloutFile = basename(rolloutPath);
|
|
417
|
+
const filenameId = sessionIdFromFilename(rolloutPath);
|
|
418
|
+
const rolloutId = () => sessionMetaSid ?? filenameId;
|
|
178
419
|
const snapshotSegment = (endOffset) => ({
|
|
179
420
|
acc: seg.acc,
|
|
180
421
|
apiCalls: seg.apiCalls,
|
|
@@ -185,9 +426,10 @@ async function scanWindow(rolloutPath, cursor) {
|
|
|
185
426
|
endTs: seg.endTs,
|
|
186
427
|
endOffset,
|
|
187
428
|
prevAtEnd: { ...prev },
|
|
188
|
-
lastTsAtEnd: lastTs
|
|
429
|
+
lastTsAtEnd: lastTs,
|
|
430
|
+
eventKeys: seg.eventKeys
|
|
189
431
|
});
|
|
190
|
-
const handleLine = (raw, endOffset) => {
|
|
432
|
+
const handleLine = (raw, lineOffset, endOffset) => {
|
|
191
433
|
if (raw.trim().length === 0) return;
|
|
192
434
|
let obj;
|
|
193
435
|
try {
|
|
@@ -209,12 +451,15 @@ async function scanWindow(rolloutPath, cursor) {
|
|
|
209
451
|
if (payload === null) return;
|
|
210
452
|
const type = obj.type;
|
|
211
453
|
if (type === "session_meta") {
|
|
212
|
-
const sid = strOrNull(payload.session_id);
|
|
454
|
+
const sid = strOrNull(payload.id) ?? strOrNull(payload.session_id);
|
|
213
455
|
if (sid !== null) sessionMetaSid = sid;
|
|
214
456
|
const c = strOrNull(payload.cwd);
|
|
215
457
|
if (c !== null) sessionMetaCwd = c;
|
|
216
458
|
const source = payload.source;
|
|
217
459
|
if (isRecord(source) && Object.hasOwn(source, "subagent")) isSubagentRollout = true;
|
|
460
|
+
if (strOrNull(payload.forked_from_id) !== null) isForkedRollout = true;
|
|
461
|
+
const orig = strOrNull(payload.originator);
|
|
462
|
+
if (orig !== null) originator = orig;
|
|
218
463
|
return;
|
|
219
464
|
}
|
|
220
465
|
if (type === "turn_context") {
|
|
@@ -242,6 +487,11 @@ async function scanWindow(rolloutPath, cursor) {
|
|
|
242
487
|
if (info === null) return;
|
|
243
488
|
const total = readTotals(info.total_token_usage);
|
|
244
489
|
if (total === null) return;
|
|
490
|
+
const eventKey = codexEventFingerprint(rolloutFile, rolloutId(), lineOffset, total);
|
|
491
|
+
if (opts.excludeEvents?.has(eventKey) === true) {
|
|
492
|
+
prev = total;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
245
495
|
let step = {
|
|
246
496
|
input: total.input - prev.input,
|
|
247
497
|
cached: total.cached - prev.cached,
|
|
@@ -252,6 +502,8 @@ async function scanWindow(rolloutPath, cursor) {
|
|
|
252
502
|
}
|
|
253
503
|
addTotals(acc, step);
|
|
254
504
|
addTotals(seg.acc, step);
|
|
505
|
+
seg.eventKeys.push(eventKey);
|
|
506
|
+
windowEventKeys.push(eventKey);
|
|
255
507
|
prev = total;
|
|
256
508
|
if (!isZeroTotals(step)) {
|
|
257
509
|
apiCalls++;
|
|
@@ -267,7 +519,7 @@ async function scanWindow(rolloutPath, cursor) {
|
|
|
267
519
|
let lineStart = startOffset;
|
|
268
520
|
for (let pos = startOffset; pos < fileSize; pos++) {
|
|
269
521
|
if (buffer[pos] !== NEWLINE) continue;
|
|
270
|
-
handleLine(buffer.toString("utf8", lineStart, pos), pos + 1);
|
|
522
|
+
handleLine(buffer.toString("utf8", lineStart, pos), lineStart, pos + 1);
|
|
271
523
|
lineStart = pos + 1;
|
|
272
524
|
}
|
|
273
525
|
const newOffset = lineStart;
|
|
@@ -281,8 +533,11 @@ async function scanWindow(rolloutPath, cursor) {
|
|
|
281
533
|
model: lastModel,
|
|
282
534
|
prompt: windowPrompt,
|
|
283
535
|
cwd: windowTurnCtxCwd ?? sessionMetaCwd,
|
|
284
|
-
sessionId:
|
|
536
|
+
sessionId: rolloutId(),
|
|
537
|
+
eventKeys: windowEventKeys,
|
|
285
538
|
isSubagentRollout,
|
|
539
|
+
isForkedRollout,
|
|
540
|
+
originator,
|
|
286
541
|
firstTs,
|
|
287
542
|
lastTs,
|
|
288
543
|
newOffset
|
|
@@ -296,11 +551,13 @@ function windowCursor(scan) {
|
|
|
296
551
|
lastTs: scan.lastTs,
|
|
297
552
|
seenMessageKeys: [],
|
|
298
553
|
// 去重は codexTotals の差分方式が担う
|
|
299
|
-
codexTotals: { ...scan.prev }
|
|
554
|
+
codexTotals: { ...scan.prev },
|
|
555
|
+
codexOriginator: scan.originator,
|
|
556
|
+
codexModel: scan.model
|
|
300
557
|
};
|
|
301
558
|
}
|
|
302
|
-
async function aggregateCodexTurn(rolloutPath, cursor) {
|
|
303
|
-
const scan = await scanWindow(rolloutPath, cursor);
|
|
559
|
+
async function aggregateCodexTurn(rolloutPath, cursor, opts = {}) {
|
|
560
|
+
const scan = await scanWindow(rolloutPath, cursor, opts);
|
|
304
561
|
if (scan === null || isZeroTotals(scan.acc)) return null;
|
|
305
562
|
return {
|
|
306
563
|
sessionId: scan.sessionId,
|
|
@@ -314,12 +571,74 @@ async function aggregateCodexTurn(rolloutPath, cursor) {
|
|
|
314
571
|
// rollout に無い
|
|
315
572
|
firstTs: scan.firstTs,
|
|
316
573
|
lastTs: scan.lastTs,
|
|
317
|
-
newCursor: windowCursor(scan)
|
|
574
|
+
newCursor: windowCursor(scan),
|
|
575
|
+
originator: scan.originator,
|
|
576
|
+
isSubagentRollout: scan.isSubagentRollout,
|
|
577
|
+
isForkedRollout: scan.isForkedRollout,
|
|
578
|
+
codexEventKeys: scan.eventKeys
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function codexConsumedCursor(rolloutPath, cursor, opts = {}) {
|
|
582
|
+
const scan = await scanWindow(rolloutPath, cursor, opts);
|
|
583
|
+
return scan === null ? null : windowCursor(scan);
|
|
584
|
+
}
|
|
585
|
+
async function scanCodexTurns(rolloutPath, cursor, opts = {}) {
|
|
586
|
+
const scan = await scanWindow(rolloutPath, cursor, opts);
|
|
587
|
+
if (scan === null) return null;
|
|
588
|
+
const newCursor = windowCursor(scan);
|
|
589
|
+
if (isZeroTotals(scan.acc)) return { drafts: [], newCursor };
|
|
590
|
+
return { drafts: draftsFromScan(scan), newCursor };
|
|
591
|
+
}
|
|
592
|
+
async function codexResumePointAtTs(rolloutPath, floorTs) {
|
|
593
|
+
const buffer = await readAll(rolloutPath);
|
|
594
|
+
if (buffer === null) return null;
|
|
595
|
+
const seed = readSessionMetaSeed(buffer);
|
|
596
|
+
let prev = zeroTotals();
|
|
597
|
+
let lastModel = null;
|
|
598
|
+
let consumedEnd = 0;
|
|
599
|
+
let lineStart = 0;
|
|
600
|
+
for (let pos = 0; pos < buffer.length; pos++) {
|
|
601
|
+
if (buffer[pos] !== NEWLINE) continue;
|
|
602
|
+
const raw = buffer.toString("utf8", lineStart, pos);
|
|
603
|
+
let obj = null;
|
|
604
|
+
try {
|
|
605
|
+
obj = JSON.parse(raw);
|
|
606
|
+
} catch {
|
|
607
|
+
obj = null;
|
|
608
|
+
}
|
|
609
|
+
if (isRecord(obj)) {
|
|
610
|
+
const ts = strOrNull(obj.timestamp);
|
|
611
|
+
if (ts !== null && ts > floorTs) break;
|
|
612
|
+
const payload = isRecord(obj.payload) ? obj.payload : null;
|
|
613
|
+
if (obj.type === "turn_context" && payload !== null) {
|
|
614
|
+
const m = strOrNull(payload.model);
|
|
615
|
+
if (m !== null) lastModel = m;
|
|
616
|
+
}
|
|
617
|
+
if (obj.type === "event_msg" && payload !== null && payload.type === "token_count") {
|
|
618
|
+
const info = isRecord(payload.info) ? payload.info : null;
|
|
619
|
+
const total = info === null ? null : readTotals(info.total_token_usage);
|
|
620
|
+
if (total !== null) prev = total;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
consumedEnd = pos + 1;
|
|
624
|
+
lineStart = pos + 1;
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
offset: consumedEnd,
|
|
628
|
+
lastUuid: null,
|
|
629
|
+
lastTs: floorTs,
|
|
630
|
+
seenMessageKeys: [],
|
|
631
|
+
codexTotals: prev,
|
|
632
|
+
codexOriginator: seed?.originator ?? null,
|
|
633
|
+
codexModel: lastModel
|
|
318
634
|
};
|
|
319
635
|
}
|
|
320
|
-
async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
|
|
321
|
-
const scan = await scanWindow(rolloutPath, cursor);
|
|
636
|
+
async function splitIntoCodexTurnDrafts(rolloutPath, cursor, opts = {}) {
|
|
637
|
+
const scan = await scanWindow(rolloutPath, cursor, opts);
|
|
322
638
|
if (scan === null || isZeroTotals(scan.acc)) return null;
|
|
639
|
+
return draftsFromScan(scan);
|
|
640
|
+
}
|
|
641
|
+
function draftsFromScan(scan) {
|
|
323
642
|
const picked = scan.segments.filter((s) => !isZeroTotals(s.acc));
|
|
324
643
|
if (scan.open !== null && !isZeroTotals(scan.open.acc)) {
|
|
325
644
|
picked.push(scan.open);
|
|
@@ -327,6 +646,7 @@ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
|
|
|
327
646
|
const lastIndex = picked.length - 1;
|
|
328
647
|
return picked.map((s, i) => ({
|
|
329
648
|
isSubagentRollout: scan.isSubagentRollout,
|
|
649
|
+
isForkedRollout: scan.isForkedRollout,
|
|
330
650
|
agg: {
|
|
331
651
|
sessionId: scan.sessionId,
|
|
332
652
|
// session_meta はファイル先頭にしか無いので全ドラフト共通
|
|
@@ -336,6 +656,11 @@ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
|
|
|
336
656
|
prompt: s.prompt,
|
|
337
657
|
cwd: s.cwd,
|
|
338
658
|
gitBranch: null,
|
|
659
|
+
originator: scan.originator,
|
|
660
|
+
// session_meta はファイル先頭にしか無いので全ドラフト共通
|
|
661
|
+
isSubagentRollout: scan.isSubagentRollout,
|
|
662
|
+
isForkedRollout: scan.isForkedRollout,
|
|
663
|
+
codexEventKeys: s.eventKeys,
|
|
339
664
|
firstTs: s.firstTs,
|
|
340
665
|
lastTs: s.endTs,
|
|
341
666
|
// 最後のドラフトはウィンドウ全体を消費した状態(= aggregateCodexTurn の newCursor と同一。
|
|
@@ -346,7 +671,9 @@ async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
|
|
|
346
671
|
lastUuid: null,
|
|
347
672
|
lastTs: s.lastTsAtEnd,
|
|
348
673
|
seenMessageKeys: [],
|
|
349
|
-
codexTotals: { ...s.prevAtEnd }
|
|
674
|
+
codexTotals: { ...s.prevAtEnd },
|
|
675
|
+
codexOriginator: scan.originator,
|
|
676
|
+
codexModel: s.model
|
|
350
677
|
}
|
|
351
678
|
},
|
|
352
679
|
endTs: s.endTs
|
|
@@ -539,7 +866,21 @@ async function aggregateNewTurn(transcriptPath, cursor, opts = {}) {
|
|
|
539
866
|
|
|
540
867
|
export {
|
|
541
868
|
getUsdJpy,
|
|
869
|
+
claudeTranscriptRoots,
|
|
870
|
+
surfaceForClaudePath,
|
|
871
|
+
rootForClaudePath,
|
|
872
|
+
determineClaudeSurface,
|
|
873
|
+
listCodexRollouts,
|
|
874
|
+
findLatestCodexRollout,
|
|
875
|
+
callFingerprints,
|
|
876
|
+
setCountedCalls,
|
|
877
|
+
addCountedCalls,
|
|
878
|
+
messageKeyFilterOf,
|
|
879
|
+
anyOf,
|
|
542
880
|
aggregateCodexTurn,
|
|
881
|
+
codexConsumedCursor,
|
|
882
|
+
scanCodexTurns,
|
|
883
|
+
codexResumePointAtTs,
|
|
543
884
|
splitIntoCodexTurnDrafts,
|
|
544
885
|
extractBucket,
|
|
545
886
|
promptCandidate,
|