token-rats 0.1.0 → 0.3.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 +27 -54
- package/dist/index.js +1052 -1194
- package/package.json +4 -5
package/dist/index.js
CHANGED
|
@@ -11,12 +11,12 @@ async function installCursorCommand(opts = {}) {
|
|
|
11
11
|
const args = ["install", "-g", "better-sqlite3@^9.4.3"];
|
|
12
12
|
console.log(`\x1B[2m$ ${pm} ${args.join(" ")}\x1B[0m
|
|
13
13
|
`);
|
|
14
|
-
const exitCode = await new Promise((
|
|
14
|
+
const exitCode = await new Promise((resolve2) => {
|
|
15
15
|
const child = spawn(pm, args, { stdio: "inherit" });
|
|
16
|
-
child.on("close", (code) =>
|
|
16
|
+
child.on("close", (code) => resolve2(code ?? 1));
|
|
17
17
|
child.on("error", (err) => {
|
|
18
18
|
console.error(`\x1B[31mFailed to launch ${pm}: ${err.message}\x1B[0m`);
|
|
19
|
-
|
|
19
|
+
resolve2(1);
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
if (exitCode === 0) {
|
|
@@ -32,6 +32,419 @@ async function installCursorCommand(opts = {}) {
|
|
|
32
32
|
process.exit(exitCode);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// src/commands/install-daemon.ts
|
|
36
|
+
import { execFile } from "node:child_process";
|
|
37
|
+
import * as fs2 from "node:fs";
|
|
38
|
+
import { createRequire } from "node:module";
|
|
39
|
+
import * as os2 from "node:os";
|
|
40
|
+
import * as path2 from "node:path";
|
|
41
|
+
import { promisify } from "node:util";
|
|
42
|
+
|
|
43
|
+
// src/lib/auth-store.ts
|
|
44
|
+
import * as crypto from "node:crypto";
|
|
45
|
+
import * as fs from "node:fs";
|
|
46
|
+
import * as os from "node:os";
|
|
47
|
+
import * as path from "node:path";
|
|
48
|
+
function tokenDir() {
|
|
49
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
50
|
+
const base = xdgConfig ?? path.join(os.homedir(), ".config");
|
|
51
|
+
return path.join(base, "token-rats");
|
|
52
|
+
}
|
|
53
|
+
function tokenPath() {
|
|
54
|
+
return path.join(tokenDir(), "token");
|
|
55
|
+
}
|
|
56
|
+
function statePath() {
|
|
57
|
+
return path.join(tokenDir(), "state.json");
|
|
58
|
+
}
|
|
59
|
+
function disconnectedPath() {
|
|
60
|
+
return path.join(tokenDir(), "disconnected");
|
|
61
|
+
}
|
|
62
|
+
function saveToken(token) {
|
|
63
|
+
const dir = tokenDir();
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
fs.writeFileSync(tokenPath(), token, { encoding: "utf8", mode: 384 });
|
|
66
|
+
}
|
|
67
|
+
function loadToken() {
|
|
68
|
+
try {
|
|
69
|
+
const token = fs.readFileSync(tokenPath(), "utf8").trim();
|
|
70
|
+
return token.length > 0 ? token : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function deleteToken() {
|
|
76
|
+
try {
|
|
77
|
+
fs.unlinkSync(tokenPath());
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isLoggedIn() {
|
|
82
|
+
return loadToken() !== null;
|
|
83
|
+
}
|
|
84
|
+
function ensureDeviceId() {
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(statePath(), "utf8");
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
if (typeof parsed.deviceId === "string" && parsed.deviceId.length > 0) {
|
|
89
|
+
return parsed.deviceId;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
const dir = tokenDir();
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
const deviceId = crypto.randomUUID();
|
|
96
|
+
const state = { deviceId, createdAt: Date.now() };
|
|
97
|
+
fs.writeFileSync(statePath(), JSON.stringify(state, null, 2), {
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
mode: 384
|
|
100
|
+
});
|
|
101
|
+
return deviceId;
|
|
102
|
+
}
|
|
103
|
+
function markDisconnected() {
|
|
104
|
+
const dir = tokenDir();
|
|
105
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
106
|
+
fs.writeFileSync(disconnectedPath(), String(Date.now()), { mode: 384 });
|
|
107
|
+
}
|
|
108
|
+
function clearDisconnected() {
|
|
109
|
+
try {
|
|
110
|
+
fs.unlinkSync(disconnectedPath());
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function isDisconnected() {
|
|
115
|
+
return fs.existsSync(disconnectedPath());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/lib/log.ts
|
|
119
|
+
var ESC = "\x1B";
|
|
120
|
+
var c = {
|
|
121
|
+
reset: `${ESC}[0m`,
|
|
122
|
+
bold: `${ESC}[1m`,
|
|
123
|
+
dim: `${ESC}[2m`,
|
|
124
|
+
green: `${ESC}[32m`,
|
|
125
|
+
yellow: `${ESC}[33m`,
|
|
126
|
+
cyan: `${ESC}[36m`,
|
|
127
|
+
red: `${ESC}[31m`,
|
|
128
|
+
gray: `${ESC}[90m`
|
|
129
|
+
};
|
|
130
|
+
function strip(s) {
|
|
131
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
132
|
+
}
|
|
133
|
+
function isTTY() {
|
|
134
|
+
return process.stdout.isTTY === true;
|
|
135
|
+
}
|
|
136
|
+
function color(code, text) {
|
|
137
|
+
return isTTY() ? `${code}${text}${c.reset}` : strip(text);
|
|
138
|
+
}
|
|
139
|
+
function info(msg) {
|
|
140
|
+
console.log(color(c.cyan, ` ${msg}`));
|
|
141
|
+
}
|
|
142
|
+
function success(msg) {
|
|
143
|
+
console.log(color(c.green, `\u2713 ${msg}`));
|
|
144
|
+
}
|
|
145
|
+
function warn(msg) {
|
|
146
|
+
console.warn(color(c.yellow, `\u26A0 ${msg}`));
|
|
147
|
+
}
|
|
148
|
+
function error(msg) {
|
|
149
|
+
console.error(color(c.red, `\u2717 ${msg}`));
|
|
150
|
+
}
|
|
151
|
+
function dim(msg) {
|
|
152
|
+
console.log(color(c.dim, ` ${msg}`));
|
|
153
|
+
}
|
|
154
|
+
function bold(msg) {
|
|
155
|
+
console.log(isTTY() ? `${c.bold}${msg}${c.reset}` : msg);
|
|
156
|
+
}
|
|
157
|
+
function spinner(label) {
|
|
158
|
+
if (!isTTY()) {
|
|
159
|
+
process.stdout.write(` ${label}...
|
|
160
|
+
`);
|
|
161
|
+
return {
|
|
162
|
+
stop(finalMsg) {
|
|
163
|
+
if (finalMsg) process.stdout.write(` ${finalMsg}
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
169
|
+
let i = 0;
|
|
170
|
+
const interval = setInterval(() => {
|
|
171
|
+
process.stdout.write(`\r${c.cyan}${frames[i++ % frames.length]}${c.reset} ${label} `);
|
|
172
|
+
}, 80);
|
|
173
|
+
return {
|
|
174
|
+
stop(finalMsg) {
|
|
175
|
+
clearInterval(interval);
|
|
176
|
+
process.stdout.write("\r\x1B[K");
|
|
177
|
+
if (finalMsg) success(finalMsg);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/commands/install-daemon.ts
|
|
183
|
+
var exec = promisify(execFile);
|
|
184
|
+
var LABEL = "com.tokenrats.watch";
|
|
185
|
+
var LINUX_UNIT = "token-rats-watch.service";
|
|
186
|
+
var WINDOWS_TASK = "TokenRatsWatch";
|
|
187
|
+
function resolveCliPath() {
|
|
188
|
+
const entry = path2.resolve(process.argv[1] ?? "");
|
|
189
|
+
if (!entry.endsWith(".js")) throw new Error("Build the CLI before installing the daemon.");
|
|
190
|
+
const config = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
|
|
191
|
+
const runtime = path2.join(config, "token-rats", "runtime");
|
|
192
|
+
fs2.mkdirSync(runtime, { recursive: true });
|
|
193
|
+
const script = path2.join(runtime, "index.js");
|
|
194
|
+
if (entry !== script) fs2.copyFileSync(entry, script);
|
|
195
|
+
fs2.writeFileSync(path2.join(runtime, "package.json"), '{"type":"module"}\n');
|
|
196
|
+
const require2 = createRequire(import.meta.url);
|
|
197
|
+
const sqlPackage = path2.dirname(require2.resolve("sql.js/package.json"));
|
|
198
|
+
const destination = path2.join(runtime, "node_modules", "sql.js");
|
|
199
|
+
if (sqlPackage !== destination) fs2.cpSync(sqlPackage, destination, { recursive: true });
|
|
200
|
+
const runner = path2.join(runtime, "runner.mjs");
|
|
201
|
+
const settings = Object.fromEntries(
|
|
202
|
+
["XDG_CONFIG_HOME", "CLAUDE_CONFIG_DIR", "CODEX_HOME", "APPDATA"].flatMap(
|
|
203
|
+
(key) => process.env[key] ? [[key, process.env[key]]] : []
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
fs2.writeFileSync(
|
|
207
|
+
runner,
|
|
208
|
+
`Object.assign(process.env, ${JSON.stringify(settings)});
|
|
209
|
+
await import("./index.js");
|
|
210
|
+
`
|
|
211
|
+
);
|
|
212
|
+
return { node: process.execPath, script: runner };
|
|
213
|
+
}
|
|
214
|
+
function xml(value) {
|
|
215
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
216
|
+
}
|
|
217
|
+
function systemdArg(value) {
|
|
218
|
+
const escaped = value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%").replaceAll("$", "$$");
|
|
219
|
+
return `"${escaped}"`;
|
|
220
|
+
}
|
|
221
|
+
function darwinPlistPath() {
|
|
222
|
+
return path2.join(os2.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
223
|
+
}
|
|
224
|
+
function darwinLogDir() {
|
|
225
|
+
return path2.join(os2.homedir(), "Library", "Logs", "token-rats");
|
|
226
|
+
}
|
|
227
|
+
function darwinPlist(node, script, apiUrl) {
|
|
228
|
+
const logDir = darwinLogDir();
|
|
229
|
+
const stdout = path2.join(logDir, "watch.out.log");
|
|
230
|
+
const stderr = path2.join(logDir, "watch.err.log");
|
|
231
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
232
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
233
|
+
<plist version="1.0">
|
|
234
|
+
<dict>
|
|
235
|
+
<key>Label</key>
|
|
236
|
+
<string>${LABEL}</string>
|
|
237
|
+
<key>ProgramArguments</key>
|
|
238
|
+
<array>
|
|
239
|
+
<string>${xml(node)}</string>
|
|
240
|
+
<string>${xml(script)}</string>
|
|
241
|
+
<string>watch</string>
|
|
242
|
+
${apiUrl ? `<string>--api-url</string><string>${xml(apiUrl)}</string>` : ""}
|
|
243
|
+
</array>
|
|
244
|
+
<key>RunAtLoad</key>
|
|
245
|
+
<true/>
|
|
246
|
+
<key>KeepAlive</key>
|
|
247
|
+
<true/>
|
|
248
|
+
<key>StandardOutPath</key>
|
|
249
|
+
<string>${xml(stdout)}</string>
|
|
250
|
+
<key>StandardErrorPath</key>
|
|
251
|
+
<string>${xml(stderr)}</string>
|
|
252
|
+
</dict>
|
|
253
|
+
</plist>
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
async function darwinInstall(apiUrl) {
|
|
257
|
+
const { node, script } = resolveCliPath();
|
|
258
|
+
fs2.mkdirSync(darwinLogDir(), { recursive: true });
|
|
259
|
+
const plistPath = darwinPlistPath();
|
|
260
|
+
fs2.mkdirSync(path2.dirname(plistPath), { recursive: true });
|
|
261
|
+
fs2.writeFileSync(plistPath, darwinPlist(node, script, apiUrl), { mode: 420 });
|
|
262
|
+
await exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 0}/${LABEL}`]).catch(
|
|
263
|
+
() => void 0
|
|
264
|
+
);
|
|
265
|
+
try {
|
|
266
|
+
await exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 0}`, plistPath]);
|
|
267
|
+
} catch {
|
|
268
|
+
try {
|
|
269
|
+
await exec("launchctl", ["load", plistPath]);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Wrote ${plistPath} but failed to load it: ${err instanceof Error ? err.message : String(err)}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function darwinUninstall() {
|
|
278
|
+
const plistPath = darwinPlistPath();
|
|
279
|
+
try {
|
|
280
|
+
await exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 0}/${LABEL}`]);
|
|
281
|
+
} catch {
|
|
282
|
+
try {
|
|
283
|
+
await exec("launchctl", ["unload", plistPath]);
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
fs2.unlinkSync(plistPath);
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function darwinStatus() {
|
|
293
|
+
if (!fs2.existsSync(darwinPlistPath())) return "not-installed";
|
|
294
|
+
try {
|
|
295
|
+
const { stdout } = await exec("launchctl", ["list"]);
|
|
296
|
+
if (stdout.split("\n").some((line) => line.endsWith(LABEL) && /^\d+\s/.test(line)))
|
|
297
|
+
return "running";
|
|
298
|
+
return "stopped";
|
|
299
|
+
} catch {
|
|
300
|
+
return "stopped";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function linuxUnitPath() {
|
|
304
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
|
|
305
|
+
return path2.join(xdgConfig, "systemd", "user", LINUX_UNIT);
|
|
306
|
+
}
|
|
307
|
+
function linuxUnit(node, script, apiUrl) {
|
|
308
|
+
return `[Unit]
|
|
309
|
+
Description=Token Rats watcher \u2014 live AI usage sync
|
|
310
|
+
After=network-online.target
|
|
311
|
+
Wants=network-online.target
|
|
312
|
+
|
|
313
|
+
[Service]
|
|
314
|
+
Type=simple
|
|
315
|
+
ExecStart=${systemdArg(node)} ${systemdArg(script)} watch${apiUrl ? ` --api-url ${systemdArg(apiUrl)}` : ""}
|
|
316
|
+
Restart=on-failure
|
|
317
|
+
RestartSec=10s
|
|
318
|
+
Environment=NODE_ENV=production
|
|
319
|
+
|
|
320
|
+
[Install]
|
|
321
|
+
WantedBy=default.target
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
async function linuxInstall(apiUrl) {
|
|
325
|
+
const { node, script } = resolveCliPath();
|
|
326
|
+
const unitPath = linuxUnitPath();
|
|
327
|
+
fs2.mkdirSync(path2.dirname(unitPath), { recursive: true });
|
|
328
|
+
fs2.writeFileSync(unitPath, linuxUnit(node, script, apiUrl), { mode: 420 });
|
|
329
|
+
try {
|
|
330
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
331
|
+
await exec("systemctl", ["--user", "enable", "--now", LINUX_UNIT]);
|
|
332
|
+
await exec("systemctl", ["--user", "restart", LINUX_UNIT]);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Wrote ${unitPath} but failed to enable+start it: ${err instanceof Error ? err.message : String(err)}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function linuxUninstall() {
|
|
340
|
+
try {
|
|
341
|
+
await exec("systemctl", ["--user", "disable", "--now", LINUX_UNIT]);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
fs2.unlinkSync(linuxUnitPath());
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function linuxStatus() {
|
|
354
|
+
if (!fs2.existsSync(linuxUnitPath())) return "not-installed";
|
|
355
|
+
try {
|
|
356
|
+
const { stdout } = await exec("systemctl", ["--user", "is-active", LINUX_UNIT]);
|
|
357
|
+
return stdout.trim() === "active" ? "running" : "stopped";
|
|
358
|
+
} catch {
|
|
359
|
+
return "stopped";
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function windowsInstall(apiUrl) {
|
|
363
|
+
const { node, script } = resolveCliPath();
|
|
364
|
+
await exec("schtasks", [
|
|
365
|
+
"/Create",
|
|
366
|
+
"/SC",
|
|
367
|
+
"ONLOGON",
|
|
368
|
+
"/TN",
|
|
369
|
+
WINDOWS_TASK,
|
|
370
|
+
"/TR",
|
|
371
|
+
`"${node}" "${script}" watch${apiUrl ? ` --api-url "${apiUrl}"` : ""}`,
|
|
372
|
+
"/RL",
|
|
373
|
+
"LIMITED",
|
|
374
|
+
"/F"
|
|
375
|
+
]);
|
|
376
|
+
try {
|
|
377
|
+
await exec("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function windowsUninstall() {
|
|
382
|
+
try {
|
|
383
|
+
await exec("schtasks", ["/End", "/TN", WINDOWS_TASK]);
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
await exec("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function windowsStatus() {
|
|
392
|
+
try {
|
|
393
|
+
const { stdout } = await exec("schtasks", ["/Query", "/TN", WINDOWS_TASK, "/FO", "CSV", "/NH"]);
|
|
394
|
+
if (stdout.includes("Running")) return "running";
|
|
395
|
+
return "stopped";
|
|
396
|
+
} catch {
|
|
397
|
+
return "not-installed";
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function installDaemonCommand(apiUrl) {
|
|
401
|
+
clearDisconnected();
|
|
402
|
+
try {
|
|
403
|
+
if (process.platform === "darwin") {
|
|
404
|
+
await darwinInstall(apiUrl);
|
|
405
|
+
} else if (process.platform === "linux") {
|
|
406
|
+
await linuxInstall(apiUrl);
|
|
407
|
+
} else if (process.platform === "win32") {
|
|
408
|
+
await windowsInstall(apiUrl);
|
|
409
|
+
} else {
|
|
410
|
+
warn(`No daemon installer for platform ${process.platform}; skipping.`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
success("Background watcher installed and running.");
|
|
414
|
+
dim("It will pick up sessions from Claude Code, Codex, and Cursor every 30 seconds.");
|
|
415
|
+
dim("Manage it with `token-rats daemon-status` and `token-rats uninstall-daemon`.");
|
|
416
|
+
} catch (err) {
|
|
417
|
+
error(`Failed to install daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
418
|
+
warn("You can still run `token-rats sync` manually.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function uninstallDaemonCommand() {
|
|
422
|
+
if (process.platform === "darwin") {
|
|
423
|
+
await darwinUninstall();
|
|
424
|
+
} else if (process.platform === "linux") {
|
|
425
|
+
await linuxUninstall();
|
|
426
|
+
} else if (process.platform === "win32") {
|
|
427
|
+
await windowsUninstall();
|
|
428
|
+
} else {
|
|
429
|
+
warn(`No daemon installer for platform ${process.platform}; nothing to remove.`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
info("Daemon removed. `token-rats sync` will still work manually.");
|
|
433
|
+
}
|
|
434
|
+
async function daemonStatusCommand() {
|
|
435
|
+
let status;
|
|
436
|
+
if (process.platform === "darwin") status = await darwinStatus();
|
|
437
|
+
else if (process.platform === "linux") status = await linuxStatus();
|
|
438
|
+
else if (process.platform === "win32") status = await windowsStatus();
|
|
439
|
+
else {
|
|
440
|
+
warn(`No daemon for platform ${process.platform}.`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (status === "running") success("Token Rats watcher is running.");
|
|
444
|
+
else if (status === "stopped") warn("Token Rats watcher is installed but not running.");
|
|
445
|
+
else info("Token Rats watcher is not installed. Run `token-rats install-daemon` to start it.");
|
|
446
|
+
}
|
|
447
|
+
|
|
35
448
|
// ../../node_modules/.pnpm/zod@3.23.8/node_modules/zod/lib/index.mjs
|
|
36
449
|
var util;
|
|
37
450
|
(function(util2) {
|
|
@@ -385,8 +798,8 @@ function getErrorMap() {
|
|
|
385
798
|
return overrideErrorMap;
|
|
386
799
|
}
|
|
387
800
|
var makeIssue = (params) => {
|
|
388
|
-
const { data, path:
|
|
389
|
-
const fullPath = [...
|
|
801
|
+
const { data, path: path4, errorMaps, issueData } = params;
|
|
802
|
+
const fullPath = [...path4, ...issueData.path || []];
|
|
390
803
|
const fullIssue = {
|
|
391
804
|
...issueData,
|
|
392
805
|
path: fullPath
|
|
@@ -508,11 +921,11 @@ var errorUtil;
|
|
|
508
921
|
var _ZodEnum_cache;
|
|
509
922
|
var _ZodNativeEnum_cache;
|
|
510
923
|
var ParseInputLazyPath = class {
|
|
511
|
-
constructor(parent, value,
|
|
924
|
+
constructor(parent, value, path4, key) {
|
|
512
925
|
this._cachedPath = [];
|
|
513
926
|
this.parent = parent;
|
|
514
927
|
this.data = value;
|
|
515
|
-
this._path =
|
|
928
|
+
this._path = path4;
|
|
516
929
|
this._key = key;
|
|
517
930
|
}
|
|
518
931
|
get path() {
|
|
@@ -3947,13 +4360,28 @@ var z = /* @__PURE__ */ Object.freeze({
|
|
|
3947
4360
|
});
|
|
3948
4361
|
|
|
3949
4362
|
// ../contracts/src/session.ts
|
|
3950
|
-
var Source = z.enum(["claude-code", "cursor", "codex"]);
|
|
3951
|
-
var Provider = z.enum([
|
|
4363
|
+
var Source = z.enum(["claude-code", "cursor", "codex", "openrouter", "openai"]);
|
|
4364
|
+
var Provider = z.enum([
|
|
4365
|
+
"anthropic",
|
|
4366
|
+
"openai",
|
|
4367
|
+
"openrouter",
|
|
4368
|
+
"cursor",
|
|
4369
|
+
"ollama",
|
|
4370
|
+
"unknown"
|
|
4371
|
+
]);
|
|
4372
|
+
var SessionChannel = z.enum(["cli", "ide", "api", "proxy", "local", "unknown"]);
|
|
4373
|
+
var ID_RE = /^[A-Za-z0-9._:/+ -]+$/;
|
|
4374
|
+
var MODEL_RE = /^[A-Za-z0-9._:/+ -]+$/;
|
|
4375
|
+
var DEDUPE_KEY_RE = /^[A-Za-z0-9._:-]+$/;
|
|
4376
|
+
var CLIENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
3952
4377
|
var SessionRecord = z.object({
|
|
3953
|
-
|
|
4378
|
+
accountingVersion: z.literal(2).optional(),
|
|
4379
|
+
id: z.string().min(1).max(256).regex(ID_RE),
|
|
3954
4380
|
source: Source,
|
|
3955
4381
|
provider: Provider.optional(),
|
|
3956
|
-
|
|
4382
|
+
client: z.string().min(1).max(64).regex(CLIENT_RE).optional(),
|
|
4383
|
+
channel: SessionChannel.optional(),
|
|
4384
|
+
model: z.string().min(1).max(128).regex(MODEL_RE),
|
|
3957
4385
|
inTokens: z.number().int().nonnegative(),
|
|
3958
4386
|
outTokens: z.number().int().nonnegative(),
|
|
3959
4387
|
/** Anthropic cache reads / OpenAI `cached_input_tokens`. Billed cheap-or-free. */
|
|
@@ -3965,10 +4393,37 @@ var SessionRecord = z.object({
|
|
|
3965
4393
|
costUsdCents: z.number().int().nonnegative(),
|
|
3966
4394
|
startedAt: z.number().int().positive(),
|
|
3967
4395
|
endedAt: z.number().int().positive(),
|
|
3968
|
-
dedupeKey: z.string().min(1)
|
|
4396
|
+
dedupeKey: z.string().min(1).max(128).regex(DEDUPE_KEY_RE)
|
|
3969
4397
|
});
|
|
3970
4398
|
|
|
3971
4399
|
// ../contracts/src/user.ts
|
|
4400
|
+
var ProfileAttributionEntry = z.object({
|
|
4401
|
+
source: z.string(),
|
|
4402
|
+
tokens: z.number().int().nonnegative(),
|
|
4403
|
+
costUsdCents: z.number().int().nonnegative(),
|
|
4404
|
+
sessions: z.number().int().nonnegative()
|
|
4405
|
+
});
|
|
4406
|
+
var GithubProject = z.object({
|
|
4407
|
+
name: z.string().min(1).max(100),
|
|
4408
|
+
fullName: z.string().min(3).max(201).regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/),
|
|
4409
|
+
url: z.string().url().startsWith("https://github.com/"),
|
|
4410
|
+
description: z.string().max(500).nullable()
|
|
4411
|
+
}).superRefine((project, context) => {
|
|
4412
|
+
const repositoryName = project.fullName.split("/")[1];
|
|
4413
|
+
const canonicalUrl = `https://github.com/${project.fullName}`;
|
|
4414
|
+
if (repositoryName?.toLowerCase() !== project.name.toLowerCase()) {
|
|
4415
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "GitHub project name is invalid" });
|
|
4416
|
+
}
|
|
4417
|
+
if (project.url.toLowerCase() !== canonicalUrl.toLowerCase()) {
|
|
4418
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "GitHub project URL is invalid" });
|
|
4419
|
+
}
|
|
4420
|
+
});
|
|
4421
|
+
var GithubProjects = z.array(GithubProject).max(3).refine(
|
|
4422
|
+
(projects) => new Set(projects.map((project) => project.fullName.toLowerCase())).size === projects.length,
|
|
4423
|
+
{
|
|
4424
|
+
message: "GitHub projects must be unique"
|
|
4425
|
+
}
|
|
4426
|
+
);
|
|
3972
4427
|
var User = z.object({
|
|
3973
4428
|
id: z.string(),
|
|
3974
4429
|
handle: z.string(),
|
|
@@ -3988,13 +4443,20 @@ var User = z.object({
|
|
|
3988
4443
|
* (other readers never see this field). `null` when GitHub didn't return a
|
|
3989
4444
|
* verified email — the user sees a banner asking them to add one.
|
|
3990
4445
|
*/
|
|
3991
|
-
email: z.string().email().nullable().optional()
|
|
4446
|
+
email: z.string().email().nullable().optional(),
|
|
4447
|
+
/** Full text is self-only; public profiles receive agentInstructionsPreview instead. */
|
|
4448
|
+
agentInstructions: z.string().max(2e4).nullable().optional(),
|
|
4449
|
+
githubProjects: GithubProjects.optional()
|
|
3992
4450
|
});
|
|
3993
4451
|
var PublicProfileSettings = z.object({
|
|
3994
4452
|
publicProfile: z.boolean().optional(),
|
|
3995
|
-
bio: z.string().max(200).nullable().optional()
|
|
4453
|
+
bio: z.string().max(200).nullable().optional(),
|
|
4454
|
+
agentInstructions: z.string().max(2e4).nullable().optional(),
|
|
4455
|
+
githubProjects: GithubProjects.optional()
|
|
3996
4456
|
});
|
|
3997
4457
|
var Profile = User.extend({
|
|
4458
|
+
/** The first ten lines of the owner's saved agent instructions. */
|
|
4459
|
+
agentInstructionsPreview: z.string().max(4e3).nullable().optional(),
|
|
3998
4460
|
totals: z.object({
|
|
3999
4461
|
today: z.object({ tokens: z.number().int(), costUsdCents: z.number().int() }),
|
|
4000
4462
|
week: z.object({ tokens: z.number().int(), costUsdCents: z.number().int() }),
|
|
@@ -4002,6 +4464,10 @@ var Profile = User.extend({
|
|
|
4002
4464
|
}),
|
|
4003
4465
|
/** Number of users this user has brought in via their referral link. */
|
|
4004
4466
|
referredCount: z.number().int().nonnegative().optional(),
|
|
4467
|
+
/** All-time attribution grouped by tool/client. */
|
|
4468
|
+
sources: z.array(ProfileAttributionEntry).optional(),
|
|
4469
|
+
/** All-time attribution grouped by transport channel. */
|
|
4470
|
+
channels: z.array(ProfileAttributionEntry).optional(),
|
|
4005
4471
|
/**
|
|
4006
4472
|
* The profile owner's referral code. Self-only — present only when the
|
|
4007
4473
|
* caller is viewing their own profile, so the page can render a
|
|
@@ -4077,8 +4543,14 @@ var LeaderboardRow = z.object({
|
|
|
4077
4543
|
tokens: z.number().int().nonnegative(),
|
|
4078
4544
|
costUsdCents: z.number().int().nonnegative(),
|
|
4079
4545
|
sessions: z.number().int().nonnegative(),
|
|
4546
|
+
/** ISO 3166-1 alpha-2 country code. Null when the user has no stamped country. */
|
|
4547
|
+
country: z.string().length(2).nullable().default(null),
|
|
4080
4548
|
/** Up to 2 dominant sources by token volume, descending. May be empty. */
|
|
4081
|
-
topSources: z.array(SourceBreakdownEntry).max(2).default([])
|
|
4549
|
+
topSources: z.array(SourceBreakdownEntry).max(2).default([]),
|
|
4550
|
+
/** Up to 2 dominant clients/tools by token volume, descending. */
|
|
4551
|
+
topClients: z.array(SourceBreakdownEntry).max(2).default([]),
|
|
4552
|
+
/** Up to 2 dominant transport channels by token volume, descending. */
|
|
4553
|
+
topChannels: z.array(SourceBreakdownEntry).max(2).default([])
|
|
4082
4554
|
});
|
|
4083
4555
|
var Leaderboard = z.object({
|
|
4084
4556
|
range: LeaderboardRange,
|
|
@@ -4414,6 +4886,12 @@ var ENDPOINTS = {
|
|
|
4414
4886
|
meReferral: "/v1/me/referral",
|
|
4415
4887
|
// v1.2 Track AD — friends derived from shared private rooms
|
|
4416
4888
|
meFriends: "/v1/me/friends",
|
|
4889
|
+
// Multi-device — anonymized device list + revoke + heartbeat
|
|
4890
|
+
meDevices: "/v1/me/devices",
|
|
4891
|
+
meDeviceRevoke: (deviceId) => `/v1/me/devices/${deviceId}/revoke`,
|
|
4892
|
+
meDeviceHeartbeat: "/v1/me/devices/heartbeat",
|
|
4893
|
+
// CLI version + upgrade banner
|
|
4894
|
+
cliVersion: "/v1/cli/version",
|
|
4417
4895
|
// Phase 3 Track O — Org plan
|
|
4418
4896
|
orgs: "/v1/orgs",
|
|
4419
4897
|
org: (slug) => `/v1/orgs/${slug}`,
|
|
@@ -4547,49 +5025,147 @@ var FriendsResponse = z.object({
|
|
|
4547
5025
|
friends: z.array(FriendRow)
|
|
4548
5026
|
});
|
|
4549
5027
|
|
|
4550
|
-
// src/
|
|
4551
|
-
var
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
5028
|
+
// ../contracts/src/device.ts
|
|
5029
|
+
var DeviceTotals = z.object({
|
|
5030
|
+
tokens: z.number().int().nonnegative(),
|
|
5031
|
+
costUsdCents: z.number().int().nonnegative(),
|
|
5032
|
+
sessions: z.number().int().nonnegative()
|
|
5033
|
+
});
|
|
5034
|
+
var DeviceBreakdownEntry = z.object({
|
|
5035
|
+
value: z.string().min(1),
|
|
5036
|
+
tokens: z.number().int().nonnegative(),
|
|
5037
|
+
costUsdCents: z.number().int().nonnegative(),
|
|
5038
|
+
sessions: z.number().int().nonnegative()
|
|
5039
|
+
});
|
|
5040
|
+
var Device = z.object({
|
|
5041
|
+
deviceId: z.string().min(1),
|
|
5042
|
+
createdAt: z.number().int().nonnegative(),
|
|
5043
|
+
lastSeenAt: z.number().int().nonnegative(),
|
|
5044
|
+
lastHeartbeatAt: z.number().int().nonnegative().nullable(),
|
|
5045
|
+
/** Derived: true iff lastHeartbeatAt is within 5 min of `now`. */
|
|
5046
|
+
isLive: z.boolean(),
|
|
5047
|
+
lastUploadCount: z.number().int().nonnegative(),
|
|
5048
|
+
cliVersion: z.string().nullable(),
|
|
5049
|
+
/** Unix-ms when the user revoked this device via the web UI, else null. */
|
|
5050
|
+
revokedAt: z.number().int().nonnegative().nullable(),
|
|
5051
|
+
/** True for the synthetic pre-device-id bucket. */
|
|
5052
|
+
isLegacy: z.boolean().default(false),
|
|
5053
|
+
/** Last observed session upload timestamp for this device, if any. */
|
|
5054
|
+
lastSessionAt: z.number().int().nonnegative().nullable(),
|
|
5055
|
+
/** 30-day totals for this device, computed at request time. */
|
|
5056
|
+
totals: DeviceTotals,
|
|
5057
|
+
/** All-time totals for this device. */
|
|
5058
|
+
totalsAllTime: DeviceTotals,
|
|
5059
|
+
/** Dominant sources in the last 30 days, ordered by tokens desc. */
|
|
5060
|
+
topSources: z.array(DeviceBreakdownEntry).max(3).default([]),
|
|
5061
|
+
/** Dominant clients/tools in the last 30 days, ordered by tokens desc. */
|
|
5062
|
+
topClients: z.array(DeviceBreakdownEntry).max(3).default([]),
|
|
5063
|
+
/** Dominant transport channels in the last 30 days, ordered by tokens desc. */
|
|
5064
|
+
topChannels: z.array(DeviceBreakdownEntry).max(3).default([]),
|
|
5065
|
+
/** Dominant providers in the last 30 days, ordered by tokens desc. */
|
|
5066
|
+
topProviders: z.array(DeviceBreakdownEntry).max(3).default([]),
|
|
5067
|
+
/** Dominant models in the last 30 days, ordered by tokens desc. */
|
|
5068
|
+
topModels: z.array(DeviceBreakdownEntry).max(3).default([])
|
|
5069
|
+
});
|
|
5070
|
+
var GetMeDevicesResponse = z.object({
|
|
5071
|
+
devices: z.array(Device)
|
|
5072
|
+
});
|
|
5073
|
+
var RevokeDeviceResponse = z.object({
|
|
5074
|
+
ok: z.literal(true),
|
|
5075
|
+
revokedAt: z.number().int().nonnegative()
|
|
5076
|
+
});
|
|
5077
|
+
var DeviceHeartbeatResponse = z.object({
|
|
5078
|
+
ok: z.literal(true),
|
|
5079
|
+
lastHeartbeatAt: z.number().int().nonnegative()
|
|
5080
|
+
});
|
|
5081
|
+
|
|
5082
|
+
// ../contracts/src/cli.ts
|
|
5083
|
+
var CliVersionResponse = z.object({
|
|
5084
|
+
latest: z.string().min(1),
|
|
5085
|
+
minSupported: z.string().min(1),
|
|
5086
|
+
upgradeCommand: z.string().min(1)
|
|
5087
|
+
});
|
|
5088
|
+
|
|
5089
|
+
// ../contracts/src/community.ts
|
|
5090
|
+
var PostKind = z.enum(["idea", "agents-md", "showcase", "question"]);
|
|
5091
|
+
var CreatePostRequest = z.object({
|
|
5092
|
+
kind: PostKind,
|
|
5093
|
+
title: z.string().trim().min(3).max(160),
|
|
5094
|
+
body: z.string().trim().min(1).max(3e4)
|
|
5095
|
+
});
|
|
5096
|
+
var CreateReplyRequest = z.object({ body: z.string().trim().min(1).max(1e4) });
|
|
5097
|
+
var CommunityPost = CreatePostRequest.extend({
|
|
5098
|
+
id: z.string(),
|
|
5099
|
+
handle: z.string(),
|
|
5100
|
+
createdAt: z.number(),
|
|
5101
|
+
replies: z.number()
|
|
5102
|
+
});
|
|
5103
|
+
var CommunityReply = z.object({
|
|
5104
|
+
id: z.string(),
|
|
5105
|
+
body: z.string(),
|
|
5106
|
+
handle: z.string(),
|
|
5107
|
+
createdAt: z.number()
|
|
5108
|
+
});
|
|
5109
|
+
|
|
5110
|
+
// ../contracts/src/comparison.ts
|
|
5111
|
+
var UsageMonth = z.string().regex(/^20\d{2}-(0[1-9]|1[0-2])$/);
|
|
5112
|
+
var SubscriptionSpendRequest = z.object({
|
|
5113
|
+
month: UsageMonth,
|
|
5114
|
+
source: z.enum(["claude-code", "codex", "cursor"]),
|
|
5115
|
+
label: z.string().trim().min(1).max(80),
|
|
5116
|
+
paidUsdCents: z.number().int().min(0).max(1e8)
|
|
5117
|
+
});
|
|
5118
|
+
|
|
5119
|
+
// src/lib/api.ts
|
|
5120
|
+
var DEFAULT_API_URL = "https://api.tokenrats.com";
|
|
5121
|
+
function isTransient(status) {
|
|
5122
|
+
return status >= 500 || status === 408 || status === 429;
|
|
5123
|
+
}
|
|
5124
|
+
function sleep(ms) {
|
|
5125
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
5126
|
+
}
|
|
5127
|
+
var ApiError2 = class extends Error {
|
|
5128
|
+
constructor(status, body) {
|
|
5129
|
+
super(`API error ${status}: ${body}`);
|
|
5130
|
+
this.status = status;
|
|
5131
|
+
this.body = body;
|
|
5132
|
+
this.name = "ApiError";
|
|
5133
|
+
}
|
|
5134
|
+
};
|
|
5135
|
+
var DeviceRevokedError = class extends ApiError2 {
|
|
5136
|
+
constructor(body) {
|
|
5137
|
+
super(401, body);
|
|
5138
|
+
this.name = "DeviceRevokedError";
|
|
5139
|
+
}
|
|
5140
|
+
};
|
|
5141
|
+
var ApiClient = class {
|
|
5142
|
+
apiUrl;
|
|
5143
|
+
token;
|
|
5144
|
+
deviceId;
|
|
5145
|
+
cliVersion;
|
|
5146
|
+
constructor(opts = {}) {
|
|
5147
|
+
this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
5148
|
+
this.token = opts.token;
|
|
5149
|
+
this.deviceId = opts.deviceId;
|
|
5150
|
+
this.cliVersion = opts.cliVersion;
|
|
4572
5151
|
}
|
|
4573
5152
|
setToken(token) {
|
|
4574
5153
|
this.token = token;
|
|
4575
5154
|
}
|
|
4576
5155
|
headers() {
|
|
4577
5156
|
const h = { "Content-Type": "application/json" };
|
|
4578
|
-
if (this.token) {
|
|
4579
|
-
|
|
4580
|
-
|
|
5157
|
+
if (this.token) h.Authorization = `Bearer ${this.token}`;
|
|
5158
|
+
if (this.deviceId) h["X-Device-Id"] = this.deviceId;
|
|
5159
|
+
if (this.cliVersion) h["X-Cli-Version"] = this.cliVersion;
|
|
4581
5160
|
return h;
|
|
4582
5161
|
}
|
|
4583
|
-
/** Perform a fetch with retry+backoff. maxRetries=3, delays: 1s, 2s, 4s. */
|
|
4584
5162
|
async fetchWithRetry(url, init, maxRetries = 3) {
|
|
4585
5163
|
let attempt = 0;
|
|
4586
5164
|
let lastErr;
|
|
4587
5165
|
while (attempt <= maxRetries) {
|
|
4588
5166
|
try {
|
|
4589
|
-
const res = await fetch(url, init);
|
|
4590
|
-
if (res.ok || !isTransient(res.status))
|
|
4591
|
-
return res;
|
|
4592
|
-
}
|
|
5167
|
+
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(3e4) });
|
|
5168
|
+
if (res.ok || !isTransient(res.status)) return res;
|
|
4593
5169
|
lastErr = new ApiError2(res.status, await res.text());
|
|
4594
5170
|
} catch (err) {
|
|
4595
5171
|
lastErr = err;
|
|
@@ -4601,34 +5177,37 @@ var ApiClient = class {
|
|
|
4601
5177
|
}
|
|
4602
5178
|
throw lastErr;
|
|
4603
5179
|
}
|
|
4604
|
-
async
|
|
4605
|
-
const
|
|
5180
|
+
async failedResponseToError(res) {
|
|
5181
|
+
const body = await res.text();
|
|
5182
|
+
if (res.status === 401 && /device_revoked/.test(body)) {
|
|
5183
|
+
return new DeviceRevokedError(body);
|
|
5184
|
+
}
|
|
5185
|
+
return new ApiError2(res.status, body);
|
|
5186
|
+
}
|
|
5187
|
+
async post(path4, body) {
|
|
5188
|
+
const url = `${this.apiUrl}${path4}`;
|
|
4606
5189
|
const res = await this.fetchWithRetry(url, {
|
|
4607
5190
|
method: "POST",
|
|
4608
5191
|
headers: this.headers(),
|
|
4609
5192
|
body: JSON.stringify(body)
|
|
4610
5193
|
});
|
|
4611
|
-
if (!res.ok)
|
|
4612
|
-
throw new ApiError2(res.status, await res.text());
|
|
4613
|
-
}
|
|
5194
|
+
if (!res.ok) throw await this.failedResponseToError(res);
|
|
4614
5195
|
return res.json();
|
|
4615
5196
|
}
|
|
4616
|
-
async get(
|
|
4617
|
-
const url = `${this.apiUrl}${
|
|
5197
|
+
async get(path4) {
|
|
5198
|
+
const url = `${this.apiUrl}${path4}`;
|
|
4618
5199
|
const res = await this.fetchWithRetry(url, {
|
|
4619
5200
|
method: "GET",
|
|
4620
5201
|
headers: this.headers()
|
|
4621
5202
|
});
|
|
4622
|
-
if (!res.ok)
|
|
4623
|
-
throw new ApiError2(res.status, await res.text());
|
|
4624
|
-
}
|
|
5203
|
+
if (!res.ok) throw await this.failedResponseToError(res);
|
|
4625
5204
|
return res.json();
|
|
4626
5205
|
}
|
|
4627
5206
|
/** Initiate device-code flow. */
|
|
4628
5207
|
async cliExchange() {
|
|
4629
5208
|
return this.post(ENDPOINTS.authCliExchange, {});
|
|
4630
5209
|
}
|
|
4631
|
-
/** Poll for auth token. Returns token on success, null on pending
|
|
5210
|
+
/** Poll for auth token. Returns token on success, null on pending. */
|
|
4632
5211
|
async cliPoll(pollToken) {
|
|
4633
5212
|
const url = `${this.apiUrl}${ENDPOINTS.authCliPoll}`;
|
|
4634
5213
|
const res = await fetch(url, {
|
|
@@ -4640,12 +5219,8 @@ var ApiClient = class {
|
|
|
4640
5219
|
const data = await res.json();
|
|
4641
5220
|
return data.token;
|
|
4642
5221
|
}
|
|
4643
|
-
if (res.status === 202)
|
|
4644
|
-
|
|
4645
|
-
}
|
|
4646
|
-
if (res.status === 410) {
|
|
4647
|
-
throw new ApiError2(410, "Code expired");
|
|
4648
|
-
}
|
|
5222
|
+
if (res.status === 202) return null;
|
|
5223
|
+
if (res.status === 410) throw new ApiError2(410, "Code expired");
|
|
4649
5224
|
throw new ApiError2(res.status, await res.text());
|
|
4650
5225
|
}
|
|
4651
5226
|
/** GET /v1/me */
|
|
@@ -4656,106 +5231,78 @@ var ApiClient = class {
|
|
|
4656
5231
|
async uploadSessions(sessions) {
|
|
4657
5232
|
return this.post(ENDPOINTS.sessions, { sessions });
|
|
4658
5233
|
}
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
import * as fs from "node:fs";
|
|
4663
|
-
import * as os from "node:os";
|
|
4664
|
-
import * as path from "node:path";
|
|
4665
|
-
function tokenDir() {
|
|
4666
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
4667
|
-
const base = xdgConfig ?? path.join(os.homedir(), ".config");
|
|
4668
|
-
return path.join(base, "token-rats");
|
|
4669
|
-
}
|
|
4670
|
-
function tokenPath() {
|
|
4671
|
-
return path.join(tokenDir(), "token");
|
|
4672
|
-
}
|
|
4673
|
-
function saveToken(token) {
|
|
4674
|
-
const dir = tokenDir();
|
|
4675
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
4676
|
-
const file = tokenPath();
|
|
4677
|
-
fs.writeFileSync(file, token, { encoding: "utf8", mode: 384 });
|
|
4678
|
-
}
|
|
4679
|
-
function loadToken() {
|
|
4680
|
-
const file = tokenPath();
|
|
4681
|
-
try {
|
|
4682
|
-
const token = fs.readFileSync(file, "utf8").trim();
|
|
4683
|
-
return token.length > 0 ? token : null;
|
|
4684
|
-
} catch {
|
|
4685
|
-
return null;
|
|
5234
|
+
/** GET /v1/u/:handle */
|
|
5235
|
+
async getProfile(handle) {
|
|
5236
|
+
return this.get(ENDPOINTS.profile(handle));
|
|
4686
5237
|
}
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
5238
|
+
/** GET /v1/u/:handle/heatmap?range=30d — used to derive the current streak. */
|
|
5239
|
+
async getHeatmap(handle) {
|
|
5240
|
+
return this.get(ENDPOINTS.profileHeatmap(handle));
|
|
5241
|
+
}
|
|
5242
|
+
/** GET /v1/trending?range — global public leaderboard, used to derive rank. */
|
|
5243
|
+
async getTrending(range = "30d") {
|
|
5244
|
+
return this.get(`${ENDPOINTS.trending}?range=${range}`);
|
|
5245
|
+
}
|
|
5246
|
+
/** GET /v1/me/devices */
|
|
5247
|
+
async getDevices() {
|
|
5248
|
+
return this.get(ENDPOINTS.meDevices);
|
|
5249
|
+
}
|
|
5250
|
+
/** POST /v1/me/devices/heartbeat */
|
|
5251
|
+
async heartbeat() {
|
|
5252
|
+
return this.post(ENDPOINTS.meDeviceHeartbeat, {});
|
|
4693
5253
|
}
|
|
4694
|
-
}
|
|
4695
|
-
function isLoggedIn() {
|
|
4696
|
-
return loadToken() !== null;
|
|
4697
|
-
}
|
|
4698
|
-
|
|
4699
|
-
// src/lib/log.ts
|
|
4700
|
-
var ESC = "\x1B";
|
|
4701
|
-
var c = {
|
|
4702
|
-
reset: `${ESC}[0m`,
|
|
4703
|
-
bold: `${ESC}[1m`,
|
|
4704
|
-
dim: `${ESC}[2m`,
|
|
4705
|
-
green: `${ESC}[32m`,
|
|
4706
|
-
yellow: `${ESC}[33m`,
|
|
4707
|
-
cyan: `${ESC}[36m`,
|
|
4708
|
-
red: `${ESC}[31m`,
|
|
4709
|
-
gray: `${ESC}[90m`
|
|
4710
5254
|
};
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
5255
|
+
|
|
5256
|
+
// package.json
|
|
5257
|
+
var package_default = {
|
|
5258
|
+
name: "token-rats",
|
|
5259
|
+
version: "0.3.0",
|
|
5260
|
+
description: "Track Claude Code, Codex, and Cursor usage and compare AI subscriptions.",
|
|
5261
|
+
license: "MIT",
|
|
5262
|
+
type: "module",
|
|
5263
|
+
bin: {
|
|
5264
|
+
"token-rats": "./dist/index.js"
|
|
5265
|
+
},
|
|
5266
|
+
files: ["dist", "README.md", "LICENSE"],
|
|
5267
|
+
repository: {
|
|
5268
|
+
type: "git",
|
|
5269
|
+
url: "git+https://github.com/hsalberti/token-rats.git",
|
|
5270
|
+
directory: "packages/cli"
|
|
5271
|
+
},
|
|
5272
|
+
homepage: "https://tokenrats.com",
|
|
5273
|
+
keywords: ["claude", "claude-code", "cursor", "tokens", "leaderboard", "ai-usage"],
|
|
5274
|
+
publishConfig: {
|
|
5275
|
+
access: "public"
|
|
5276
|
+
},
|
|
5277
|
+
scripts: {
|
|
5278
|
+
build: "tsc --noEmit -p tsconfig.json && node build.mjs",
|
|
5279
|
+
typecheck: "tsc --noEmit",
|
|
5280
|
+
dev: "tsx src/index.ts",
|
|
5281
|
+
test: "vitest run",
|
|
5282
|
+
prepublishOnly: "node build.mjs"
|
|
5283
|
+
},
|
|
5284
|
+
optionalDependencies: {
|
|
5285
|
+
clipboardy: "^4.0.0",
|
|
5286
|
+
open: "^10.1.0"
|
|
5287
|
+
},
|
|
5288
|
+
devDependencies: {
|
|
5289
|
+
"@token-rats/contracts": "workspace:*",
|
|
5290
|
+
"@token-rats/parsers": "workspace:*",
|
|
5291
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
5292
|
+
"@types/node": "22.10.2",
|
|
5293
|
+
"@types/sql.js": "^1.4.11",
|
|
5294
|
+
esbuild: "^0.24.2",
|
|
5295
|
+
tsx: "4.19.2",
|
|
5296
|
+
typescript: "5.7.2",
|
|
5297
|
+
vitest: "2.1.8"
|
|
5298
|
+
},
|
|
5299
|
+
dependencies: {
|
|
5300
|
+
"sql.js": "^1.14.1"
|
|
4745
5301
|
}
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
}, 80);
|
|
4751
|
-
return {
|
|
4752
|
-
stop(finalMsg) {
|
|
4753
|
-
clearInterval(interval);
|
|
4754
|
-
process.stdout.write("\r\x1B[K");
|
|
4755
|
-
if (finalMsg) success(finalMsg);
|
|
4756
|
-
}
|
|
4757
|
-
};
|
|
4758
|
-
}
|
|
5302
|
+
};
|
|
5303
|
+
|
|
5304
|
+
// src/lib/cli-version.ts
|
|
5305
|
+
var CLI_VERSION = package_default.version;
|
|
4759
5306
|
|
|
4760
5307
|
// src/commands/login.ts
|
|
4761
5308
|
async function openBrowser(url) {
|
|
@@ -4768,12 +5315,12 @@ async function openBrowser(url) {
|
|
|
4768
5315
|
} catch {
|
|
4769
5316
|
}
|
|
4770
5317
|
try {
|
|
4771
|
-
const { execFile } = await import("node:child_process");
|
|
4772
|
-
const { promisify } = await import("node:util");
|
|
4773
|
-
const
|
|
5318
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
5319
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5320
|
+
const exec2 = promisify2(execFile2);
|
|
4774
5321
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
4775
5322
|
const args = process.platform === "win32" ? ["/c", "start", url] : [url];
|
|
4776
|
-
await
|
|
5323
|
+
await exec2(cmd, args).catch(() => null);
|
|
4777
5324
|
} catch {
|
|
4778
5325
|
}
|
|
4779
5326
|
}
|
|
@@ -4789,7 +5336,12 @@ async function copyToClipboard(text) {
|
|
|
4789
5336
|
return false;
|
|
4790
5337
|
}
|
|
4791
5338
|
async function loginCommand(opts) {
|
|
4792
|
-
const
|
|
5339
|
+
const deviceId = ensureDeviceId();
|
|
5340
|
+
const client = new ApiClient({
|
|
5341
|
+
apiUrl: opts.apiUrl,
|
|
5342
|
+
deviceId,
|
|
5343
|
+
cliVersion: CLI_VERSION
|
|
5344
|
+
});
|
|
4793
5345
|
info("Authenticating with Token Rats\u2026");
|
|
4794
5346
|
let exchange;
|
|
4795
5347
|
try {
|
|
@@ -4837,7 +5389,16 @@ async function loginCommand(opts) {
|
|
|
4837
5389
|
process.exit(1);
|
|
4838
5390
|
}
|
|
4839
5391
|
saveToken(token);
|
|
5392
|
+
clearDisconnected();
|
|
4840
5393
|
success("Logged in! Run `token-rats whoami` to verify.");
|
|
5394
|
+
dim(`Device id: ${deviceId}`);
|
|
5395
|
+
if (opts.noDaemon) {
|
|
5396
|
+
info("Skipping background watcher install (--no-daemon).");
|
|
5397
|
+
info("Run `token-rats sync` manually whenever you want to upload usage.");
|
|
5398
|
+
return;
|
|
5399
|
+
}
|
|
5400
|
+
info("Installing background watcher so usage uploads automatically\u2026");
|
|
5401
|
+
await installDaemonCommand(opts.apiUrl);
|
|
4841
5402
|
}
|
|
4842
5403
|
|
|
4843
5404
|
// src/commands/logout.ts
|
|
@@ -4850,9 +5411,9 @@ function logoutCommand() {
|
|
|
4850
5411
|
info("Logged out. Your local token has been removed.");
|
|
4851
5412
|
}
|
|
4852
5413
|
|
|
4853
|
-
// src/
|
|
4854
|
-
import
|
|
4855
|
-
import * as
|
|
5414
|
+
// src/lib/collect.ts
|
|
5415
|
+
import { createHash } from "node:crypto";
|
|
5416
|
+
import * as fs4 from "node:fs";
|
|
4856
5417
|
|
|
4857
5418
|
// ../parsers/src/hash.ts
|
|
4858
5419
|
var FNV_PRIME = 16777619;
|
|
@@ -4884,6 +5445,7 @@ function parseClaudeCode(input) {
|
|
|
4884
5445
|
text = new TextDecoder().decode(input instanceof ArrayBuffer ? new Uint8Array(input) : input);
|
|
4885
5446
|
}
|
|
4886
5447
|
const sessions = /* @__PURE__ */ new Map();
|
|
5448
|
+
const messages = /* @__PURE__ */ new Map();
|
|
4887
5449
|
for (const rawLine of text.split("\n")) {
|
|
4888
5450
|
const line = rawLine.trim();
|
|
4889
5451
|
if (line.length === 0) continue;
|
|
@@ -4917,7 +5479,8 @@ function parseClaudeCode(input) {
|
|
|
4917
5479
|
outTokens: 0,
|
|
4918
5480
|
cacheReadTokens: 0,
|
|
4919
5481
|
cacheWriteTokens: 0,
|
|
4920
|
-
model: ""
|
|
5482
|
+
model: "",
|
|
5483
|
+
modelAt: 0
|
|
4921
5484
|
};
|
|
4922
5485
|
sessions.set(sessionId, acc);
|
|
4923
5486
|
}
|
|
@@ -4929,16 +5492,32 @@ function parseClaudeCode(input) {
|
|
|
4929
5492
|
const message = ev.message;
|
|
4930
5493
|
if (typeof message !== "object" || message === null) continue;
|
|
4931
5494
|
const msg = message;
|
|
4932
|
-
if (typeof msg.model === "string" && msg.model.length > 0) {
|
|
5495
|
+
if (typeof msg.model === "string" && msg.model.length > 0 && timestamp >= acc.modelAt) {
|
|
4933
5496
|
acc.model = msg.model;
|
|
5497
|
+
acc.modelAt = timestamp;
|
|
4934
5498
|
}
|
|
4935
5499
|
const usage = msg.usage;
|
|
4936
5500
|
if (typeof usage === "object" && usage !== null) {
|
|
4937
5501
|
const u = usage;
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
5502
|
+
const counts = {
|
|
5503
|
+
input: toNonNegInt(u.input_tokens),
|
|
5504
|
+
output: toNonNegInt(u.output_tokens),
|
|
5505
|
+
read: toNonNegInt(u.cache_read_input_tokens),
|
|
5506
|
+
write: toNonNegInt(u.cache_creation_input_tokens)
|
|
5507
|
+
};
|
|
5508
|
+
const key = typeof msg.id === "string" ? `${sessionId}:${msg.id}` : null;
|
|
5509
|
+
const old = key ? messages.get(key) : void 0;
|
|
5510
|
+
const next = {
|
|
5511
|
+
input: Math.max(old?.input ?? 0, counts.input),
|
|
5512
|
+
output: Math.max(old?.output ?? 0, counts.output),
|
|
5513
|
+
read: Math.max(old?.read ?? 0, counts.read),
|
|
5514
|
+
write: Math.max(old?.write ?? 0, counts.write)
|
|
5515
|
+
};
|
|
5516
|
+
acc.inTokens += next.input - (old?.input ?? 0);
|
|
5517
|
+
acc.outTokens += next.output - (old?.output ?? 0);
|
|
5518
|
+
acc.cacheReadTokens += next.read - (old?.read ?? 0);
|
|
5519
|
+
acc.cacheWriteTokens += next.write - (old?.write ?? 0);
|
|
5520
|
+
if (key) messages.set(key, next);
|
|
4942
5521
|
}
|
|
4943
5522
|
}
|
|
4944
5523
|
const results = [];
|
|
@@ -4959,6 +5538,8 @@ function parseClaudeCode(input) {
|
|
|
4959
5538
|
id: `claude-code:${acc.sessionId}`,
|
|
4960
5539
|
source: "claude-code",
|
|
4961
5540
|
provider: "anthropic",
|
|
5541
|
+
client: "claude-code",
|
|
5542
|
+
channel: "cli",
|
|
4962
5543
|
model,
|
|
4963
5544
|
inTokens: acc.inTokens,
|
|
4964
5545
|
outTokens: acc.outTokens,
|
|
@@ -5003,10 +5584,16 @@ function parseCodex(input) {
|
|
|
5003
5584
|
const payload = typeof ev.payload === "object" && ev.payload !== null ? ev.payload : null;
|
|
5004
5585
|
if (type === "session_meta" && payload) {
|
|
5005
5586
|
const id = typeof payload.id === "string" ? payload.id : null;
|
|
5006
|
-
if (!id)
|
|
5587
|
+
if (!id) {
|
|
5588
|
+
currentSessionId = null;
|
|
5589
|
+
continue;
|
|
5590
|
+
}
|
|
5007
5591
|
currentSessionId = id;
|
|
5008
5592
|
const metaTs = parseTimestamp(payload.timestamp) || ts;
|
|
5009
5593
|
const acc2 = upsert(sessions, id);
|
|
5594
|
+
const metaSource = typeof payload.source === "string" ? payload.source : null;
|
|
5595
|
+
acc2.channel = metaSource === "api" ? "api" : metaSource ? "cli" : "unknown";
|
|
5596
|
+
acc2.client = acc2.channel === "cli" ? "codex-cli" : "codex";
|
|
5010
5597
|
if (metaTs > 0 && (acc2.startedAt === 0 || metaTs < acc2.startedAt)) {
|
|
5011
5598
|
acc2.startedAt = metaTs;
|
|
5012
5599
|
}
|
|
@@ -5020,7 +5607,10 @@ function parseCodex(input) {
|
|
|
5020
5607
|
if (ts > acc.endedAt) acc.endedAt = ts;
|
|
5021
5608
|
}
|
|
5022
5609
|
if (type === "turn_context" && payload && typeof payload.model === "string") {
|
|
5023
|
-
acc.
|
|
5610
|
+
if (ts >= acc.modelAt) {
|
|
5611
|
+
acc.model = payload.model;
|
|
5612
|
+
acc.modelAt = ts;
|
|
5613
|
+
}
|
|
5024
5614
|
continue;
|
|
5025
5615
|
}
|
|
5026
5616
|
if (type === "event_msg" && payload && payload.type === "token_count") {
|
|
@@ -5033,8 +5623,10 @@ function parseCodex(input) {
|
|
|
5033
5623
|
const cachedInput = toNonNegInt2(t.cached_input_tokens);
|
|
5034
5624
|
const output = toNonNegInt2(t.output_tokens);
|
|
5035
5625
|
const reasoning = toNonNegInt2(t.reasoning_output_tokens);
|
|
5626
|
+
if (ts < acc.usageAt) continue;
|
|
5627
|
+
acc.usageAt = ts;
|
|
5036
5628
|
acc.inTokens = Math.max(0, inputTotal - cachedInput);
|
|
5037
|
-
acc.outTokens = output
|
|
5629
|
+
acc.outTokens = output;
|
|
5038
5630
|
acc.cacheReadTokens = cachedInput;
|
|
5039
5631
|
acc.reasoningTokens = reasoning;
|
|
5040
5632
|
}
|
|
@@ -5051,6 +5643,8 @@ function parseCodex(input) {
|
|
|
5051
5643
|
id: `codex:${acc.sessionId}`,
|
|
5052
5644
|
source: "codex",
|
|
5053
5645
|
provider: "openai",
|
|
5646
|
+
client: acc.client || "codex-cli",
|
|
5647
|
+
channel: acc.channel,
|
|
5054
5648
|
model,
|
|
5055
5649
|
inTokens: acc.inTokens,
|
|
5056
5650
|
outTokens: acc.outTokens,
|
|
@@ -5075,7 +5669,11 @@ function upsert(map, sessionId) {
|
|
|
5075
5669
|
outTokens: 0,
|
|
5076
5670
|
cacheReadTokens: 0,
|
|
5077
5671
|
reasoningTokens: 0,
|
|
5078
|
-
model: ""
|
|
5672
|
+
model: "",
|
|
5673
|
+
usageAt: 0,
|
|
5674
|
+
modelAt: 0,
|
|
5675
|
+
client: "codex-cli",
|
|
5676
|
+
channel: "unknown"
|
|
5079
5677
|
};
|
|
5080
5678
|
map.set(sessionId, acc);
|
|
5081
5679
|
}
|
|
@@ -5132,6 +5730,8 @@ function parseCursor(input) {
|
|
|
5132
5730
|
id: `cursor:${id}`,
|
|
5133
5731
|
source: "cursor",
|
|
5134
5732
|
provider: "cursor",
|
|
5733
|
+
client: "cursor",
|
|
5734
|
+
channel: "ide",
|
|
5135
5735
|
model,
|
|
5136
5736
|
inTokens,
|
|
5137
5737
|
outTokens,
|
|
@@ -5145,31 +5745,31 @@ function parseCursor(input) {
|
|
|
5145
5745
|
}
|
|
5146
5746
|
|
|
5147
5747
|
// src/lib/cursor-extract.ts
|
|
5148
|
-
import { existsSync, readdirSync
|
|
5748
|
+
import { existsSync as existsSync3, readdirSync } from "node:fs";
|
|
5149
5749
|
import { readFile } from "node:fs/promises";
|
|
5150
|
-
import { homedir as
|
|
5151
|
-
import { join as
|
|
5750
|
+
import { homedir as homedir3 } from "node:os";
|
|
5751
|
+
import { join as join3 } from "node:path";
|
|
5152
5752
|
function cursorWorkspaceStorageDir() {
|
|
5153
|
-
const home =
|
|
5154
|
-
const rel =
|
|
5753
|
+
const home = homedir3();
|
|
5754
|
+
const rel = join3("Cursor", "User", "workspaceStorage");
|
|
5155
5755
|
if (process.platform === "darwin") {
|
|
5156
|
-
return
|
|
5756
|
+
return join3(home, "Library", "Application Support", rel);
|
|
5157
5757
|
}
|
|
5158
5758
|
if (process.platform === "win32") {
|
|
5159
|
-
const appData = process.env.APPDATA ??
|
|
5160
|
-
return
|
|
5759
|
+
const appData = process.env.APPDATA ?? join3(home, "AppData", "Roaming");
|
|
5760
|
+
return join3(appData, rel);
|
|
5161
5761
|
}
|
|
5162
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
5163
|
-
return
|
|
5762
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join3(home, ".config");
|
|
5763
|
+
return join3(xdgConfig, rel);
|
|
5164
5764
|
}
|
|
5165
5765
|
function discoverWorkspaceDbs() {
|
|
5166
5766
|
const root = cursorWorkspaceStorageDir();
|
|
5167
|
-
if (!
|
|
5767
|
+
if (!existsSync3(root)) return [];
|
|
5168
5768
|
const out = [];
|
|
5169
5769
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
5170
5770
|
if (!entry.isDirectory()) continue;
|
|
5171
|
-
const candidate =
|
|
5172
|
-
if (
|
|
5771
|
+
const candidate = join3(root, entry.name, "state.vscdb");
|
|
5772
|
+
if (existsSync3(candidate)) out.push(candidate);
|
|
5173
5773
|
}
|
|
5174
5774
|
return out;
|
|
5175
5775
|
}
|
|
@@ -5272,7 +5872,7 @@ async function extractCursorGenerations() {
|
|
|
5272
5872
|
dbCount: 0
|
|
5273
5873
|
};
|
|
5274
5874
|
}
|
|
5275
|
-
const
|
|
5875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5276
5876
|
const rows = [];
|
|
5277
5877
|
let openFailures = 0;
|
|
5278
5878
|
for (const p of dbPaths) {
|
|
@@ -5284,8 +5884,8 @@ async function extractCursorGenerations() {
|
|
|
5284
5884
|
continue;
|
|
5285
5885
|
}
|
|
5286
5886
|
for (const r of perDb) {
|
|
5287
|
-
if (
|
|
5288
|
-
|
|
5887
|
+
if (seen.has(r.id)) continue;
|
|
5888
|
+
seen.add(r.id);
|
|
5289
5889
|
rows.push(r);
|
|
5290
5890
|
}
|
|
5291
5891
|
}
|
|
@@ -5298,373 +5898,17 @@ async function extractCursorGenerations() {
|
|
|
5298
5898
|
}
|
|
5299
5899
|
return { rows, dbCount: dbPaths.length, skipped: null };
|
|
5300
5900
|
}
|
|
5301
|
-
async function extractCursorGenerationsDelta(lastMtimes, cap = 10) {
|
|
5302
|
-
const dbPaths = discoverWorkspaceDbs();
|
|
5303
|
-
if (dbPaths.length === 0) {
|
|
5304
|
-
return { rows: [], newMtimes: /* @__PURE__ */ new Map(), scanned: 0, opened: 0 };
|
|
5305
|
-
}
|
|
5306
|
-
const stamped = [];
|
|
5307
|
-
for (const p of dbPaths) {
|
|
5308
|
-
try {
|
|
5309
|
-
stamped.push({ path: p, mtimeMs: statSync(p).mtimeMs });
|
|
5310
|
-
} catch {
|
|
5311
|
-
}
|
|
5312
|
-
}
|
|
5313
|
-
stamped.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
5314
|
-
const head = stamped.slice(0, cap);
|
|
5315
|
-
const newMtimes = new Map(lastMtimes);
|
|
5316
|
-
const rows = [];
|
|
5317
|
-
let opened = 0;
|
|
5318
|
-
for (const { path: path7, mtimeMs } of head) {
|
|
5319
|
-
const prev = lastMtimes.get(path7) ?? 0;
|
|
5320
|
-
if (mtimeMs <= prev) continue;
|
|
5321
|
-
opened++;
|
|
5322
|
-
try {
|
|
5323
|
-
const perDb = await readGenerationsFromDb(path7);
|
|
5324
|
-
rows.push(...perDb);
|
|
5325
|
-
newMtimes.set(path7, mtimeMs);
|
|
5326
|
-
} catch {
|
|
5327
|
-
}
|
|
5328
|
-
}
|
|
5329
|
-
return { rows, newMtimes, scanned: stamped.length, opened };
|
|
5330
|
-
}
|
|
5331
5901
|
|
|
5332
|
-
// src/lib/
|
|
5333
|
-
import { spawnSync } from "node:child_process";
|
|
5902
|
+
// src/lib/discover.ts
|
|
5334
5903
|
import * as fs3 from "node:fs";
|
|
5335
5904
|
import * as os3 from "node:os";
|
|
5336
5905
|
import * as path3 from "node:path";
|
|
5337
|
-
|
|
5338
|
-
// src/lib/daemon/paths.ts
|
|
5339
|
-
import * as fs2 from "node:fs";
|
|
5340
|
-
import * as os2 from "node:os";
|
|
5341
|
-
import * as path2 from "node:path";
|
|
5342
|
-
function configBase() {
|
|
5343
|
-
if (process.platform === "darwin") {
|
|
5344
|
-
return path2.join(os2.homedir(), "Library", "Application Support");
|
|
5345
|
-
}
|
|
5346
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
5347
|
-
return xdg ?? path2.join(os2.homedir(), ".config");
|
|
5348
|
-
}
|
|
5349
|
-
function daemonStateDir() {
|
|
5350
|
-
return path2.join(configBase(), "token-rats");
|
|
5351
|
-
}
|
|
5352
|
-
function daemonLogDir() {
|
|
5353
|
-
return path2.join(daemonStateDir(), "logs");
|
|
5354
|
-
}
|
|
5355
|
-
function daemonStateFile() {
|
|
5356
|
-
return path2.join(daemonStateDir(), "daemon-state.json");
|
|
5357
|
-
}
|
|
5358
|
-
function cliEntrypoint() {
|
|
5359
|
-
const node = process.execPath;
|
|
5360
|
-
const script = fs2.realpathSync(process.argv[1] ?? "");
|
|
5361
|
-
return { node, script };
|
|
5362
|
-
}
|
|
5363
|
-
function entrypointIsEphemeral() {
|
|
5364
|
-
const { script } = cliEntrypoint();
|
|
5365
|
-
return script.includes("_npx") || script.includes(".npm/_cacache");
|
|
5366
|
-
}
|
|
5367
|
-
function ensureDaemonDirs() {
|
|
5368
|
-
fs2.mkdirSync(daemonStateDir(), { recursive: true });
|
|
5369
|
-
fs2.mkdirSync(daemonLogDir(), { recursive: true });
|
|
5370
|
-
}
|
|
5371
|
-
|
|
5372
|
-
// src/lib/daemon/linux.ts
|
|
5373
|
-
var UNIT = "token-rats-watch.service";
|
|
5374
|
-
function unitPath() {
|
|
5375
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? path3.join(os3.homedir(), ".config");
|
|
5376
|
-
return path3.join(xdg, "systemd", "user", UNIT);
|
|
5377
|
-
}
|
|
5378
|
-
function renderUnit(node, script, logDir) {
|
|
5379
|
-
return `[Unit]
|
|
5380
|
-
Description=Token Rats background watcher
|
|
5381
|
-
After=default.target
|
|
5382
|
-
|
|
5383
|
-
[Service]
|
|
5384
|
-
Type=simple
|
|
5385
|
-
ExecStart=${node} ${script} watch --daemon
|
|
5386
|
-
Restart=on-failure
|
|
5387
|
-
RestartSec=30
|
|
5388
|
-
StandardOutput=append:${path3.join(logDir, "watch.log")}
|
|
5389
|
-
StandardError=append:${path3.join(logDir, "watch.err.log")}
|
|
5390
|
-
Nice=10
|
|
5391
|
-
|
|
5392
|
-
[Install]
|
|
5393
|
-
WantedBy=default.target
|
|
5394
|
-
`;
|
|
5395
|
-
}
|
|
5396
|
-
function systemctl(args) {
|
|
5397
|
-
const r = spawnSync("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
5398
|
-
return {
|
|
5399
|
-
ok: r.status === 0,
|
|
5400
|
-
stdout: r.stdout ?? "",
|
|
5401
|
-
stderr: r.stderr ?? ""
|
|
5402
|
-
};
|
|
5403
|
-
}
|
|
5404
|
-
var SystemdUnavailableError = class extends Error {
|
|
5405
|
-
constructor() {
|
|
5406
|
-
super("systemctl --user is unavailable on this system");
|
|
5407
|
-
this.name = "SystemdUnavailableError";
|
|
5408
|
-
}
|
|
5409
|
-
};
|
|
5410
|
-
function assertSystemd() {
|
|
5411
|
-
const r = spawnSync("systemctl", ["--user", "--version"], { encoding: "utf8" });
|
|
5412
|
-
if (r.status !== 0) throw new SystemdUnavailableError();
|
|
5413
|
-
}
|
|
5414
|
-
function lingerEnabled() {
|
|
5415
|
-
const user = os3.userInfo().username;
|
|
5416
|
-
const r = spawnSync("loginctl", ["show-user", user], { encoding: "utf8" });
|
|
5417
|
-
if (r.status !== 0) return false;
|
|
5418
|
-
return /Linger=yes/.test(r.stdout);
|
|
5419
|
-
}
|
|
5420
|
-
function installLinux(node, script) {
|
|
5421
|
-
assertSystemd();
|
|
5422
|
-
ensureDaemonDirs();
|
|
5423
|
-
const file = unitPath();
|
|
5424
|
-
fs3.mkdirSync(path3.dirname(file), { recursive: true });
|
|
5425
|
-
const logDir = daemonLogDir();
|
|
5426
|
-
fs3.writeFileSync(file, renderUnit(node, script, logDir), { encoding: "utf8", mode: 420 });
|
|
5427
|
-
const reload = systemctl(["daemon-reload"]);
|
|
5428
|
-
if (!reload.ok) {
|
|
5429
|
-
throw new Error(`systemctl daemon-reload failed: ${reload.stderr.trim()}`);
|
|
5430
|
-
}
|
|
5431
|
-
const enable = systemctl(["enable", "--now", UNIT]);
|
|
5432
|
-
if (!enable.ok) {
|
|
5433
|
-
throw new Error(`systemctl enable --now failed: ${enable.stderr.trim()}`);
|
|
5434
|
-
}
|
|
5435
|
-
return { unit: file, logDir };
|
|
5436
|
-
}
|
|
5437
|
-
function uninstallLinux() {
|
|
5438
|
-
systemctl(["disable", "--now", UNIT]);
|
|
5439
|
-
try {
|
|
5440
|
-
fs3.rmSync(unitPath());
|
|
5441
|
-
} catch {
|
|
5442
|
-
}
|
|
5443
|
-
systemctl(["daemon-reload"]);
|
|
5444
|
-
}
|
|
5445
|
-
function statusLinux() {
|
|
5446
|
-
const installed = fs3.existsSync(unitPath());
|
|
5447
|
-
if (!installed) return { installed: false, running: false };
|
|
5448
|
-
const r = systemctl(["show", UNIT, "--property=ActiveState,MainPID"]);
|
|
5449
|
-
const active = /ActiveState=active/.test(r.stdout);
|
|
5450
|
-
const pidMatch = r.stdout.match(/MainPID=(\d+)/);
|
|
5451
|
-
const pid = pidMatch?.[1] ? Number(pidMatch[1]) : void 0;
|
|
5452
|
-
return {
|
|
5453
|
-
installed: true,
|
|
5454
|
-
running: active && pid !== void 0 && pid > 0,
|
|
5455
|
-
...pid && pid > 0 ? { pid } : {}
|
|
5456
|
-
};
|
|
5457
|
-
}
|
|
5458
|
-
|
|
5459
|
-
// src/lib/daemon/macos.ts
|
|
5460
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5461
|
-
import * as fs4 from "node:fs";
|
|
5462
|
-
import * as os4 from "node:os";
|
|
5463
|
-
import * as path4 from "node:path";
|
|
5464
|
-
var LABEL = "com.tokenrats.watch";
|
|
5465
|
-
function plistPath() {
|
|
5466
|
-
return path4.join(os4.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
5467
|
-
}
|
|
5468
|
-
function xmlEscape(s) {
|
|
5469
|
-
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
5470
|
-
}
|
|
5471
|
-
function renderPlist(node, script, logDir) {
|
|
5472
|
-
const stdout = xmlEscape(path4.join(logDir, "watch.log"));
|
|
5473
|
-
const stderr = xmlEscape(path4.join(logDir, "watch.err.log"));
|
|
5474
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5475
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5476
|
-
<plist version="1.0">
|
|
5477
|
-
<dict>
|
|
5478
|
-
<key>Label</key><string>${LABEL}</string>
|
|
5479
|
-
<key>ProgramArguments</key>
|
|
5480
|
-
<array>
|
|
5481
|
-
<string>${xmlEscape(node)}</string>
|
|
5482
|
-
<string>${xmlEscape(script)}</string>
|
|
5483
|
-
<string>watch</string>
|
|
5484
|
-
<string>--daemon</string>
|
|
5485
|
-
</array>
|
|
5486
|
-
<key>RunAtLoad</key><true/>
|
|
5487
|
-
<key>KeepAlive</key>
|
|
5488
|
-
<dict>
|
|
5489
|
-
<key>SuccessfulExit</key><false/>
|
|
5490
|
-
<key>Crashed</key><true/>
|
|
5491
|
-
</dict>
|
|
5492
|
-
<key>ThrottleInterval</key><integer>30</integer>
|
|
5493
|
-
<key>StandardOutPath</key><string>${stdout}</string>
|
|
5494
|
-
<key>StandardErrorPath</key><string>${stderr}</string>
|
|
5495
|
-
<key>ProcessType</key><string>Background</string>
|
|
5496
|
-
</dict>
|
|
5497
|
-
</plist>
|
|
5498
|
-
`;
|
|
5499
|
-
}
|
|
5500
|
-
function uid() {
|
|
5501
|
-
const fn = process.getuid;
|
|
5502
|
-
if (typeof fn !== "function") throw new Error("process.getuid unavailable");
|
|
5503
|
-
return fn();
|
|
5504
|
-
}
|
|
5505
|
-
function launchctl(args) {
|
|
5506
|
-
const r = spawnSync2("launchctl", args, { encoding: "utf8" });
|
|
5507
|
-
return {
|
|
5508
|
-
ok: r.status === 0,
|
|
5509
|
-
stdout: r.stdout ?? "",
|
|
5510
|
-
stderr: r.stderr ?? ""
|
|
5511
|
-
};
|
|
5512
|
-
}
|
|
5513
|
-
function installMacos(node, script) {
|
|
5514
|
-
ensureDaemonDirs();
|
|
5515
|
-
const dir = path4.dirname(plistPath());
|
|
5516
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
5517
|
-
const logDir = daemonLogDir();
|
|
5518
|
-
const file = plistPath();
|
|
5519
|
-
fs4.writeFileSync(file, renderPlist(node, script, logDir), { encoding: "utf8", mode: 420 });
|
|
5520
|
-
const target = `gui/${uid()}`;
|
|
5521
|
-
launchctl(["bootout", target, file]);
|
|
5522
|
-
const r = launchctl(["bootstrap", target, file]);
|
|
5523
|
-
if (!r.ok) {
|
|
5524
|
-
throw new Error(`launchctl bootstrap failed: ${r.stderr.trim() || r.stdout.trim()}`);
|
|
5525
|
-
}
|
|
5526
|
-
launchctl(["kickstart", "-k", `${target}/${LABEL}`]);
|
|
5527
|
-
return { plist: file, logDir };
|
|
5528
|
-
}
|
|
5529
|
-
function uninstallMacos() {
|
|
5530
|
-
const file = plistPath();
|
|
5531
|
-
const target = `gui/${uid()}`;
|
|
5532
|
-
launchctl(["bootout", target, file]);
|
|
5533
|
-
try {
|
|
5534
|
-
fs4.rmSync(file);
|
|
5535
|
-
} catch {
|
|
5536
|
-
}
|
|
5537
|
-
}
|
|
5538
|
-
function statusMacos() {
|
|
5539
|
-
const file = plistPath();
|
|
5540
|
-
const installed = fs4.existsSync(file);
|
|
5541
|
-
if (!installed) return { installed: false, running: false };
|
|
5542
|
-
const r = launchctl(["print", `gui/${uid()}/${LABEL}`]);
|
|
5543
|
-
const match = r.stdout.match(/\bpid\s*=\s*(\d+)/);
|
|
5544
|
-
if (match?.[1]) return { installed: true, running: true, pid: Number(match[1]) };
|
|
5545
|
-
return { installed: true, running: false };
|
|
5546
|
-
}
|
|
5547
|
-
|
|
5548
|
-
// src/lib/daemon/state.ts
|
|
5549
|
-
import * as fs5 from "node:fs";
|
|
5550
|
-
function readDaemonState() {
|
|
5551
|
-
try {
|
|
5552
|
-
const raw = fs5.readFileSync(daemonStateFile(), "utf8");
|
|
5553
|
-
const parsed = JSON.parse(raw);
|
|
5554
|
-
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
5555
|
-
} catch {
|
|
5556
|
-
return {};
|
|
5557
|
-
}
|
|
5558
|
-
}
|
|
5559
|
-
function writeDaemonState(state) {
|
|
5560
|
-
ensureDaemonDirs();
|
|
5561
|
-
fs5.writeFileSync(daemonStateFile(), `${JSON.stringify(state, null, 2)}
|
|
5562
|
-
`, {
|
|
5563
|
-
encoding: "utf8",
|
|
5564
|
-
mode: 384
|
|
5565
|
-
});
|
|
5566
|
-
}
|
|
5567
|
-
function markDeclined() {
|
|
5568
|
-
const s = readDaemonState();
|
|
5569
|
-
s.declinedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5570
|
-
writeDaemonState(s);
|
|
5571
|
-
}
|
|
5572
|
-
function markInstalled(method, script, version) {
|
|
5573
|
-
const s = readDaemonState();
|
|
5574
|
-
s.installedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5575
|
-
s.installedScript = script;
|
|
5576
|
-
s.method = method;
|
|
5577
|
-
s.cliVersion = version;
|
|
5578
|
-
s.declinedAt = void 0;
|
|
5579
|
-
writeDaemonState(s);
|
|
5580
|
-
}
|
|
5581
|
-
function markUninstalled() {
|
|
5582
|
-
const s = readDaemonState();
|
|
5583
|
-
s.method = "none";
|
|
5584
|
-
s.installedAt = void 0;
|
|
5585
|
-
s.installedScript = void 0;
|
|
5586
|
-
writeDaemonState(s);
|
|
5587
|
-
}
|
|
5588
|
-
|
|
5589
|
-
// src/lib/daemon/install.ts
|
|
5590
|
-
function platformMethod() {
|
|
5591
|
-
if (process.platform === "darwin") return "launchd";
|
|
5592
|
-
if (process.platform === "linux") return "systemd";
|
|
5593
|
-
return "unsupported";
|
|
5594
|
-
}
|
|
5595
|
-
function isDaemonSupported() {
|
|
5596
|
-
return platformMethod() !== "unsupported";
|
|
5597
|
-
}
|
|
5598
|
-
var DaemonUnsupportedError = class extends Error {
|
|
5599
|
-
constructor(platform) {
|
|
5600
|
-
super(`Background daemon is not yet supported on ${platform}`);
|
|
5601
|
-
this.name = "DaemonUnsupportedError";
|
|
5602
|
-
}
|
|
5603
|
-
};
|
|
5604
|
-
async function installDaemon(version) {
|
|
5605
|
-
const method = platformMethod();
|
|
5606
|
-
const { node, script } = cliEntrypoint();
|
|
5607
|
-
const ephemeralWarning = entrypointIsEphemeral();
|
|
5608
|
-
if (method === "launchd") {
|
|
5609
|
-
const r = installMacos(node, script);
|
|
5610
|
-
markInstalled("launchd", script, version);
|
|
5611
|
-
return { method: "launchd", logDir: r.logDir, ephemeralWarning };
|
|
5612
|
-
}
|
|
5613
|
-
if (method === "systemd") {
|
|
5614
|
-
try {
|
|
5615
|
-
const r = installLinux(node, script);
|
|
5616
|
-
markInstalled("systemd", script, version);
|
|
5617
|
-
return {
|
|
5618
|
-
method: "systemd",
|
|
5619
|
-
logDir: r.logDir,
|
|
5620
|
-
ephemeralWarning,
|
|
5621
|
-
lingerSet: lingerEnabled()
|
|
5622
|
-
};
|
|
5623
|
-
} catch (err) {
|
|
5624
|
-
if (err instanceof SystemdUnavailableError) {
|
|
5625
|
-
throw new DaemonUnsupportedError(process.platform);
|
|
5626
|
-
}
|
|
5627
|
-
throw err;
|
|
5628
|
-
}
|
|
5629
|
-
}
|
|
5630
|
-
throw new DaemonUnsupportedError(process.platform);
|
|
5631
|
-
}
|
|
5632
|
-
async function uninstallDaemon() {
|
|
5633
|
-
const method = platformMethod();
|
|
5634
|
-
if (method === "launchd") uninstallMacos();
|
|
5635
|
-
else if (method === "systemd") uninstallLinux();
|
|
5636
|
-
markUninstalled();
|
|
5637
|
-
}
|
|
5638
|
-
async function daemonStatus() {
|
|
5639
|
-
const method = platformMethod();
|
|
5640
|
-
const state = readDaemonState();
|
|
5641
|
-
const base = {
|
|
5642
|
-
method,
|
|
5643
|
-
installedScript: state.installedScript,
|
|
5644
|
-
installedAt: state.installedAt,
|
|
5645
|
-
declinedAt: state.declinedAt
|
|
5646
|
-
};
|
|
5647
|
-
if (method === "launchd") {
|
|
5648
|
-
const s = statusMacos();
|
|
5649
|
-
return { ...base, installed: s.installed, running: s.running, ...s.pid && { pid: s.pid } };
|
|
5650
|
-
}
|
|
5651
|
-
if (method === "systemd") {
|
|
5652
|
-
const s = statusLinux();
|
|
5653
|
-
return { ...base, installed: s.installed, running: s.running, ...s.pid && { pid: s.pid } };
|
|
5654
|
-
}
|
|
5655
|
-
return { ...base, installed: false, running: false };
|
|
5656
|
-
}
|
|
5657
|
-
|
|
5658
|
-
// src/lib/discover.ts
|
|
5659
|
-
import * as fs6 from "node:fs";
|
|
5660
|
-
import * as os5 from "node:os";
|
|
5661
|
-
import * as path5 from "node:path";
|
|
5662
5906
|
function findJsonlFiles(dir) {
|
|
5663
5907
|
const results = [];
|
|
5664
|
-
if (!
|
|
5665
|
-
const entries =
|
|
5908
|
+
if (!fs3.existsSync(dir)) return results;
|
|
5909
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
5666
5910
|
for (const entry of entries) {
|
|
5667
|
-
const full =
|
|
5911
|
+
const full = path3.join(dir, entry.name);
|
|
5668
5912
|
if (entry.isDirectory()) {
|
|
5669
5913
|
results.push(...findJsonlFiles(full));
|
|
5670
5914
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -5674,43 +5918,50 @@ function findJsonlFiles(dir) {
|
|
|
5674
5918
|
return results;
|
|
5675
5919
|
}
|
|
5676
5920
|
function claudeCodeProjectsDir() {
|
|
5677
|
-
const home =
|
|
5921
|
+
const home = os3.homedir();
|
|
5922
|
+
if (process.env.CLAUDE_CONFIG_DIR) return path3.join(process.env.CLAUDE_CONFIG_DIR, "projects");
|
|
5678
5923
|
if (process.platform === "win32") {
|
|
5679
5924
|
const profile = process.env.USERPROFILE ?? home;
|
|
5680
|
-
return
|
|
5925
|
+
return path3.join(profile, ".claude", "projects");
|
|
5681
5926
|
}
|
|
5682
|
-
return
|
|
5927
|
+
return path3.join(home, ".claude", "projects");
|
|
5683
5928
|
}
|
|
5684
5929
|
function discoverClaudeCodeFiles() {
|
|
5685
5930
|
const dir = claudeCodeProjectsDir();
|
|
5686
5931
|
return findJsonlFiles(dir);
|
|
5687
5932
|
}
|
|
5688
5933
|
function codexSessionsDirs() {
|
|
5689
|
-
const home =
|
|
5934
|
+
const home = os3.homedir();
|
|
5690
5935
|
const candidates = [];
|
|
5936
|
+
if (process.env.CODEX_HOME) {
|
|
5937
|
+
candidates.push(path3.join(process.env.CODEX_HOME, "sessions"));
|
|
5938
|
+
candidates.push(path3.join(process.env.CODEX_HOME, "archived_sessions"));
|
|
5939
|
+
}
|
|
5691
5940
|
if (process.platform === "win32") {
|
|
5692
5941
|
const profile = process.env.USERPROFILE ?? home;
|
|
5693
|
-
candidates.push(
|
|
5694
|
-
|
|
5695
|
-
|
|
5942
|
+
candidates.push(path3.join(profile, ".codex", "sessions"));
|
|
5943
|
+
candidates.push(path3.join(profile, ".codex", "archived_sessions"));
|
|
5944
|
+
const appData = process.env.APPDATA ?? path3.join(profile, "AppData", "Roaming");
|
|
5945
|
+
candidates.push(path3.join(appData, "Codex", "sessions"));
|
|
5696
5946
|
} else {
|
|
5697
|
-
candidates.push(
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5947
|
+
candidates.push(path3.join(home, ".codex", "sessions"));
|
|
5948
|
+
candidates.push(path3.join(home, ".codex", "archived_sessions"));
|
|
5949
|
+
const snapRoot = path3.join(home, "snap", "codex");
|
|
5950
|
+
if (fs3.existsSync(snapRoot)) {
|
|
5951
|
+
for (const entry of fs3.readdirSync(snapRoot, { withFileTypes: true })) {
|
|
5701
5952
|
if (entry.isDirectory()) {
|
|
5702
|
-
candidates.push(
|
|
5953
|
+
candidates.push(path3.join(snapRoot, entry.name, "sessions"));
|
|
5703
5954
|
}
|
|
5704
5955
|
}
|
|
5705
5956
|
}
|
|
5706
5957
|
}
|
|
5707
|
-
const
|
|
5958
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5708
5959
|
const dirs = [];
|
|
5709
5960
|
for (const c2 of candidates) {
|
|
5710
5961
|
try {
|
|
5711
|
-
const real =
|
|
5712
|
-
if (real && !
|
|
5713
|
-
|
|
5962
|
+
const real = fs3.existsSync(c2) ? fs3.realpathSync(c2) : null;
|
|
5963
|
+
if (real && !seen.has(real)) {
|
|
5964
|
+
seen.add(real);
|
|
5714
5965
|
dirs.push(c2);
|
|
5715
5966
|
}
|
|
5716
5967
|
} catch {
|
|
@@ -5726,11 +5977,126 @@ function discoverCodexFiles() {
|
|
|
5726
5977
|
return out;
|
|
5727
5978
|
}
|
|
5728
5979
|
|
|
5729
|
-
// src/lib/
|
|
5730
|
-
|
|
5980
|
+
// src/lib/collect.ts
|
|
5981
|
+
async function collectSessions() {
|
|
5982
|
+
function read(files) {
|
|
5983
|
+
return files.map((file) => fs4.readFileSync(file, "utf8")).join("\n");
|
|
5984
|
+
}
|
|
5985
|
+
const claude = parseClaudeCode(read(discoverClaudeCodeFiles()));
|
|
5986
|
+
const codex = parseCodex(read(discoverCodexFiles()));
|
|
5987
|
+
const { rows } = await extractCursorGenerations();
|
|
5988
|
+
const cursor = parseCursor(JSON.stringify(rows));
|
|
5989
|
+
return [...claude, ...codex, ...cursor].map((record) => ({
|
|
5990
|
+
...record,
|
|
5991
|
+
accountingVersion: 2,
|
|
5992
|
+
dedupeKey: createHash("sha256").update(
|
|
5993
|
+
JSON.stringify([
|
|
5994
|
+
record.id,
|
|
5995
|
+
record.source,
|
|
5996
|
+
record.model,
|
|
5997
|
+
record.startedAt,
|
|
5998
|
+
record.inTokens,
|
|
5999
|
+
record.outTokens,
|
|
6000
|
+
record.cacheReadTokens ?? 0,
|
|
6001
|
+
record.cacheWriteTokens ?? 0,
|
|
6002
|
+
record.reasoningTokens ?? 0
|
|
6003
|
+
])
|
|
6004
|
+
).digest("hex")
|
|
6005
|
+
}));
|
|
6006
|
+
}
|
|
6007
|
+
var SyncQueue = class {
|
|
6008
|
+
acknowledged = /* @__PURE__ */ new Map();
|
|
6009
|
+
async flush(records, upload) {
|
|
6010
|
+
const fresh = records.filter((r) => this.acknowledged.get(r.id) !== JSON.stringify(r));
|
|
6011
|
+
for (let i = 0; i < fresh.length; i += 90) {
|
|
6012
|
+
const batch = fresh.slice(i, i + 90);
|
|
6013
|
+
await upload(batch);
|
|
6014
|
+
for (const record of batch) this.acknowledged.set(record.id, JSON.stringify(record));
|
|
6015
|
+
}
|
|
6016
|
+
return fresh.length;
|
|
6017
|
+
}
|
|
6018
|
+
};
|
|
6019
|
+
function createCollector() {
|
|
6020
|
+
let signature = "";
|
|
6021
|
+
let records = [];
|
|
6022
|
+
return async () => {
|
|
6023
|
+
const files = [
|
|
6024
|
+
...discoverClaudeCodeFiles(),
|
|
6025
|
+
...discoverCodexFiles(),
|
|
6026
|
+
...discoverWorkspaceDbs().flatMap((file) => [file, `${file}-wal`])
|
|
6027
|
+
].sort();
|
|
6028
|
+
const next = JSON.stringify(
|
|
6029
|
+
files.map((file) => {
|
|
6030
|
+
try {
|
|
6031
|
+
const stat = fs4.statSync(file);
|
|
6032
|
+
return [file, stat.size, stat.mtimeMs];
|
|
6033
|
+
} catch {
|
|
6034
|
+
return [file, null];
|
|
6035
|
+
}
|
|
6036
|
+
})
|
|
6037
|
+
);
|
|
6038
|
+
if (next !== signature) {
|
|
6039
|
+
records = await collectSessions();
|
|
6040
|
+
signature = next;
|
|
6041
|
+
}
|
|
6042
|
+
return records;
|
|
6043
|
+
};
|
|
6044
|
+
}
|
|
5731
6045
|
|
|
5732
6046
|
// src/commands/sync.ts
|
|
5733
|
-
var BATCH_SIZE =
|
|
6047
|
+
var BATCH_SIZE = 90;
|
|
6048
|
+
var WEB_ORIGIN = "https://tokenrats.com";
|
|
6049
|
+
function currentStreakFromDays(days, todayUtc) {
|
|
6050
|
+
const active = days.slice().sort();
|
|
6051
|
+
if (active.length === 0) return 0;
|
|
6052
|
+
const last = active[active.length - 1];
|
|
6053
|
+
const yesterday = (() => {
|
|
6054
|
+
const d = /* @__PURE__ */ new Date(`${todayUtc}T00:00:00Z`);
|
|
6055
|
+
d.setUTCDate(d.getUTCDate() - 1);
|
|
6056
|
+
return d.toISOString().slice(0, 10);
|
|
6057
|
+
})();
|
|
6058
|
+
if (last !== todayUtc && last !== yesterday) return 0;
|
|
6059
|
+
let streak = 1;
|
|
6060
|
+
for (let i = active.length - 1; i >= 1; i--) {
|
|
6061
|
+
const prev = (/* @__PURE__ */ new Date(`${active[i - 1]}T00:00:00Z`)).getTime();
|
|
6062
|
+
const curr = (/* @__PURE__ */ new Date(`${active[i]}T00:00:00Z`)).getTime();
|
|
6063
|
+
if (Math.round((curr - prev) / 864e5) === 1) streak++;
|
|
6064
|
+
else break;
|
|
6065
|
+
}
|
|
6066
|
+
return streak;
|
|
6067
|
+
}
|
|
6068
|
+
async function printSyncHero(client) {
|
|
6069
|
+
let handle;
|
|
6070
|
+
try {
|
|
6071
|
+
const me = await client.getMe();
|
|
6072
|
+
handle = me.user.handle;
|
|
6073
|
+
} catch {
|
|
6074
|
+
return;
|
|
6075
|
+
}
|
|
6076
|
+
const todayUtc = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6077
|
+
const [trending, heatmap] = await Promise.all([
|
|
6078
|
+
client.getTrending("30d").catch(() => null),
|
|
6079
|
+
client.getHeatmap(handle).catch(() => null)
|
|
6080
|
+
]);
|
|
6081
|
+
const rank = trending?.rows.find((r) => r.handle === handle)?.rank ?? null;
|
|
6082
|
+
const streak = heatmap === null ? 0 : currentStreakFromDays(
|
|
6083
|
+
heatmap.heatmap.days.filter((d) => d.sessions > 0).map((d) => d.day),
|
|
6084
|
+
todayUtc
|
|
6085
|
+
);
|
|
6086
|
+
const profileUrl = `${WEB_ORIGIN}/u/${handle}`;
|
|
6087
|
+
console.log("");
|
|
6088
|
+
bold("\u{1F400} Your Token Rats standing");
|
|
6089
|
+
if (rank !== null) info(`Global rank: #${rank}`);
|
|
6090
|
+
if (streak > 0) info(`Current streak: ${streak} day${streak === 1 ? "" : "s"} \u{1F525}`);
|
|
6091
|
+
info(`Profile: ${profileUrl}`);
|
|
6092
|
+
const rankPart = rank !== null ? `ranked #${rank} globally` : "on the board";
|
|
6093
|
+
const streakPart = streak > 0 ? ` on a ${streak}-day streak` : "";
|
|
6094
|
+
const tweet = `I'm ${rankPart}${streakPart} on @tokenrats \u2014 tracking my AI coding tokens. \u{1F400}`;
|
|
6095
|
+
const intent = `https://x.com/intent/tweet?text=${encodeURIComponent(tweet)}&url=${encodeURIComponent(profileUrl)}`;
|
|
6096
|
+
console.log("");
|
|
6097
|
+
dim("Brag about it \u2014 Post to X:");
|
|
6098
|
+
console.log(` ${process.stdout.isTTY ? `${c.cyan}${intent}${c.reset}` : intent}`);
|
|
6099
|
+
}
|
|
5734
6100
|
function chunk(arr, size) {
|
|
5735
6101
|
const chunks = [];
|
|
5736
6102
|
for (let i = 0; i < arr.length; i += size) {
|
|
@@ -5738,127 +6104,20 @@ function chunk(arr, size) {
|
|
|
5738
6104
|
}
|
|
5739
6105
|
return chunks;
|
|
5740
6106
|
}
|
|
5741
|
-
function mergeBySessionId(records) {
|
|
5742
|
-
const byId = /* @__PURE__ */ new Map();
|
|
5743
|
-
for (const r of records) {
|
|
5744
|
-
const existing = byId.get(r.id);
|
|
5745
|
-
if (!existing) {
|
|
5746
|
-
byId.set(r.id, { ...r });
|
|
5747
|
-
continue;
|
|
5748
|
-
}
|
|
5749
|
-
existing.inTokens += r.inTokens;
|
|
5750
|
-
existing.outTokens += r.outTokens;
|
|
5751
|
-
if (r.startedAt < existing.startedAt) existing.startedAt = r.startedAt;
|
|
5752
|
-
if (r.endedAt > existing.endedAt) {
|
|
5753
|
-
existing.endedAt = r.endedAt;
|
|
5754
|
-
existing.model = r.model;
|
|
5755
|
-
}
|
|
5756
|
-
}
|
|
5757
|
-
for (const rec of byId.values()) {
|
|
5758
|
-
rec.costUsdCents = 0;
|
|
5759
|
-
rec.dedupeKey = computeDedupeKey(
|
|
5760
|
-
rec.source,
|
|
5761
|
-
rec.model,
|
|
5762
|
-
rec.startedAt,
|
|
5763
|
-
rec.inTokens,
|
|
5764
|
-
rec.outTokens
|
|
5765
|
-
);
|
|
5766
|
-
}
|
|
5767
|
-
return Array.from(byId.values());
|
|
5768
|
-
}
|
|
5769
6107
|
async function syncCommand(opts) {
|
|
5770
6108
|
const token = loadToken();
|
|
5771
6109
|
if (!token && !opts.dryRun) {
|
|
5772
6110
|
error("Not logged in. Run `token-rats login` first.");
|
|
5773
6111
|
process.exit(1);
|
|
5774
6112
|
}
|
|
5775
|
-
const client = opts.dryRun ? null : new ApiClient({
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
}
|
|
5781
|
-
const
|
|
5782
|
-
|
|
5783
|
-
let text;
|
|
5784
|
-
try {
|
|
5785
|
-
text = fs7.readFileSync(file, "utf8");
|
|
5786
|
-
} catch {
|
|
5787
|
-
if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
|
|
5788
|
-
continue;
|
|
5789
|
-
}
|
|
5790
|
-
try {
|
|
5791
|
-
const records = parseClaudeCode(text);
|
|
5792
|
-
claudeSessions.push(...records);
|
|
5793
|
-
if (opts.verbose) dim(` ${file}: ${records.length} session(s)`);
|
|
5794
|
-
} catch {
|
|
5795
|
-
if (opts.verbose) warn(`Failed to parse ${file} \u2014 skipping`);
|
|
5796
|
-
}
|
|
5797
|
-
}
|
|
5798
|
-
const codexFiles = discoverCodexFiles();
|
|
5799
|
-
if (opts.verbose) {
|
|
5800
|
-
info(`Found ${codexFiles.length} Codex rollout file(s)`);
|
|
5801
|
-
for (const f of codexFiles) dim(` ${f}`);
|
|
5802
|
-
}
|
|
5803
|
-
const codexSessions = [];
|
|
5804
|
-
for (const file of codexFiles) {
|
|
5805
|
-
let text;
|
|
5806
|
-
try {
|
|
5807
|
-
text = fs7.readFileSync(file, "utf8");
|
|
5808
|
-
} catch {
|
|
5809
|
-
if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
|
|
5810
|
-
continue;
|
|
5811
|
-
}
|
|
5812
|
-
try {
|
|
5813
|
-
const records = parseCodex(text);
|
|
5814
|
-
codexSessions.push(...records);
|
|
5815
|
-
if (opts.verbose) dim(` ${file}: ${records.length} session(s)`);
|
|
5816
|
-
} catch {
|
|
5817
|
-
if (opts.verbose) warn(`Failed to parse ${file} \u2014 skipping`);
|
|
5818
|
-
}
|
|
5819
|
-
}
|
|
5820
|
-
const cursorSessions = [];
|
|
5821
|
-
const { rows, skipped, dbCount } = await extractCursorGenerations();
|
|
5822
|
-
if (skipped) {
|
|
5823
|
-
warn(skipped);
|
|
5824
|
-
} else if (dbCount === 0) {
|
|
5825
|
-
if (opts.verbose) info("No Cursor workspace storage found \u2014 skipping Cursor source");
|
|
5826
|
-
} else if (rows.length > 0) {
|
|
5827
|
-
if (opts.verbose) info(`Scanned ${dbCount} Cursor workspace DB(s)`);
|
|
5828
|
-
try {
|
|
5829
|
-
const records = parseCursor(JSON.stringify(rows));
|
|
5830
|
-
cursorSessions.push(...records);
|
|
5831
|
-
if (opts.verbose) {
|
|
5832
|
-
dim(
|
|
5833
|
-
` Cursor: ${rows.length} generation event(s) \u2192 ${records.length} session(s) (tokens estimated, see help)`
|
|
5834
|
-
);
|
|
5835
|
-
}
|
|
5836
|
-
} catch {
|
|
5837
|
-
if (opts.verbose) warn("Failed to parse Cursor rows \u2014 skipping");
|
|
5838
|
-
}
|
|
5839
|
-
} else if (opts.verbose) {
|
|
5840
|
-
dim(` Scanned ${dbCount} Cursor workspace DB(s): no AI generations found`);
|
|
5841
|
-
}
|
|
5842
|
-
const mergedClaude = mergeBySessionId(claudeSessions);
|
|
5843
|
-
if (opts.verbose && mergedClaude.length !== claudeSessions.length) {
|
|
5844
|
-
dim(
|
|
5845
|
-
` Merged ${claudeSessions.length} Claude Code records into ${mergedClaude.length} sessions (subagent files folded into parents)`
|
|
5846
|
-
);
|
|
5847
|
-
}
|
|
5848
|
-
const mergedCodex = mergeBySessionId(codexSessions);
|
|
5849
|
-
const seen2 = /* @__PURE__ */ new Set();
|
|
5850
|
-
const allSessions = [];
|
|
5851
|
-
for (const s of [...mergedClaude, ...mergedCodex, ...cursorSessions]) {
|
|
5852
|
-
if (!seen2.has(s.dedupeKey)) {
|
|
5853
|
-
seen2.add(s.dedupeKey);
|
|
5854
|
-
allSessions.push(s);
|
|
5855
|
-
}
|
|
5856
|
-
}
|
|
5857
|
-
const sources = [];
|
|
5858
|
-
if (claudeSessions.length > 0) sources.push("Claude Code");
|
|
5859
|
-
if (codexSessions.length > 0) sources.push("Codex");
|
|
5860
|
-
if (cursorSessions.length > 0) sources.push("Cursor");
|
|
5861
|
-
const sourceStr = sources.length > 0 ? sources.join(" + ") : "no sources";
|
|
6113
|
+
const client = opts.dryRun ? null : new ApiClient({
|
|
6114
|
+
apiUrl: opts.apiUrl,
|
|
6115
|
+
token: token ?? void 0,
|
|
6116
|
+
deviceId: ensureDeviceId(),
|
|
6117
|
+
cliVersion: CLI_VERSION
|
|
6118
|
+
});
|
|
6119
|
+
const allSessions = await collectSessions();
|
|
6120
|
+
const sourceStr = [...new Set(allSessions.map((record) => record.source))].join(" + ") || "no sources";
|
|
5862
6121
|
if (allSessions.length === 0) {
|
|
5863
6122
|
info(`No sessions found from ${sourceStr}.`);
|
|
5864
6123
|
return;
|
|
@@ -5890,6 +6149,14 @@ async function syncCommand(opts) {
|
|
|
5890
6149
|
totalDuplicates += res.duplicates;
|
|
5891
6150
|
} catch (err) {
|
|
5892
6151
|
spin.stop();
|
|
6152
|
+
if (err instanceof DeviceRevokedError) {
|
|
6153
|
+
markDisconnected();
|
|
6154
|
+
deleteToken();
|
|
6155
|
+
error(
|
|
6156
|
+
"This device was disconnected from the Token Rats web UI. Run `token-rats login` to reconnect."
|
|
6157
|
+
);
|
|
6158
|
+
process.exit(1);
|
|
6159
|
+
}
|
|
5893
6160
|
if (err instanceof ApiError2 && err.status === 401) {
|
|
5894
6161
|
error("Session expired. Run `token-rats login` to re-authenticate.");
|
|
5895
6162
|
process.exit(1);
|
|
@@ -5904,459 +6171,80 @@ async function syncCommand(opts) {
|
|
|
5904
6171
|
success(
|
|
5905
6172
|
`Synced ${allSessions.length} sessions (${totalAccepted} new, ${totalDuplicates} already on server) from ${sourceStr}`
|
|
5906
6173
|
);
|
|
5907
|
-
await maybeWireDaemon(opts);
|
|
5908
|
-
}
|
|
5909
|
-
async function maybeWireDaemon(opts) {
|
|
5910
|
-
if (opts.noDaemon) return;
|
|
5911
|
-
if (process.env.TOKEN_RATS_NO_DAEMON === "1") return;
|
|
5912
|
-
if (!isDaemonSupported()) return;
|
|
5913
|
-
const status = await daemonStatus();
|
|
5914
|
-
const state = readDaemonState();
|
|
5915
|
-
if (status.installed) {
|
|
5916
|
-
try {
|
|
5917
|
-
await installDaemon(CLI_VERSION);
|
|
5918
|
-
if (opts.verbose) dim("Refreshed daemon registration (self-heal).");
|
|
5919
|
-
} catch (err) {
|
|
5920
|
-
warn(`Could not refresh daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
5921
|
-
}
|
|
5922
|
-
return;
|
|
5923
|
-
}
|
|
5924
|
-
if (state.declinedAt) {
|
|
5925
|
-
if (opts.verbose) dim(`Daemon prompt skipped \u2014 declined on ${state.declinedAt}`);
|
|
5926
|
-
return;
|
|
5927
|
-
}
|
|
5928
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
5929
|
-
info("");
|
|
5930
|
-
info("Background sync watches Claude Code + Cursor + Codex and uploads new");
|
|
5931
|
-
info("sessions automatically \u2014 no more re-running `token-rats sync`.");
|
|
5932
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
5933
|
-
let answer;
|
|
5934
|
-
try {
|
|
5935
|
-
answer = (await rl.question("Install background sync? [Y/n] ")).trim().toLowerCase();
|
|
5936
|
-
} finally {
|
|
5937
|
-
rl.close();
|
|
5938
|
-
}
|
|
5939
|
-
if (answer === "n" || answer === "no") {
|
|
5940
|
-
markDeclined();
|
|
5941
|
-
info("Skipped. Enable later with `token-rats watch --install`.");
|
|
5942
|
-
return;
|
|
5943
|
-
}
|
|
5944
6174
|
try {
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
info("Disable any time with `token-rats watch --uninstall`.");
|
|
5948
|
-
if (r.ephemeralWarning) {
|
|
5949
|
-
warn(
|
|
5950
|
-
"You ran sync via npx \u2014 the daemon may break when the npx cache clears.\nRun `npm i -g token-rats` for a durable install."
|
|
5951
|
-
);
|
|
5952
|
-
}
|
|
5953
|
-
if (r.method === "systemd" && r.lingerSet === false) {
|
|
5954
|
-
warn(
|
|
5955
|
-
"For the watcher to survive logout, run once:\n sudo loginctl enable-linger $USER"
|
|
5956
|
-
);
|
|
5957
|
-
}
|
|
5958
|
-
} catch (err) {
|
|
5959
|
-
error(`Install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5960
|
-
info("You can retry later with `token-rats watch --install`.");
|
|
6175
|
+
await printSyncHero(client);
|
|
6176
|
+
} catch {
|
|
5961
6177
|
}
|
|
5962
6178
|
}
|
|
5963
6179
|
|
|
5964
6180
|
// src/commands/watch.ts
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
function makeLogger(daemon, verbose) {
|
|
5971
|
-
if (daemon) {
|
|
5972
|
-
const emit = (level, msg, extra) => {
|
|
5973
|
-
const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra });
|
|
5974
|
-
process.stderr.write(`${line}
|
|
5975
|
-
`);
|
|
5976
|
-
};
|
|
5977
|
-
return {
|
|
5978
|
-
info: (m, e) => emit("info", m, e),
|
|
5979
|
-
warn: (m, e) => emit("warn", m, e),
|
|
5980
|
-
error: (m, e) => emit("error", m, e),
|
|
5981
|
-
debug: (m, e) => {
|
|
5982
|
-
if (verbose) emit("debug", m, e);
|
|
5983
|
-
}
|
|
5984
|
-
};
|
|
5985
|
-
}
|
|
5986
|
-
return {
|
|
5987
|
-
info: (m) => info(m),
|
|
5988
|
-
warn: (m) => warn(m),
|
|
5989
|
-
error: (m) => error(m),
|
|
5990
|
-
debug: (m) => {
|
|
5991
|
-
if (verbose) dim(m);
|
|
5992
|
-
}
|
|
5993
|
-
};
|
|
5994
|
-
}
|
|
5995
|
-
var seen = /* @__PURE__ */ new Set();
|
|
5996
|
-
var reauthBackoffUntil = 0;
|
|
5997
|
-
var health = {
|
|
5998
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5999
|
-
uploaded: 0,
|
|
6000
|
-
duplicates: 0,
|
|
6001
|
-
errors: 0,
|
|
6002
|
-
lastSourceAt: {}
|
|
6003
|
-
};
|
|
6004
|
-
async function upload(client, records, log, daemon) {
|
|
6005
|
-
if (records.length === 0) return;
|
|
6006
|
-
if (Date.now() < reauthBackoffUntil) {
|
|
6007
|
-
log.debug("Skipping upload \u2014 in reauth backoff", { records: records.length });
|
|
6181
|
+
async function watchCommand(opts) {
|
|
6182
|
+
const token = loadToken();
|
|
6183
|
+
if (!token || isDisconnected()) {
|
|
6184
|
+
error("Run `token-rats login` before you start the tracker.");
|
|
6185
|
+
process.exitCode = 1;
|
|
6008
6186
|
return;
|
|
6009
6187
|
}
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
} catch (err) {
|
|
6016
|
-
health.errors++;
|
|
6017
|
-
if (err instanceof ApiError2 && err.status === 401) {
|
|
6018
|
-
if (daemon) {
|
|
6019
|
-
reauthBackoffUntil = Date.now() + REAUTH_BACKOFF_MS;
|
|
6020
|
-
log.error("Auth expired. Run `token-rats login` to recover.", {
|
|
6021
|
-
backoffUntil: new Date(reauthBackoffUntil).toISOString()
|
|
6022
|
-
});
|
|
6023
|
-
return;
|
|
6024
|
-
}
|
|
6025
|
-
error("Session expired. Run `token-rats login` to re-authenticate.");
|
|
6026
|
-
process.exit(1);
|
|
6027
|
-
}
|
|
6028
|
-
log.warn(`Upload failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6029
|
-
}
|
|
6030
|
-
}
|
|
6031
|
-
function freshClaudeRecords(filePath, log) {
|
|
6032
|
-
let text;
|
|
6033
|
-
try {
|
|
6034
|
-
text = fs8.readFileSync(filePath, "utf8");
|
|
6035
|
-
} catch {
|
|
6036
|
-
log.debug(`Could not read ${filePath} \u2014 skipping`);
|
|
6037
|
-
return [];
|
|
6038
|
-
}
|
|
6039
|
-
let records;
|
|
6040
|
-
try {
|
|
6041
|
-
records = parseClaudeCode(text);
|
|
6042
|
-
} catch {
|
|
6043
|
-
log.debug(`Failed to parse ${filePath} \u2014 skipping`);
|
|
6044
|
-
return [];
|
|
6045
|
-
}
|
|
6046
|
-
const fresh = [];
|
|
6047
|
-
for (const r of records) {
|
|
6048
|
-
if (seen.has(r.dedupeKey)) continue;
|
|
6049
|
-
seen.add(r.dedupeKey);
|
|
6050
|
-
fresh.push(r);
|
|
6051
|
-
}
|
|
6052
|
-
if (fresh.length > 0) {
|
|
6053
|
-
health.lastSourceAt["claude-code"] = (/* @__PURE__ */ new Date()).toISOString();
|
|
6054
|
-
log.debug(`${filePath}: ${fresh.length} new Claude Code session(s)`);
|
|
6055
|
-
}
|
|
6056
|
-
return fresh;
|
|
6057
|
-
}
|
|
6058
|
-
function freshCodexRecords(filePath, log) {
|
|
6059
|
-
let text;
|
|
6060
|
-
try {
|
|
6061
|
-
text = fs8.readFileSync(filePath, "utf8");
|
|
6062
|
-
} catch {
|
|
6063
|
-
return [];
|
|
6064
|
-
}
|
|
6065
|
-
let records;
|
|
6066
|
-
try {
|
|
6067
|
-
records = parseCodex(text);
|
|
6068
|
-
} catch {
|
|
6069
|
-
return [];
|
|
6070
|
-
}
|
|
6071
|
-
const fresh = [];
|
|
6072
|
-
for (const r of records) {
|
|
6073
|
-
if (seen.has(r.dedupeKey)) continue;
|
|
6074
|
-
seen.add(r.dedupeKey);
|
|
6075
|
-
fresh.push(r);
|
|
6076
|
-
}
|
|
6077
|
-
if (fresh.length > 0) {
|
|
6078
|
-
health.lastSourceAt.codex = (/* @__PURE__ */ new Date()).toISOString();
|
|
6079
|
-
log.debug(`${filePath}: ${fresh.length} new Codex session(s)`);
|
|
6188
|
+
const interval = opts.interval ?? 3e4;
|
|
6189
|
+
if (!Number.isFinite(interval) || interval < 1e3) {
|
|
6190
|
+
error("The interval must be at least 1000 milliseconds.");
|
|
6191
|
+
process.exitCode = 1;
|
|
6192
|
+
return;
|
|
6080
6193
|
}
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
);
|
|
6194
|
+
const client = new ApiClient({
|
|
6195
|
+
apiUrl: opts.apiUrl,
|
|
6196
|
+
token,
|
|
6197
|
+
deviceId: ensureDeviceId(),
|
|
6198
|
+
cliVersion: CLI_VERSION
|
|
6199
|
+
});
|
|
6200
|
+
const queue = new SyncQueue();
|
|
6201
|
+
const collect = createCollector();
|
|
6202
|
+
let stopped = false;
|
|
6203
|
+
let wake;
|
|
6204
|
+
const stop = () => {
|
|
6205
|
+
stopped = true;
|
|
6206
|
+
wake?.();
|
|
6095
6207
|
};
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
if (!fs8.existsSync(dir)) return out;
|
|
6100
|
-
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
6101
|
-
const full = path6.join(dir, entry.name);
|
|
6102
|
-
if (entry.isDirectory()) out.push(...findJsonlFiles2(full));
|
|
6103
|
-
else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(full);
|
|
6104
|
-
}
|
|
6105
|
-
return out;
|
|
6106
|
-
}
|
|
6107
|
-
async function watchJsonlDir(dir, debounceMs, onChange, log) {
|
|
6108
|
-
if (!fs8.existsSync(dir)) {
|
|
6109
|
-
log.debug(`Directory missing \u2014 not watching: ${dir}`);
|
|
6110
|
-
return () => {
|
|
6111
|
-
};
|
|
6112
|
-
}
|
|
6113
|
-
const debouncedChange = makeDebounced((p) => {
|
|
6114
|
-
if (p.endsWith(".jsonl")) onChange(p);
|
|
6115
|
-
}, debounceMs);
|
|
6208
|
+
process.once("SIGINT", stop);
|
|
6209
|
+
process.once("SIGTERM", stop);
|
|
6210
|
+
info("Tracking Claude Code, Codex, and Cursor. Press Ctrl-C to stop.");
|
|
6116
6211
|
try {
|
|
6117
|
-
|
|
6118
|
-
const chokidarMod = await dynImport("chokidar");
|
|
6119
|
-
const watcher = chokidarMod.watch(`${dir}/**/*.jsonl`, {
|
|
6120
|
-
ignoreInitial: true,
|
|
6121
|
-
persistent: true,
|
|
6122
|
-
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
6123
|
-
});
|
|
6124
|
-
watcher.on("add", (p) => {
|
|
6125
|
-
if (typeof p === "string") debouncedChange(p);
|
|
6126
|
-
});
|
|
6127
|
-
watcher.on("change", (p) => {
|
|
6128
|
-
if (typeof p === "string") debouncedChange(p);
|
|
6129
|
-
});
|
|
6130
|
-
return () => {
|
|
6131
|
-
watcher.close();
|
|
6132
|
-
};
|
|
6133
|
-
} catch {
|
|
6134
|
-
}
|
|
6135
|
-
if (process.platform === "linux") {
|
|
6136
|
-
let lastMtimes = /* @__PURE__ */ new Map();
|
|
6137
|
-
for (const f of findJsonlFiles2(dir)) {
|
|
6212
|
+
while (!stopped) {
|
|
6138
6213
|
try {
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
if (mtime > prev) {
|
|
6150
|
-
lastMtimes.set(f, mtime);
|
|
6151
|
-
debouncedChange(f);
|
|
6152
|
-
}
|
|
6153
|
-
} catch {
|
|
6214
|
+
const count = await queue.flush(await collect(), (batch) => client.uploadSessions(batch));
|
|
6215
|
+
if (count === 0) await client.uploadSessions([]);
|
|
6216
|
+
await client.heartbeat();
|
|
6217
|
+
if (count > 0) success(`Synced ${count} changed record(s).`);
|
|
6218
|
+
} catch (err) {
|
|
6219
|
+
if (err instanceof DeviceRevokedError) {
|
|
6220
|
+
markDisconnected();
|
|
6221
|
+
deleteToken();
|
|
6222
|
+
error("Device disconnected. Run `token-rats login` to reconnect.");
|
|
6223
|
+
break;
|
|
6154
6224
|
}
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
debouncedChange(f);
|
|
6225
|
+
if (err instanceof ApiError2 && err.status === 401) {
|
|
6226
|
+
error("Session expired. Run `token-rats login` again.");
|
|
6227
|
+
process.exitCode = 1;
|
|
6228
|
+
break;
|
|
6160
6229
|
}
|
|
6230
|
+
warn(
|
|
6231
|
+
`Sync failed. The next scan will retry: ${err instanceof Error ? err.message : String(err)}`
|
|
6232
|
+
);
|
|
6161
6233
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
const watcher = watch(dir, { recursive: true, signal: controller.signal });
|
|
6171
|
-
for await (const event of watcher) {
|
|
6172
|
-
if (event.filename?.endsWith(".jsonl")) {
|
|
6173
|
-
debouncedChange(path6.join(dir, event.filename));
|
|
6174
|
-
}
|
|
6175
|
-
}
|
|
6176
|
-
} catch (err) {
|
|
6177
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
6178
|
-
log.warn(`Watcher error on ${dir}: ${err.message}`);
|
|
6179
|
-
}
|
|
6180
|
-
}
|
|
6181
|
-
})();
|
|
6182
|
-
return () => controller.abort();
|
|
6183
|
-
}
|
|
6184
|
-
function startCursorLoop(client, log, daemon) {
|
|
6185
|
-
let lastMtimes = /* @__PURE__ */ new Map();
|
|
6186
|
-
let running = false;
|
|
6187
|
-
let stopped = false;
|
|
6188
|
-
const tick = async () => {
|
|
6189
|
-
if (running || stopped) return;
|
|
6190
|
-
running = true;
|
|
6191
|
-
try {
|
|
6192
|
-
const { rows, newMtimes, scanned, opened } = await extractCursorGenerationsDelta(
|
|
6193
|
-
lastMtimes,
|
|
6194
|
-
CURSOR_DB_CAP
|
|
6195
|
-
);
|
|
6196
|
-
lastMtimes = newMtimes;
|
|
6197
|
-
if (rows.length === 0) {
|
|
6198
|
-
if (opened > 0) log.debug(`Cursor poll: ${scanned} DB(s), opened ${opened}, no new rows`);
|
|
6199
|
-
return;
|
|
6200
|
-
}
|
|
6201
|
-
let records;
|
|
6202
|
-
try {
|
|
6203
|
-
records = parseCursor(JSON.stringify(rows));
|
|
6204
|
-
} catch {
|
|
6205
|
-
log.warn("Failed to parse Cursor rows");
|
|
6206
|
-
return;
|
|
6207
|
-
}
|
|
6208
|
-
const fresh = [];
|
|
6209
|
-
for (const r of records) {
|
|
6210
|
-
if (seen.has(r.dedupeKey)) continue;
|
|
6211
|
-
seen.add(r.dedupeKey);
|
|
6212
|
-
fresh.push(r);
|
|
6213
|
-
}
|
|
6214
|
-
if (fresh.length === 0) {
|
|
6215
|
-
log.debug(`Cursor poll: ${rows.length} row(s), all duplicates`);
|
|
6216
|
-
return;
|
|
6217
|
-
}
|
|
6218
|
-
health.lastSourceAt.cursor = (/* @__PURE__ */ new Date()).toISOString();
|
|
6219
|
-
log.debug(
|
|
6220
|
-
`Cursor poll: ${fresh.length} fresh session(s) (scanned ${scanned}, opened ${opened})`
|
|
6221
|
-
);
|
|
6222
|
-
await upload(client, fresh, log, daemon);
|
|
6223
|
-
} finally {
|
|
6224
|
-
running = false;
|
|
6225
|
-
}
|
|
6226
|
-
};
|
|
6227
|
-
const initial = setTimeout(tick, 5e3);
|
|
6228
|
-
const handle = setInterval(tick, CURSOR_POLL_MS);
|
|
6229
|
-
return () => {
|
|
6230
|
-
stopped = true;
|
|
6231
|
-
clearTimeout(initial);
|
|
6232
|
-
clearInterval(handle);
|
|
6233
|
-
};
|
|
6234
|
-
}
|
|
6235
|
-
async function runWatcher(opts) {
|
|
6236
|
-
const daemon = opts.daemon === true;
|
|
6237
|
-
const verbose = opts.verbose === true;
|
|
6238
|
-
const log = makeLogger(daemon, verbose);
|
|
6239
|
-
const token = loadToken();
|
|
6240
|
-
if (!token) {
|
|
6241
|
-
log.error("Not logged in. Run `token-rats login` first.");
|
|
6242
|
-
if (!daemon) process.exit(1);
|
|
6243
|
-
reauthBackoffUntil = Date.now() + REAUTH_BACKOFF_MS;
|
|
6244
|
-
}
|
|
6245
|
-
const client = new ApiClient({ apiUrl: opts.apiUrl, token: token ?? void 0 });
|
|
6246
|
-
const debounceMs = opts.interval ?? 2e3;
|
|
6247
|
-
if (!daemon) {
|
|
6248
|
-
info("Watching Claude Code + Cursor + Codex");
|
|
6249
|
-
info("Press Ctrl-C to stop.\n");
|
|
6250
|
-
} else {
|
|
6251
|
-
log.info("watch starting", { debounceMs, cursorPollMs: CURSOR_POLL_MS });
|
|
6252
|
-
}
|
|
6253
|
-
const ccDir = claudeCodeProjectsDir();
|
|
6254
|
-
for (const f of findJsonlFiles2(ccDir)) freshClaudeRecords(f, log);
|
|
6255
|
-
for (const d of codexSessionsDirs()) {
|
|
6256
|
-
for (const f of findJsonlFiles2(d)) freshCodexRecords(f, log);
|
|
6257
|
-
}
|
|
6258
|
-
log.debug(`Seeded ${seen.size} dedupe key(s) from initial snapshot`);
|
|
6259
|
-
const cleanups = [];
|
|
6260
|
-
cleanups.push(
|
|
6261
|
-
await watchJsonlDir(
|
|
6262
|
-
ccDir,
|
|
6263
|
-
debounceMs,
|
|
6264
|
-
async (filePath) => {
|
|
6265
|
-
const fresh = freshClaudeRecords(filePath, log);
|
|
6266
|
-
await upload(client, fresh, log, daemon);
|
|
6267
|
-
},
|
|
6268
|
-
log
|
|
6269
|
-
)
|
|
6270
|
-
);
|
|
6271
|
-
for (const codexDir of codexSessionsDirs()) {
|
|
6272
|
-
cleanups.push(
|
|
6273
|
-
await watchJsonlDir(
|
|
6274
|
-
codexDir,
|
|
6275
|
-
debounceMs,
|
|
6276
|
-
async (filePath) => {
|
|
6277
|
-
const fresh = freshCodexRecords(filePath, log);
|
|
6278
|
-
await upload(client, fresh, log, daemon);
|
|
6279
|
-
},
|
|
6280
|
-
log
|
|
6281
|
-
)
|
|
6282
|
-
);
|
|
6283
|
-
}
|
|
6284
|
-
cleanups.push(startCursorLoop(client, log, daemon));
|
|
6285
|
-
process.on("SIGUSR1", () => {
|
|
6286
|
-
log.info("health", {
|
|
6287
|
-
uptimeSec: Math.round(process.uptime()),
|
|
6288
|
-
uploaded: health.uploaded,
|
|
6289
|
-
duplicates: health.duplicates,
|
|
6290
|
-
errors: health.errors,
|
|
6291
|
-
lastSourceAt: health.lastSourceAt,
|
|
6292
|
-
seenKeys: seen.size,
|
|
6293
|
-
reauthBackoffUntil: reauthBackoffUntil > Date.now() ? new Date(reauthBackoffUntil).toISOString() : null
|
|
6294
|
-
});
|
|
6295
|
-
});
|
|
6296
|
-
const shutdown = () => {
|
|
6297
|
-
log.info("watch stopping");
|
|
6298
|
-
for (const c2 of cleanups) c2();
|
|
6299
|
-
process.exit(0);
|
|
6300
|
-
};
|
|
6301
|
-
process.on("SIGINT", shutdown);
|
|
6302
|
-
process.on("SIGTERM", shutdown);
|
|
6303
|
-
}
|
|
6304
|
-
async function handleInstall(opts) {
|
|
6305
|
-
if (!isDaemonSupported()) {
|
|
6306
|
-
error(`Background daemon not supported on ${process.platform} yet.`);
|
|
6307
|
-
process.exit(1);
|
|
6308
|
-
}
|
|
6309
|
-
try {
|
|
6310
|
-
const r = await installDaemon(opts.version ?? "0.0.0");
|
|
6311
|
-
success(`Background watcher installed (${r.method}).`);
|
|
6312
|
-
info(`Logs: ${r.logDir}/watch.log`);
|
|
6313
|
-
if (r.ephemeralWarning) {
|
|
6314
|
-
warn(
|
|
6315
|
-
"You ran this via npx \u2014 the daemon may break when the npx cache is cleared.\nRun `npm i -g token-rats` (or `pnpm add -g token-rats`) for a durable install."
|
|
6316
|
-
);
|
|
6317
|
-
}
|
|
6318
|
-
if (r.method === "systemd" && r.lingerSet === false) {
|
|
6319
|
-
warn(
|
|
6320
|
-
"For the watcher to survive logout, run once:\n sudo loginctl enable-linger $USER\nWithout it, the watcher resumes at your next login."
|
|
6321
|
-
);
|
|
6234
|
+
if (stopped) break;
|
|
6235
|
+
await new Promise((resolve2) => {
|
|
6236
|
+
const timer = setTimeout(resolve2, interval);
|
|
6237
|
+
wake = () => {
|
|
6238
|
+
clearTimeout(timer);
|
|
6239
|
+
resolve2();
|
|
6240
|
+
};
|
|
6241
|
+
});
|
|
6322
6242
|
}
|
|
6323
|
-
}
|
|
6324
|
-
|
|
6325
|
-
process.
|
|
6326
|
-
}
|
|
6327
|
-
}
|
|
6328
|
-
async function handleUninstall() {
|
|
6329
|
-
try {
|
|
6330
|
-
await uninstallDaemon();
|
|
6331
|
-
success("Background watcher uninstalled.");
|
|
6332
|
-
} catch (err) {
|
|
6333
|
-
error(`Uninstall failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6334
|
-
process.exit(1);
|
|
6335
|
-
}
|
|
6336
|
-
}
|
|
6337
|
-
async function handleStatus() {
|
|
6338
|
-
const s = await daemonStatus();
|
|
6339
|
-
if (s.method === "unsupported") {
|
|
6340
|
-
info(`Daemon not supported on ${process.platform}.`);
|
|
6341
|
-
return;
|
|
6342
|
-
}
|
|
6343
|
-
info(`Method: ${s.method}`);
|
|
6344
|
-
info(`Installed: ${s.installed ? "yes" : "no"}`);
|
|
6345
|
-
info(`Running: ${s.running ? `yes (pid ${s.pid ?? "?"})` : "no"}`);
|
|
6346
|
-
if (s.installedAt) info(`Installed at: ${s.installedAt}`);
|
|
6347
|
-
if (s.installedScript) info(`Script: ${s.installedScript}`);
|
|
6348
|
-
if (s.declinedAt) info(`Declined at: ${s.declinedAt}`);
|
|
6349
|
-
info(`Log dir: ${daemonLogDir()}`);
|
|
6350
|
-
if (process.platform === "linux") {
|
|
6351
|
-
info(`Linger: ${lingerEnabled() ? "yes (survives logout)" : "no (dies on logout)"}`);
|
|
6243
|
+
} finally {
|
|
6244
|
+
process.removeListener("SIGINT", stop);
|
|
6245
|
+
process.removeListener("SIGTERM", stop);
|
|
6352
6246
|
}
|
|
6353
6247
|
}
|
|
6354
|
-
async function watchCommand(opts) {
|
|
6355
|
-
if (opts.install) return handleInstall(opts);
|
|
6356
|
-
if (opts.uninstall) return handleUninstall();
|
|
6357
|
-
if (opts.status) return handleStatus();
|
|
6358
|
-
return runWatcher(opts);
|
|
6359
|
-
}
|
|
6360
6248
|
|
|
6361
6249
|
// src/commands/whoami.ts
|
|
6362
6250
|
async function whoamiCommand(opts) {
|
|
@@ -6365,11 +6253,25 @@ async function whoamiCommand(opts) {
|
|
|
6365
6253
|
error("Not logged in. Run `token-rats login` first.");
|
|
6366
6254
|
process.exit(1);
|
|
6367
6255
|
}
|
|
6368
|
-
const client = new ApiClient({
|
|
6256
|
+
const client = new ApiClient({
|
|
6257
|
+
apiUrl: opts.apiUrl,
|
|
6258
|
+
token,
|
|
6259
|
+
deviceId: ensureDeviceId(),
|
|
6260
|
+
cliVersion: CLI_VERSION
|
|
6261
|
+
});
|
|
6369
6262
|
try {
|
|
6370
6263
|
const res = await client.getMe();
|
|
6371
6264
|
info(`Signed in as \x1B[1m@${res.user.handle}\x1B[0m`);
|
|
6265
|
+
info(`Device id: ${ensureDeviceId()}`);
|
|
6372
6266
|
} catch (err) {
|
|
6267
|
+
if (err instanceof DeviceRevokedError) {
|
|
6268
|
+
markDisconnected();
|
|
6269
|
+
deleteToken();
|
|
6270
|
+
error(
|
|
6271
|
+
"This device was disconnected from the Token Rats web UI. Run `token-rats login` to reconnect."
|
|
6272
|
+
);
|
|
6273
|
+
process.exit(1);
|
|
6274
|
+
}
|
|
6373
6275
|
if (err instanceof ApiError2 && err.status === 401) {
|
|
6374
6276
|
error("Your session has expired. Run `token-rats login` to re-authenticate.");
|
|
6375
6277
|
process.exit(1);
|
|
@@ -6385,25 +6287,23 @@ function getVersion() {
|
|
|
6385
6287
|
}
|
|
6386
6288
|
function printHelp() {
|
|
6387
6289
|
console.log(`
|
|
6388
|
-
\x1B[1mtoken-rats\x1B[0m \u2014
|
|
6290
|
+
\x1B[1mtoken-rats\x1B[0m \u2014 AI usage tracker and community \x1B[2mv${getVersion()}\x1B[0m
|
|
6389
6291
|
|
|
6390
6292
|
\x1B[1mUsage:\x1B[0m
|
|
6391
6293
|
token-rats <command> [flags]
|
|
6392
6294
|
|
|
6393
6295
|
\x1B[1mCommands:\x1B[0m
|
|
6394
|
-
login
|
|
6395
|
-
sync
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
(sql.js works out of the box \u2014 this is opt-in speed-up)
|
|
6406
|
-
help Show this help message
|
|
6296
|
+
login Authenticate with Token Rats (opens browser); installs the background watcher by default
|
|
6297
|
+
sync Read local Claude Code, Codex + Cursor logs and upload counts
|
|
6298
|
+
watch Watch logs in real-time; upload new sessions as they appear
|
|
6299
|
+
whoami Show the currently signed-in account + device id
|
|
6300
|
+
logout Clear your stored credentials
|
|
6301
|
+
install-daemon Install the background watcher (runs at logon)
|
|
6302
|
+
uninstall-daemon Remove the background watcher
|
|
6303
|
+
daemon-status Show whether the background watcher is running
|
|
6304
|
+
install-cursor Install better-sqlite3 globally for faster Cursor reads
|
|
6305
|
+
(sql.js works out of the box \u2014 this is opt-in speed-up)
|
|
6306
|
+
help Show this help message
|
|
6407
6307
|
|
|
6408
6308
|
\x1B[1mFlags (all commands):\x1B[0m
|
|
6409
6309
|
--api-url <url> Override API URL (default: https://api.tokenrats.com)
|
|
@@ -6411,16 +6311,9 @@ function printHelp() {
|
|
|
6411
6311
|
\x1B[1mFlags (sync only):\x1B[0m
|
|
6412
6312
|
--dry-run Parse but do not upload; print what would be sent
|
|
6413
6313
|
--verbose Print discovered files and per-file record counts
|
|
6414
|
-
--no-daemon Skip the post-sync prompt to install background watcher
|
|
6415
|
-
(TOKEN_RATS_NO_DAEMON=1 does the same globally)
|
|
6416
6314
|
|
|
6417
6315
|
\x1B[1mFlags (watch only):\x1B[0m
|
|
6418
|
-
--
|
|
6419
|
-
--uninstall Remove the background watcher
|
|
6420
|
-
--status Show whether the watcher is installed and running
|
|
6421
|
-
--daemon Internal: structured logs for launchd/systemd. Not for
|
|
6422
|
-
interactive use.
|
|
6423
|
-
--interval <ms> Debounce window in ms before uploading (default: 2000)
|
|
6316
|
+
--interval <ms> Scan interval in ms (default: 30000, minimum: 1000)
|
|
6424
6317
|
--verbose Print file change events and upload detail
|
|
6425
6318
|
|
|
6426
6319
|
\x1B[1mPrivacy:\x1B[0m
|
|
@@ -6449,10 +6342,6 @@ function parseArgs(argv) {
|
|
|
6449
6342
|
let verbose = false;
|
|
6450
6343
|
let interval;
|
|
6451
6344
|
let noDaemon = false;
|
|
6452
|
-
let daemon = false;
|
|
6453
|
-
let install = false;
|
|
6454
|
-
let uninstall = false;
|
|
6455
|
-
let status = false;
|
|
6456
6345
|
let i = 0;
|
|
6457
6346
|
while (i < argv.length) {
|
|
6458
6347
|
const arg = argv[i];
|
|
@@ -6470,14 +6359,6 @@ function parseArgs(argv) {
|
|
|
6470
6359
|
interval = Number(arg.slice("--interval=".length));
|
|
6471
6360
|
} else if (arg === "--no-daemon") {
|
|
6472
6361
|
noDaemon = true;
|
|
6473
|
-
} else if (arg === "--daemon") {
|
|
6474
|
-
daemon = true;
|
|
6475
|
-
} else if (arg === "--install") {
|
|
6476
|
-
install = true;
|
|
6477
|
-
} else if (arg === "--uninstall") {
|
|
6478
|
-
uninstall = true;
|
|
6479
|
-
} else if (arg === "--status") {
|
|
6480
|
-
status = true;
|
|
6481
6362
|
} else if (arg === "--version" || arg === "-V") {
|
|
6482
6363
|
console.log(getVersion());
|
|
6483
6364
|
process.exit(0);
|
|
@@ -6490,56 +6371,24 @@ function parseArgs(argv) {
|
|
|
6490
6371
|
i++;
|
|
6491
6372
|
}
|
|
6492
6373
|
const [command = null, ...rest] = positional;
|
|
6493
|
-
return {
|
|
6494
|
-
command,
|
|
6495
|
-
apiUrl,
|
|
6496
|
-
dryRun,
|
|
6497
|
-
verbose,
|
|
6498
|
-
interval,
|
|
6499
|
-
noDaemon,
|
|
6500
|
-
daemon,
|
|
6501
|
-
install,
|
|
6502
|
-
uninstall,
|
|
6503
|
-
status,
|
|
6504
|
-
rest
|
|
6505
|
-
};
|
|
6374
|
+
return { command, apiUrl, dryRun, verbose, interval, noDaemon, rest };
|
|
6506
6375
|
}
|
|
6507
6376
|
async function main() {
|
|
6508
6377
|
const args = parseArgs(process.argv.slice(2));
|
|
6509
|
-
const {
|
|
6510
|
-
command,
|
|
6511
|
-
apiUrl,
|
|
6512
|
-
dryRun,
|
|
6513
|
-
verbose,
|
|
6514
|
-
interval,
|
|
6515
|
-
noDaemon,
|
|
6516
|
-
daemon,
|
|
6517
|
-
install,
|
|
6518
|
-
uninstall,
|
|
6519
|
-
status
|
|
6520
|
-
} = args;
|
|
6378
|
+
const { command, apiUrl, dryRun, verbose, interval, noDaemon } = args;
|
|
6521
6379
|
if (!command || command === "help") {
|
|
6522
6380
|
printHelp();
|
|
6523
6381
|
process.exit(0);
|
|
6524
6382
|
}
|
|
6525
6383
|
switch (command) {
|
|
6526
6384
|
case "login":
|
|
6527
|
-
await loginCommand({ apiUrl });
|
|
6385
|
+
await loginCommand({ apiUrl, noDaemon });
|
|
6528
6386
|
break;
|
|
6529
6387
|
case "sync":
|
|
6530
|
-
await syncCommand({ apiUrl, dryRun, verbose
|
|
6388
|
+
await syncCommand({ apiUrl, dryRun, verbose });
|
|
6531
6389
|
break;
|
|
6532
6390
|
case "watch":
|
|
6533
|
-
await watchCommand({
|
|
6534
|
-
apiUrl,
|
|
6535
|
-
verbose,
|
|
6536
|
-
interval,
|
|
6537
|
-
daemon,
|
|
6538
|
-
install,
|
|
6539
|
-
uninstall,
|
|
6540
|
-
status,
|
|
6541
|
-
version: getVersion()
|
|
6542
|
-
});
|
|
6391
|
+
await watchCommand({ apiUrl, verbose, interval });
|
|
6543
6392
|
break;
|
|
6544
6393
|
case "whoami":
|
|
6545
6394
|
await whoamiCommand({ apiUrl });
|
|
@@ -6550,6 +6399,15 @@ async function main() {
|
|
|
6550
6399
|
case "install-cursor":
|
|
6551
6400
|
await installCursorCommand();
|
|
6552
6401
|
break;
|
|
6402
|
+
case "install-daemon":
|
|
6403
|
+
await installDaemonCommand(apiUrl);
|
|
6404
|
+
break;
|
|
6405
|
+
case "uninstall-daemon":
|
|
6406
|
+
await uninstallDaemonCommand();
|
|
6407
|
+
break;
|
|
6408
|
+
case "daemon-status":
|
|
6409
|
+
await daemonStatusCommand();
|
|
6410
|
+
break;
|
|
6553
6411
|
default:
|
|
6554
6412
|
console.error(`\x1B[31mUnknown command: ${command}\x1B[0m`);
|
|
6555
6413
|
console.error("Run \x1B[1mtoken-rats help\x1B[0m for a list of commands.");
|