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,100 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
paths
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WONMZ466.js";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
6
|
+
// src/data-lock.ts
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
import { hostname } from "os";
|
|
7
9
|
import {
|
|
8
|
-
closeSync,
|
|
9
10
|
existsSync,
|
|
10
|
-
|
|
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(
|
|
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}.${
|
|
33
|
+
const tmp = join(dir, `owner.${owner.token}.${randomUUID()}.tmp`);
|
|
115
34
|
try {
|
|
116
|
-
|
|
35
|
+
writeFileSync(tmp, `${JSON.stringify(owner)}
|
|
117
36
|
`, "utf8");
|
|
118
|
-
|
|
37
|
+
renameSync(tmp, ownerFile(dir));
|
|
119
38
|
} finally {
|
|
120
|
-
|
|
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}-${
|
|
45
|
+
const quarantine = `${dir}.${label}-${token}-${randomUUID()}`;
|
|
127
46
|
try {
|
|
128
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
66
|
+
renameSync(staging, fixed);
|
|
148
67
|
return { token, owner };
|
|
149
68
|
} catch {
|
|
150
|
-
|
|
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}-${
|
|
102
|
+
const orphan = `${p.dataLockDir}.orphan-${first.token}-${randomUUID()}`;
|
|
184
103
|
try {
|
|
185
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
};
|
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
import {
|
|
3
3
|
notifyOS,
|
|
4
4
|
notifySlack
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-TTD3BGOL.js";
|
|
6
6
|
import {
|
|
7
7
|
codexHome,
|
|
8
|
-
detectCodex
|
|
9
|
-
} from "./chunk-HTYUYKFW.js";
|
|
10
|
-
import {
|
|
8
|
+
detectCodex,
|
|
11
9
|
loadPriceTable
|
|
12
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-Q45AAPJY.js";
|
|
13
11
|
import {
|
|
14
12
|
configFilePath,
|
|
15
13
|
paths,
|
|
16
14
|
readConfig
|
|
17
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-WONMZ466.js";
|
|
18
16
|
|
|
19
17
|
// src/setup.ts
|
|
20
18
|
import {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-DGXUSPS4.js";
|
|
5
5
|
import {
|
|
6
6
|
formatSummary
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-XOGYLFX3.js";
|
|
8
8
|
|
|
9
9
|
// src/notify/os.ts
|
|
10
10
|
import { spawn } from "child_process";
|
|
@@ -181,10 +181,10 @@ function spawnNotifyChild(title, body) {
|
|
|
181
181
|
}
|
|
182
182
|
return { child: spawnLinuxNotify(title, body), swallowError: backend.swallowError };
|
|
183
183
|
}
|
|
184
|
-
async function notifyOS(record, cfg, todayUSD) {
|
|
184
|
+
async function notifyOS(record, cfg, todayUSD, summaryOverride) {
|
|
185
185
|
try {
|
|
186
186
|
if (!cfg?.notify?.os) return;
|
|
187
|
-
const { title, body } = formatSummary(record, cfg, todayUSD);
|
|
187
|
+
const { title, body } = summaryOverride ?? formatSummary(record, cfg, todayUSD);
|
|
188
188
|
if (process.env.CCCN_DRY_RUN === "1") {
|
|
189
189
|
writeDryRun("os", { title, body });
|
|
190
190
|
return;
|
|
@@ -221,12 +221,12 @@ async function notifyOS(record, cfg, todayUSD) {
|
|
|
221
221
|
|
|
222
222
|
// src/notify/slack.ts
|
|
223
223
|
var SEND_TIMEOUT_MS = 3e3;
|
|
224
|
-
async function notifySlack(record, cfg, todayUSD) {
|
|
224
|
+
async function notifySlack(record, cfg, todayUSD, summaryOverride) {
|
|
225
225
|
try {
|
|
226
226
|
const slackCfg = cfg?.notify?.slack;
|
|
227
227
|
const webhookUrl = slackCfg?.webhookUrl;
|
|
228
228
|
if (!webhookUrl) return;
|
|
229
|
-
const { title, body } = formatSummary(record, cfg, todayUSD);
|
|
229
|
+
const { title, body } = summaryOverride ?? formatSummary(record, cfg, todayUSD);
|
|
230
230
|
const line1 = body.split("\n")[0] ?? "";
|
|
231
231
|
const rawPrompt = record.prompt ?? "";
|
|
232
232
|
let promptText;
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
rmSync,
|
|
29
29
|
statSync
|
|
30
30
|
} from "fs";
|
|
31
|
+
import { randomUUID } from "crypto";
|
|
31
32
|
import { join } from "path";
|
|
32
33
|
import { homedir } from "os";
|
|
33
34
|
|
|
@@ -216,9 +217,21 @@ function loadCursor(transcriptPath) {
|
|
|
216
217
|
const cursor = parsed[transcriptPath];
|
|
217
218
|
return cursor ?? null;
|
|
218
219
|
}
|
|
220
|
+
function cursorPaths() {
|
|
221
|
+
const p = paths();
|
|
222
|
+
if (!existsSync(p.cursorsFile)) return /* @__PURE__ */ new Set();
|
|
223
|
+
try {
|
|
224
|
+
const parsed = JSON.parse(readFileSync(p.cursorsFile, "utf8"));
|
|
225
|
+
if (!isPlainObject(parsed)) return /* @__PURE__ */ new Set();
|
|
226
|
+
return new Set(Object.keys(parsed));
|
|
227
|
+
} catch (err) {
|
|
228
|
+
logError("cursorPaths", err);
|
|
229
|
+
return /* @__PURE__ */ new Set();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
219
232
|
function sanitizeCursor(raw) {
|
|
220
233
|
if (!isPlainObject(raw)) return null;
|
|
221
|
-
const { offset, lastUuid, lastTs, seenMessageKeys, codexTotals } = raw;
|
|
234
|
+
const { offset, lastUuid, lastTs, seenMessageKeys, codexTotals, codexOriginator, codexModel } = raw;
|
|
222
235
|
if (typeof offset !== "number" || !Number.isFinite(offset)) return null;
|
|
223
236
|
if (lastUuid !== null && typeof lastUuid !== "string") return null;
|
|
224
237
|
if (lastTs !== null && typeof lastTs !== "string") return null;
|
|
@@ -235,8 +248,44 @@ function sanitizeCursor(raw) {
|
|
|
235
248
|
cursor.codexTotals = { input, cached, output };
|
|
236
249
|
}
|
|
237
250
|
}
|
|
251
|
+
if (Object.hasOwn(raw, "codexOriginator")) {
|
|
252
|
+
if (codexOriginator === null || typeof codexOriginator === "string") {
|
|
253
|
+
cursor.codexOriginator = codexOriginator;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (Object.hasOwn(raw, "codexModel")) {
|
|
257
|
+
if (codexModel === null || typeof codexModel === "string") {
|
|
258
|
+
cursor.codexModel = codexModel;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
238
261
|
return cursor;
|
|
239
262
|
}
|
|
263
|
+
function loadAllCursors() {
|
|
264
|
+
const p = paths();
|
|
265
|
+
if (!existsSync(p.cursorsFile)) return {};
|
|
266
|
+
try {
|
|
267
|
+
const raw = readFileSync(p.cursorsFile, "utf8");
|
|
268
|
+
const parsed = JSON.parse(raw);
|
|
269
|
+
if (!isPlainObject(parsed)) {
|
|
270
|
+
logError("loadAllCursors", new Error("cursors.json root is not an object"));
|
|
271
|
+
return {};
|
|
272
|
+
}
|
|
273
|
+
return parsed;
|
|
274
|
+
} catch (err) {
|
|
275
|
+
logError("loadAllCursors", err);
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function saveAllCursors(dict) {
|
|
280
|
+
const p = paths();
|
|
281
|
+
const tmpFile = `${p.cursorsFile}.${randomUUID()}.tmp`;
|
|
282
|
+
try {
|
|
283
|
+
writeFileSync(tmpFile, JSON.stringify(dict), "utf8");
|
|
284
|
+
renameSync(tmpFile, p.cursorsFile);
|
|
285
|
+
} finally {
|
|
286
|
+
rmSync(tmpFile, { force: true });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
240
289
|
function saveCursor(transcriptPath, c) {
|
|
241
290
|
const p = paths();
|
|
242
291
|
let dict = {};
|
|
@@ -252,9 +301,83 @@ function saveCursor(transcriptPath, c) {
|
|
|
252
301
|
}
|
|
253
302
|
}
|
|
254
303
|
dict[transcriptPath] = c;
|
|
255
|
-
const tmpFile = `${p.cursorsFile}.tmp`;
|
|
256
|
-
|
|
257
|
-
|
|
304
|
+
const tmpFile = `${p.cursorsFile}.${randomUUID()}.tmp`;
|
|
305
|
+
try {
|
|
306
|
+
writeFileSync(tmpFile, JSON.stringify(dict), "utf8");
|
|
307
|
+
renameSync(tmpFile, p.cursorsFile);
|
|
308
|
+
} finally {
|
|
309
|
+
rmSync(tmpFile, { force: true });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function pendingAppendPath() {
|
|
313
|
+
return join(paths().cacheDir, "pending-append.json");
|
|
314
|
+
}
|
|
315
|
+
function readPendingAppends() {
|
|
316
|
+
let raw;
|
|
317
|
+
try {
|
|
318
|
+
raw = readFileSync(pendingAppendPath(), "utf8");
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err?.code === "ENOENT") return { dict: {}, trusted: true };
|
|
321
|
+
logError("readPendingAppends", err);
|
|
322
|
+
return { dict: {}, trusted: false };
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const parsed = JSON.parse(raw);
|
|
326
|
+
if (!isPlainObject(parsed)) {
|
|
327
|
+
logError("readPendingAppends", new Error("pending-append.json root is not an object"));
|
|
328
|
+
return { dict: {}, trusted: false };
|
|
329
|
+
}
|
|
330
|
+
return { dict: parsed, trusted: true };
|
|
331
|
+
} catch (err) {
|
|
332
|
+
logError("readPendingAppends", err);
|
|
333
|
+
return { dict: {}, trusted: false };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function writePendingAppends(dict) {
|
|
337
|
+
const file = pendingAppendPath();
|
|
338
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
339
|
+
try {
|
|
340
|
+
writeFileSync(tmp, JSON.stringify(dict), "utf8");
|
|
341
|
+
renameSync(tmp, file);
|
|
342
|
+
} finally {
|
|
343
|
+
rmSync(tmp, { force: true });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
var PENDING_ALL = "*";
|
|
347
|
+
function hasPendingAppend(transcriptPath) {
|
|
348
|
+
try {
|
|
349
|
+
const { dict, trusted } = readPendingAppends();
|
|
350
|
+
if (!trusted) return true;
|
|
351
|
+
return Object.hasOwn(dict, PENDING_ALL) || Object.hasOwn(dict, transcriptPath);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
logError("hasPendingAppend", err);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function markPendingAppend(transcriptPath, ingestKey) {
|
|
358
|
+
const { dict, trusted } = readPendingAppends();
|
|
359
|
+
if (!trusted) dict[PENDING_ALL] = "unreadable-marker";
|
|
360
|
+
dict[transcriptPath] = ingestKey;
|
|
361
|
+
writePendingAppends(dict);
|
|
362
|
+
}
|
|
363
|
+
function clearPendingAppend(transcriptPath) {
|
|
364
|
+
try {
|
|
365
|
+
const { dict, trusted } = readPendingAppends();
|
|
366
|
+
if (!trusted) return;
|
|
367
|
+
if (!Object.hasOwn(dict, transcriptPath)) return;
|
|
368
|
+
delete dict[transcriptPath];
|
|
369
|
+
writePendingAppends(dict);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
logError("clearPendingAppend", err);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function hasUnresolvedPendingMarker() {
|
|
375
|
+
try {
|
|
376
|
+
const { dict, trusted } = readPendingAppends();
|
|
377
|
+
return !trusted || Object.hasOwn(dict, PENDING_ALL);
|
|
378
|
+
} catch {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
258
381
|
}
|
|
259
382
|
function appendTurn(record) {
|
|
260
383
|
const p = paths();
|
|
@@ -309,6 +432,53 @@ function readTurns(days) {
|
|
|
309
432
|
}
|
|
310
433
|
return result;
|
|
311
434
|
}
|
|
435
|
+
function floorKey(scope, sessionId) {
|
|
436
|
+
return `${scope}\0${sessionId}`;
|
|
437
|
+
}
|
|
438
|
+
function loadHistoryIndex() {
|
|
439
|
+
const index = { countedCalls: /* @__PURE__ */ new Set(), ingestKeys: /* @__PURE__ */ new Set(), legacyFloors: /* @__PURE__ */ new Map() };
|
|
440
|
+
let raw;
|
|
441
|
+
try {
|
|
442
|
+
raw = readFileSync(paths().historyFile, "utf8");
|
|
443
|
+
} catch {
|
|
444
|
+
return index;
|
|
445
|
+
}
|
|
446
|
+
for (const line of raw.split("\n")) {
|
|
447
|
+
if (line.length === 0) continue;
|
|
448
|
+
let rec;
|
|
449
|
+
try {
|
|
450
|
+
rec = JSON.parse(line);
|
|
451
|
+
} catch {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
let hasFingerprints = false;
|
|
455
|
+
if (Array.isArray(rec.countedCalls)) {
|
|
456
|
+
for (const fp of rec.countedCalls) {
|
|
457
|
+
if (typeof fp === "string" && fp.length > 0) {
|
|
458
|
+
index.countedCalls.add(fp);
|
|
459
|
+
hasFingerprints = true;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (typeof rec.ingestKey === "string" && rec.ingestKey.length > 0) index.ingestKeys.add(rec.ingestKey);
|
|
464
|
+
if (hasFingerprints) continue;
|
|
465
|
+
const { sessionId, ts } = rec;
|
|
466
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) continue;
|
|
467
|
+
if (typeof ts !== "string") continue;
|
|
468
|
+
const ms = Date.parse(ts);
|
|
469
|
+
if (!Number.isFinite(ms)) continue;
|
|
470
|
+
const iso = new Date(ms).toISOString();
|
|
471
|
+
const isCodex = rec.source === "codex";
|
|
472
|
+
const scopes = isCodex ? ["codex"] : ["claude"];
|
|
473
|
+
if (!isCodex && rec.subagents !== void 0 && rec.subagents !== null) scopes.push("claude-sa");
|
|
474
|
+
for (const scope of scopes) {
|
|
475
|
+
const key = floorKey(scope, sessionId);
|
|
476
|
+
const cur = index.legacyFloors.get(key);
|
|
477
|
+
if (cur === void 0 || cur < iso) index.legacyFloors.set(key, iso);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return index;
|
|
481
|
+
}
|
|
312
482
|
function todayTotalUSD() {
|
|
313
483
|
const now = /* @__PURE__ */ new Date();
|
|
314
484
|
const y = now.getFullYear();
|
|
@@ -1090,11 +1260,21 @@ export {
|
|
|
1090
1260
|
writeMuteState,
|
|
1091
1261
|
clearMuteState,
|
|
1092
1262
|
loadCursor,
|
|
1263
|
+
cursorPaths,
|
|
1093
1264
|
sanitizeCursor,
|
|
1265
|
+
loadAllCursors,
|
|
1266
|
+
saveAllCursors,
|
|
1094
1267
|
saveCursor,
|
|
1268
|
+
pendingAppendPath,
|
|
1269
|
+
hasPendingAppend,
|
|
1270
|
+
markPendingAppend,
|
|
1271
|
+
clearPendingAppend,
|
|
1272
|
+
hasUnresolvedPendingMarker,
|
|
1095
1273
|
appendTurn,
|
|
1096
1274
|
resetHistoryAndCursors,
|
|
1097
1275
|
readTurns,
|
|
1276
|
+
floorKey,
|
|
1277
|
+
loadHistoryIndex,
|
|
1098
1278
|
todayTotalUSD,
|
|
1099
1279
|
currentMonthTotals,
|
|
1100
1280
|
logError
|
|
@@ -80,11 +80,22 @@ function formatSummary(record, cfg, todayUSD) {
|
|
|
80
80
|
return { title, body: `${line1}
|
|
81
81
|
${line2}` };
|
|
82
82
|
}
|
|
83
|
+
function formatIngestSummary(input, cfg) {
|
|
84
|
+
const label = cfg.costLabel === "api_equivalent" ? "API\u63DB\u7B97 " : "";
|
|
85
|
+
const title = `\u{1F4B0} ${label}${formatUSD(input.totalUSD)}(${formatJPY(input.totalJPY)})| hook\u975E\u4F9D\u5B58\u306E\u53D6\u308A\u8FBC\u307F ${input.recordCount}\u4EF6`;
|
|
86
|
+
const bySurfaceLine = Object.entries(input.bySurface).sort((a, b) => b[1].usd - a[1].usd).map(([surface, v]) => `${surface}: ${v.turns}\u4EF6 ${formatUSD(v.usd)}`).join(" / ");
|
|
87
|
+
const cachePct = input.totalTokens > 0 ? Math.round(input.cacheTokens / input.totalTokens * 100) : 0;
|
|
88
|
+
const tokensPart = input.totalTokens > 0 ? ` \xB7 \u8A08 ${formatTokens(input.totalTokens)} tokens(cache ${cachePct}%)` : "";
|
|
89
|
+
const body = `${bySurfaceLine}${tokensPart}
|
|
90
|
+
hook \u975E\u4F9D\u5B58\u306E\u5897\u5206\u53D6\u308A\u8FBC\u307F(scan)\u306B\u3088\u308B\u65B0\u898F\u5206\u3067\u3059`;
|
|
91
|
+
return { title, body };
|
|
92
|
+
}
|
|
83
93
|
|
|
84
94
|
export {
|
|
85
95
|
formatUSD,
|
|
86
96
|
formatJPY,
|
|
87
97
|
formatTokens,
|
|
88
98
|
modelDisplayName,
|
|
89
|
-
formatSummary
|
|
99
|
+
formatSummary,
|
|
100
|
+
formatIngestSummary
|
|
90
101
|
};
|