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,100 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  paths
4
- } from "./chunk-OOAC5ULQ.js";
4
+ } from "./chunk-WONMZ466.js";
5
5
 
6
- // src/dashboard-state.ts
6
+ // src/data-lock.ts
7
+ import { randomUUID } from "crypto";
8
+ import { hostname } from "os";
7
9
  import {
8
- closeSync,
9
10
  existsSync,
10
- openSync,
11
+ mkdirSync,
11
12
  readFileSync,
12
- readSync,
13
13
  renameSync,
14
14
  rmSync,
15
15
  writeFileSync
16
16
  } from "fs";
17
- import { randomUUID } from "crypto";
18
- function localDate(now) {
19
- const y = now.getFullYear();
20
- const m = String(now.getMonth() + 1).padStart(2, "0");
21
- const d = String(now.getDate()).padStart(2, "0");
22
- return `${y}-${m}-${d}`;
23
- }
24
- function timeZone() {
25
- return Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
26
- }
27
- function makeFullDashboardState(now = /* @__PURE__ */ new Date()) {
28
- return { localDate: localDate(now), timeZone: timeZone(), generatedAt: now.toISOString() };
29
- }
30
- function readState() {
31
- const file = paths().dashboardFullStateFile;
32
- if (!existsSync(file)) return null;
33
- try {
34
- const value = JSON.parse(readFileSync(file, "utf8"));
35
- if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
36
- const v = value;
37
- if (typeof v.localDate !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v.localDate) || typeof v.timeZone !== "string" || v.timeZone.length === 0 || typeof v.generatedAt !== "string" || !Number.isFinite(Date.parse(v.generatedAt))) {
38
- return null;
39
- }
40
- return v;
41
- } catch {
42
- return null;
43
- }
44
- }
45
- function isFullDashboardDue(now = /* @__PURE__ */ new Date()) {
46
- const p = paths();
47
- if (!existsSync(p.fullDashboardFile)) return true;
48
- try {
49
- const fd = openSync(p.fullDashboardFile, "r");
50
- try {
51
- const head = Buffer.alloc(512);
52
- const n = readSync(fd, head, 0, head.length, 0);
53
- if (head.toString("utf8", 0, n).includes('name="cccn-placeholder"')) return true;
54
- } finally {
55
- closeSync(fd);
56
- }
57
- } catch {
58
- return true;
59
- }
60
- const state = readState();
61
- if (state === null) return true;
62
- const expected = makeFullDashboardState(now);
63
- if (state.timeZone !== expected.timeZone) return true;
64
- if (state.localDate !== expected.localDate) return true;
65
- if (state.localDate > expected.localDate) return true;
66
- if (Date.parse(state.generatedAt) > now.getTime()) return true;
67
- return false;
68
- }
69
- function writeFullDashboardStateAtomic(state) {
70
- const file = paths().dashboardFullStateFile;
71
- const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
72
- try {
73
- writeFileSync(tmp, `${JSON.stringify(state)}
74
- `, "utf8");
75
- renameSync(tmp, file);
76
- } finally {
77
- rmSync(tmp, { force: true });
78
- }
79
- }
80
- function invalidateCanonicalDashboards() {
81
- const p = paths();
82
- for (const file of [p.recentDashboardFile, p.fullDashboardFile, p.dashboardFullStateFile]) {
83
- rmSync(file, { force: true });
84
- }
85
- }
86
-
87
- // src/data-lock.ts
88
- import { randomUUID as randomUUID2 } from "crypto";
89
- import { hostname } from "os";
90
- import {
91
- existsSync as existsSync2,
92
- mkdirSync,
93
- readFileSync as readFileSync2,
94
- renameSync as renameSync2,
95
- rmSync as rmSync2,
96
- writeFileSync as writeFileSync2
97
- } from "fs";
98
17
  import { join } from "path";
99
18
  var DATA_LOCK_LEASE_MS = 3e4;
100
19
  var HEARTBEAT_MS = 5e3;
@@ -103,7 +22,7 @@ function ownerFile(dir) {
103
22
  }
