@aaa-sentinel/ai-usage 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +120 -2
- package/dist/cli.js +1378 -0
- package/dist/hook.js +67 -0
- package/package.json +42 -6
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
|
|
10
|
+
// src/launcher.ts
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
var LauncherError = class extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "LauncherError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var TASK_HOURLY = "AAA Sentinel AI usage";
|
|
20
|
+
var TASK_LOGON = "AAA Sentinel AI usage logon";
|
|
21
|
+
var UNIT = "aaa-sentinel-ai-usage";
|
|
22
|
+
var LABEL = "com.aaa-sentinel.ai-usage";
|
|
23
|
+
async function run(exec, file, args) {
|
|
24
|
+
const result = await exec(file, args);
|
|
25
|
+
if (result.code !== 0) {
|
|
26
|
+
throw new LauncherError(result.stderr || `${file} sali\xF3 con c\xF3digo ${result.code}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function xmlEscape(value) {
|
|
30
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
31
|
+
}
|
|
32
|
+
function formatCommand(args) {
|
|
33
|
+
return args.map((arg) => /[\s"]/.test(arg) ? `"${arg.replaceAll('"', '\\"')}"` : arg).join(" ");
|
|
34
|
+
}
|
|
35
|
+
function cmdArg(arg) {
|
|
36
|
+
return `"${arg.replaceAll('"', '""')}"`;
|
|
37
|
+
}
|
|
38
|
+
function createLauncher(options) {
|
|
39
|
+
const { platform, exec, args, homeDir, scriptDir } = options;
|
|
40
|
+
const command = formatCommand(args);
|
|
41
|
+
const uid = options.uid ?? 0;
|
|
42
|
+
const windowsCmd = join(scriptDir, "run.cmd");
|
|
43
|
+
return {
|
|
44
|
+
async install() {
|
|
45
|
+
if (platform === "win32") {
|
|
46
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
47
|
+
const previousCmd = existsSync(windowsCmd) ? readFileSync(windowsCmd, "utf8") : null;
|
|
48
|
+
writeFileSync(windowsCmd, `@echo off\r
|
|
49
|
+
${args.map(cmdArg).join(" ")}\r
|
|
50
|
+
`);
|
|
51
|
+
const tr = cmdArg(windowsCmd);
|
|
52
|
+
let hourlyCreated = false;
|
|
53
|
+
try {
|
|
54
|
+
await run(exec, "schtasks", ["/Create", "/F", "/TN", TASK_HOURLY, "/SC", "HOURLY", "/ST", "00:00", "/IT", "/TR", tr]);
|
|
55
|
+
hourlyCreated = true;
|
|
56
|
+
await run(exec, "schtasks", ["/Create", "/F", "/TN", TASK_LOGON, "/SC", "ONLOGON", "/IT", "/TR", tr]);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (previousCmd === null && hourlyCreated) await exec("schtasks", ["/Delete", "/TN", TASK_HOURLY, "/F"]);
|
|
59
|
+
if (previousCmd === null) rmSync(windowsCmd, { force: true });
|
|
60
|
+
else writeFileSync(windowsCmd, previousCmd);
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (platform === "darwin") {
|
|
66
|
+
const plistPath = join(homeDir, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
67
|
+
mkdirSync(join(homeDir, "Library", "LaunchAgents"), { recursive: true });
|
|
68
|
+
const program = args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
69
|
+
const previousPlist = existsSync(plistPath) ? readFileSync(plistPath, "utf8") : null;
|
|
70
|
+
writeFileSync(
|
|
71
|
+
plistPath,
|
|
72
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
73
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
74
|
+
<plist version="1.0"><dict>
|
|
75
|
+
<key>Label</key><string>${LABEL}</string>
|
|
76
|
+
<key>RunAtLoad</key><true/>
|
|
77
|
+
<key>StartInterval</key><integer>3600</integer>
|
|
78
|
+
<key>ProgramArguments</key>
|
|
79
|
+
<array>
|
|
80
|
+
${program}
|
|
81
|
+
</array>
|
|
82
|
+
</dict></plist>
|
|
83
|
+
`
|
|
84
|
+
);
|
|
85
|
+
const domain = `gui/${uid}`;
|
|
86
|
+
await exec("launchctl", ["bootout", `${domain}/${LABEL}`]);
|
|
87
|
+
try {
|
|
88
|
+
await run(exec, "launchctl", ["bootstrap", domain, plistPath]);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (previousPlist === null) rmSync(plistPath, { force: true });
|
|
91
|
+
else {
|
|
92
|
+
writeFileSync(plistPath, previousPlist);
|
|
93
|
+
await exec("launchctl", ["bootstrap", domain, plistPath]);
|
|
94
|
+
}
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (platform === "linux") {
|
|
100
|
+
const dir = join(homeDir, ".config", "systemd", "user");
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
const servicePath = join(dir, `${UNIT}.service`);
|
|
103
|
+
const timerPath = join(dir, `${UNIT}.timer`);
|
|
104
|
+
const previousService = existsSync(servicePath) ? readFileSync(servicePath, "utf8") : null;
|
|
105
|
+
const previousTimer = existsSync(timerPath) ? readFileSync(timerPath, "utf8") : null;
|
|
106
|
+
writeFileSync(servicePath, `[Service]
|
|
107
|
+
Type=oneshot
|
|
108
|
+
ExecStart=${command.replaceAll("%", "%%")}
|
|
109
|
+
`);
|
|
110
|
+
writeFileSync(
|
|
111
|
+
timerPath,
|
|
112
|
+
`[Timer]
|
|
113
|
+
OnStartupSec=1min
|
|
114
|
+
OnUnitActiveSec=1h
|
|
115
|
+
Persistent=true
|
|
116
|
+
|
|
117
|
+
[Install]
|
|
118
|
+
WantedBy=timers.target
|
|
119
|
+
`
|
|
120
|
+
);
|
|
121
|
+
try {
|
|
122
|
+
await run(exec, "systemctl", ["--user", "daemon-reload"]);
|
|
123
|
+
await run(exec, "systemctl", ["--user", "enable", "--now", `${UNIT}.timer`]);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (previousService === null) rmSync(servicePath, { force: true });
|
|
126
|
+
else writeFileSync(servicePath, previousService);
|
|
127
|
+
if (previousTimer === null) rmSync(timerPath, { force: true });
|
|
128
|
+
else writeFileSync(timerPath, previousTimer);
|
|
129
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
throw new LauncherError(`No hay arranque para ${platform}`);
|
|
135
|
+
},
|
|
136
|
+
async uninstall() {
|
|
137
|
+
if (platform === "win32") {
|
|
138
|
+
await exec("schtasks", ["/Delete", "/TN", TASK_HOURLY, "/F"]);
|
|
139
|
+
await exec("schtasks", ["/Delete", "/TN", TASK_LOGON, "/F"]);
|
|
140
|
+
rmSync(windowsCmd, { force: true });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (platform === "darwin") {
|
|
144
|
+
await exec("launchctl", ["bootout", `gui/${uid}/${LABEL}`]);
|
|
145
|
+
rmSync(join(homeDir, "Library", "LaunchAgents", `${LABEL}.plist`), { force: true });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (platform === "linux") {
|
|
149
|
+
await exec("systemctl", ["--user", "disable", "--now", `${UNIT}.timer`]);
|
|
150
|
+
const dir = join(homeDir, ".config", "systemd", "user");
|
|
151
|
+
rmSync(join(dir, `${UNIT}.service`), { force: true });
|
|
152
|
+
rmSync(join(dir, `${UNIT}.timer`), { force: true });
|
|
153
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async installed() {
|
|
158
|
+
if (platform === "win32") {
|
|
159
|
+
const hourly = await exec("schtasks", ["/Query", "/TN", TASK_HOURLY]);
|
|
160
|
+
const logon = await exec("schtasks", ["/Query", "/TN", TASK_LOGON]);
|
|
161
|
+
return hourly.code === 0 && logon.code === 0;
|
|
162
|
+
}
|
|
163
|
+
if (platform === "darwin") {
|
|
164
|
+
const result = await exec("launchctl", ["print", `gui/${uid}/${LABEL}`]);
|
|
165
|
+
return result.code === 0;
|
|
166
|
+
}
|
|
167
|
+
if (platform === "linux") {
|
|
168
|
+
const result = await exec("systemctl", ["--user", "is-active", `${UNIT}.timer`]);
|
|
169
|
+
return result.code === 0;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/session.ts
|
|
177
|
+
import { randomBytes } from "node:crypto";
|
|
178
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
179
|
+
import { dirname, join as join4 } from "node:path";
|
|
180
|
+
|
|
181
|
+
// src/cursor.ts
|
|
182
|
+
var MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
183
|
+
var TOKEN_MAX = 1e12;
|
|
184
|
+
function finiteInt(value) {
|
|
185
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > TOKEN_MAX) return void 0;
|
|
186
|
+
const rounded = Math.round(value);
|
|
187
|
+
if (rounded > TOKEN_MAX) return void 0;
|
|
188
|
+
return rounded;
|
|
189
|
+
}
|
|
190
|
+
function readSpoolEvent(value) {
|
|
191
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
192
|
+
const record = value;
|
|
193
|
+
if (typeof record["at"] !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(record["at"])) return null;
|
|
194
|
+
if (typeof record["tokensKnown"] !== "boolean") return null;
|
|
195
|
+
const event = { at: record["at"], tokensKnown: record["tokensKnown"] };
|
|
196
|
+
if (typeof record["model"] === "string" && MODEL_PATTERN.test(record["model"])) event.model = record["model"];
|
|
197
|
+
if (!event.tokensKnown) return event;
|
|
198
|
+
const input = finiteInt(record["inputTokens"]);
|
|
199
|
+
const output = finiteInt(record["outputTokens"]);
|
|
200
|
+
const cacheRead = finiteInt(record["cacheReadTokens"]);
|
|
201
|
+
const cacheWrite = finiteInt(record["cacheWriteTokens"]);
|
|
202
|
+
if (input === void 0 || output === void 0 || cacheRead === void 0 || cacheWrite === void 0) return null;
|
|
203
|
+
event.inputTokens = input;
|
|
204
|
+
event.outputTokens = output;
|
|
205
|
+
event.cacheReadTokens = cacheRead;
|
|
206
|
+
event.cacheWriteTokens = cacheWrite;
|
|
207
|
+
return event;
|
|
208
|
+
}
|
|
209
|
+
function aggregateByDay(events) {
|
|
210
|
+
const groups = /* @__PURE__ */ new Map();
|
|
211
|
+
for (const event of events) {
|
|
212
|
+
const day = event.at.slice(0, 10);
|
|
213
|
+
const key = `${day}
|
|
214
|
+
${event.model ?? ""}`;
|
|
215
|
+
const list = groups.get(key) ?? [];
|
|
216
|
+
list.push(event);
|
|
217
|
+
groups.set(key, list);
|
|
218
|
+
}
|
|
219
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const [key, list] of groups) {
|
|
221
|
+
const day = key.slice(0, 10);
|
|
222
|
+
const model = list.find((event) => event.model)?.model;
|
|
223
|
+
const tokensKnown = list.every((event) => event.tokensKnown);
|
|
224
|
+
const row = {
|
|
225
|
+
inputTokens: tokensKnown ? list.reduce((sum, event) => sum + (event.inputTokens ?? 0), 0) : 0,
|
|
226
|
+
outputTokens: tokensKnown ? list.reduce((sum, event) => sum + (event.outputTokens ?? 0), 0) : 0,
|
|
227
|
+
cacheReadTokens: tokensKnown ? list.reduce((sum, event) => sum + (event.cacheReadTokens ?? 0), 0) : 0,
|
|
228
|
+
cacheWriteTokens: tokensKnown ? list.reduce((sum, event) => sum + (event.cacheWriteTokens ?? 0), 0) : 0,
|
|
229
|
+
turns: list.length,
|
|
230
|
+
tokensKnown
|
|
231
|
+
};
|
|
232
|
+
if (model) row.model = model;
|
|
233
|
+
const rows = byDay.get(day) ?? [];
|
|
234
|
+
rows.push(row);
|
|
235
|
+
byDay.set(day, rows);
|
|
236
|
+
}
|
|
237
|
+
for (const rows of byDay.values()) {
|
|
238
|
+
rows.sort((left, right) => (left.model ?? "").localeCompare(right.model ?? ""));
|
|
239
|
+
}
|
|
240
|
+
return byDay;
|
|
241
|
+
}
|
|
242
|
+
function asRecord(value) {
|
|
243
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
function mergeCursorHook(existing, command) {
|
|
247
|
+
let parsed = {};
|
|
248
|
+
if (existing.trim()) parsed = JSON.parse(existing);
|
|
249
|
+
const root = asRecord(parsed) ?? {};
|
|
250
|
+
const hooks = asRecord(root["hooks"]) ?? {};
|
|
251
|
+
const stopRaw = hooks["stop"];
|
|
252
|
+
const stop = Array.isArray(stopRaw) ? stopRaw.filter((entry) => asRecord(entry)?.["command"] !== command) : [];
|
|
253
|
+
const kept = stop.flatMap((entry) => {
|
|
254
|
+
const record = asRecord(entry);
|
|
255
|
+
if (!record || typeof record["command"] !== "string") return [];
|
|
256
|
+
return [record];
|
|
257
|
+
});
|
|
258
|
+
return `${JSON.stringify({ ...root, version: root["version"] ?? 1, hooks: { ...hooks, stop: [...kept, { command }] } }, null, 2)}
|
|
259
|
+
`;
|
|
260
|
+
}
|
|
261
|
+
function removeCursorHook(existing, command) {
|
|
262
|
+
if (!existing.trim()) return existing;
|
|
263
|
+
const root = asRecord(JSON.parse(existing));
|
|
264
|
+
if (!root) return existing;
|
|
265
|
+
const hooks = asRecord(root["hooks"]);
|
|
266
|
+
if (!hooks || !Array.isArray(hooks["stop"])) return existing;
|
|
267
|
+
const stop = hooks["stop"].filter((entry) => asRecord(entry)?.["command"] !== command);
|
|
268
|
+
if (stop.length === 0) {
|
|
269
|
+
const rest = {};
|
|
270
|
+
for (const [key, value] of Object.entries(hooks)) {
|
|
271
|
+
if (key !== "stop") rest[key] = value;
|
|
272
|
+
}
|
|
273
|
+
return `${JSON.stringify({ ...root, hooks: rest }, null, 2)}
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
return `${JSON.stringify({ ...root, hooks: { ...hooks, stop } }, null, 2)}
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/claude.ts
|
|
281
|
+
import { openSync, readSync, closeSync, readdirSync, statSync } from "node:fs";
|
|
282
|
+
import { join as join2, relative } from "node:path";
|
|
283
|
+
var MODEL_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
284
|
+
var TOKEN_MAX2 = 1e12;
|
|
285
|
+
var MAX_CHUNK = 2e6;
|
|
286
|
+
function emptyClaudeState(since) {
|
|
287
|
+
return since ? { since, files: {}, days: {} } : { files: {}, days: {} };
|
|
288
|
+
}
|
|
289
|
+
function finiteInt2(value) {
|
|
290
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > TOKEN_MAX2) return void 0;
|
|
291
|
+
const rounded = Math.round(value);
|
|
292
|
+
if (rounded > TOKEN_MAX2) return void 0;
|
|
293
|
+
return rounded;
|
|
294
|
+
}
|
|
295
|
+
function presentInt(usage, key) {
|
|
296
|
+
if (!(key in usage)) return 0;
|
|
297
|
+
const parsed = finiteInt2(usage[key]);
|
|
298
|
+
return parsed === void 0 ? null : parsed;
|
|
299
|
+
}
|
|
300
|
+
function readCounters(counters, turns) {
|
|
301
|
+
const input = finiteInt2(counters["input_tokens"]);
|
|
302
|
+
const output = finiteInt2(counters["output_tokens"]);
|
|
303
|
+
const cacheRead = presentInt(counters, "cache_read_input_tokens");
|
|
304
|
+
const cacheWrite = presentInt(counters, "cache_creation_input_tokens");
|
|
305
|
+
if (input === void 0 || output === void 0 || cacheRead === null || cacheWrite === null) return null;
|
|
306
|
+
return { inputTokens: input, outputTokens: output, cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite, turns };
|
|
307
|
+
}
|
|
308
|
+
function advisorParts(iterations) {
|
|
309
|
+
if (!Array.isArray(iterations)) return [];
|
|
310
|
+
return iterations.flatMap((iteration) => {
|
|
311
|
+
if (!iteration || typeof iteration !== "object" || Array.isArray(iteration)) return [];
|
|
312
|
+
const record = iteration;
|
|
313
|
+
if (record["type"] !== "advisor_message") return [];
|
|
314
|
+
const model = record["model"];
|
|
315
|
+
const bucket = readCounters(record, 0);
|
|
316
|
+
if (!bucket) return [];
|
|
317
|
+
return [{ modelKey: typeof model === "string" && MODEL_PATTERN2.test(model) ? model : "", bucket }];
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function parseAssistantLine(line) {
|
|
321
|
+
let parsed;
|
|
322
|
+
try {
|
|
323
|
+
parsed = JSON.parse(line);
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
328
|
+
const record = parsed;
|
|
329
|
+
if (record["type"] !== "assistant") return null;
|
|
330
|
+
const at = record["timestamp"];
|
|
331
|
+
if (typeof at !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(at)) return null;
|
|
332
|
+
const message = record["message"];
|
|
333
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) return null;
|
|
334
|
+
const usage = message["usage"];
|
|
335
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
|
|
336
|
+
const counters = usage;
|
|
337
|
+
const executor = readCounters(counters, 1);
|
|
338
|
+
if (!executor) return null;
|
|
339
|
+
const requestId = typeof record["requestId"] === "string" ? record["requestId"] : "";
|
|
340
|
+
const messageId = typeof message["id"] === "string" ? String(message["id"]) : "";
|
|
341
|
+
const uuid = typeof record["uuid"] === "string" ? record["uuid"] : "";
|
|
342
|
+
const id = requestId || messageId || uuid;
|
|
343
|
+
if (!id) return null;
|
|
344
|
+
const modelRaw = message["model"];
|
|
345
|
+
const modelKey = typeof modelRaw === "string" && MODEL_PATTERN2.test(modelRaw) ? modelRaw : "";
|
|
346
|
+
return { id, day: at.slice(0, 10), parts: [{ modelKey, bucket: executor }, ...advisorParts(counters["iterations"])] };
|
|
347
|
+
}
|
|
348
|
+
function bucketAt(days, day, modelKey) {
|
|
349
|
+
const models = days[day] ?? {};
|
|
350
|
+
days[day] = models;
|
|
351
|
+
const current = models[modelKey] ?? { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, turns: 0 };
|
|
352
|
+
models[modelKey] = current;
|
|
353
|
+
return current;
|
|
354
|
+
}
|
|
355
|
+
function addBucket(target, delta, sign) {
|
|
356
|
+
target.inputTokens += sign * delta.inputTokens;
|
|
357
|
+
target.outputTokens += sign * delta.outputTokens;
|
|
358
|
+
target.cacheReadTokens += sign * delta.cacheReadTokens;
|
|
359
|
+
target.cacheWriteTokens += sign * delta.cacheWriteTokens;
|
|
360
|
+
target.turns += sign * delta.turns;
|
|
361
|
+
if (target.inputTokens < 0) target.inputTokens = 0;
|
|
362
|
+
if (target.outputTokens < 0) target.outputTokens = 0;
|
|
363
|
+
if (target.cacheReadTokens < 0) target.cacheReadTokens = 0;
|
|
364
|
+
if (target.cacheWriteTokens < 0) target.cacheWriteTokens = 0;
|
|
365
|
+
if (target.turns < 0) target.turns = 0;
|
|
366
|
+
}
|
|
367
|
+
function foldClaudeTranscript(days, open, chunk, since) {
|
|
368
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
369
|
+
let pending = open;
|
|
370
|
+
const text = chunk.toString("utf8");
|
|
371
|
+
let start = 0;
|
|
372
|
+
while (start < text.length) {
|
|
373
|
+
const newline = text.indexOf("\n", start);
|
|
374
|
+
if (newline === -1) break;
|
|
375
|
+
const line = text.slice(start, newline).replace(/\r$/, "");
|
|
376
|
+
start = newline + 1;
|
|
377
|
+
const turn = line ? parseAssistantLine(line) : null;
|
|
378
|
+
if (!turn || since && turn.day < since) continue;
|
|
379
|
+
if (pending && pending.id === turn.id) {
|
|
380
|
+
for (const part of pending.parts) {
|
|
381
|
+
const previous = days[pending.day]?.[part.modelKey];
|
|
382
|
+
if (!previous) continue;
|
|
383
|
+
addBucket(previous, part.bucket, -1);
|
|
384
|
+
dirty.add(pending.day);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
pending = turn;
|
|
388
|
+
for (const part of turn.parts) addBucket(bucketAt(days, turn.day, part.modelKey), part.bucket, 1);
|
|
389
|
+
dirty.add(turn.day);
|
|
390
|
+
}
|
|
391
|
+
const consumed = start === 0 ? 0 : Buffer.byteLength(text.slice(0, start));
|
|
392
|
+
return { open: pending, consumed, dirty: [...dirty] };
|
|
393
|
+
}
|
|
394
|
+
function pruneClaudeDays(state, before) {
|
|
395
|
+
for (const day of Object.keys(state.days)) {
|
|
396
|
+
if (day < before) delete state.days[day];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function rowsForDay(days, day) {
|
|
400
|
+
const models = days[day] ?? {};
|
|
401
|
+
return Object.keys(models).sort().map((modelKey) => {
|
|
402
|
+
const bucket = models[modelKey];
|
|
403
|
+
const row = {
|
|
404
|
+
inputTokens: bucket.inputTokens,
|
|
405
|
+
outputTokens: bucket.outputTokens,
|
|
406
|
+
cacheReadTokens: bucket.cacheReadTokens,
|
|
407
|
+
cacheWriteTokens: bucket.cacheWriteTokens,
|
|
408
|
+
turns: bucket.turns,
|
|
409
|
+
tokensKnown: true
|
|
410
|
+
};
|
|
411
|
+
if (modelKey) row.model = modelKey;
|
|
412
|
+
return row;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function copyBucket(bucket) {
|
|
416
|
+
return {
|
|
417
|
+
inputTokens: bucket.inputTokens,
|
|
418
|
+
outputTokens: bucket.outputTokens,
|
|
419
|
+
cacheReadTokens: bucket.cacheReadTokens,
|
|
420
|
+
cacheWriteTokens: bucket.cacheWriteTokens,
|
|
421
|
+
turns: bucket.turns
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function parseOpen(value) {
|
|
425
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
426
|
+
const record = value;
|
|
427
|
+
const day = record["day"];
|
|
428
|
+
if (typeof record["id"] !== "string" || typeof day !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(day)) return void 0;
|
|
429
|
+
if (!Array.isArray(record["parts"])) return void 0;
|
|
430
|
+
const parts = [];
|
|
431
|
+
for (const raw of record["parts"]) {
|
|
432
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
433
|
+
const part = raw;
|
|
434
|
+
const modelKey = typeof part["modelKey"] === "string" ? part["modelKey"] : "";
|
|
435
|
+
if (modelKey && !MODEL_PATTERN2.test(modelKey) || !isBucket(part["bucket"])) return void 0;
|
|
436
|
+
parts.push({ modelKey, bucket: copyBucket(part["bucket"]) });
|
|
437
|
+
}
|
|
438
|
+
return { id: record["id"], day, parts };
|
|
439
|
+
}
|
|
440
|
+
function isBucket(value) {
|
|
441
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
442
|
+
const record = value;
|
|
443
|
+
return ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "turns"].every((key) => finiteInt2(record[key]) !== void 0);
|
|
444
|
+
}
|
|
445
|
+
function parseClaudeState(value) {
|
|
446
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return emptyClaudeState();
|
|
447
|
+
const record = value;
|
|
448
|
+
const since = record["since"];
|
|
449
|
+
const state = emptyClaudeState(typeof since === "string" && /^\d{4}-\d{2}-\d{2}$/.test(since) ? since : void 0);
|
|
450
|
+
const files = record["files"];
|
|
451
|
+
if (files && typeof files === "object" && !Array.isArray(files)) {
|
|
452
|
+
for (const [key, mark] of Object.entries(files)) {
|
|
453
|
+
if (!mark || typeof mark !== "object" || Array.isArray(mark)) continue;
|
|
454
|
+
const offset = finiteInt2(mark["offset"]);
|
|
455
|
+
if (offset === void 0) continue;
|
|
456
|
+
const next = { offset };
|
|
457
|
+
if (mark["skipLine"] === true) next.skipLine = true;
|
|
458
|
+
const open = parseOpen(mark["open"]);
|
|
459
|
+
if (open) next.open = open;
|
|
460
|
+
state.files[key] = next;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const days = record["days"];
|
|
464
|
+
if (days && typeof days === "object" && !Array.isArray(days)) {
|
|
465
|
+
for (const [day, models] of Object.entries(days)) {
|
|
466
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !models || typeof models !== "object" || Array.isArray(models)) continue;
|
|
467
|
+
const parsed = {};
|
|
468
|
+
for (const [modelKey, bucket] of Object.entries(models)) {
|
|
469
|
+
if (modelKey && !MODEL_PATTERN2.test(modelKey) || !isBucket(bucket)) continue;
|
|
470
|
+
parsed[modelKey] = {
|
|
471
|
+
inputTokens: bucket.inputTokens,
|
|
472
|
+
outputTokens: bucket.outputTokens,
|
|
473
|
+
cacheReadTokens: bucket.cacheReadTokens,
|
|
474
|
+
cacheWriteTokens: bucket.cacheWriteTokens,
|
|
475
|
+
turns: bucket.turns
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (Object.keys(parsed).length > 0) state.days[day] = parsed;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return state;
|
|
482
|
+
}
|
|
483
|
+
function listJsonl(dir) {
|
|
484
|
+
const found = [];
|
|
485
|
+
const walk = (current) => {
|
|
486
|
+
let entries;
|
|
487
|
+
try {
|
|
488
|
+
entries = readdirSync(current, { withFileTypes: true, encoding: "utf8" });
|
|
489
|
+
} catch {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
if (entry.isSymbolicLink()) continue;
|
|
494
|
+
const path = join2(current, entry.name);
|
|
495
|
+
if (entry.isDirectory()) walk(path);
|
|
496
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) found.push(path);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
walk(dir);
|
|
500
|
+
return found;
|
|
501
|
+
}
|
|
502
|
+
function readChunk(path, offset) {
|
|
503
|
+
const size = statSync(path).size;
|
|
504
|
+
const start = Math.min(offset, size);
|
|
505
|
+
const length = Math.min(size - start, MAX_CHUNK);
|
|
506
|
+
if (length <= 0) return Buffer.alloc(0);
|
|
507
|
+
const buffer = Buffer.alloc(length);
|
|
508
|
+
const fd = openSync(path, "r");
|
|
509
|
+
try {
|
|
510
|
+
readSync(fd, buffer, 0, length, start);
|
|
511
|
+
} finally {
|
|
512
|
+
closeSync(fd);
|
|
513
|
+
}
|
|
514
|
+
return buffer;
|
|
515
|
+
}
|
|
516
|
+
function readClaudeIncrement(dir, previous) {
|
|
517
|
+
const state = parseClaudeState(JSON.parse(JSON.stringify(previous)));
|
|
518
|
+
const known = state.files;
|
|
519
|
+
state.files = {};
|
|
520
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
521
|
+
let caughtUp = true;
|
|
522
|
+
for (const path of listJsonl(dir)) {
|
|
523
|
+
const key = relative(dir, path).split("\\").join("/");
|
|
524
|
+
const size = statSync(path).size;
|
|
525
|
+
const mark = known[key] ?? { offset: 0 };
|
|
526
|
+
if (mark.offset > size) {
|
|
527
|
+
state.files[key] = { offset: size };
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
let buffer = readChunk(path, mark.offset);
|
|
531
|
+
if (buffer.length === 0) {
|
|
532
|
+
state.files[key] = mark;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (mark.skipLine) {
|
|
536
|
+
const newline = buffer.indexOf(10);
|
|
537
|
+
if (newline === -1) {
|
|
538
|
+
mark.offset += buffer.length;
|
|
539
|
+
state.files[key] = mark;
|
|
540
|
+
if (mark.offset < size) caughtUp = false;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
buffer = buffer.subarray(newline + 1);
|
|
544
|
+
mark.offset += newline + 1;
|
|
545
|
+
delete mark.skipLine;
|
|
546
|
+
}
|
|
547
|
+
const folded = foldClaudeTranscript(state.days, mark.open, buffer, state.since);
|
|
548
|
+
for (const day of folded.dirty) dirty.add(day);
|
|
549
|
+
mark.offset += folded.consumed;
|
|
550
|
+
if (folded.open) mark.open = folded.open;
|
|
551
|
+
if (folded.consumed === 0 && buffer.length === MAX_CHUNK) {
|
|
552
|
+
mark.skipLine = true;
|
|
553
|
+
mark.offset += buffer.length;
|
|
554
|
+
}
|
|
555
|
+
state.files[key] = mark;
|
|
556
|
+
if (mark.offset < size) caughtUp = false;
|
|
557
|
+
}
|
|
558
|
+
return { state, dirtyDays: [...dirty].sort(), caughtUp };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/codex.ts
|
|
562
|
+
import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, readSync as readSync2, statSync as statSync2 } from "node:fs";
|
|
563
|
+
import { join as join3, relative as relative2 } from "node:path";
|
|
564
|
+
var MODEL_PATTERN3 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
565
|
+
var TOKEN_MAX3 = 1e12;
|
|
566
|
+
var MAX_CHUNK2 = 2e6;
|
|
567
|
+
function emptyCodexState(since) {
|
|
568
|
+
return since ? { since, files: {}, days: {} } : { files: {}, days: {} };
|
|
569
|
+
}
|
|
570
|
+
function classifyCodexAuth(raw) {
|
|
571
|
+
let parsed;
|
|
572
|
+
try {
|
|
573
|
+
parsed = JSON.parse(raw);
|
|
574
|
+
} catch {
|
|
575
|
+
return "unknown";
|
|
576
|
+
}
|
|
577
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "unknown";
|
|
578
|
+
const record = parsed;
|
|
579
|
+
const mode = record["auth_mode"];
|
|
580
|
+
if (mode === "apikey" || mode === "api_key" || mode === "api") return "api_key";
|
|
581
|
+
const key = record["OPENAI_API_KEY"];
|
|
582
|
+
if (typeof key === "string" && key.length > 0) return "api_key";
|
|
583
|
+
const tokens = record["tokens"];
|
|
584
|
+
if (tokens && typeof tokens === "object" && !Array.isArray(tokens)) return "chatgpt";
|
|
585
|
+
return "unknown";
|
|
586
|
+
}
|
|
587
|
+
function finiteInt3(value) {
|
|
588
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > TOKEN_MAX3) return void 0;
|
|
589
|
+
return value;
|
|
590
|
+
}
|
|
591
|
+
function bucketAt2(days, day, modelKey) {
|
|
592
|
+
const models = days[day] ?? (days[day] = {});
|
|
593
|
+
return models[modelKey] ?? (models[modelKey] = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, turns: 0 });
|
|
594
|
+
}
|
|
595
|
+
function addDelta(bucket, input, output, cacheRead) {
|
|
596
|
+
bucket.inputTokens += input;
|
|
597
|
+
bucket.outputTokens += output;
|
|
598
|
+
bucket.cacheReadTokens += cacheRead;
|
|
599
|
+
bucket.turns += 1;
|
|
600
|
+
}
|
|
601
|
+
function foldCodexTranscript(days, cursor, chunk, since) {
|
|
602
|
+
const text = chunk.toString("utf8");
|
|
603
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
604
|
+
let modelKey = cursor?.modelKey ?? "";
|
|
605
|
+
let consumed = 0;
|
|
606
|
+
let start = 0;
|
|
607
|
+
while (start < text.length) {
|
|
608
|
+
const end = text.indexOf("\n", start);
|
|
609
|
+
if (end === -1) break;
|
|
610
|
+
const line = text.slice(start, end);
|
|
611
|
+
start = end + 1;
|
|
612
|
+
consumed = Buffer.byteLength(text.slice(0, start));
|
|
613
|
+
if (!line.trim()) continue;
|
|
614
|
+
let parsed;
|
|
615
|
+
try {
|
|
616
|
+
parsed = JSON.parse(line);
|
|
617
|
+
} catch {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
621
|
+
const record = parsed;
|
|
622
|
+
const payload = record["payload"];
|
|
623
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) continue;
|
|
624
|
+
const body = payload;
|
|
625
|
+
if (record["type"] === "turn_context" && typeof body["model"] === "string" && MODEL_PATTERN3.test(body["model"])) {
|
|
626
|
+
modelKey = body["model"];
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (record["type"] !== "event_msg" || body["type"] !== "token_count") continue;
|
|
630
|
+
const at = record["timestamp"];
|
|
631
|
+
if (typeof at !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(at)) continue;
|
|
632
|
+
const day = at.slice(0, 10);
|
|
633
|
+
const info = body["info"];
|
|
634
|
+
if (!info || typeof info !== "object" || Array.isArray(info)) continue;
|
|
635
|
+
const total = info["total_token_usage"];
|
|
636
|
+
if (!total || typeof total !== "object" || Array.isArray(total)) continue;
|
|
637
|
+
const counters = total;
|
|
638
|
+
const input = finiteInt3(counters["input_tokens"]);
|
|
639
|
+
const output = finiteInt3(counters["output_tokens"]);
|
|
640
|
+
const cached = finiteInt3(counters["cached_input_tokens"]) ?? 0;
|
|
641
|
+
if (input === void 0 || output === void 0) continue;
|
|
642
|
+
const next = {
|
|
643
|
+
modelKey,
|
|
644
|
+
day,
|
|
645
|
+
inputTokens: Math.max(0, input - cached),
|
|
646
|
+
outputTokens: output,
|
|
647
|
+
cacheReadTokens: cached
|
|
648
|
+
};
|
|
649
|
+
const deltaInput = Math.max(0, next.inputTokens - (cursor?.inputTokens ?? 0));
|
|
650
|
+
const deltaOutput = Math.max(0, next.outputTokens - (cursor?.outputTokens ?? 0));
|
|
651
|
+
const deltaCache = Math.max(0, next.cacheReadTokens - (cursor?.cacheReadTokens ?? 0));
|
|
652
|
+
cursor = next;
|
|
653
|
+
if (since && day < since) continue;
|
|
654
|
+
if (deltaInput === 0 && deltaOutput === 0 && deltaCache === 0) continue;
|
|
655
|
+
addDelta(bucketAt2(days, next.day, next.modelKey), deltaInput, deltaOutput, deltaCache);
|
|
656
|
+
dirty.add(day);
|
|
657
|
+
}
|
|
658
|
+
return cursor ? { cursor, dirty: [...dirty], consumed } : { dirty: [...dirty], consumed };
|
|
659
|
+
}
|
|
660
|
+
function rowsForCodexDay(days, day) {
|
|
661
|
+
const models = days[day];
|
|
662
|
+
if (!models) return [];
|
|
663
|
+
return Object.entries(models).filter(([, bucket]) => bucket.turns > 0 || bucket.inputTokens > 0 || bucket.outputTokens > 0).sort(([left], [right]) => left.localeCompare(right)).map(([modelKey, bucket]) => ({
|
|
664
|
+
...modelKey ? { model: modelKey } : {},
|
|
665
|
+
inputTokens: bucket.inputTokens,
|
|
666
|
+
outputTokens: bucket.outputTokens,
|
|
667
|
+
cacheReadTokens: bucket.cacheReadTokens,
|
|
668
|
+
cacheWriteTokens: bucket.cacheWriteTokens,
|
|
669
|
+
turns: bucket.turns,
|
|
670
|
+
tokensKnown: true
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
function isCursor(value) {
|
|
674
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
675
|
+
const record = value;
|
|
676
|
+
return typeof record["day"] === "string" && /^\d{4}-\d{2}-\d{2}$/.test(record["day"]) && typeof record["modelKey"] === "string" && (record["modelKey"] === "" || MODEL_PATTERN3.test(record["modelKey"])) && finiteInt3(record["inputTokens"]) !== void 0 && finiteInt3(record["outputTokens"]) !== void 0 && finiteInt3(record["cacheReadTokens"]) !== void 0;
|
|
677
|
+
}
|
|
678
|
+
function parseCodexState(value) {
|
|
679
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return emptyCodexState();
|
|
680
|
+
const record = value;
|
|
681
|
+
const since = record["since"];
|
|
682
|
+
const state = emptyCodexState(typeof since === "string" && /^\d{4}-\d{2}-\d{2}$/.test(since) ? since : void 0);
|
|
683
|
+
const files = record["files"];
|
|
684
|
+
if (files && typeof files === "object" && !Array.isArray(files)) {
|
|
685
|
+
for (const [key, mark] of Object.entries(files)) {
|
|
686
|
+
if (!mark || typeof mark !== "object" || Array.isArray(mark)) continue;
|
|
687
|
+
const offset = finiteInt3(mark["offset"]);
|
|
688
|
+
if (offset === void 0) continue;
|
|
689
|
+
const next = { offset };
|
|
690
|
+
if (mark["skipLine"] === true) next.skipLine = true;
|
|
691
|
+
const cursor = mark["cursor"];
|
|
692
|
+
if (isCursor(cursor)) {
|
|
693
|
+
next.cursor = {
|
|
694
|
+
modelKey: cursor.modelKey,
|
|
695
|
+
day: cursor.day,
|
|
696
|
+
inputTokens: cursor.inputTokens,
|
|
697
|
+
outputTokens: cursor.outputTokens,
|
|
698
|
+
cacheReadTokens: cursor.cacheReadTokens
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
state.files[key] = next;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const days = record["days"];
|
|
705
|
+
if (days && typeof days === "object" && !Array.isArray(days)) {
|
|
706
|
+
for (const [day, models] of Object.entries(days)) {
|
|
707
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !models || typeof models !== "object" || Array.isArray(models)) continue;
|
|
708
|
+
const parsed = {};
|
|
709
|
+
for (const [modelKey, bucket] of Object.entries(models)) {
|
|
710
|
+
if (modelKey && !MODEL_PATTERN3.test(modelKey) || !bucket || typeof bucket !== "object" || Array.isArray(bucket)) continue;
|
|
711
|
+
const counters = bucket;
|
|
712
|
+
const inputTokens = finiteInt3(counters["inputTokens"]);
|
|
713
|
+
const outputTokens = finiteInt3(counters["outputTokens"]);
|
|
714
|
+
const cacheReadTokens = finiteInt3(counters["cacheReadTokens"]);
|
|
715
|
+
const cacheWriteTokens = finiteInt3(counters["cacheWriteTokens"]);
|
|
716
|
+
const turns = finiteInt3(counters["turns"]);
|
|
717
|
+
if (inputTokens === void 0 || outputTokens === void 0 || cacheReadTokens === void 0 || cacheWriteTokens === void 0 || turns === void 0) continue;
|
|
718
|
+
parsed[modelKey] = { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, turns };
|
|
719
|
+
}
|
|
720
|
+
if (Object.keys(parsed).length > 0) state.days[day] = parsed;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return state;
|
|
724
|
+
}
|
|
725
|
+
function listJsonl2(dir) {
|
|
726
|
+
const found = [];
|
|
727
|
+
const walk = (current) => {
|
|
728
|
+
let entries;
|
|
729
|
+
try {
|
|
730
|
+
entries = readdirSync2(current, { withFileTypes: true, encoding: "utf8" });
|
|
731
|
+
} catch {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
for (const entry of entries) {
|
|
735
|
+
if (entry.isSymbolicLink()) continue;
|
|
736
|
+
const path = join3(current, entry.name);
|
|
737
|
+
if (entry.isDirectory()) walk(path);
|
|
738
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) found.push(path);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
walk(dir);
|
|
742
|
+
return found;
|
|
743
|
+
}
|
|
744
|
+
function readChunk2(path, offset) {
|
|
745
|
+
const size = statSync2(path).size;
|
|
746
|
+
const start = Math.min(offset, size);
|
|
747
|
+
const length = Math.min(size - start, MAX_CHUNK2);
|
|
748
|
+
if (length <= 0) return Buffer.alloc(0);
|
|
749
|
+
const buffer = Buffer.alloc(length);
|
|
750
|
+
const fd = openSync2(path, "r");
|
|
751
|
+
try {
|
|
752
|
+
readSync2(fd, buffer, 0, length, start);
|
|
753
|
+
} finally {
|
|
754
|
+
closeSync2(fd);
|
|
755
|
+
}
|
|
756
|
+
return buffer;
|
|
757
|
+
}
|
|
758
|
+
function readCodexAuth(home) {
|
|
759
|
+
try {
|
|
760
|
+
return classifyCodexAuth(readFileSync2(join3(home, "auth.json"), "utf8"));
|
|
761
|
+
} catch {
|
|
762
|
+
return "unknown";
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function readCodexIncrement(dir, previous) {
|
|
766
|
+
const state = parseCodexState(JSON.parse(JSON.stringify(previous)));
|
|
767
|
+
const known = state.files;
|
|
768
|
+
state.files = {};
|
|
769
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
770
|
+
let caughtUp = true;
|
|
771
|
+
for (const path of listJsonl2(dir)) {
|
|
772
|
+
const key = relative2(dir, path).split("\\").join("/");
|
|
773
|
+
const size = statSync2(path).size;
|
|
774
|
+
const mark = known[key] ?? { offset: 0 };
|
|
775
|
+
if (mark.offset > size) {
|
|
776
|
+
state.files[key] = { offset: size };
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
let buffer = readChunk2(path, mark.offset);
|
|
780
|
+
if (buffer.length === 0) {
|
|
781
|
+
state.files[key] = mark;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (mark.skipLine) {
|
|
785
|
+
const newline = buffer.indexOf(10);
|
|
786
|
+
if (newline === -1) {
|
|
787
|
+
mark.offset += buffer.length;
|
|
788
|
+
state.files[key] = mark;
|
|
789
|
+
if (mark.offset < size) caughtUp = false;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
buffer = buffer.subarray(newline + 1);
|
|
793
|
+
mark.offset += newline + 1;
|
|
794
|
+
delete mark.skipLine;
|
|
795
|
+
}
|
|
796
|
+
const folded = foldCodexTranscript(state.days, mark.cursor, buffer, state.since);
|
|
797
|
+
for (const day of folded.dirty) dirty.add(day);
|
|
798
|
+
mark.offset += folded.consumed;
|
|
799
|
+
if (folded.cursor) mark.cursor = folded.cursor;
|
|
800
|
+
if (folded.consumed === 0 && buffer.length === MAX_CHUNK2) {
|
|
801
|
+
mark.skipLine = true;
|
|
802
|
+
mark.offset += buffer.length;
|
|
803
|
+
}
|
|
804
|
+
state.files[key] = mark;
|
|
805
|
+
if (mark.offset < size) caughtUp = false;
|
|
806
|
+
}
|
|
807
|
+
return { state, dirtyDays: [...dirty].sort(), caughtUp };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/copilot.ts
|
|
811
|
+
var MODEL_PATTERN4 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
812
|
+
var API_VERSION = "2026-03-10";
|
|
813
|
+
function authFailure(result) {
|
|
814
|
+
if (result.code === 0) return false;
|
|
815
|
+
const text = `${result.stdout}
|
|
816
|
+
${result.stderr}`;
|
|
817
|
+
if (text.includes("ENOENT")) return false;
|
|
818
|
+
return /\b401\b/.test(text) || /\b403\b/.test(text) || /must authenticate|authentication required|Forbidden/i.test(text);
|
|
819
|
+
}
|
|
820
|
+
function finiteInt4(value) {
|
|
821
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 1e12) return void 0;
|
|
822
|
+
return value;
|
|
823
|
+
}
|
|
824
|
+
function quantityOf(item) {
|
|
825
|
+
return finiteInt4(item["grossQuantity"]) ?? finiteInt4(item["quantity"]) ?? finiteInt4(item["netQuantity"]);
|
|
826
|
+
}
|
|
827
|
+
function copilotModelKey(model) {
|
|
828
|
+
if (typeof model !== "string") return "";
|
|
829
|
+
const key = model.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9._-]/g, "").slice(0, 80);
|
|
830
|
+
return MODEL_PATTERN4.test(key) ? key : "";
|
|
831
|
+
}
|
|
832
|
+
function rowFromItem(item) {
|
|
833
|
+
const modelKey = copilotModelKey(item["model"]);
|
|
834
|
+
const unit = typeof item["unitType"] === "string" ? item["unitType"].toLowerCase() : "";
|
|
835
|
+
const count = quantityOf(item);
|
|
836
|
+
if (!unit.includes("request") || count === void 0) return null;
|
|
837
|
+
return {
|
|
838
|
+
...modelKey ? { model: modelKey } : {},
|
|
839
|
+
inputTokens: 0,
|
|
840
|
+
outputTokens: 0,
|
|
841
|
+
cacheReadTokens: 0,
|
|
842
|
+
cacheWriteTokens: 0,
|
|
843
|
+
turns: count,
|
|
844
|
+
tokensKnown: false
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function addRow(rows, row) {
|
|
848
|
+
const key = row.model ?? "_";
|
|
849
|
+
const current = rows.get(key);
|
|
850
|
+
if (!current) {
|
|
851
|
+
rows.set(key, row);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
current.inputTokens += row.inputTokens;
|
|
855
|
+
current.outputTokens += row.outputTokens;
|
|
856
|
+
current.cacheReadTokens += row.cacheReadTokens;
|
|
857
|
+
current.cacheWriteTokens += row.cacheWriteTokens;
|
|
858
|
+
current.turns += row.turns;
|
|
859
|
+
current.tokensKnown = current.tokensKnown && row.tokensKnown;
|
|
860
|
+
}
|
|
861
|
+
function rowsFromCopilotUsage(payload, day) {
|
|
862
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return [];
|
|
863
|
+
const items = payload["usageItems"];
|
|
864
|
+
if (!Array.isArray(items)) return [];
|
|
865
|
+
const rows = /* @__PURE__ */ new Map();
|
|
866
|
+
for (const item of items) {
|
|
867
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
868
|
+
const record = item;
|
|
869
|
+
if (typeof record["date"] === "string" && record["date"] !== day) continue;
|
|
870
|
+
const row = rowFromItem(record);
|
|
871
|
+
if (row) addRow(rows, row);
|
|
872
|
+
}
|
|
873
|
+
return [...rows.values()];
|
|
874
|
+
}
|
|
875
|
+
async function ghJson(gh, path) {
|
|
876
|
+
const result = await gh([
|
|
877
|
+
"api",
|
|
878
|
+
"--hostname",
|
|
879
|
+
"github.com",
|
|
880
|
+
"-H",
|
|
881
|
+
"Accept: application/vnd.github+json",
|
|
882
|
+
"-H",
|
|
883
|
+
`X-GitHub-Api-Version: ${API_VERSION}`,
|
|
884
|
+
path
|
|
885
|
+
]);
|
|
886
|
+
if (authFailure(result)) return { auth: true, payload: null };
|
|
887
|
+
if (result.code !== 0) return { auth: false, payload: null };
|
|
888
|
+
try {
|
|
889
|
+
return { auth: false, payload: JSON.parse(result.stdout) };
|
|
890
|
+
} catch {
|
|
891
|
+
return { auth: false, payload: null };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function readCopilotDay(gh, day) {
|
|
895
|
+
const user = await ghJson(gh, "user");
|
|
896
|
+
if (user.auth) return { status: "auth" };
|
|
897
|
+
if (!user.payload) return { status: "unavailable" };
|
|
898
|
+
const login2 = user.payload && typeof user.payload === "object" && !Array.isArray(user.payload) ? user.payload["login"] : void 0;
|
|
899
|
+
if (typeof login2 !== "string" || !/^[A-Za-z0-9-]{1,39}$/.test(login2)) return { status: "empty" };
|
|
900
|
+
const [year, month, date] = day.split("-");
|
|
901
|
+
const query = `year=${year}&month=${Number(month)}&day=${Number(date)}`;
|
|
902
|
+
const base = `/users/${login2}/settings/billing`;
|
|
903
|
+
const premium = await ghJson(gh, `${base}/premium_request/usage?${query}`);
|
|
904
|
+
if (premium.auth) return { status: "auth" };
|
|
905
|
+
const rows = rowsFromCopilotUsage(premium.payload, day);
|
|
906
|
+
return rows.length > 0 ? { status: "rows", rows } : { status: "empty" };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/transport.ts
|
|
910
|
+
var USAGE_SOURCES = ["cursor", "claude_code", "github_copilot", "codex"];
|
|
911
|
+
function isUsageSource(value) {
|
|
912
|
+
return typeof value === "string" && USAGE_SOURCES.includes(value);
|
|
913
|
+
}
|
|
914
|
+
function resolveUsageBaseUrl(raw) {
|
|
915
|
+
let url;
|
|
916
|
+
try {
|
|
917
|
+
url = new URL(raw);
|
|
918
|
+
} catch {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
922
|
+
if (url.protocol !== "https:" && !loopback) return null;
|
|
923
|
+
return raw.replace(/\/+$/, "");
|
|
924
|
+
}
|
|
925
|
+
async function fetchUsageSource(options) {
|
|
926
|
+
let response;
|
|
927
|
+
try {
|
|
928
|
+
response = await options.fetchImpl(`${options.baseUrl.replace(/\/$/, "")}/v1/ai-usage/telemetry`, {
|
|
929
|
+
method: "GET",
|
|
930
|
+
redirect: "error",
|
|
931
|
+
headers: { authorization: `Bearer ${options.token}` }
|
|
932
|
+
});
|
|
933
|
+
} catch {
|
|
934
|
+
return { status: 0 };
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
const body = await response.json();
|
|
938
|
+
if (response.ok && isUsageSource(body.source)) return { status: response.status, source: body.source };
|
|
939
|
+
} catch {
|
|
940
|
+
}
|
|
941
|
+
return { status: response.status };
|
|
942
|
+
}
|
|
943
|
+
async function postTelemetry(options) {
|
|
944
|
+
const response = await options.fetchImpl(`${options.baseUrl.replace(/\/$/, "")}/v1/ai-usage/telemetry`, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
redirect: "error",
|
|
947
|
+
headers: {
|
|
948
|
+
authorization: `Bearer ${options.token}`,
|
|
949
|
+
"content-type": "application/json"
|
|
950
|
+
},
|
|
951
|
+
body: JSON.stringify({
|
|
952
|
+
day: options.day,
|
|
953
|
+
source: options.source ?? "cursor",
|
|
954
|
+
...options.device ? { device: options.device } : {},
|
|
955
|
+
rows: options.rows.map((row) => ({
|
|
956
|
+
inputTokens: row.inputTokens,
|
|
957
|
+
outputTokens: row.outputTokens,
|
|
958
|
+
cacheReadTokens: row.cacheReadTokens,
|
|
959
|
+
cacheWriteTokens: row.cacheWriteTokens,
|
|
960
|
+
turns: row.turns,
|
|
961
|
+
tokensKnown: row.tokensKnown,
|
|
962
|
+
...row.model ? { model: row.model } : {}
|
|
963
|
+
}))
|
|
964
|
+
})
|
|
965
|
+
});
|
|
966
|
+
let code;
|
|
967
|
+
try {
|
|
968
|
+
const body = await response.json();
|
|
969
|
+
if (typeof body.code === "string") code = body.code;
|
|
970
|
+
} catch {
|
|
971
|
+
code = void 0;
|
|
972
|
+
}
|
|
973
|
+
return code === void 0 ? { ok: response.ok, status: response.status } : { ok: response.ok, status: response.status, code };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// src/session.ts
|
|
977
|
+
var EXIT_USAGE = 2;
|
|
978
|
+
var EXIT_LAUNCHER = 3;
|
|
979
|
+
var EXIT_REPORT = 4;
|
|
980
|
+
var EXIT_COPILOT = 5;
|
|
981
|
+
var MAX_ROWS = 40;
|
|
982
|
+
var TOKEN_PATTERN = /^sau_[A-Za-z0-9_-]{20,200}$/;
|
|
983
|
+
var DEVICE_PATTERN = /^[a-f0-9]{16}$/;
|
|
984
|
+
function newDevice() {
|
|
985
|
+
return randomBytes(8).toString("hex");
|
|
986
|
+
}
|
|
987
|
+
function readJson(path) {
|
|
988
|
+
if (!existsSync2(path)) return {};
|
|
989
|
+
try {
|
|
990
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
991
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
992
|
+
const record = parsed;
|
|
993
|
+
const config = {};
|
|
994
|
+
if (typeof record["token"] === "string") config.token = record["token"];
|
|
995
|
+
if (typeof record["device"] === "string" && DEVICE_PATTERN.test(record["device"])) config.device = record["device"];
|
|
996
|
+
if (typeof record["lastAcceptedDay"] === "string") config.lastAcceptedDay = record["lastAcceptedDay"];
|
|
997
|
+
if (record["claude"]) config.claude = parseClaudeState(record["claude"]);
|
|
998
|
+
if (record["codex"]) config.codex = parseCodexState(record["codex"]);
|
|
999
|
+
if (isUsageSource(record["source"])) config.source = record["source"];
|
|
1000
|
+
return config;
|
|
1001
|
+
} catch {
|
|
1002
|
+
return {};
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function writeJson(path, value) {
|
|
1006
|
+
mkdirSync2(dirname(path), { recursive: true, mode: 448 });
|
|
1007
|
+
writeFileSync2(path, `${JSON.stringify(value)}
|
|
1008
|
+
`, { mode: 384 });
|
|
1009
|
+
chmodSync(path, 384);
|
|
1010
|
+
}
|
|
1011
|
+
function restoreFile(path, previous) {
|
|
1012
|
+
if (previous === null) {
|
|
1013
|
+
if (existsSync2(path)) unlinkSync(path);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
writeFileSync2(path, previous);
|
|
1017
|
+
}
|
|
1018
|
+
function readSpool(dir) {
|
|
1019
|
+
if (!existsSync2(dir)) return [];
|
|
1020
|
+
return readdirSync3(dir).filter((name) => name.endsWith(".json")).map((name) => {
|
|
1021
|
+
const path = join4(dir, name);
|
|
1022
|
+
try {
|
|
1023
|
+
return { path, event: readSpoolEvent(JSON.parse(readFileSync3(path, "utf8"))) };
|
|
1024
|
+
} catch {
|
|
1025
|
+
return { path, event: null };
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
function utcDay(date) {
|
|
1030
|
+
return date.toISOString().slice(0, 10);
|
|
1031
|
+
}
|
|
1032
|
+
function clearSpool(dir) {
|
|
1033
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
1034
|
+
}
|
|
1035
|
+
async function login(options) {
|
|
1036
|
+
const token = options.token.replace(/^\uFEFF/, "").trim();
|
|
1037
|
+
if (!TOKEN_PATTERN.test(token)) {
|
|
1038
|
+
return { code: EXIT_USAGE, message: "El token no tiene el formato de un alta de uso." };
|
|
1039
|
+
}
|
|
1040
|
+
const previousConfig = existsSync2(options.paths.configPath) ? readFileSync3(options.paths.configPath, "utf8") : null;
|
|
1041
|
+
const previousHooks = existsSync2(options.paths.hooksPath) ? readFileSync3(options.paths.hooksPath, "utf8") : null;
|
|
1042
|
+
const config = readJson(options.paths.configPath);
|
|
1043
|
+
const tokenChanged = config.token !== void 0 && config.token !== token;
|
|
1044
|
+
const next = { token, device: config.device ?? newDevice() };
|
|
1045
|
+
if (config.lastAcceptedDay) next.lastAcceptedDay = config.lastAcceptedDay;
|
|
1046
|
+
const since = utcDay(options.now ?? /* @__PURE__ */ new Date());
|
|
1047
|
+
next.claude = config.claude && !tokenChanged ? config.claude : emptyClaudeState(since);
|
|
1048
|
+
next.codex = config.codex && !tokenChanged ? config.codex : emptyCodexState(since);
|
|
1049
|
+
if (config.source && !tokenChanged) next.source = config.source;
|
|
1050
|
+
writeJson(options.paths.configPath, next);
|
|
1051
|
+
try {
|
|
1052
|
+
mkdirSync2(dirname(options.paths.hooksPath), { recursive: true });
|
|
1053
|
+
writeFileSync2(options.paths.hooksPath, mergeCursorHook(previousHooks ?? "", options.hookCommand));
|
|
1054
|
+
await options.launcher.install();
|
|
1055
|
+
if (tokenChanged) clearSpool(options.paths.spoolPath);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
restoreFile(options.paths.configPath, previousConfig);
|
|
1058
|
+
restoreFile(options.paths.hooksPath, previousHooks);
|
|
1059
|
+
const launcherFailed = err instanceof LauncherError;
|
|
1060
|
+
return {
|
|
1061
|
+
code: launcherFailed ? EXIT_LAUNCHER : EXIT_USAGE,
|
|
1062
|
+
message: launcherFailed ? `No se pudo registrar el arranque. No se ha dejado el token instalado. ${err.message}` : `No se pudo instalar el hook de Cursor. No se ha dejado el token instalado. ${err.message}`
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
return { code: 0, message: "Agente instalado. El arranque subir\xE1 el uso; no hace falta lanzarlo en cada sesi\xF3n." };
|
|
1066
|
+
}
|
|
1067
|
+
async function logout(options) {
|
|
1068
|
+
let launcherError;
|
|
1069
|
+
try {
|
|
1070
|
+
await options.launcher.uninstall();
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
launcherError = err;
|
|
1073
|
+
}
|
|
1074
|
+
let hookError = false;
|
|
1075
|
+
if (existsSync2(options.paths.hooksPath)) {
|
|
1076
|
+
try {
|
|
1077
|
+
const current = readFileSync3(options.paths.hooksPath, "utf8");
|
|
1078
|
+
writeFileSync2(options.paths.hooksPath, removeCursorHook(current, options.hookCommand));
|
|
1079
|
+
} catch {
|
|
1080
|
+
hookError = true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
clearSpool(options.paths.spoolPath);
|
|
1084
|
+
const config = readJson(options.paths.configPath);
|
|
1085
|
+
if (config.token || config.lastAcceptedDay) {
|
|
1086
|
+
const next = {};
|
|
1087
|
+
if (config.lastAcceptedDay) next.lastAcceptedDay = config.lastAcceptedDay;
|
|
1088
|
+
writeJson(options.paths.configPath, next);
|
|
1089
|
+
}
|
|
1090
|
+
if (launcherError) {
|
|
1091
|
+
return { code: EXIT_LAUNCHER, message: `No se pudo quitar el arranque. ${launcherError.message}` };
|
|
1092
|
+
}
|
|
1093
|
+
if (hookError) {
|
|
1094
|
+
return { code: EXIT_USAGE, message: "El arranque se quit\xF3, pero el hook de Cursor no: su fichero no es JSON." };
|
|
1095
|
+
}
|
|
1096
|
+
return { code: 0, message: "Agente retirado." };
|
|
1097
|
+
}
|
|
1098
|
+
function dropCursorHook(paths, hookCommand) {
|
|
1099
|
+
if (!hookCommand || !existsSync2(paths.hooksPath)) return;
|
|
1100
|
+
try {
|
|
1101
|
+
const current = readFileSync3(paths.hooksPath, "utf8");
|
|
1102
|
+
const next = removeCursorHook(current, hookCommand);
|
|
1103
|
+
if (next !== current) writeFileSync2(paths.hooksPath, next);
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function report(options) {
|
|
1108
|
+
const config = readJson(options.paths.configPath);
|
|
1109
|
+
const token = config.token;
|
|
1110
|
+
if (!token) return { code: EXIT_USAGE, message: "No hay token. Hay que hacer login en esta conexi\xF3n." };
|
|
1111
|
+
const device = config.device ?? newDevice();
|
|
1112
|
+
const baseUrl = resolveUsageBaseUrl(options.baseUrl);
|
|
1113
|
+
if (!baseUrl) return { code: EXIT_USAGE, message: "La URL del API tiene que ser https." };
|
|
1114
|
+
const today = utcDay(options.now ?? /* @__PURE__ */ new Date());
|
|
1115
|
+
const probe = await fetchUsageSource({ baseUrl, token, fetchImpl: options.fetchImpl });
|
|
1116
|
+
if (probe.status === 401) {
|
|
1117
|
+
return { code: EXIT_REPORT, message: "El servidor no reconoce el token. Hay que repetir el login con un token nuevo." };
|
|
1118
|
+
}
|
|
1119
|
+
const source = probe.source ?? config.source;
|
|
1120
|
+
if (!source) {
|
|
1121
|
+
return { code: EXIT_REPORT, message: `No se pudo saber qu\xE9 herramienta informa esta conexi\xF3n (${probe.status}). Se reintentar\xE1.` };
|
|
1122
|
+
}
|
|
1123
|
+
const files = readSpool(options.paths.spoolPath);
|
|
1124
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
1125
|
+
const outcome = { error: void 0, mismatch: false, acceptedAny: false };
|
|
1126
|
+
let lastAccepted = config.lastAcceptedDay;
|
|
1127
|
+
let claudeState = config.claude;
|
|
1128
|
+
let codexState = config.codex;
|
|
1129
|
+
const send = async (day, rows, kept) => {
|
|
1130
|
+
if (rows.length === 0) return "skip";
|
|
1131
|
+
if (rows.length > MAX_ROWS) {
|
|
1132
|
+
outcome.error = `El d\xEDa ${day} tiene m\xE1s de ${MAX_ROWS} modelos y no se ha subido.`;
|
|
1133
|
+
return "stop";
|
|
1134
|
+
}
|
|
1135
|
+
const result = await postTelemetry({
|
|
1136
|
+
baseUrl,
|
|
1137
|
+
token,
|
|
1138
|
+
day,
|
|
1139
|
+
rows,
|
|
1140
|
+
fetchImpl: options.fetchImpl,
|
|
1141
|
+
source,
|
|
1142
|
+
...source === "github_copilot" ? {} : { device }
|
|
1143
|
+
});
|
|
1144
|
+
if (result.ok) {
|
|
1145
|
+
outcome.acceptedAny = true;
|
|
1146
|
+
accepted.add(day);
|
|
1147
|
+
if (!lastAccepted || day > lastAccepted) lastAccepted = day;
|
|
1148
|
+
return "ok";
|
|
1149
|
+
}
|
|
1150
|
+
if (result.code === "AI_USAGE_SOURCE_MISMATCH") {
|
|
1151
|
+
outcome.mismatch = true;
|
|
1152
|
+
return "stop";
|
|
1153
|
+
}
|
|
1154
|
+
if (source === "github_copilot" && result.code === "AI_USAGE_DAY_INVALID") return "skip";
|
|
1155
|
+
outcome.error = `El d\xEDa ${day} no se acept\xF3 (${result.code ?? result.status}).${kept}`;
|
|
1156
|
+
return "stop";
|
|
1157
|
+
};
|
|
1158
|
+
let copilotAuth = false;
|
|
1159
|
+
let copilotUnavailable = false;
|
|
1160
|
+
let codexAuth;
|
|
1161
|
+
if (source === "cursor") {
|
|
1162
|
+
const byDay = aggregateByDay(files.flatMap((file) => file.event ? [file.event] : []));
|
|
1163
|
+
for (const day of [...byDay.keys()].sort()) {
|
|
1164
|
+
if (await send(day, byDay.get(day) ?? [], " El spool de ese d\xEDa sigue en el port\xE1til.") === "stop") break;
|
|
1165
|
+
}
|
|
1166
|
+
} else if (source === "claude_code") {
|
|
1167
|
+
const claudeDir = options.claudeProjectsDir;
|
|
1168
|
+
const ready = claudeDir && existsSync2(claudeDir) ? readClaudeIncrement(claudeDir, config.claude ?? emptyClaudeState(today)) : null;
|
|
1169
|
+
if (ready) {
|
|
1170
|
+
for (const day of ready.dirtyDays) {
|
|
1171
|
+
if (await send(day, rowsForDay(ready.state.days, day), " La transcripci\xF3n sigue en el port\xE1til.") === "stop") break;
|
|
1172
|
+
}
|
|
1173
|
+
if (!outcome.error) {
|
|
1174
|
+
claudeState = ready.state;
|
|
1175
|
+
if (ready.caughtUp) pruneClaudeDays(claudeState, previousUtcDay(today));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
} else if (source === "github_copilot") {
|
|
1179
|
+
if (options.gh) {
|
|
1180
|
+
for (const day of [previousUtcDay(today), today]) {
|
|
1181
|
+
const read = await readCopilotDay(options.gh, day);
|
|
1182
|
+
if (read.status === "auth") {
|
|
1183
|
+
copilotAuth = true;
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
if (read.status === "unavailable") {
|
|
1187
|
+
copilotUnavailable = true;
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
if (read.status !== "rows") continue;
|
|
1191
|
+
if (await send(day, read.rows, "") === "stop") break;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
} else {
|
|
1195
|
+
const codexHome2 = options.codexHome;
|
|
1196
|
+
if (codexHome2 && existsSync2(codexHome2)) {
|
|
1197
|
+
const auth = readCodexAuth(codexHome2);
|
|
1198
|
+
const sessions = join4(codexHome2, "sessions");
|
|
1199
|
+
if (auth !== "chatgpt") codexAuth = auth;
|
|
1200
|
+
else if (existsSync2(sessions)) {
|
|
1201
|
+
const ready = readCodexIncrement(sessions, codexState ?? emptyCodexState(today));
|
|
1202
|
+
for (const day of ready.dirtyDays) {
|
|
1203
|
+
if (await send(day, rowsForCodexDay(ready.state.days, day), " La sesi\xF3n sigue en el port\xE1til.") === "stop") break;
|
|
1204
|
+
}
|
|
1205
|
+
if (!outcome.error) {
|
|
1206
|
+
codexState = ready.state;
|
|
1207
|
+
if (ready.caughtUp) {
|
|
1208
|
+
const before = previousUtcDay(today);
|
|
1209
|
+
for (const day of Object.keys(codexState.days)) {
|
|
1210
|
+
if (day < before) delete codexState.days[day];
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const dropSpool = source !== "cursor" || outcome.mismatch;
|
|
1218
|
+
if (dropSpool) dropCursorHook(options.paths, options.hookCommand);
|
|
1219
|
+
for (const file of files) {
|
|
1220
|
+
const day = file.event?.at.slice(0, 10);
|
|
1221
|
+
if (dropSpool || day && day < today && accepted.has(day)) unlinkSync(file.path);
|
|
1222
|
+
}
|
|
1223
|
+
const next = { token, device };
|
|
1224
|
+
if (lastAccepted) next.lastAcceptedDay = lastAccepted;
|
|
1225
|
+
if (claudeState) next.claude = claudeState;
|
|
1226
|
+
if (codexState) next.codex = codexState;
|
|
1227
|
+
if (!outcome.mismatch) next.source = source;
|
|
1228
|
+
writeJson(options.paths.configPath, next);
|
|
1229
|
+
if (outcome.error) return { code: EXIT_REPORT, message: outcome.error };
|
|
1230
|
+
if (outcome.mismatch) {
|
|
1231
|
+
return { code: 0, message: "El uso de esta herramienta no corresponde a esta conexi\xF3n y no se ha subido." };
|
|
1232
|
+
}
|
|
1233
|
+
if (copilotAuth && !outcome.acceptedAny) {
|
|
1234
|
+
return { code: EXIT_COPILOT, message: "gh no tiene permiso para leer el uso de Copilot. No se ha escrito una fila vac\xEDa." };
|
|
1235
|
+
}
|
|
1236
|
+
if (copilotUnavailable) {
|
|
1237
|
+
return { code: EXIT_COPILOT, message: "No se pudo usar gh. Hace falta GitHub CLI con gh auth login en la cuenta de Copilot." };
|
|
1238
|
+
}
|
|
1239
|
+
if (!outcome.acceptedAny) {
|
|
1240
|
+
if (codexAuth === "api_key") {
|
|
1241
|
+
return { code: 0, message: "Codex usa una API key. Ese gasto no se atribuye a la cuenta individual." };
|
|
1242
|
+
}
|
|
1243
|
+
if (codexAuth === "unknown") {
|
|
1244
|
+
return { code: 0, message: "No se pudo confirmar que Codex use la suscripci\xF3n de ChatGPT. No se ha subido nada." };
|
|
1245
|
+
}
|
|
1246
|
+
return { code: 0, message: "No hay uso nuevo que subir." };
|
|
1247
|
+
}
|
|
1248
|
+
return { code: 0, message: `Uso aceptado hasta ${lastAccepted ?? "hoy"}.` };
|
|
1249
|
+
}
|
|
1250
|
+
function previousUtcDay(day) {
|
|
1251
|
+
return new Date(Date.parse(`${day}T00:00:00Z`) - 864e5).toISOString().slice(0, 10);
|
|
1252
|
+
}
|
|
1253
|
+
async function status(options) {
|
|
1254
|
+
const config = readJson(options.paths.configPath);
|
|
1255
|
+
const installed = await options.launcher.installed();
|
|
1256
|
+
const lines = [
|
|
1257
|
+
`agente: ${config.token ? "instalado" : "ausente"}`,
|
|
1258
|
+
`arranque: ${installed ? "s\xED" : "no"}`,
|
|
1259
|
+
`herramienta: ${config.source ?? "sin comprobar"}`,
|
|
1260
|
+
`\xFAltimo d\xEDa aceptado: ${config.lastAcceptedDay ?? "ninguno"}`
|
|
1261
|
+
];
|
|
1262
|
+
return { code: 0, message: lines.join("\n") };
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/cli.ts
|
|
1266
|
+
var DEFAULT_BASE_URL = "https://api.aaa-sentinel.com";
|
|
1267
|
+
var execFileAsync = promisify(execFile);
|
|
1268
|
+
function nodeExec(file, args) {
|
|
1269
|
+
return execFileAsync(file, args, { windowsHide: true }).then(
|
|
1270
|
+
() => ({ code: 0, stderr: "" }),
|
|
1271
|
+
(error) => ({
|
|
1272
|
+
code: typeof error.code === "number" ? error.code : 1,
|
|
1273
|
+
stderr: [error.stderr, error.stdout].filter((part) => part).join("\n") || error.message
|
|
1274
|
+
})
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
function pathsFromEnv() {
|
|
1278
|
+
const home = process.env["SENTINEL_USAGE_HOME"] ?? join5(homedir(), ".config", "aaa-sentinel");
|
|
1279
|
+
return {
|
|
1280
|
+
configPath: join5(home, "usage.json"),
|
|
1281
|
+
spoolPath: join5(home, "spool"),
|
|
1282
|
+
hooksPath: process.env["SENTINEL_USAGE_CURSOR_HOOKS"] ?? join5(homedir(), ".cursor", "hooks.json")
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function codexHome() {
|
|
1286
|
+
return process.env["SENTINEL_USAGE_CODEX_HOME"] || process.env["CODEX_HOME"] || join5(homedir(), ".codex");
|
|
1287
|
+
}
|
|
1288
|
+
function ghRun(args) {
|
|
1289
|
+
return execFileAsync("gh", args, { windowsHide: true }).then(
|
|
1290
|
+
(result) => ({ code: 0, stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? "") }),
|
|
1291
|
+
(error) => ({
|
|
1292
|
+
code: error.code === "ENOENT" ? 127 : typeof error.code === "number" ? error.code : 1,
|
|
1293
|
+
stdout: String(error.stdout ?? ""),
|
|
1294
|
+
stderr: error.code === "ENOENT" ? "ENOENT" : String(error.stderr ?? error.message)
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
function claudeProjectsDir() {
|
|
1299
|
+
if (process.env["SENTINEL_USAGE_CLAUDE_DIR"]) return process.env["SENTINEL_USAGE_CLAUDE_DIR"];
|
|
1300
|
+
return join5(process.env["CLAUDE_CONFIG_DIR"] ?? join5(homedir(), ".claude"), "projects");
|
|
1301
|
+
}
|
|
1302
|
+
function hookArgs(spoolPath) {
|
|
1303
|
+
const hookJs = fileURLToPath(new URL("./hook.js", import.meta.url));
|
|
1304
|
+
return [process.execPath, hookJs, spoolPath];
|
|
1305
|
+
}
|
|
1306
|
+
function reportArgs() {
|
|
1307
|
+
const cliJs = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
1308
|
+
return [process.execPath, cliJs, "report"];
|
|
1309
|
+
}
|
|
1310
|
+
async function main() {
|
|
1311
|
+
const [command, tokenArg] = [process.argv[2], process.argv[3]];
|
|
1312
|
+
const paths = pathsFromEnv();
|
|
1313
|
+
const hookCommand = formatCommand(hookArgs(paths.spoolPath));
|
|
1314
|
+
const uid = process.getuid?.();
|
|
1315
|
+
const launcher = createLauncher({
|
|
1316
|
+
platform: process.platform,
|
|
1317
|
+
exec: nodeExec,
|
|
1318
|
+
// El hook solo escribe el spool. Si la tarea lo relanza, el informe no sale del portátil.
|
|
1319
|
+
args: reportArgs(),
|
|
1320
|
+
homeDir: homedir(),
|
|
1321
|
+
scriptDir: dirname2(paths.configPath),
|
|
1322
|
+
...uid !== void 0 ? { uid } : {}
|
|
1323
|
+
});
|
|
1324
|
+
if (command === "login") {
|
|
1325
|
+
if (tokenArg) {
|
|
1326
|
+
console.log("Pasa el token por la entrada est\xE1ndar, no como argumento: quedar\xEDa en el historial del shell.");
|
|
1327
|
+
return 2;
|
|
1328
|
+
}
|
|
1329
|
+
if (process.stdin.isTTY) {
|
|
1330
|
+
console.log("Redirige el token a la entrada est\xE1ndar. No se lee desde el teclado para no mostrarlo.");
|
|
1331
|
+
return 2;
|
|
1332
|
+
}
|
|
1333
|
+
const token = (await readStdin()).trim();
|
|
1334
|
+
const result = await login({ token, paths, launcher, hookCommand });
|
|
1335
|
+
console.log(result.message);
|
|
1336
|
+
return result.code;
|
|
1337
|
+
}
|
|
1338
|
+
if (command === "logout") {
|
|
1339
|
+
const result = await logout({ paths, launcher, hookCommand });
|
|
1340
|
+
console.log(result.message);
|
|
1341
|
+
return result.code;
|
|
1342
|
+
}
|
|
1343
|
+
if (command === "report") {
|
|
1344
|
+
const baseUrl = process.env["SENTINEL_USAGE_BASE_URL"] ?? DEFAULT_BASE_URL;
|
|
1345
|
+
const result = await report({
|
|
1346
|
+
paths,
|
|
1347
|
+
baseUrl,
|
|
1348
|
+
fetchImpl: fetch,
|
|
1349
|
+
claudeProjectsDir: claudeProjectsDir(),
|
|
1350
|
+
codexHome: codexHome(),
|
|
1351
|
+
gh: ghRun,
|
|
1352
|
+
hookCommand
|
|
1353
|
+
});
|
|
1354
|
+
console.log(result.message);
|
|
1355
|
+
return result.code;
|
|
1356
|
+
}
|
|
1357
|
+
if (command === "status") {
|
|
1358
|
+
const result = await status({ paths, launcher });
|
|
1359
|
+
console.log(result.message);
|
|
1360
|
+
return result.code;
|
|
1361
|
+
}
|
|
1362
|
+
console.log("Uso: sentinel-usage login|logout|report|status");
|
|
1363
|
+
return 2;
|
|
1364
|
+
}
|
|
1365
|
+
function readStdin() {
|
|
1366
|
+
return new Promise((resolve, reject) => {
|
|
1367
|
+
const chunks = [];
|
|
1368
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
1369
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
1370
|
+
process.stdin.on("error", reject);
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
main().then((code) => {
|
|
1374
|
+
process.exitCode = code;
|
|
1375
|
+
}).catch((error) => {
|
|
1376
|
+
console.error(`sentinel-usage: ${error.message}`);
|
|
1377
|
+
process.exitCode = 1;
|
|
1378
|
+
});
|