ccc-notifier 0.4.0 → 0.6.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 +129 -122
- package/dist/{budget-EHWPEYXY.js → budget-V7CCMJHF.js} +1 -1
- package/dist/{chunk-J6Y3RMMC.js → chunk-27SJELD2.js} +107 -22
- package/dist/{chunk-2VPBBIDW.js → chunk-4JSRGJCZ.js} +61 -22
- package/dist/chunk-6HTETN26.js +226 -0
- package/dist/{chunk-5LHZOZZO.js → chunk-CH34OJ7Q.js} +241 -110
- package/dist/chunk-D4D76RGQ.js +547 -0
- package/dist/chunk-HLJAJS2W.js +55 -0
- package/dist/chunk-OOAC5ULQ.js +1101 -0
- package/dist/chunk-OYD6H3ZZ.js +124 -0
- package/dist/{chunk-TUZVISLD.js → chunk-T6Z2ZAN4.js} +1 -1
- package/dist/cli.js +388 -77
- package/dist/{dashboard-GYKABTTN.js → dashboard-NORNHUPT.js} +3 -4
- package/dist/{history-6OUJLXJT.js → history-DL4SG3PG.js} +3 -5
- package/dist/{mute-GBDNN5PB.js → mute-UIR5P5N5.js} +2 -2
- package/dist/{setup-W3CSTHJC.js → setup-3SRNQ5PG.js} +4 -3
- package/dist/subagent-store-US4TJJQR.js +31 -0
- package/dist/sweep-RUPPGAWJ.js +795 -0
- package/dist/{track-QT2U3E2R.js → track-H4AIT575.js} +53 -31
- package/package.json +1 -1
- package/dist/chunk-26CISNOE.js +0 -324
- package/dist/chunk-CKVK3UCA.js +0 -357
- package/dist/chunk-LHKBGA5K.js +0 -462
- package/dist/chunk-ZHIZZ6V5.js +0 -92
- package/dist/sweep-IBSV4LRD.js +0 -604
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/fx.ts
|
|
4
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
7
|
+
var CACHE_FILE_NAME = "fx.json";
|
|
8
|
+
var FX_SOURCES = [
|
|
9
|
+
"https://api.frankfurter.dev/v1/latest?base=USD&symbols=JPY",
|
|
10
|
+
"https://open.er-api.com/v6/latest/USD"
|
|
11
|
+
];
|
|
12
|
+
function cacheFilePath(cacheDir) {
|
|
13
|
+
return join(cacheDir, CACHE_FILE_NAME);
|
|
14
|
+
}
|
|
15
|
+
function isPositiveFiniteNumber(v) {
|
|
16
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
17
|
+
}
|
|
18
|
+
function parseFxCache(raw) {
|
|
19
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
20
|
+
const obj = raw;
|
|
21
|
+
if (!isPositiveFiniteNumber(obj.rate)) return null;
|
|
22
|
+
if (typeof obj.fetchedAt !== "string") return null;
|
|
23
|
+
return { rate: obj.rate, fetchedAt: obj.fetchedAt };
|
|
24
|
+
}
|
|
25
|
+
async function readFxCache(cacheDir) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await readFile(cacheFilePath(cacheDir), "utf8");
|
|
28
|
+
return parseFxCache(JSON.parse(raw));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function writeFxCache(cacheDir, cache) {
|
|
34
|
+
try {
|
|
35
|
+
await mkdir(cacheDir, { recursive: true });
|
|
36
|
+
await writeFile(cacheFilePath(cacheDir), JSON.stringify(cache), "utf8");
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function isFresh(fetchedAt, cacheHours) {
|
|
41
|
+
const fetchedMs = Date.parse(fetchedAt);
|
|
42
|
+
if (Number.isNaN(fetchedMs)) return false;
|
|
43
|
+
const ageMs = Date.now() - fetchedMs;
|
|
44
|
+
return ageMs <= cacheHours * 60 * 60 * 1e3;
|
|
45
|
+
}
|
|
46
|
+
function extractJpyRate(json) {
|
|
47
|
+
if (typeof json !== "object" || json === null) return null;
|
|
48
|
+
const rates = json.rates;
|
|
49
|
+
if (typeof rates !== "object" || rates === null) return null;
|
|
50
|
+
const jpy = rates.JPY;
|
|
51
|
+
return isPositiveFiniteNumber(jpy) ? jpy : null;
|
|
52
|
+
}
|
|
53
|
+
async function fetchJpyRate(url) {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
58
|
+
const json = await res.json();
|
|
59
|
+
return extractJpyRate(json);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
} finally {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function getUsdJpy(cfg, cacheDir) {
|
|
67
|
+
const cache = await readFxCache(cacheDir);
|
|
68
|
+
if (cache && isFresh(cache.fetchedAt, cfg.fx.cacheHours)) {
|
|
69
|
+
return { rate: cache.rate, source: "cache", fetchedAt: cache.fetchedAt };
|
|
70
|
+
}
|
|
71
|
+
for (const url of FX_SOURCES) {
|
|
72
|
+
const rate = await fetchJpyRate(url);
|
|
73
|
+
if (rate !== null) {
|
|
74
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
75
|
+
await writeFxCache(cacheDir, { rate, fetchedAt });
|
|
76
|
+
return { rate, source: "live", fetchedAt };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (cache) {
|
|
80
|
+
return { rate: cache.rate, source: "cache", fetchedAt: cache.fetchedAt };
|
|
81
|
+
}
|
|
82
|
+
return { rate: cfg.fx.fallbackRate, source: "fixed", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/codex/transcript.ts
|
|
86
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
87
|
+
import { basename } from "path";
|
|
88
|
+
var NEWLINE = 10;
|
|
89
|
+
function isRecord(v) {
|
|
90
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
91
|
+
}
|
|
92
|
+
function numOf(v) {
|
|
93
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
94
|
+
}
|
|
95
|
+
function strOrNull(v) {
|
|
96
|
+
return typeof v === "string" ? v : null;
|
|
97
|
+
}
|
|
98
|
+
function zeroTotals() {
|
|
99
|
+
return { input: 0, cached: 0, output: 0 };
|
|
100
|
+
}
|
|
101
|
+
function isZeroTotals(t) {
|
|
102
|
+
return t.input === 0 && t.cached === 0 && t.output === 0;
|
|
103
|
+
}
|
|
104
|
+
function addTotals(target, d) {
|
|
105
|
+
target.input += d.input;
|
|
106
|
+
target.cached += d.cached;
|
|
107
|
+
target.output += d.output;
|
|
108
|
+
}
|
|
109
|
+
function readTotals(v) {
|
|
110
|
+
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
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function totalsToBuckets(acc) {
|
|
118
|
+
return {
|
|
119
|
+
input: Math.max(0, acc.input - acc.cached),
|
|
120
|
+
output: acc.output,
|
|
121
|
+
cacheWrite5m: 0,
|
|
122
|
+
cacheWrite1h: 0,
|
|
123
|
+
cacheRead: acc.cached
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function sessionIdFromFilename(rolloutPath) {
|
|
127
|
+
const m = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(
|
|
128
|
+
basename(rolloutPath)
|
|
129
|
+
);
|
|
130
|
+
return m !== null ? m[1] : "";
|
|
131
|
+
}
|
|
132
|
+
async function readAll(path) {
|
|
133
|
+
try {
|
|
134
|
+
return await readFile2(path);
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function newSegmentBuf() {
|
|
140
|
+
return {
|
|
141
|
+
acc: zeroTotals(),
|
|
142
|
+
apiCalls: 0,
|
|
143
|
+
prompt: null,
|
|
144
|
+
turnCtxCwd: null,
|
|
145
|
+
firstTs: null,
|
|
146
|
+
endTs: null,
|
|
147
|
+
hasLines: false
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function scanWindow(rolloutPath, cursor) {
|
|
151
|
+
const buffer = await readAll(rolloutPath);
|
|
152
|
+
if (buffer === null) return null;
|
|
153
|
+
const fileSize = buffer.length;
|
|
154
|
+
let startOffset;
|
|
155
|
+
let rescan;
|
|
156
|
+
if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE) {
|
|
157
|
+
startOffset = cursor.offset;
|
|
158
|
+
rescan = false;
|
|
159
|
+
} else {
|
|
160
|
+
startOffset = 0;
|
|
161
|
+
rescan = cursor !== null;
|
|
162
|
+
}
|
|
163
|
+
const tsFloor = cursor?.lastTs ?? null;
|
|
164
|
+
const initTotals = cursor?.codexTotals;
|
|
165
|
+
let prev = initTotals !== void 0 ? { ...initTotals } : zeroTotals();
|
|
166
|
+
const acc = zeroTotals();
|
|
167
|
+
let apiCalls = 0;
|
|
168
|
+
let lastModel = null;
|
|
169
|
+
let windowPrompt = null;
|
|
170
|
+
let windowTurnCtxCwd = null;
|
|
171
|
+
let sessionMetaCwd = null;
|
|
172
|
+
let sessionMetaSid = null;
|
|
173
|
+
let isSubagentRollout = false;
|
|
174
|
+
let firstTs = null;
|
|
175
|
+
let lastTs = null;
|
|
176
|
+
const segments = [];
|
|
177
|
+
let seg = newSegmentBuf();
|
|
178
|
+
const snapshotSegment = (endOffset) => ({
|
|
179
|
+
acc: seg.acc,
|
|
180
|
+
apiCalls: seg.apiCalls,
|
|
181
|
+
prompt: seg.prompt,
|
|
182
|
+
model: lastModel,
|
|
183
|
+
cwd: seg.turnCtxCwd ?? sessionMetaCwd,
|
|
184
|
+
firstTs: seg.firstTs,
|
|
185
|
+
endTs: seg.endTs,
|
|
186
|
+
endOffset,
|
|
187
|
+
prevAtEnd: { ...prev },
|
|
188
|
+
lastTsAtEnd: lastTs
|
|
189
|
+
});
|
|
190
|
+
const handleLine = (raw, endOffset) => {
|
|
191
|
+
if (raw.trim().length === 0) return;
|
|
192
|
+
let obj;
|
|
193
|
+
try {
|
|
194
|
+
obj = JSON.parse(raw);
|
|
195
|
+
} catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!isRecord(obj)) return;
|
|
199
|
+
const ts = strOrNull(obj.timestamp);
|
|
200
|
+
if (rescan && tsFloor !== null && ts !== null && ts <= tsFloor) return;
|
|
201
|
+
if (ts !== null) {
|
|
202
|
+
if (firstTs === null || ts < firstTs) firstTs = ts;
|
|
203
|
+
if (lastTs === null || ts > lastTs) lastTs = ts;
|
|
204
|
+
if (seg.firstTs === null || ts < seg.firstTs) seg.firstTs = ts;
|
|
205
|
+
seg.endTs = ts;
|
|
206
|
+
}
|
|
207
|
+
seg.hasLines = true;
|
|
208
|
+
const payload = isRecord(obj.payload) ? obj.payload : null;
|
|
209
|
+
if (payload === null) return;
|
|
210
|
+
const type = obj.type;
|
|
211
|
+
if (type === "session_meta") {
|
|
212
|
+
const sid = strOrNull(payload.session_id);
|
|
213
|
+
if (sid !== null) sessionMetaSid = sid;
|
|
214
|
+
const c = strOrNull(payload.cwd);
|
|
215
|
+
if (c !== null) sessionMetaCwd = c;
|
|
216
|
+
const source = payload.source;
|
|
217
|
+
if (isRecord(source) && Object.hasOwn(source, "subagent")) isSubagentRollout = true;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (type === "turn_context") {
|
|
221
|
+
const m = strOrNull(payload.model);
|
|
222
|
+
if (m !== null) lastModel = m;
|
|
223
|
+
const c = strOrNull(payload.cwd);
|
|
224
|
+
if (c !== null) {
|
|
225
|
+
seg.turnCtxCwd = c;
|
|
226
|
+
windowTurnCtxCwd = c;
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (type !== "event_msg") return;
|
|
231
|
+
const kind = payload.type;
|
|
232
|
+
if (kind === "user_message") {
|
|
233
|
+
const msg = strOrNull(payload.message);
|
|
234
|
+
if (msg !== null) {
|
|
235
|
+
seg.prompt = msg;
|
|
236
|
+
windowPrompt = msg;
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (kind === "token_count") {
|
|
241
|
+
const info = isRecord(payload.info) ? payload.info : null;
|
|
242
|
+
if (info === null) return;
|
|
243
|
+
const total = readTotals(info.total_token_usage);
|
|
244
|
+
if (total === null) return;
|
|
245
|
+
let step = {
|
|
246
|
+
input: total.input - prev.input,
|
|
247
|
+
cached: total.cached - prev.cached,
|
|
248
|
+
output: total.output - prev.output
|
|
249
|
+
};
|
|
250
|
+
if (step.input < 0 || step.cached < 0 || step.output < 0) {
|
|
251
|
+
step = readTotals(info.last_token_usage) ?? zeroTotals();
|
|
252
|
+
}
|
|
253
|
+
addTotals(acc, step);
|
|
254
|
+
addTotals(seg.acc, step);
|
|
255
|
+
prev = total;
|
|
256
|
+
if (!isZeroTotals(step)) {
|
|
257
|
+
apiCalls++;
|
|
258
|
+
seg.apiCalls++;
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (kind === "task_complete") {
|
|
263
|
+
segments.push(snapshotSegment(endOffset));
|
|
264
|
+
seg = newSegmentBuf();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
let lineStart = startOffset;
|
|
268
|
+
for (let pos = startOffset; pos < fileSize; pos++) {
|
|
269
|
+
if (buffer[pos] !== NEWLINE) continue;
|
|
270
|
+
handleLine(buffer.toString("utf8", lineStart, pos), pos + 1);
|
|
271
|
+
lineStart = pos + 1;
|
|
272
|
+
}
|
|
273
|
+
const newOffset = lineStart;
|
|
274
|
+
const open = seg.hasLines ? snapshotSegment(newOffset) : null;
|
|
275
|
+
return {
|
|
276
|
+
segments,
|
|
277
|
+
open,
|
|
278
|
+
acc,
|
|
279
|
+
prev,
|
|
280
|
+
apiCalls,
|
|
281
|
+
model: lastModel,
|
|
282
|
+
prompt: windowPrompt,
|
|
283
|
+
cwd: windowTurnCtxCwd ?? sessionMetaCwd,
|
|
284
|
+
sessionId: sessionMetaSid ?? sessionIdFromFilename(rolloutPath),
|
|
285
|
+
isSubagentRollout,
|
|
286
|
+
firstTs,
|
|
287
|
+
lastTs,
|
|
288
|
+
newOffset
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function windowCursor(scan) {
|
|
292
|
+
return {
|
|
293
|
+
offset: scan.newOffset,
|
|
294
|
+
lastUuid: null,
|
|
295
|
+
// rollout に uuid 行は無い
|
|
296
|
+
lastTs: scan.lastTs,
|
|
297
|
+
seenMessageKeys: [],
|
|
298
|
+
// 去重は codexTotals の差分方式が担う
|
|
299
|
+
codexTotals: { ...scan.prev }
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function aggregateCodexTurn(rolloutPath, cursor) {
|
|
303
|
+
const scan = await scanWindow(rolloutPath, cursor);
|
|
304
|
+
if (scan === null || isZeroTotals(scan.acc)) return null;
|
|
305
|
+
return {
|
|
306
|
+
sessionId: scan.sessionId,
|
|
307
|
+
main: { [scan.model ?? "unknown"]: totalsToBuckets(scan.acc) },
|
|
308
|
+
sidechain: {},
|
|
309
|
+
// Codex にサブエージェント概念は無い
|
|
310
|
+
apiCalls: scan.apiCalls,
|
|
311
|
+
prompt: scan.prompt,
|
|
312
|
+
cwd: scan.cwd,
|
|
313
|
+
gitBranch: null,
|
|
314
|
+
// rollout に無い
|
|
315
|
+
firstTs: scan.firstTs,
|
|
316
|
+
lastTs: scan.lastTs,
|
|
317
|
+
newCursor: windowCursor(scan)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async function splitIntoCodexTurnDrafts(rolloutPath, cursor) {
|
|
321
|
+
const scan = await scanWindow(rolloutPath, cursor);
|
|
322
|
+
if (scan === null || isZeroTotals(scan.acc)) return null;
|
|
323
|
+
const picked = scan.segments.filter((s) => !isZeroTotals(s.acc));
|
|
324
|
+
if (scan.open !== null && !isZeroTotals(scan.open.acc)) {
|
|
325
|
+
picked.push(scan.open);
|
|
326
|
+
}
|
|
327
|
+
const lastIndex = picked.length - 1;
|
|
328
|
+
return picked.map((s, i) => ({
|
|
329
|
+
isSubagentRollout: scan.isSubagentRollout,
|
|
330
|
+
agg: {
|
|
331
|
+
sessionId: scan.sessionId,
|
|
332
|
+
// session_meta はファイル先頭にしか無いので全ドラフト共通
|
|
333
|
+
main: { [s.model ?? "unknown"]: totalsToBuckets(s.acc) },
|
|
334
|
+
sidechain: {},
|
|
335
|
+
apiCalls: s.apiCalls,
|
|
336
|
+
prompt: s.prompt,
|
|
337
|
+
cwd: s.cwd,
|
|
338
|
+
gitBranch: null,
|
|
339
|
+
firstTs: s.firstTs,
|
|
340
|
+
lastTs: s.endTs,
|
|
341
|
+
// 最後のドラフトはウィンドウ全体を消費した状態(= aggregateCodexTurn の newCursor と同一。
|
|
342
|
+
// 末尾の usage ゼロな行の読み捨てもここに含まれる)。途中のドラフトはそのセグメント末尾を
|
|
343
|
+
// 指す有効な再開点(そこから読み直せば残りが差分になる)。
|
|
344
|
+
newCursor: i === lastIndex ? windowCursor(scan) : {
|
|
345
|
+
offset: s.endOffset,
|
|
346
|
+
lastUuid: null,
|
|
347
|
+
lastTs: s.lastTsAtEnd,
|
|
348
|
+
seenMessageKeys: [],
|
|
349
|
+
codexTotals: { ...s.prevAtEnd }
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
endTs: s.endTs
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/transcript.ts
|
|
357
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
358
|
+
var NEWLINE2 = 10;
|
|
359
|
+
var MAX_SEEN_KEYS = 500;
|
|
360
|
+
var SYNTHETIC_MODEL = "<synthetic>";
|
|
361
|
+
function isRecord2(v) {
|
|
362
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
363
|
+
}
|
|
364
|
+
function numOf2(v) {
|
|
365
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
366
|
+
}
|
|
367
|
+
function strOrNull2(v) {
|
|
368
|
+
return typeof v === "string" ? v : null;
|
|
369
|
+
}
|
|
370
|
+
function emptyBuckets() {
|
|
371
|
+
return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
372
|
+
}
|
|
373
|
+
function addToModel(target, model, b) {
|
|
374
|
+
const cur = target[model] ?? emptyBuckets();
|
|
375
|
+
cur.input += b.input;
|
|
376
|
+
cur.output += b.output;
|
|
377
|
+
cur.cacheWrite5m += b.cacheWrite5m;
|
|
378
|
+
cur.cacheWrite1h += b.cacheWrite1h;
|
|
379
|
+
cur.cacheRead += b.cacheRead;
|
|
380
|
+
target[model] = cur;
|
|
381
|
+
}
|
|
382
|
+
function extractBucket(usage) {
|
|
383
|
+
const input = numOf2(usage.input_tokens);
|
|
384
|
+
const output = numOf2(usage.output_tokens);
|
|
385
|
+
const cacheRead = numOf2(usage.cache_read_input_tokens);
|
|
386
|
+
let cacheWrite5m;
|
|
387
|
+
let cacheWrite1h;
|
|
388
|
+
const cc = usage.cache_creation;
|
|
389
|
+
if (isRecord2(cc)) {
|
|
390
|
+
cacheWrite5m = numOf2(cc.ephemeral_5m_input_tokens);
|
|
391
|
+
cacheWrite1h = numOf2(cc.ephemeral_1h_input_tokens);
|
|
392
|
+
} else {
|
|
393
|
+
cacheWrite5m = numOf2(usage.cache_creation_input_tokens);
|
|
394
|
+
cacheWrite1h = 0;
|
|
395
|
+
}
|
|
396
|
+
return { input, output, cacheWrite5m, cacheWrite1h, cacheRead };
|
|
397
|
+
}
|
|
398
|
+
function promptCandidate(content) {
|
|
399
|
+
if (typeof content === "string") return content;
|
|
400
|
+
if (Array.isArray(content)) {
|
|
401
|
+
let hasToolResult = false;
|
|
402
|
+
const texts = [];
|
|
403
|
+
for (const block of content) {
|
|
404
|
+
if (!isRecord2(block)) continue;
|
|
405
|
+
if (block.type === "tool_result") hasToolResult = true;
|
|
406
|
+
else if (block.type === "text" && typeof block.text === "string") texts.push(block.text);
|
|
407
|
+
}
|
|
408
|
+
if (hasToolResult) return null;
|
|
409
|
+
return texts.join("\n");
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
async function readAll2(path) {
|
|
414
|
+
try {
|
|
415
|
+
return await readFile3(path);
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function aggregateNewTurn(transcriptPath, cursor, opts = {}) {
|
|
421
|
+
const buffer = await readAll2(transcriptPath);
|
|
422
|
+
if (buffer === null) return null;
|
|
423
|
+
const fileSize = buffer.length;
|
|
424
|
+
let startOffset;
|
|
425
|
+
let rescan;
|
|
426
|
+
if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE2) {
|
|
427
|
+
startOffset = cursor.offset;
|
|
428
|
+
rescan = false;
|
|
429
|
+
} else {
|
|
430
|
+
startOffset = 0;
|
|
431
|
+
rescan = cursor !== null;
|
|
432
|
+
}
|
|
433
|
+
const seenKeys = new Set(cursor?.seenMessageKeys ?? []);
|
|
434
|
+
const tsFloor = cursor?.lastTs ?? null;
|
|
435
|
+
const pending = /* @__PURE__ */ new Map();
|
|
436
|
+
const consumedKeys = /* @__PURE__ */ new Set();
|
|
437
|
+
let sessionId = "";
|
|
438
|
+
let cwd = null;
|
|
439
|
+
let gitBranch = null;
|
|
440
|
+
let firstTs = null;
|
|
441
|
+
let lastTs = null;
|
|
442
|
+
let lastUuid = null;
|
|
443
|
+
let prompt = null;
|
|
444
|
+
const handleLine = (raw) => {
|
|
445
|
+
if (raw.trim().length === 0) return;
|
|
446
|
+
let obj;
|
|
447
|
+
try {
|
|
448
|
+
obj = JSON.parse(raw);
|
|
449
|
+
} catch {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!isRecord2(obj)) return;
|
|
453
|
+
const ts = strOrNull2(obj.timestamp);
|
|
454
|
+
if (rescan && tsFloor !== null && ts !== null && ts <= tsFloor) return;
|
|
455
|
+
const isSide = obj.isSidechain === true;
|
|
456
|
+
const sid = strOrNull2(obj.sessionId);
|
|
457
|
+
if (sid !== null) sessionId = sid;
|
|
458
|
+
if (!isSide) {
|
|
459
|
+
const c = strOrNull2(obj.cwd);
|
|
460
|
+
if (c !== null) cwd = c;
|
|
461
|
+
const gb = strOrNull2(obj.gitBranch);
|
|
462
|
+
if (gb !== null) gitBranch = gb;
|
|
463
|
+
}
|
|
464
|
+
if (ts !== null) {
|
|
465
|
+
if (firstTs === null || ts < firstTs) firstTs = ts;
|
|
466
|
+
if (lastTs === null || ts > lastTs) lastTs = ts;
|
|
467
|
+
}
|
|
468
|
+
const uuid = strOrNull2(obj.uuid);
|
|
469
|
+
if (uuid !== null) lastUuid = uuid;
|
|
470
|
+
const type = obj.type;
|
|
471
|
+
const message = isRecord2(obj.message) ? obj.message : null;
|
|
472
|
+
if (type === "user" && !isSide && message !== null) {
|
|
473
|
+
const cand = promptCandidate(message.content);
|
|
474
|
+
if (cand !== null) {
|
|
475
|
+
const t = cand.trim();
|
|
476
|
+
if (t.length > 0 && !t.startsWith("<")) prompt = t;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (type === "assistant" && message !== null) {
|
|
480
|
+
const usage = message.usage;
|
|
481
|
+
if (isRecord2(usage)) {
|
|
482
|
+
const rawModel = message.model;
|
|
483
|
+
if (rawModel !== SYNTHETIC_MODEL) {
|
|
484
|
+
const id = strOrNull2(message.id) ?? "";
|
|
485
|
+
const reqId = strOrNull2(obj.requestId) ?? "";
|
|
486
|
+
const key = `${id}:${reqId}`;
|
|
487
|
+
if (!seenKeys.has(key)) {
|
|
488
|
+
const tsMs = ts === null ? NaN : Date.parse(ts);
|
|
489
|
+
const outsidePeriod = typeof opts.minTimestampMs === "number" && (!Number.isFinite(tsMs) || tsMs < opts.minTimestampMs);
|
|
490
|
+
if (opts.excludeMessageKeys?.has(key) || outsidePeriod) {
|
|
491
|
+
consumedKeys.add(key);
|
|
492
|
+
pending.delete(key);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const model = strOrNull2(rawModel) ?? "unknown";
|
|
496
|
+
pending.set(key, { model, isSidechain: isSide, bucket: extractBucket(usage) });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
let lineStart = startOffset;
|
|
503
|
+
for (let pos = startOffset; pos < fileSize; pos++) {
|
|
504
|
+
if (buffer[pos] !== NEWLINE2) continue;
|
|
505
|
+
handleLine(buffer.toString("utf8", lineStart, pos));
|
|
506
|
+
lineStart = pos + 1;
|
|
507
|
+
}
|
|
508
|
+
const newOffset = lineStart;
|
|
509
|
+
if (pending.size === 0 && !opts.returnEmpty) return null;
|
|
510
|
+
const main = {};
|
|
511
|
+
const sidechain = {};
|
|
512
|
+
const newKeys = [];
|
|
513
|
+
for (const [key, pm] of pending) {
|
|
514
|
+
newKeys.push(key);
|
|
515
|
+
if (pm.isSidechain) addToModel(sidechain, pm.model, pm.bucket);
|
|
516
|
+
else addToModel(main, pm.model, pm.bucket);
|
|
517
|
+
}
|
|
518
|
+
const combined = [...cursor?.seenMessageKeys ?? [], ...consumedKeys, ...newKeys];
|
|
519
|
+
const seenMessageKeys = combined.length > MAX_SEEN_KEYS ? combined.slice(combined.length - MAX_SEEN_KEYS) : combined;
|
|
520
|
+
return {
|
|
521
|
+
sessionId,
|
|
522
|
+
main,
|
|
523
|
+
sidechain,
|
|
524
|
+
apiCalls: pending.size,
|
|
525
|
+
messageKeys: newKeys,
|
|
526
|
+
prompt,
|
|
527
|
+
cwd,
|
|
528
|
+
gitBranch,
|
|
529
|
+
firstTs,
|
|
530
|
+
lastTs,
|
|
531
|
+
newCursor: {
|
|
532
|
+
offset: newOffset,
|
|
533
|
+
lastUuid: lastUuid ?? cursor?.lastUuid ?? null,
|
|
534
|
+
lastTs: lastTs ?? cursor?.lastTs ?? null,
|
|
535
|
+
seenMessageKeys
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export {
|
|
541
|
+
getUsdJpy,
|
|
542
|
+
aggregateCodexTurn,
|
|
543
|
+
splitIntoCodexTurnDrafts,
|
|
544
|
+
extractBucket,
|
|
545
|
+
promptCandidate,
|
|
546
|
+
aggregateNewTurn
|
|
547
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/codex/sessions.ts
|
|
4
|
+
import { promises as fsp } from "fs";
|
|
5
|
+
import { join, resolve } from "path";
|
|
6
|
+
var CODEX_MAX_DEPTH = 4;
|
|
7
|
+
async function listCodexRollouts(sessionsRoot) {
|
|
8
|
+
const rollouts = [];
|
|
9
|
+
let unreadableDirs = 0;
|
|
10
|
+
const root = resolve(sessionsRoot);
|
|
11
|
+
const rootStat = await fsp.lstat(root).catch(() => null);
|
|
12
|
+
if (rootStat === null || !rootStat.isDirectory()) {
|
|
13
|
+
return { rollouts, unreadableDirs: 1 };
|
|
14
|
+
}
|
|
15
|
+
const walk = async (dir, depth) => {
|
|
16
|
+
const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
17
|
+
if (entries === null) {
|
|
18
|
+
unreadableDirs += 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const full = resolve(join(dir, entry.name));
|
|
23
|
+
if (entry.isFile()) {
|
|
24
|
+
if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) rollouts.push(full);
|
|
25
|
+
} else if (entry.isDirectory() && depth < CODEX_MAX_DEPTH) {
|
|
26
|
+
await walk(full, depth + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
await walk(root, 1);
|
|
31
|
+
return { rollouts, unreadableDirs };
|
|
32
|
+
}
|
|
33
|
+
async function findLatestCodexRollout(sessionsRoot) {
|
|
34
|
+
const discovery = await listCodexRollouts(sessionsRoot);
|
|
35
|
+
let latest = null;
|
|
36
|
+
let latestMtime = -Infinity;
|
|
37
|
+
let unreadableFiles = 0;
|
|
38
|
+
for (const path of discovery.rollouts) {
|
|
39
|
+
const stat = await fsp.lstat(path).catch(() => null);
|
|
40
|
+
if (stat === null || !stat.isFile()) {
|
|
41
|
+
unreadableFiles += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (stat.mtimeMs > latestMtime || stat.mtimeMs === latestMtime && (latest === null || path < latest)) {
|
|
45
|
+
latest = path;
|
|
46
|
+
latestMtime = stat.mtimeMs;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { latest, unreadableDirs: discovery.unreadableDirs, unreadableFiles };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
listCodexRollouts,
|
|
54
|
+
findLatestCodexRollout
|
|
55
|
+
};
|