104
23
  function readOwner(dir) {
105
24
  try {
106
- const v = JSON.parse(readFileSync2(ownerFile(dir), "utf8"));
25
+ const v = JSON.parse(readFileSync(ownerFile(dir), "utf8"));
107
26
  if (typeof v.token !== "string" || typeof v.pid !== "number" || typeof v.hostname !== "string" || typeof v.acquiredAt !== "string" || typeof v.heartbeatAt !== "string") return null;
108
27
  return v;
109
28
  } catch {
@@ -111,21 +30,21 @@ function readOwner(dir) {
111
30
  }
112
31
  }
113
32
  function writeOwner(dir, owner) {
114
- const tmp = join(dir, `owner.${owner.token}.${randomUUID2()}.tmp`);
33
+ const tmp = join(dir, `owner.${owner.token}.${randomUUID()}.tmp`);
115
34
  try {
116
- writeFileSync2(tmp, `${JSON.stringify(owner)}
35
+ writeFileSync(tmp, `${JSON.stringify(owner)}
117
36
  `, "utf8");
118
- renameSync2(tmp, ownerFile(dir));
37
+ renameSync(tmp, ownerFile(dir));
119
38
  } finally {
120
- rmSync2(tmp, { force: true });
39
+ rmSync(tmp, { force: true });
121
40
  }
122
41
  }
123
42
  function quarantineOwned(dir, token, label) {
124
43
  const before = readOwner(dir);
125
44
  if (before?.token !== token) return false;
126
- const quarantine = `${dir}.${label}-${token}-${randomUUID2()}`;
45
+ const quarantine = `${dir}.${label}-${token}-${randomUUID()}`;
127
46
  try {
128
- renameSync2(dir, quarantine);
47
+ renameSync(dir, quarantine);
129
48
  } catch {
130
49
  return false;
131
50
  }
@@ -133,21 +52,21 @@ function quarantineOwned(dir, token, label) {
133
52
  if (moved?.token !== token) {
134
53
  return false;
135
54
  }
136
- rmSync2(quarantine, { recursive: true, force: true });
55
+ rmSync(quarantine, { recursive: true, force: true });
137
56
  return true;
138
57
  }
139
58
  function claimDir(fixed, label, now, metadataWriter = writeOwner) {
140
- const token = randomUUID2();
59
+ const token = randomUUID();
141
60
  const staging = `${fixed}.${label}-${token}`;
142
61
  const iso = now.toISOString();
143
62
  const owner = { token, pid: process.pid, hostname: hostname(), acquiredAt: iso, heartbeatAt: iso };
144
63
  try {
145
64
  mkdirSync(staging);
146
65
  metadataWriter(staging, owner);
147
- renameSync2(staging, fixed);
66
+ renameSync(staging, fixed);
148
67
  return { token, owner };
149
68
  } catch {
150
- rmSync2(staging, { recursive: true, force: true });
69
+ rmSync(staging, { recursive: true, force: true });
151
70
  return null;
152
71
  }
153
72
  }
@@ -180,15 +99,15 @@ function tryReclaim(now, leaseMs) {
180
99
  if (!processDefinitelyDead(first.pid)) return false;
181
100
  const second = readOwner(p.dataLockDir);
182
101
  if (second === null || second.token !== first.token || second.heartbeatAt !== first.heartbeatAt) return false;
183
- const orphan = `${p.dataLockDir}.orphan-${first.token}-${randomUUID2()}`;
102
+ const orphan = `${p.dataLockDir}.orphan-${first.token}-${randomUUID()}`;
184
103
  try {
185
- renameSync2(p.dataLockDir, orphan);
104
+ renameSync(p.dataLockDir, orphan);
186
105
  } catch {
187
106
  return false;
188
107
  }
189
108
  const moved = readOwner(orphan);
190
109
  if (moved?.token !== first.token || moved.heartbeatAt !== first.heartbeatAt) return false;
191
- rmSync2(orphan, { recursive: true, force: true });
110
+ rmSync(orphan, { recursive: true, force: true });
192
111
  return true;
193
112
  } finally {
194
113
  quarantineOwned(p.dataReclaimDir, guard.token, "released");
@@ -198,15 +117,15 @@ function acquireDataLock(opts = {}) {
198
117
  const now = opts.now ?? /* @__PURE__ */ new Date();
199
118
  const leaseMs = opts.leaseMs ?? DATA_LOCK_LEASE_MS;
200
119
  const p = paths();
201
- if (existsSync2(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
120
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
202
121
  let claim = claimDir(p.dataLockDir, "acquire", now, opts.metadataWriter);
203
122
  if (claim === null) {
204
123
  if (!tryReclaim(now, leaseMs)) return null;
205
- if (existsSync2(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
124
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) return null;
206
125
  claim = claimDir(p.dataLockDir, "acquire", now, opts.metadataWriter);
207
126
  if (claim === null) return null;
208
127
  }
209
- if (existsSync2(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) {
128
+ if (existsSync(p.dataReclaimDir) && reclaimerGuardBlocks(now, leaseMs)) {
210
129
  quarantineOwned(p.dataLockDir, claim.token, "yielded");
211
130
  return null;
212
131
  }
@@ -249,9 +168,5 @@ async function waitForDataLock(timeoutMs, pollMs = 25) {
249
168
  }
250
169
 
251
170
  export {
252
- makeFullDashboardState,
253
- isFullDashboardDue,
254
- writeFullDashboardStateAtomic,
255
- invalidateCanonicalDashboards,
256
171
  waitForDataLock
257
172
  };
@@ -220,7 +220,24 @@ async function loadPriceTable(cacheDir, opts) {
220
220
  }
221
221
  }
222
222
 
223
+ // src/codex/env.ts
224
+ import { statSync } from "fs";
225
+ import { homedir } from "os";
226
+ import { join } from "path";
227
+ function codexHome() {
228
+ return process.env.CCCN_CODEX_HOME || join(homedir(), ".codex");
229
+ }
230
+ function detectCodex() {
231
+ try {
232
+ return statSync(codexHome()).isDirectory();
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+
223
238
  export {
224
239
  computeCost,
225
- loadPriceTable
240
+ loadPriceTable,
241
+ codexHome,
242
+ detectCodex
226
243
  };
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ notifyOS,
4
+ notifySlack
5
+ } from "./chunk-JLDI4D5E.js";
6
+ import {
7
+ attachSubagentGroups,
8
+ collectSubagentUsage,
9
+ normalizeCodexOriginator,
10
+ splitIntoTurnDrafts
11
+ } from "./chunk-6BXD6HU5.js";
12
+ import {
13
+ anyOf,
14
+ callFingerprints,
15
+ claudeTranscriptRoots,
16
+ codexResumePointAtTs,
17
+ getUsdJpy,
18
+ listCodexRollouts,
19
+ messageKeyFilterOf,
20
+ scanCodexTurns,
21
+ setCountedCalls,
22
+ surfaceForClaudePath
23
+ } from "./chunk-OCKZZVOG.js";
24
+ import {
25
+ codexHome,
26
+ computeCost,
27
+ loadPriceTable
28
+ } from "./chunk-Q45AAPJY.js";
29
+ import {
30
+ waitForDataLock
31
+ } from "./chunk-PXMXEFM7.js";
32
+ import {
33
+ formatIngestSummary
34
+ } from "./chunk-XG73C6CC.js";
35
+ import {
36
+ appendTurn,
37
+ floorKey,
38
+ isMuted,
39
+ loadAllCursors,
40
+ loadHistoryIndex,
41
+ logError,
42
+ paths,
43
+ readConfig,
44
+ readConfigReadOnly,
45
+ sanitizeCursor,
46
+ saveAllCursors
47
+ } from "./chunk-WONMZ466.js";
48
+
49
+ // src/ingest.ts
50
+ import { promises as fsp, readFileSync, renameSync, writeFileSync } from "fs";
51
+ import { basename, join } from "path";
52
+ function emptyBuckets() {
53
+ return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
54
+ }
55
+ function sumBuckets(usage) {
56
+ const total = emptyBuckets();
57
+ for (const b of Object.values(usage)) {
58
+ total.input += b.input;
59
+ total.output += b.output;
60
+ total.cacheWrite5m += b.cacheWrite5m;
61
+ total.cacheWrite1h += b.cacheWrite1h;
62
+ total.cacheRead += b.cacheRead;
63
+ }
64
+ return total;
65
+ }
66
+ function collectModels(main, sidechain) {
67
+ const models = [];
68
+ for (const m of Object.keys(main)) if (!models.includes(m)) models.push(m);
69
+ for (const m of Object.keys(sidechain)) if (!models.includes(m)) models.push(m);
70
+ return models;
71
+ }
72
+ async function listProjectDirsBestEffort(root) {
73
+ try {
74
+ const entries = await fsp.readdir(root, { withFileTypes: true });
75
+ return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name));
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+ async function listTranscriptsBestEffort(projectDir) {
81
+ try {
82
+ const entries = await fsp.readdir(projectDir, { withFileTypes: true });
83
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join(projectDir, e.name));
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+ async function discoverClaudeFiles(roots) {
89
+ const files = [];
90
+ for (const root of roots) {
91
+ for (const dir of await listProjectDirsBestEffort(root.path)) {
92
+ files.push(...await listTranscriptsBestEffort(dir));
93
+ }
94
+ }
95
+ return files;
96
+ }
97
+ async function discoverCodexFiles() {
98
+ const sessionsRoot = join(codexHome(), "sessions");
99
+ try {
100
+ const stat = await fsp.lstat(sessionsRoot);
101
+ if (!stat.isDirectory()) return [];
102
+ } catch {
103
+ return [];
104
+ }
105
+ try {
106
+ const discovery = await listCodexRollouts(sessionsRoot);
107
+ return discovery.rollouts;
108
+ } catch {
109
+ return [];
110
+ }
111
+ }
112
+ function ingestMtimeCachePath() {
113
+ return join(paths().cacheDir, "ingest-mtimes.json");
114
+ }
115
+ function loadIngestMtimeCache() {
116
+ try {
117
+ const raw = readFileSync(ingestMtimeCachePath(), "utf8");
118
+ const parsed = JSON.parse(raw);
119
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
120
+ return parsed;
121
+ }
122
+ } catch {
123
+ }
124
+ return {};
125
+ }
126
+ function saveIngestMtimeCache(cache) {
127
+ try {
128
+ const file = ingestMtimeCachePath();
129
+ const tmp = `${file}.tmp`;
130
+ writeFileSync(tmp, JSON.stringify(cache), "utf8");
131
+ renameSync(tmp, file);
132
+ } catch {
133
+ }
134
+ }
135
+ async function fileMtimeMs(filePath) {
136
+ return (await fsp.stat(filePath)).mtimeMs;
137
+ }
138
+ async function claudeMtimeSignature(filePath) {
139
+ let newest = (await fsp.stat(filePath)).mtimeMs;
140
+ const dir = join(
141
+ filePath.endsWith(".jsonl") ? filePath.slice(0, -".jsonl".length) : filePath,
142
+ "subagents"
143
+ );
144
+ let entries;
145
+ try {
146
+ entries = await fsp.readdir(dir, { withFileTypes: true });
147
+ } catch (err) {
148
+ if (err?.code === "ENOENT") return newest;
149
+ throw err;
150
+ }
151
+ for (const entry of entries) {
152
+ if (!entry.isFile() || !entry.name.startsWith("agent-") || !entry.name.endsWith(".jsonl")) continue;
153
+ try {
154
+ const m = (await fsp.stat(join(dir, entry.name))).mtimeMs;
155
+ if (m > newest) newest = m;
156
+ } catch (err) {
157
+ if (err?.code !== "ENOENT") throw err;
158
+ }
159
+ }
160
+ return newest;
161
+ }
162
+ function recoveryCursor(floorTs) {
163
+ return { offset: 0, lastUuid: null, lastTs: floorTs, seenMessageKeys: [] };
164
+ }
165
+ function buildClaudeDraftRecord(draft, ts, table, fx, surface) {
166
+ const main = draft.mainPerModel;
167
+ const sidechain = draft.sidechainPerModel;
168
+ const breakdown = computeCost(main, sidechain, table);
169
+ const sidechainHasModels = Object.keys(sidechain).length > 0;
170
+ const rec = {
171
+ schemaVersion: 1,
172
+ ts,
173
+ sessionId: draft.sessionId,
174
+ project: draft.cwd ?? "",
175
+ gitBranch: draft.gitBranch,
176
+ models: collectModels(main, sidechain),
177
+ tokens: sumBuckets(main),
178
+ sidechainTokens: sidechainHasModels ? sumBuckets(sidechain) : null,
179
+ apiCalls: draft.apiCalls,
180
+ costUSD: breakdown.usd,
181
+ costByModel: breakdown.byModel,
182
+ costJPY: breakdown.usd * fx.rate,
183
+ fxRate: fx.rate,
184
+ fxSource: fx.source,
185
+ prompt: draft.prompt,
186
+ ingest: "scan",
187
+ surface
188
+ };
189
+ if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
190
+ setCountedCalls(rec, callFingerprints(draft.messageKeys));
191
+ return rec;
192
+ }
193
+ function sessionIdOfDrafts(drafts) {
194
+ for (let i = drafts.length - 1; i >= 0; i--) {
195
+ if (drafts[i].sessionId.length > 0) return drafts[i].sessionId;
196
+ }
197
+ return "";
198
+ }
199
+ function sessionIdFromTranscriptPath(filePath) {
200
+ const name = basename(filePath);
201
+ return name.endsWith(".jsonl") ? name.slice(0, -".jsonl".length) : "";
202
+ }
203
+ function buildCodexRecord(agg, table, fx) {
204
+ const main = agg.main;
205
+ const breakdown = computeCost(main, {}, table);
206
+ const surface = normalizeCodexOriginator(agg.originator);
207
+ const rec = {
208
+ schemaVersion: 1,
209
+ ts: agg.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(),
210
+ sessionId: agg.sessionId,
211
+ project: agg.cwd ?? "",
212
+ gitBranch: null,
213
+ models: collectModels(main, {}),
214
+ tokens: sumBuckets(main),
215
+ sidechainTokens: null,
216
+ apiCalls: agg.apiCalls,
217
+ costUSD: breakdown.usd,
218
+ costByModel: breakdown.byModel,
219
+ costJPY: breakdown.usd * fx.rate,
220
+ fxRate: fx.rate,
221
+ fxSource: fx.source,
222
+ prompt: agg.prompt ?? "",
223
+ ingest: "scan",
224
+ source: "codex",
225
+ surface
226
+ };
227
+ if (typeof agg.originator === "string") rec.originator = agg.originator;
228
+ if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
229
+ setCountedCalls(rec, agg.codexEventKeys ?? []);
230
+ return rec;
231
+ }
232
+ function emptyResult(dryRun, fx, lockAcquired) {
233
+ return {
234
+ dryRun,
235
+ lockAcquired,
236
+ records: [],
237
+ scannedFiles: 0,
238
+ skippedByMtime: 0,
239
+ skippedDuplicates: 0,
240
+ failures: 0,
241
+ totalUSD: 0,
242
+ totalJPY: 0,
243
+ fxRate: fx.rate,
244
+ fxSource: fx.source,
245
+ bySurface: {}
246
+ };
247
+ }
248
+ function addRecord(result, rec) {
249
+ const usd = rec.costUSD + (rec.subagents?.costUSD ?? 0);
250
+ const jpy = rec.costJPY + (rec.subagents?.costUSD ?? 0) * rec.fxRate;
251
+ result.records.push(rec);
252
+ result.totalUSD += usd;
253
+ result.totalJPY += jpy;
254
+ const surface = rec.surface ?? "cli";
255
+ const cur = result.bySurface[surface] ?? { turns: 0, usd: 0 };
256
+ cur.turns += 1;
257
+ cur.usd += usd;
258
+ result.bySurface[surface] = cur;
259
+ }
260
+ async function runIngest(opts) {
261
+ const dryRun = opts.dryRun;
262
+ const cfg = opts.cfg ?? (dryRun ? readConfigReadOnly() : readConfig());
263
+ const cacheDir = paths().cacheDir;
264
+ const table = await loadPriceTable(cacheDir, { offline: opts.offlinePricing ?? true });
265
+ const fx = await getUsdJpy(cfg, cacheDir);
266
+ const claudeRoots = opts.claudeRoots ?? await claudeTranscriptRoots();
267
+ const includeCodex = opts.includeCodex ?? true;
268
+ const claudeFiles = await discoverClaudeFiles(claudeRoots);
269
+ const codexFiles = includeCodex ? await discoverCodexFiles() : [];
270
+ if (dryRun) {
271
+ return await processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, true);
272
+ }
273
+ const lock = await waitForDataLock(1e3);
274
+ if (lock === null) {
275
+ logError("ingest", new Error("data lock timeout; ingest skipped this cycle"));
276
+ return emptyResult(false, fx, false);
277
+ }
278
+ try {
279
+ return await processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, false);
280
+ } finally {
281
+ lock.release();
282
+ }
283
+ }
284
+ async function processFiles(claudeFiles, codexFiles, claudeRoots, table, fx, dryRun) {
285
+ const mtimeCache = loadIngestMtimeCache();
286
+ const nextMtimeCache = { ...mtimeCache };
287
+ const result = emptyResult(dryRun, fx, true);
288
+ const cursorsDict = loadAllCursors();
289
+ let cursorsChanged = false;
290
+ let index = null;
291
+ const historyIndex = () => index ??= loadHistoryIndex();
292
+ const recordedFloor = (scope, sessionId) => {
293
+ if (sessionId.length === 0) return null;
294
+ return historyIndex().legacyFloors.get(floorKey(scope, sessionId)) ?? null;
295
+ };
296
+ const countedFilter = () => messageKeyFilterOf(historyIndex().countedCalls);
297
+ const commit = (filePath, records, newCursor, subagentCursors = []) => {
298
+ const fresh = [];
299
+ for (const rec of records) {
300
+ const key = rec.ingestKey;
301
+ if (key !== void 0 && historyIndex().ingestKeys.has(key)) {
302
+ result.skippedDuplicates += 1;
303
+ continue;
304
+ }
305
+ if (key !== void 0) historyIndex().ingestKeys.add(key);
306
+ for (const fp of rec.countedCalls ?? []) historyIndex().countedCalls.add(fp);
307
+ fresh.push(rec);
308
+ }
309
+ if (!dryRun) {
310
+ for (const rec of fresh) appendTurn(rec);
311
+ cursorsDict[filePath] = newCursor;
312
+ for (const nc of subagentCursors) cursorsDict[nc.path] = nc.cursor;
313
+ cursorsChanged = true;
314
+ }
315
+ for (const rec of fresh) addRecord(result, rec);
316
+ };
317
+ const carryCursor = (next, prev, floor) => ({
318
+ ...next,
319
+ lastUuid: next.lastUuid ?? prev?.lastUuid ?? null,
320
+ lastTs: next.lastTs ?? prev?.lastTs ?? floor
321
+ });
322
+ const processClaudeFile = async (filePath) => {
323
+ const cursor = sanitizeCursor(cursorsDict[filePath]);
324
+ const counted = countedFilter();
325
+ let split = await splitIntoTurnDrafts(filePath, cursor, { excludeMessageKeys: counted });
326
+ if (split.unreadable === true) throw new Error(`transcript \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F: ${filePath}`);
327
+ const sessionId = sessionIdOfDrafts(split.drafts) || sessionIdFromTranscriptPath(filePath);
328
+ let floor = null;
329
+ if (cursor === null) {
330
+ floor = recordedFloor("claude", sessionId);
331
+ if (floor !== null) {
332
+ split = await splitIntoTurnDrafts(filePath, recoveryCursor(floor), { excludeMessageKeys: counted });
333
+ }
334
+ }
335
+ const newCursor = carryCursor(split.newCursor, cursor, floor);
336
+ const surface = surfaceForClaudePath(filePath, claudeRoots);
337
+ const records = split.drafts.map(
338
+ (draft) => buildClaudeDraftRecord(draft, draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString(), table, fx, surface)
339
+ );
340
+ const saFloor = recordedFloor("claude-sa", sessionId);
341
+ let sa = null;
342
+ try {
343
+ sa = await collectSubagentUsage(filePath, {
344
+ excludeMessageKeys: anyOf([new Set(split.messageKeys), counted]),
345
+ recovery: () => ({ minTimestampMs: saFloor === null ? null : Date.parse(saFloor) + 1 }),
346
+ readCursor: (path) => sanitizeCursor(cursorsDict[path])
347
+ });
348
+ } catch (err) {
349
+ logError("ingest:subagents", err);
350
+ sa = null;
351
+ }
352
+ if (sa !== null && sa.apiCalls > 0) {
353
+ attachSubagentGroups(records, sa, table, fx, surface, "scan");
354
+ }
355
+ commit(filePath, records, newCursor, sa?.newCursors ?? []);
356
+ };
357
+ const processCodexFile = async (filePath) => {
358
+ const cursor = sanitizeCursor(cursorsDict[filePath]);
359
+ const scanOpts = { excludeEvents: historyIndex().countedCalls };
360
+ const scan = await scanCodexTurns(filePath, cursor, scanOpts);
361
+ if (scan === null) throw new Error(`rollout \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F: ${filePath}`);
362
+ const drafts = scan.drafts;
363
+ if (drafts.length === 0) {
364
+ commit(filePath, [], scan.newCursor);
365
+ return;
366
+ }
367
+ const windowCursor = scan.newCursor;
368
+ if (drafts[0].isSubagentRollout) {
369
+ commit(filePath, [], windowCursor);
370
+ return;
371
+ }
372
+ if (cursor !== null) {
373
+ commit(filePath, drafts.map((draft) => buildCodexRecord(draft.agg, table, fx)), windowCursor);
374
+ return;
375
+ }
376
+ const floor = recordedFloor("codex", drafts[0].agg.sessionId);
377
+ const target = floor === null ? drafts : (await scanCodexTurns(filePath, await codexResumePointAtTs(filePath, floor), scanOpts))?.drafts ?? [];
378
+ commit(filePath, target.map((draft) => buildCodexRecord(draft.agg, table, fx)), windowCursor);
379
+ };
380
+ const runOver = async (files, context, handle, signature = fileMtimeMs) => {
381
+ for (const filePath of files) {
382
+ let mtimeMs;
383
+ try {
384
+ mtimeMs = await signature(filePath);
385
+ } catch (err) {
386
+ if (err?.code !== "ENOENT") {
387
+ result.failures += 1;
388
+ logError(context, err);
389
+ }
390
+ continue;
391
+ }
392
+ const cached = mtimeCache[filePath];
393
+ if (cached !== void 0 && cached >= mtimeMs && sanitizeCursor(cursorsDict[filePath]) !== null) {
394
+ result.skippedByMtime += 1;
395
+ continue;
396
+ }
397
+ result.scannedFiles += 1;
398
+ try {
399
+ await handle(filePath);
400
+ nextMtimeCache[filePath] = mtimeMs;
401
+ } catch (err) {
402
+ result.failures += 1;
403
+ logError(context, err);
404
+ }
405
+ }
406
+ };
407
+ await runOver(claudeFiles, "ingest:claude", processClaudeFile, claudeMtimeSignature);
408
+ await runOver(codexFiles, "ingest:codex", processCodexFile);
409
+ if (!dryRun) {
410
+ if (cursorsChanged) saveAllCursors(cursorsDict);
411
+ saveIngestMtimeCache(nextMtimeCache);
412
+ }
413
+ return result;
414
+ }
415
+ async function notifyIngestSummary(result, cfg) {
416
+ if (result.dryRun) return;
417
+ if (result.records.length === 0) return;
418
+ if (!(cfg.notify.os || cfg.notify.slack)) return;
419
+ if (result.totalUSD < cfg.minNotifyUSD) return;
420
+ if (isMuted()) return;
421
+ const bySurface = {};
422
+ for (const [surface, v] of Object.entries(result.bySurface)) {
423
+ if (v) bySurface[surface] = v;
424
+ }
425
+ const summary = formatIngestSummary(
426
+ { recordCount: result.records.length, totalUSD: result.totalUSD, totalJPY: result.totalJPY, bySurface },
427
+ cfg
428
+ );
429
+ const record = {
430
+ schemaVersion: 1,
431
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
432
+ sessionId: "ingest-scan",
433
+ project: "",
434
+ gitBranch: null,
435
+ models: [],
436
+ tokens: emptyBuckets(),
437
+ sidechainTokens: null,
438
+ apiCalls: 0,
439
+ costUSD: result.totalUSD,
440
+ costJPY: result.totalJPY,
441
+ fxRate: result.fxRate,
442
+ fxSource: result.fxSource,
443
+ prompt: ""
444
+ };
445
+ await Promise.allSettled([
446
+ notifyOS(record, cfg, void 0, summary),
447
+ notifySlack(record, cfg, void 0, summary)
448
+ ]);
449
+ }
450
+
451
+ export {
452
+ runIngest,
453
+ notifyIngestSummary
454
+ };