token-rats 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -54
- package/dist/index.js +844 -989
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -32,6 +32,379 @@ 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 * as os2 from "node:os";
|
|
39
|
+
import * as path2 from "node:path";
|
|
40
|
+
import { promisify } from "node:util";
|
|
41
|
+
|
|
42
|
+
// src/lib/auth-store.ts
|
|
43
|
+
import * as crypto from "node:crypto";
|
|
44
|
+
import * as fs from "node:fs";
|
|
45
|
+
import * as os from "node:os";
|
|
46
|
+
import * as path from "node:path";
|
|
47
|
+
function tokenDir() {
|
|
48
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
49
|
+
const base = xdgConfig ?? path.join(os.homedir(), ".config");
|
|
50
|
+
return path.join(base, "token-rats");
|
|
51
|
+
}
|
|
52
|
+
function tokenPath() {
|
|
53
|
+
return path.join(tokenDir(), "token");
|
|
54
|
+
}
|
|
55
|
+
function statePath() {
|
|
56
|
+
return path.join(tokenDir(), "state.json");
|
|
57
|
+
}
|
|
58
|
+
function disconnectedPath() {
|
|
59
|
+
return path.join(tokenDir(), "disconnected");
|
|
60
|
+
}
|
|
61
|
+
function saveToken(token) {
|
|
62
|
+
const dir = tokenDir();
|
|
63
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
+
fs.writeFileSync(tokenPath(), token, { encoding: "utf8", mode: 384 });
|
|
65
|
+
}
|
|
66
|
+
function loadToken() {
|
|
67
|
+
try {
|
|
68
|
+
const token = fs.readFileSync(tokenPath(), "utf8").trim();
|
|
69
|
+
return token.length > 0 ? token : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function deleteToken() {
|
|
75
|
+
try {
|
|
76
|
+
fs.unlinkSync(tokenPath());
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isLoggedIn() {
|
|
81
|
+
return loadToken() !== null;
|
|
82
|
+
}
|
|
83
|
+
function ensureDeviceId() {
|
|
84
|
+
try {
|
|
85
|
+
const raw = fs.readFileSync(statePath(), "utf8");
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
if (typeof parsed.deviceId === "string" && parsed.deviceId.length > 0) {
|
|
88
|
+
return parsed.deviceId;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
const dir = tokenDir();
|
|
93
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
94
|
+
const deviceId = crypto.randomUUID();
|
|
95
|
+
const state = { deviceId, createdAt: Date.now() };
|
|
96
|
+
fs.writeFileSync(statePath(), JSON.stringify(state, null, 2), {
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
mode: 384
|
|
99
|
+
});
|
|
100
|
+
return deviceId;
|
|
101
|
+
}
|
|
102
|
+
function markDisconnected() {
|
|
103
|
+
const dir = tokenDir();
|
|
104
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
105
|
+
fs.writeFileSync(disconnectedPath(), String(Date.now()), { mode: 384 });
|
|
106
|
+
}
|
|
107
|
+
function clearDisconnected() {
|
|
108
|
+
try {
|
|
109
|
+
fs.unlinkSync(disconnectedPath());
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function isDisconnected() {
|
|
114
|
+
return fs.existsSync(disconnectedPath());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/lib/log.ts
|
|
118
|
+
var ESC = "\x1B";
|
|
119
|
+
var c = {
|
|
120
|
+
reset: `${ESC}[0m`,
|
|
121
|
+
bold: `${ESC}[1m`,
|
|
122
|
+
dim: `${ESC}[2m`,
|
|
123
|
+
green: `${ESC}[32m`,
|
|
124
|
+
yellow: `${ESC}[33m`,
|
|
125
|
+
cyan: `${ESC}[36m`,
|
|
126
|
+
red: `${ESC}[31m`,
|
|
127
|
+
gray: `${ESC}[90m`
|
|
128
|
+
};
|
|
129
|
+
function strip(s) {
|
|
130
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
131
|
+
}
|
|
132
|
+
function isTTY() {
|
|
133
|
+
return process.stdout.isTTY === true;
|
|
134
|
+
}
|
|
135
|
+
function color(code, text) {
|
|
136
|
+
return isTTY() ? `${code}${text}${c.reset}` : strip(text);
|
|
137
|
+
}
|
|
138
|
+
function info(msg) {
|
|
139
|
+
console.log(color(c.cyan, ` ${msg}`));
|
|
140
|
+
}
|
|
141
|
+
function success(msg) {
|
|
142
|
+
console.log(color(c.green, `\u2713 ${msg}`));
|
|
143
|
+
}
|
|
144
|
+
function warn(msg) {
|
|
145
|
+
console.warn(color(c.yellow, `\u26A0 ${msg}`));
|
|
146
|
+
}
|
|
147
|
+
function error(msg) {
|
|
148
|
+
console.error(color(c.red, `\u2717 ${msg}`));
|
|
149
|
+
}
|
|
150
|
+
function dim(msg) {
|
|
151
|
+
console.log(color(c.dim, ` ${msg}`));
|
|
152
|
+
}
|
|
153
|
+
function spinner(label) {
|
|
154
|
+
if (!isTTY()) {
|
|
155
|
+
process.stdout.write(` ${label}...
|
|
156
|
+
`);
|
|
157
|
+
return {
|
|
158
|
+
stop(finalMsg) {
|
|
159
|
+
if (finalMsg) process.stdout.write(` ${finalMsg}
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
165
|
+
let i = 0;
|
|
166
|
+
const interval = setInterval(() => {
|
|
167
|
+
process.stdout.write(`\r${c.cyan}${frames[i++ % frames.length]}${c.reset} ${label} `);
|
|
168
|
+
}, 80);
|
|
169
|
+
return {
|
|
170
|
+
stop(finalMsg) {
|
|
171
|
+
clearInterval(interval);
|
|
172
|
+
process.stdout.write("\r\x1B[K");
|
|
173
|
+
if (finalMsg) success(finalMsg);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/commands/install-daemon.ts
|
|
179
|
+
var exec = promisify(execFile);
|
|
180
|
+
var LABEL = "com.tokenrats.watch";
|
|
181
|
+
var LINUX_UNIT = "token-rats-watch.service";
|
|
182
|
+
var WINDOWS_TASK = "TokenRatsWatch";
|
|
183
|
+
function resolveCliPath() {
|
|
184
|
+
const script = process.argv[1] ?? "token-rats";
|
|
185
|
+
return { node: process.execPath, script };
|
|
186
|
+
}
|
|
187
|
+
function darwinPlistPath() {
|
|
188
|
+
return path2.join(os2.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
189
|
+
}
|
|
190
|
+
function darwinLogDir() {
|
|
191
|
+
return path2.join(os2.homedir(), "Library", "Logs", "token-rats");
|
|
192
|
+
}
|
|
193
|
+
function darwinPlist(node, script) {
|
|
194
|
+
const logDir = darwinLogDir();
|
|
195
|
+
const stdout = path2.join(logDir, "watch.out.log");
|
|
196
|
+
const stderr = path2.join(logDir, "watch.err.log");
|
|
197
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
198
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
199
|
+
<plist version="1.0">
|
|
200
|
+
<dict>
|
|
201
|
+
<key>Label</key>
|
|
202
|
+
<string>${LABEL}</string>
|
|
203
|
+
<key>ProgramArguments</key>
|
|
204
|
+
<array>
|
|
205
|
+
<string>${node}</string>
|
|
206
|
+
<string>${script}</string>
|
|
207
|
+
<string>watch</string>
|
|
208
|
+
</array>
|
|
209
|
+
<key>RunAtLoad</key>
|
|
210
|
+
<true/>
|
|
211
|
+
<key>KeepAlive</key>
|
|
212
|
+
<true/>
|
|
213
|
+
<key>StandardOutPath</key>
|
|
214
|
+
<string>${stdout}</string>
|
|
215
|
+
<key>StandardErrorPath</key>
|
|
216
|
+
<string>${stderr}</string>
|
|
217
|
+
</dict>
|
|
218
|
+
</plist>
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
async function darwinInstall() {
|
|
222
|
+
const { node, script } = resolveCliPath();
|
|
223
|
+
fs2.mkdirSync(darwinLogDir(), { recursive: true });
|
|
224
|
+
const plistPath = darwinPlistPath();
|
|
225
|
+
fs2.mkdirSync(path2.dirname(plistPath), { recursive: true });
|
|
226
|
+
fs2.writeFileSync(plistPath, darwinPlist(node, script), { mode: 420 });
|
|
227
|
+
try {
|
|
228
|
+
await exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 0}`, plistPath]);
|
|
229
|
+
} catch {
|
|
230
|
+
try {
|
|
231
|
+
await exec("launchctl", ["load", plistPath]);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Wrote ${plistPath} but failed to load it: ${err instanceof Error ? err.message : String(err)}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function darwinUninstall() {
|
|
240
|
+
const plistPath = darwinPlistPath();
|
|
241
|
+
try {
|
|
242
|
+
await exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 0}/${LABEL}`]);
|
|
243
|
+
} catch {
|
|
244
|
+
try {
|
|
245
|
+
await exec("launchctl", ["unload", plistPath]);
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
fs2.unlinkSync(plistPath);
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function darwinStatus() {
|
|
255
|
+
if (!fs2.existsSync(darwinPlistPath())) return "not-installed";
|
|
256
|
+
try {
|
|
257
|
+
const { stdout } = await exec("launchctl", ["list"]);
|
|
258
|
+
if (stdout.split("\n").some((line) => line.endsWith(LABEL))) return "running";
|
|
259
|
+
return "stopped";
|
|
260
|
+
} catch {
|
|
261
|
+
return "stopped";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function linuxUnitPath() {
|
|
265
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
|
|
266
|
+
return path2.join(xdgConfig, "systemd", "user", LINUX_UNIT);
|
|
267
|
+
}
|
|
268
|
+
function linuxUnit(node, script) {
|
|
269
|
+
return `[Unit]
|
|
270
|
+
Description=Token Rats watcher \u2014 live AI usage sync
|
|
271
|
+
After=network-online.target
|
|
272
|
+
Wants=network-online.target
|
|
273
|
+
|
|
274
|
+
[Service]
|
|
275
|
+
Type=simple
|
|
276
|
+
ExecStart=${node} ${script} watch
|
|
277
|
+
Restart=on-failure
|
|
278
|
+
RestartSec=10s
|
|
279
|
+
Environment=NODE_ENV=production
|
|
280
|
+
|
|
281
|
+
[Install]
|
|
282
|
+
WantedBy=default.target
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
async function linuxInstall() {
|
|
286
|
+
const { node, script } = resolveCliPath();
|
|
287
|
+
const unitPath = linuxUnitPath();
|
|
288
|
+
fs2.mkdirSync(path2.dirname(unitPath), { recursive: true });
|
|
289
|
+
fs2.writeFileSync(unitPath, linuxUnit(node, script), { mode: 420 });
|
|
290
|
+
try {
|
|
291
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
292
|
+
await exec("systemctl", ["--user", "enable", "--now", LINUX_UNIT]);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Wrote ${unitPath} but failed to enable+start it: ${err instanceof Error ? err.message : String(err)}`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function linuxUninstall() {
|
|
300
|
+
try {
|
|
301
|
+
await exec("systemctl", ["--user", "disable", "--now", LINUX_UNIT]);
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
fs2.unlinkSync(linuxUnitPath());
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function linuxStatus() {
|
|
314
|
+
if (!fs2.existsSync(linuxUnitPath())) return "not-installed";
|
|
315
|
+
try {
|
|
316
|
+
const { stdout } = await exec("systemctl", ["--user", "is-active", LINUX_UNIT]);
|
|
317
|
+
return stdout.trim() === "active" ? "running" : "stopped";
|
|
318
|
+
} catch {
|
|
319
|
+
return "stopped";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function windowsInstall() {
|
|
323
|
+
const { node, script } = resolveCliPath();
|
|
324
|
+
await exec("schtasks", [
|
|
325
|
+
"/Create",
|
|
326
|
+
"/SC",
|
|
327
|
+
"ONLOGON",
|
|
328
|
+
"/TN",
|
|
329
|
+
WINDOWS_TASK,
|
|
330
|
+
"/TR",
|
|
331
|
+
`"${node}" "${script}" watch`,
|
|
332
|
+
"/RL",
|
|
333
|
+
"LIMITED",
|
|
334
|
+
"/F"
|
|
335
|
+
]);
|
|
336
|
+
try {
|
|
337
|
+
await exec("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function windowsUninstall() {
|
|
342
|
+
try {
|
|
343
|
+
await exec("schtasks", ["/End", "/TN", WINDOWS_TASK]);
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
await exec("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function windowsStatus() {
|
|
352
|
+
try {
|
|
353
|
+
const { stdout } = await exec("schtasks", ["/Query", "/TN", WINDOWS_TASK, "/FO", "CSV", "/NH"]);
|
|
354
|
+
if (stdout.includes("Running")) return "running";
|
|
355
|
+
return "stopped";
|
|
356
|
+
} catch {
|
|
357
|
+
return "not-installed";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function installDaemonCommand() {
|
|
361
|
+
clearDisconnected();
|
|
362
|
+
try {
|
|
363
|
+
if (process.platform === "darwin") {
|
|
364
|
+
await darwinInstall();
|
|
365
|
+
} else if (process.platform === "linux") {
|
|
366
|
+
await linuxInstall();
|
|
367
|
+
} else if (process.platform === "win32") {
|
|
368
|
+
await windowsInstall();
|
|
369
|
+
} else {
|
|
370
|
+
warn(`No daemon installer for platform ${process.platform}; skipping.`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
success("Background watcher installed and running.");
|
|
374
|
+
dim("It will pick up new sessions from Claude Code logs in real time.");
|
|
375
|
+
dim("Manage it with `token-rats daemon-status` and `token-rats uninstall-daemon`.");
|
|
376
|
+
} catch (err) {
|
|
377
|
+
error(`Failed to install daemon: ${err instanceof Error ? err.message : String(err)}`);
|
|
378
|
+
warn("You can still run `token-rats sync` manually.");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function uninstallDaemonCommand() {
|
|
382
|
+
if (process.platform === "darwin") {
|
|
383
|
+
await darwinUninstall();
|
|
384
|
+
} else if (process.platform === "linux") {
|
|
385
|
+
await linuxUninstall();
|
|
386
|
+
} else if (process.platform === "win32") {
|
|
387
|
+
await windowsUninstall();
|
|
388
|
+
} else {
|
|
389
|
+
warn(`No daemon installer for platform ${process.platform}; nothing to remove.`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
info("Daemon removed. `token-rats sync` will still work manually.");
|
|
393
|
+
}
|
|
394
|
+
async function daemonStatusCommand() {
|
|
395
|
+
let status;
|
|
396
|
+
if (process.platform === "darwin") status = await darwinStatus();
|
|
397
|
+
else if (process.platform === "linux") status = await linuxStatus();
|
|
398
|
+
else if (process.platform === "win32") status = await windowsStatus();
|
|
399
|
+
else {
|
|
400
|
+
warn(`No daemon for platform ${process.platform}.`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (status === "running") success("Token Rats watcher is running.");
|
|
404
|
+
else if (status === "stopped") warn("Token Rats watcher is installed but not running.");
|
|
405
|
+
else info("Token Rats watcher is not installed. Run `token-rats install-daemon` to start it.");
|
|
406
|
+
}
|
|
407
|
+
|
|
35
408
|
// ../../node_modules/.pnpm/zod@3.23.8/node_modules/zod/lib/index.mjs
|
|
36
409
|
var util;
|
|
37
410
|
(function(util2) {
|
|
@@ -385,8 +758,8 @@ function getErrorMap() {
|
|
|
385
758
|
return overrideErrorMap;
|
|
386
759
|
}
|
|
387
760
|
var makeIssue = (params) => {
|
|
388
|
-
const { data, path:
|
|
389
|
-
const fullPath = [...
|
|
761
|
+
const { data, path: path5, errorMaps, issueData } = params;
|
|
762
|
+
const fullPath = [...path5, ...issueData.path || []];
|
|
390
763
|
const fullIssue = {
|
|
391
764
|
...issueData,
|
|
392
765
|
path: fullPath
|
|
@@ -508,11 +881,11 @@ var errorUtil;
|
|
|
508
881
|
var _ZodEnum_cache;
|
|
509
882
|
var _ZodNativeEnum_cache;
|
|
510
883
|
var ParseInputLazyPath = class {
|
|
511
|
-
constructor(parent, value,
|
|
884
|
+
constructor(parent, value, path5, key) {
|
|
512
885
|
this._cachedPath = [];
|
|
513
886
|
this.parent = parent;
|
|
514
887
|
this.data = value;
|
|
515
|
-
this._path =
|
|
888
|
+
this._path = path5;
|
|
516
889
|
this._key = key;
|
|
517
890
|
}
|
|
518
891
|
get path() {
|
|
@@ -3948,11 +4321,21 @@ var z = /* @__PURE__ */ Object.freeze({
|
|
|
3948
4321
|
|
|
3949
4322
|
// ../contracts/src/session.ts
|
|
3950
4323
|
var Source = z.enum(["claude-code", "cursor", "codex"]);
|
|
3951
|
-
var Provider = z.enum([
|
|
4324
|
+
var Provider = z.enum([
|
|
4325
|
+
"anthropic",
|
|
4326
|
+
"openai",
|
|
4327
|
+
"openrouter",
|
|
4328
|
+
"cursor",
|
|
4329
|
+
"ollama",
|
|
4330
|
+
"unknown"
|
|
4331
|
+
]);
|
|
4332
|
+
var SessionChannel = z.enum(["cli", "ide", "api", "proxy", "local", "unknown"]);
|
|
3952
4333
|
var SessionRecord = z.object({
|
|
3953
4334
|
id: z.string().min(1),
|
|
3954
4335
|
source: Source,
|
|
3955
4336
|
provider: Provider.optional(),
|
|
4337
|
+
client: z.string().min(1).max(64).optional(),
|
|
4338
|
+
channel: SessionChannel.optional(),
|
|
3956
4339
|
model: z.string().min(1),
|
|
3957
4340
|
inTokens: z.number().int().nonnegative(),
|
|
3958
4341
|
outTokens: z.number().int().nonnegative(),
|
|
@@ -3969,6 +4352,12 @@ var SessionRecord = z.object({
|
|
|
3969
4352
|
});
|
|
3970
4353
|
|
|
3971
4354
|
// ../contracts/src/user.ts
|
|
4355
|
+
var ProfileAttributionEntry = z.object({
|
|
4356
|
+
source: z.string(),
|
|
4357
|
+
tokens: z.number().int().nonnegative(),
|
|
4358
|
+
costUsdCents: z.number().int().nonnegative(),
|
|
4359
|
+
sessions: z.number().int().nonnegative()
|
|
4360
|
+
});
|
|
3972
4361
|
var User = z.object({
|
|
3973
4362
|
id: z.string(),
|
|
3974
4363
|
handle: z.string(),
|
|
@@ -4002,6 +4391,10 @@ var Profile = User.extend({
|
|
|
4002
4391
|
}),
|
|
4003
4392
|
/** Number of users this user has brought in via their referral link. */
|
|
4004
4393
|
referredCount: z.number().int().nonnegative().optional(),
|
|
4394
|
+
/** All-time attribution grouped by tool/client. */
|
|
4395
|
+
sources: z.array(ProfileAttributionEntry).optional(),
|
|
4396
|
+
/** All-time attribution grouped by transport channel. */
|
|
4397
|
+
channels: z.array(ProfileAttributionEntry).optional(),
|
|
4005
4398
|
/**
|
|
4006
4399
|
* The profile owner's referral code. Self-only — present only when the
|
|
4007
4400
|
* caller is viewing their own profile, so the page can render a
|
|
@@ -4077,8 +4470,14 @@ var LeaderboardRow = z.object({
|
|
|
4077
4470
|
tokens: z.number().int().nonnegative(),
|
|
4078
4471
|
costUsdCents: z.number().int().nonnegative(),
|
|
4079
4472
|
sessions: z.number().int().nonnegative(),
|
|
4473
|
+
/** ISO 3166-1 alpha-2 country code. Null when the user has no stamped country. */
|
|
4474
|
+
country: z.string().length(2).nullable().default(null),
|
|
4080
4475
|
/** Up to 2 dominant sources by token volume, descending. May be empty. */
|
|
4081
|
-
topSources: z.array(SourceBreakdownEntry).max(2).default([])
|
|
4476
|
+
topSources: z.array(SourceBreakdownEntry).max(2).default([]),
|
|
4477
|
+
/** Up to 2 dominant clients/tools by token volume, descending. */
|
|
4478
|
+
topClients: z.array(SourceBreakdownEntry).max(2).default([]),
|
|
4479
|
+
/** Up to 2 dominant transport channels by token volume, descending. */
|
|
4480
|
+
topChannels: z.array(SourceBreakdownEntry).max(2).default([])
|
|
4082
4481
|
});
|
|
4083
4482
|
var Leaderboard = z.object({
|
|
4084
4483
|
range: LeaderboardRange,
|
|
@@ -4414,6 +4813,12 @@ var ENDPOINTS = {
|
|
|
4414
4813
|
meReferral: "/v1/me/referral",
|
|
4415
4814
|
// v1.2 Track AD — friends derived from shared private rooms
|
|
4416
4815
|
meFriends: "/v1/me/friends",
|
|
4816
|
+
// Multi-device — anonymized device list + revoke + heartbeat
|
|
4817
|
+
meDevices: "/v1/me/devices",
|
|
4818
|
+
meDeviceRevoke: (deviceId) => `/v1/me/devices/${deviceId}/revoke`,
|
|
4819
|
+
meDeviceHeartbeat: "/v1/me/devices/heartbeat",
|
|
4820
|
+
// CLI version + upgrade banner
|
|
4821
|
+
cliVersion: "/v1/cli/version",
|
|
4417
4822
|
// Phase 3 Track O — Org plan
|
|
4418
4823
|
orgs: "/v1/orgs",
|
|
4419
4824
|
org: (slug) => `/v1/orgs/${slug}`,
|
|
@@ -4547,6 +4952,67 @@ var FriendsResponse = z.object({
|
|
|
4547
4952
|
friends: z.array(FriendRow)
|
|
4548
4953
|
});
|
|
4549
4954
|
|
|
4955
|
+
// ../contracts/src/device.ts
|
|
4956
|
+
var DeviceTotals = z.object({
|
|
4957
|
+
tokens: z.number().int().nonnegative(),
|
|
4958
|
+
costUsdCents: z.number().int().nonnegative(),
|
|
4959
|
+
sessions: z.number().int().nonnegative()
|
|
4960
|
+
});
|
|
4961
|
+
var DeviceBreakdownEntry = z.object({
|
|
4962
|
+
value: z.string().min(1),
|
|
4963
|
+
tokens: z.number().int().nonnegative(),
|
|
4964
|
+
costUsdCents: z.number().int().nonnegative(),
|
|
4965
|
+
sessions: z.number().int().nonnegative()
|
|
4966
|
+
});
|
|
4967
|
+
var Device = z.object({
|
|
4968
|
+
deviceId: z.string().min(1),
|
|
4969
|
+
createdAt: z.number().int().nonnegative(),
|
|
4970
|
+
lastSeenAt: z.number().int().nonnegative(),
|
|
4971
|
+
lastHeartbeatAt: z.number().int().nonnegative().nullable(),
|
|
4972
|
+
/** Derived: true iff lastHeartbeatAt is within 5 min of `now`. */
|
|
4973
|
+
isLive: z.boolean(),
|
|
4974
|
+
lastUploadCount: z.number().int().nonnegative(),
|
|
4975
|
+
cliVersion: z.string().nullable(),
|
|
4976
|
+
/** Unix-ms when the user revoked this device via the web UI, else null. */
|
|
4977
|
+
revokedAt: z.number().int().nonnegative().nullable(),
|
|
4978
|
+
/** True for the synthetic pre-device-id bucket. */
|
|
4979
|
+
isLegacy: z.boolean().default(false),
|
|
4980
|
+
/** Last observed session upload timestamp for this device, if any. */
|
|
4981
|
+
lastSessionAt: z.number().int().nonnegative().nullable(),
|
|
4982
|
+
/** 30-day totals for this device, computed at request time. */
|
|
4983
|
+
totals: DeviceTotals,
|
|
4984
|
+
/** All-time totals for this device. */
|
|
4985
|
+
totalsAllTime: DeviceTotals,
|
|
4986
|
+
/** Dominant sources in the last 30 days, ordered by tokens desc. */
|
|
4987
|
+
topSources: z.array(DeviceBreakdownEntry).max(3).default([]),
|
|
4988
|
+
/** Dominant clients/tools in the last 30 days, ordered by tokens desc. */
|
|
4989
|
+
topClients: z.array(DeviceBreakdownEntry).max(3).default([]),
|
|
4990
|
+
/** Dominant transport channels in the last 30 days, ordered by tokens desc. */
|
|
4991
|
+
topChannels: z.array(DeviceBreakdownEntry).max(3).default([]),
|
|
4992
|
+
/** Dominant providers in the last 30 days, ordered by tokens desc. */
|
|
4993
|
+
topProviders: z.array(DeviceBreakdownEntry).max(3).default([]),
|
|
4994
|
+
/** Dominant models in the last 30 days, ordered by tokens desc. */
|
|
4995
|
+
topModels: z.array(DeviceBreakdownEntry).max(3).default([])
|
|
4996
|
+
});
|
|
4997
|
+
var GetMeDevicesResponse = z.object({
|
|
4998
|
+
devices: z.array(Device)
|
|
4999
|
+
});
|
|
5000
|
+
var RevokeDeviceResponse = z.object({
|
|
5001
|
+
ok: z.literal(true),
|
|
5002
|
+
revokedAt: z.number().int().nonnegative()
|
|
5003
|
+
});
|
|
5004
|
+
var DeviceHeartbeatResponse = z.object({
|
|
5005
|
+
ok: z.literal(true),
|
|
5006
|
+
lastHeartbeatAt: z.number().int().nonnegative()
|
|
5007
|
+
});
|
|
5008
|
+
|
|
5009
|
+
// ../contracts/src/cli.ts
|
|
5010
|
+
var CliVersionResponse = z.object({
|
|
5011
|
+
latest: z.string().min(1),
|
|
5012
|
+
minSupported: z.string().min(1),
|
|
5013
|
+
upgradeCommand: z.string().min(1)
|
|
5014
|
+
});
|
|
5015
|
+
|
|
4550
5016
|
// src/lib/api.ts
|
|
4551
5017
|
var DEFAULT_API_URL = "https://api.tokenrats.com";
|
|
4552
5018
|
function isTransient(status) {
|
|
@@ -4563,33 +5029,40 @@ var ApiError2 = class extends Error {
|
|
|
4563
5029
|
this.name = "ApiError";
|
|
4564
5030
|
}
|
|
4565
5031
|
};
|
|
5032
|
+
var DeviceRevokedError = class extends ApiError2 {
|
|
5033
|
+
constructor(body) {
|
|
5034
|
+
super(401, body);
|
|
5035
|
+
this.name = "DeviceRevokedError";
|
|
5036
|
+
}
|
|
5037
|
+
};
|
|
4566
5038
|
var ApiClient = class {
|
|
4567
5039
|
apiUrl;
|
|
4568
5040
|
token;
|
|
5041
|
+
deviceId;
|
|
5042
|
+
cliVersion;
|
|
4569
5043
|
constructor(opts = {}) {
|
|
4570
5044
|
this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
4571
5045
|
this.token = opts.token;
|
|
5046
|
+
this.deviceId = opts.deviceId;
|
|
5047
|
+
this.cliVersion = opts.cliVersion;
|
|
4572
5048
|
}
|
|
4573
5049
|
setToken(token) {
|
|
4574
5050
|
this.token = token;
|
|
4575
5051
|
}
|
|
4576
5052
|
headers() {
|
|
4577
5053
|
const h = { "Content-Type": "application/json" };
|
|
4578
|
-
if (this.token) {
|
|
4579
|
-
|
|
4580
|
-
|
|
5054
|
+
if (this.token) h.Authorization = `Bearer ${this.token}`;
|
|
5055
|
+
if (this.deviceId) h["X-Device-Id"] = this.deviceId;
|
|
5056
|
+
if (this.cliVersion) h["X-Cli-Version"] = this.cliVersion;
|
|
4581
5057
|
return h;
|
|
4582
5058
|
}
|
|
4583
|
-
/** Perform a fetch with retry+backoff. maxRetries=3, delays: 1s, 2s, 4s. */
|
|
4584
5059
|
async fetchWithRetry(url, init, maxRetries = 3) {
|
|
4585
5060
|
let attempt = 0;
|
|
4586
5061
|
let lastErr;
|
|
4587
5062
|
while (attempt <= maxRetries) {
|
|
4588
5063
|
try {
|
|
4589
5064
|
const res = await fetch(url, init);
|
|
4590
|
-
if (res.ok || !isTransient(res.status))
|
|
4591
|
-
return res;
|
|
4592
|
-
}
|
|
5065
|
+
if (res.ok || !isTransient(res.status)) return res;
|
|
4593
5066
|
lastErr = new ApiError2(res.status, await res.text());
|
|
4594
5067
|
} catch (err) {
|
|
4595
5068
|
lastErr = err;
|
|
@@ -4601,34 +5074,37 @@ var ApiClient = class {
|
|
|
4601
5074
|
}
|
|
4602
5075
|
throw lastErr;
|
|
4603
5076
|
}
|
|
4604
|
-
async
|
|
4605
|
-
const
|
|
5077
|
+
async failedResponseToError(res) {
|
|
5078
|
+
const body = await res.text();
|
|
5079
|
+
if (res.status === 401 && /device_revoked/.test(body)) {
|
|
5080
|
+
return new DeviceRevokedError(body);
|
|
5081
|
+
}
|
|
5082
|
+
return new ApiError2(res.status, body);
|
|
5083
|
+
}
|
|
5084
|
+
async post(path5, body) {
|
|
5085
|
+
const url = `${this.apiUrl}${path5}`;
|
|
4606
5086
|
const res = await this.fetchWithRetry(url, {
|
|
4607
5087
|
method: "POST",
|
|
4608
5088
|
headers: this.headers(),
|
|
4609
5089
|
body: JSON.stringify(body)
|
|
4610
5090
|
});
|
|
4611
|
-
if (!res.ok)
|
|
4612
|
-
throw new ApiError2(res.status, await res.text());
|
|
4613
|
-
}
|
|
5091
|
+
if (!res.ok) throw await this.failedResponseToError(res);
|
|
4614
5092
|
return res.json();
|
|
4615
5093
|
}
|
|
4616
|
-
async get(
|
|
4617
|
-
const url = `${this.apiUrl}${
|
|
5094
|
+
async get(path5) {
|
|
5095
|
+
const url = `${this.apiUrl}${path5}`;
|
|
4618
5096
|
const res = await this.fetchWithRetry(url, {
|
|
4619
5097
|
method: "GET",
|
|
4620
5098
|
headers: this.headers()
|
|
4621
5099
|
});
|
|
4622
|
-
if (!res.ok)
|
|
4623
|
-
throw new ApiError2(res.status, await res.text());
|
|
4624
|
-
}
|
|
5100
|
+
if (!res.ok) throw await this.failedResponseToError(res);
|
|
4625
5101
|
return res.json();
|
|
4626
5102
|
}
|
|
4627
5103
|
/** Initiate device-code flow. */
|
|
4628
5104
|
async cliExchange() {
|
|
4629
5105
|
return this.post(ENDPOINTS.authCliExchange, {});
|
|
4630
5106
|
}
|
|
4631
|
-
/** Poll for auth token. Returns token on success, null on pending
|
|
5107
|
+
/** Poll for auth token. Returns token on success, null on pending. */
|
|
4632
5108
|
async cliPoll(pollToken) {
|
|
4633
5109
|
const url = `${this.apiUrl}${ENDPOINTS.authCliPoll}`;
|
|
4634
5110
|
const res = await fetch(url, {
|
|
@@ -4640,12 +5116,8 @@ var ApiClient = class {
|
|
|
4640
5116
|
const data = await res.json();
|
|
4641
5117
|
return data.token;
|
|
4642
5118
|
}
|
|
4643
|
-
if (res.status === 202)
|
|
4644
|
-
|
|
4645
|
-
}
|
|
4646
|
-
if (res.status === 410) {
|
|
4647
|
-
throw new ApiError2(410, "Code expired");
|
|
4648
|
-
}
|
|
5119
|
+
if (res.status === 202) return null;
|
|
5120
|
+
if (res.status === 410) throw new ApiError2(410, "Code expired");
|
|
4649
5121
|
throw new ApiError2(res.status, await res.text());
|
|
4650
5122
|
}
|
|
4651
5123
|
/** GET /v1/me */
|
|
@@ -4656,106 +5128,18 @@ var ApiClient = class {
|
|
|
4656
5128
|
async uploadSessions(sessions) {
|
|
4657
5129
|
return this.post(ENDPOINTS.sessions, { sessions });
|
|
4658
5130
|
}
|
|
5131
|
+
/** GET /v1/me/devices */
|
|
5132
|
+
async getDevices() {
|
|
5133
|
+
return this.get(ENDPOINTS.meDevices);
|
|
5134
|
+
}
|
|
5135
|
+
/** POST /v1/me/devices/heartbeat */
|
|
5136
|
+
async heartbeat() {
|
|
5137
|
+
return this.post(ENDPOINTS.meDeviceHeartbeat, {});
|
|
5138
|
+
}
|
|
4659
5139
|
};
|
|
4660
5140
|
|
|
4661
|
-
// src/lib/
|
|
4662
|
-
|
|
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;
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
4688
|
-
function deleteToken() {
|
|
4689
|
-
const file = tokenPath();
|
|
4690
|
-
try {
|
|
4691
|
-
fs.unlinkSync(file);
|
|
4692
|
-
} catch {
|
|
4693
|
-
}
|
|
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
|
-
};
|
|
4711
|
-
function strip(s) {
|
|
4712
|
-
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
4713
|
-
}
|
|
4714
|
-
function isTTY() {
|
|
4715
|
-
return process.stdout.isTTY === true;
|
|
4716
|
-
}
|
|
4717
|
-
function color(code, text) {
|
|
4718
|
-
return isTTY() ? `${code}${text}${c.reset}` : strip(text);
|
|
4719
|
-
}
|
|
4720
|
-
function info(msg) {
|
|
4721
|
-
console.log(color(c.cyan, ` ${msg}`));
|
|
4722
|
-
}
|
|
4723
|
-
function success(msg) {
|
|
4724
|
-
console.log(color(c.green, `\u2713 ${msg}`));
|
|
4725
|
-
}
|
|
4726
|
-
function warn(msg) {
|
|
4727
|
-
console.warn(color(c.yellow, `\u26A0 ${msg}`));
|
|
4728
|
-
}
|
|
4729
|
-
function error(msg) {
|
|
4730
|
-
console.error(color(c.red, `\u2717 ${msg}`));
|
|
4731
|
-
}
|
|
4732
|
-
function dim(msg) {
|
|
4733
|
-
console.log(color(c.dim, ` ${msg}`));
|
|
4734
|
-
}
|
|
4735
|
-
function spinner(label) {
|
|
4736
|
-
if (!isTTY()) {
|
|
4737
|
-
process.stdout.write(` ${label}...
|
|
4738
|
-
`);
|
|
4739
|
-
return {
|
|
4740
|
-
stop(finalMsg) {
|
|
4741
|
-
if (finalMsg) process.stdout.write(` ${finalMsg}
|
|
4742
|
-
`);
|
|
4743
|
-
}
|
|
4744
|
-
};
|
|
4745
|
-
}
|
|
4746
|
-
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4747
|
-
let i = 0;
|
|
4748
|
-
const interval = setInterval(() => {
|
|
4749
|
-
process.stdout.write(`\r${c.cyan}${frames[i++ % frames.length]}${c.reset} ${label} `);
|
|
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
|
-
}
|
|
5141
|
+
// src/lib/cli-version.ts
|
|
5142
|
+
var CLI_VERSION = "0.2.0";
|
|
4759
5143
|
|
|
4760
5144
|
// src/commands/login.ts
|
|
4761
5145
|
async function openBrowser(url) {
|
|
@@ -4768,12 +5152,12 @@ async function openBrowser(url) {
|
|
|
4768
5152
|
} catch {
|
|
4769
5153
|
}
|
|
4770
5154
|
try {
|
|
4771
|
-
const { execFile } = await import("node:child_process");
|
|
4772
|
-
const { promisify } = await import("node:util");
|
|
4773
|
-
const
|
|
5155
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
5156
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5157
|
+
const exec2 = promisify2(execFile2);
|
|
4774
5158
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
4775
5159
|
const args = process.platform === "win32" ? ["/c", "start", url] : [url];
|
|
4776
|
-
await
|
|
5160
|
+
await exec2(cmd, args).catch(() => null);
|
|
4777
5161
|
} catch {
|
|
4778
5162
|
}
|
|
4779
5163
|
}
|
|
@@ -4789,7 +5173,12 @@ async function copyToClipboard(text) {
|
|
|
4789
5173
|
return false;
|
|
4790
5174
|
}
|
|
4791
5175
|
async function loginCommand(opts) {
|
|
4792
|
-
const
|
|
5176
|
+
const deviceId = ensureDeviceId();
|
|
5177
|
+
const client = new ApiClient({
|
|
5178
|
+
apiUrl: opts.apiUrl,
|
|
5179
|
+
deviceId,
|
|
5180
|
+
cliVersion: CLI_VERSION
|
|
5181
|
+
});
|
|
4793
5182
|
info("Authenticating with Token Rats\u2026");
|
|
4794
5183
|
let exchange;
|
|
4795
5184
|
try {
|
|
@@ -4837,7 +5226,16 @@ async function loginCommand(opts) {
|
|
|
4837
5226
|
process.exit(1);
|
|
4838
5227
|
}
|
|
4839
5228
|
saveToken(token);
|
|
5229
|
+
clearDisconnected();
|
|
4840
5230
|
success("Logged in! Run `token-rats whoami` to verify.");
|
|
5231
|
+
dim(`Device id: ${deviceId}`);
|
|
5232
|
+
if (opts.noDaemon) {
|
|
5233
|
+
info("Skipping background watcher install (--no-daemon).");
|
|
5234
|
+
info("Run `token-rats sync` manually whenever you want to upload usage.");
|
|
5235
|
+
return;
|
|
5236
|
+
}
|
|
5237
|
+
info("Installing background watcher so usage uploads automatically\u2026");
|
|
5238
|
+
await installDaemonCommand();
|
|
4841
5239
|
}
|
|
4842
5240
|
|
|
4843
5241
|
// src/commands/logout.ts
|
|
@@ -4851,8 +5249,7 @@ function logoutCommand() {
|
|
|
4851
5249
|
}
|
|
4852
5250
|
|
|
4853
5251
|
// src/commands/sync.ts
|
|
4854
|
-
import * as
|
|
4855
|
-
import * as readline from "node:readline/promises";
|
|
5252
|
+
import * as fs4 from "node:fs";
|
|
4856
5253
|
|
|
4857
5254
|
// ../parsers/src/hash.ts
|
|
4858
5255
|
var FNV_PRIME = 16777619;
|
|
@@ -4959,6 +5356,8 @@ function parseClaudeCode(input) {
|
|
|
4959
5356
|
id: `claude-code:${acc.sessionId}`,
|
|
4960
5357
|
source: "claude-code",
|
|
4961
5358
|
provider: "anthropic",
|
|
5359
|
+
client: "claude-code",
|
|
5360
|
+
channel: "cli",
|
|
4962
5361
|
model,
|
|
4963
5362
|
inTokens: acc.inTokens,
|
|
4964
5363
|
outTokens: acc.outTokens,
|
|
@@ -5007,6 +5406,9 @@ function parseCodex(input) {
|
|
|
5007
5406
|
currentSessionId = id;
|
|
5008
5407
|
const metaTs = parseTimestamp(payload.timestamp) || ts;
|
|
5009
5408
|
const acc2 = upsert(sessions, id);
|
|
5409
|
+
const metaSource = typeof payload.source === "string" ? payload.source : null;
|
|
5410
|
+
acc2.channel = metaSource === "cli" ? "cli" : metaSource === "api" ? "api" : "unknown";
|
|
5411
|
+
acc2.client = acc2.channel === "cli" ? "codex-cli" : "codex";
|
|
5010
5412
|
if (metaTs > 0 && (acc2.startedAt === 0 || metaTs < acc2.startedAt)) {
|
|
5011
5413
|
acc2.startedAt = metaTs;
|
|
5012
5414
|
}
|
|
@@ -5051,6 +5453,8 @@ function parseCodex(input) {
|
|
|
5051
5453
|
id: `codex:${acc.sessionId}`,
|
|
5052
5454
|
source: "codex",
|
|
5053
5455
|
provider: "openai",
|
|
5456
|
+
client: acc.client || "codex-cli",
|
|
5457
|
+
channel: acc.channel,
|
|
5054
5458
|
model,
|
|
5055
5459
|
inTokens: acc.inTokens,
|
|
5056
5460
|
outTokens: acc.outTokens,
|
|
@@ -5075,7 +5479,9 @@ function upsert(map, sessionId) {
|
|
|
5075
5479
|
outTokens: 0,
|
|
5076
5480
|
cacheReadTokens: 0,
|
|
5077
5481
|
reasoningTokens: 0,
|
|
5078
|
-
model: ""
|
|
5482
|
+
model: "",
|
|
5483
|
+
client: "codex-cli",
|
|
5484
|
+
channel: "unknown"
|
|
5079
5485
|
};
|
|
5080
5486
|
map.set(sessionId, acc);
|
|
5081
5487
|
}
|
|
@@ -5132,6 +5538,8 @@ function parseCursor(input) {
|
|
|
5132
5538
|
id: `cursor:${id}`,
|
|
5133
5539
|
source: "cursor",
|
|
5134
5540
|
provider: "cursor",
|
|
5541
|
+
client: "cursor",
|
|
5542
|
+
channel: "ide",
|
|
5135
5543
|
model,
|
|
5136
5544
|
inTokens,
|
|
5137
5545
|
outTokens,
|
|
@@ -5145,31 +5553,31 @@ function parseCursor(input) {
|
|
|
5145
5553
|
}
|
|
5146
5554
|
|
|
5147
5555
|
// src/lib/cursor-extract.ts
|
|
5148
|
-
import { existsSync, readdirSync
|
|
5556
|
+
import { existsSync as existsSync3, readdirSync } from "node:fs";
|
|
5149
5557
|
import { readFile } from "node:fs/promises";
|
|
5150
|
-
import { homedir as
|
|
5151
|
-
import { join as
|
|
5558
|
+
import { homedir as homedir3 } from "node:os";
|
|
5559
|
+
import { join as join3 } from "node:path";
|
|
5152
5560
|
function cursorWorkspaceStorageDir() {
|
|
5153
|
-
const home =
|
|
5154
|
-
const rel =
|
|
5561
|
+
const home = homedir3();
|
|
5562
|
+
const rel = join3("Cursor", "User", "workspaceStorage");
|
|
5155
5563
|
if (process.platform === "darwin") {
|
|
5156
|
-
return
|
|
5564
|
+
return join3(home, "Library", "Application Support", rel);
|
|
5157
5565
|
}
|
|
5158
5566
|
if (process.platform === "win32") {
|
|
5159
|
-
const appData = process.env.APPDATA ??
|
|
5160
|
-
return
|
|
5567
|
+
const appData = process.env.APPDATA ?? join3(home, "AppData", "Roaming");
|
|
5568
|
+
return join3(appData, rel);
|
|
5161
5569
|
}
|
|
5162
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
5163
|
-
return
|
|
5570
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join3(home, ".config");
|
|
5571
|
+
return join3(xdgConfig, rel);
|
|
5164
5572
|
}
|
|
5165
5573
|
function discoverWorkspaceDbs() {
|
|
5166
5574
|
const root = cursorWorkspaceStorageDir();
|
|
5167
|
-
if (!
|
|
5575
|
+
if (!existsSync3(root)) return [];
|
|
5168
5576
|
const out = [];
|
|
5169
5577
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
5170
5578
|
if (!entry.isDirectory()) continue;
|
|
5171
|
-
const candidate =
|
|
5172
|
-
if (
|
|
5579
|
+
const candidate = join3(root, entry.name, "state.vscdb");
|
|
5580
|
+
if (existsSync3(candidate)) out.push(candidate);
|
|
5173
5581
|
}
|
|
5174
5582
|
return out;
|
|
5175
5583
|
}
|
|
@@ -5298,373 +5706,17 @@ async function extractCursorGenerations() {
|
|
|
5298
5706
|
}
|
|
5299
5707
|
return { rows, dbCount: dbPaths.length, skipped: null };
|
|
5300
5708
|
}
|
|
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
5709
|
|
|
5332
|
-
// src/lib/
|
|
5333
|
-
import { spawnSync } from "node:child_process";
|
|
5710
|
+
// src/lib/discover.ts
|
|
5334
5711
|
import * as fs3 from "node:fs";
|
|
5335
5712
|
import * as os3 from "node:os";
|
|
5336
5713
|
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
5714
|
function findJsonlFiles(dir) {
|
|
5663
5715
|
const results = [];
|
|
5664
|
-
if (!
|
|
5665
|
-
const entries =
|
|
5716
|
+
if (!fs3.existsSync(dir)) return results;
|
|
5717
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
5666
5718
|
for (const entry of entries) {
|
|
5667
|
-
const full =
|
|
5719
|
+
const full = path3.join(dir, entry.name);
|
|
5668
5720
|
if (entry.isDirectory()) {
|
|
5669
5721
|
results.push(...findJsonlFiles(full));
|
|
5670
5722
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -5674,32 +5726,32 @@ function findJsonlFiles(dir) {
|
|
|
5674
5726
|
return results;
|
|
5675
5727
|
}
|
|
5676
5728
|
function claudeCodeProjectsDir() {
|
|
5677
|
-
const home =
|
|
5729
|
+
const home = os3.homedir();
|
|
5678
5730
|
if (process.platform === "win32") {
|
|
5679
5731
|
const profile = process.env.USERPROFILE ?? home;
|
|
5680
|
-
return
|
|
5732
|
+
return path3.join(profile, ".claude", "projects");
|
|
5681
5733
|
}
|
|
5682
|
-
return
|
|
5734
|
+
return path3.join(home, ".claude", "projects");
|
|
5683
5735
|
}
|
|
5684
5736
|
function discoverClaudeCodeFiles() {
|
|
5685
5737
|
const dir = claudeCodeProjectsDir();
|
|
5686
5738
|
return findJsonlFiles(dir);
|
|
5687
5739
|
}
|
|
5688
5740
|
function codexSessionsDirs() {
|
|
5689
|
-
const home =
|
|
5741
|
+
const home = os3.homedir();
|
|
5690
5742
|
const candidates = [];
|
|
5691
5743
|
if (process.platform === "win32") {
|
|
5692
5744
|
const profile = process.env.USERPROFILE ?? home;
|
|
5693
|
-
candidates.push(
|
|
5694
|
-
const appData = process.env.APPDATA ??
|
|
5695
|
-
candidates.push(
|
|
5745
|
+
candidates.push(path3.join(profile, ".codex", "sessions"));
|
|
5746
|
+
const appData = process.env.APPDATA ?? path3.join(profile, "AppData", "Roaming");
|
|
5747
|
+
candidates.push(path3.join(appData, "Codex", "sessions"));
|
|
5696
5748
|
} else {
|
|
5697
|
-
candidates.push(
|
|
5698
|
-
const snapRoot =
|
|
5699
|
-
if (
|
|
5700
|
-
for (const entry of
|
|
5749
|
+
candidates.push(path3.join(home, ".codex", "sessions"));
|
|
5750
|
+
const snapRoot = path3.join(home, "snap", "codex");
|
|
5751
|
+
if (fs3.existsSync(snapRoot)) {
|
|
5752
|
+
for (const entry of fs3.readdirSync(snapRoot, { withFileTypes: true })) {
|
|
5701
5753
|
if (entry.isDirectory()) {
|
|
5702
|
-
candidates.push(
|
|
5754
|
+
candidates.push(path3.join(snapRoot, entry.name, "sessions"));
|
|
5703
5755
|
}
|
|
5704
5756
|
}
|
|
5705
5757
|
}
|
|
@@ -5708,7 +5760,7 @@ function codexSessionsDirs() {
|
|
|
5708
5760
|
const dirs = [];
|
|
5709
5761
|
for (const c2 of candidates) {
|
|
5710
5762
|
try {
|
|
5711
|
-
const real =
|
|
5763
|
+
const real = fs3.existsSync(c2) ? fs3.realpathSync(c2) : null;
|
|
5712
5764
|
if (real && !seen2.has(real)) {
|
|
5713
5765
|
seen2.add(real);
|
|
5714
5766
|
dirs.push(c2);
|
|
@@ -5726,9 +5778,6 @@ function discoverCodexFiles() {
|
|
|
5726
5778
|
return out;
|
|
5727
5779
|
}
|
|
5728
5780
|
|
|
5729
|
-
// src/lib/version.ts
|
|
5730
|
-
var CLI_VERSION = "0.1.0";
|
|
5731
|
-
|
|
5732
5781
|
// src/commands/sync.ts
|
|
5733
5782
|
var BATCH_SIZE = 500;
|
|
5734
5783
|
function chunk(arr, size) {
|
|
@@ -5772,7 +5821,12 @@ async function syncCommand(opts) {
|
|
|
5772
5821
|
error("Not logged in. Run `token-rats login` first.");
|
|
5773
5822
|
process.exit(1);
|
|
5774
5823
|
}
|
|
5775
|
-
const client = opts.dryRun ? null : new ApiClient({
|
|
5824
|
+
const client = opts.dryRun ? null : new ApiClient({
|
|
5825
|
+
apiUrl: opts.apiUrl,
|
|
5826
|
+
token: token ?? void 0,
|
|
5827
|
+
deviceId: ensureDeviceId(),
|
|
5828
|
+
cliVersion: CLI_VERSION
|
|
5829
|
+
});
|
|
5776
5830
|
const claudeFiles = discoverClaudeCodeFiles();
|
|
5777
5831
|
if (opts.verbose) {
|
|
5778
5832
|
info(`Found ${claudeFiles.length} Claude Code file(s) in ~/.claude/projects/`);
|
|
@@ -5782,7 +5836,7 @@ async function syncCommand(opts) {
|
|
|
5782
5836
|
for (const file of claudeFiles) {
|
|
5783
5837
|
let text;
|
|
5784
5838
|
try {
|
|
5785
|
-
text =
|
|
5839
|
+
text = fs4.readFileSync(file, "utf8");
|
|
5786
5840
|
} catch {
|
|
5787
5841
|
if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
|
|
5788
5842
|
continue;
|
|
@@ -5804,7 +5858,7 @@ async function syncCommand(opts) {
|
|
|
5804
5858
|
for (const file of codexFiles) {
|
|
5805
5859
|
let text;
|
|
5806
5860
|
try {
|
|
5807
|
-
text =
|
|
5861
|
+
text = fs4.readFileSync(file, "utf8");
|
|
5808
5862
|
} catch {
|
|
5809
5863
|
if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
|
|
5810
5864
|
continue;
|
|
@@ -5890,6 +5944,14 @@ async function syncCommand(opts) {
|
|
|
5890
5944
|
totalDuplicates += res.duplicates;
|
|
5891
5945
|
} catch (err) {
|
|
5892
5946
|
spin.stop();
|
|
5947
|
+
if (err instanceof DeviceRevokedError) {
|
|
5948
|
+
markDisconnected();
|
|
5949
|
+
deleteToken();
|
|
5950
|
+
error(
|
|
5951
|
+
"This device was disconnected from the Token Rats web UI. Run `token-rats login` to reconnect."
|
|
5952
|
+
);
|
|
5953
|
+
process.exit(1);
|
|
5954
|
+
}
|
|
5893
5955
|
if (err instanceof ApiError2 && err.status === 401) {
|
|
5894
5956
|
error("Session expired. Run `token-rats login` to re-authenticate.");
|
|
5895
5957
|
process.exit(1);
|
|
@@ -5904,459 +5966,282 @@ async function syncCommand(opts) {
|
|
|
5904
5966
|
success(
|
|
5905
5967
|
`Synced ${allSessions.length} sessions (${totalAccepted} new, ${totalDuplicates} already on server) from ${sourceStr}`
|
|
5906
5968
|
);
|
|
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
|
-
try {
|
|
5945
|
-
const r = await installDaemon(CLI_VERSION);
|
|
5946
|
-
success(`Background sync enabled (${r.method}). Logs: ${r.logDir}/watch.log`);
|
|
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`.");
|
|
5961
|
-
}
|
|
5962
5969
|
}
|
|
5963
5970
|
|
|
5964
5971
|
// src/commands/watch.ts
|
|
5965
|
-
import * as
|
|
5966
|
-
import * as
|
|
5967
|
-
|
|
5968
|
-
var
|
|
5969
|
-
var REAUTH_BACKOFF_MS = 10 * 60 * 1e3;
|
|
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
|
-
}
|
|
5972
|
+
import * as fs5 from "node:fs";
|
|
5973
|
+
import * as os4 from "node:os";
|
|
5974
|
+
import * as path4 from "node:path";
|
|
5975
|
+
var HEARTBEAT_MS = 6e4;
|
|
5995
5976
|
var seen = /* @__PURE__ */ new Set();
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
log.debug("Skipping upload \u2014 in reauth backoff", { records: records.length });
|
|
6008
|
-
return;
|
|
5977
|
+
function stateFilePath() {
|
|
5978
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? path4.join(os4.homedir(), ".config");
|
|
5979
|
+
return path4.join(xdgConfig, "token-rats", "watch-state.json");
|
|
5980
|
+
}
|
|
5981
|
+
function loadWatchState() {
|
|
5982
|
+
try {
|
|
5983
|
+
const raw = fs5.readFileSync(stateFilePath(), "utf8");
|
|
5984
|
+
const parsed = JSON.parse(raw);
|
|
5985
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
5986
|
+
} catch {
|
|
5987
|
+
return {};
|
|
6009
5988
|
}
|
|
5989
|
+
}
|
|
5990
|
+
function saveWatchState(state) {
|
|
6010
5991
|
try {
|
|
6011
|
-
const
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
log.info("upload", { accepted: res.accepted, duplicates: res.duplicates });
|
|
5992
|
+
const file = stateFilePath();
|
|
5993
|
+
fs5.mkdirSync(path4.dirname(file), { recursive: true });
|
|
5994
|
+
fs5.writeFileSync(file, JSON.stringify(state), { mode: 384 });
|
|
6015
5995
|
} catch (err) {
|
|
6016
|
-
|
|
6017
|
-
|
|
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);
|
|
5996
|
+
if (err instanceof Error) {
|
|
5997
|
+
console.warn(`watch-state write failed: ${err.message}`);
|
|
6027
5998
|
}
|
|
6028
|
-
log.warn(`Upload failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6029
5999
|
}
|
|
6030
6000
|
}
|
|
6031
|
-
function
|
|
6032
|
-
let text;
|
|
6001
|
+
function statFile(p) {
|
|
6033
6002
|
try {
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
log.debug(`Could not read ${filePath} \u2014 skipping`);
|
|
6037
|
-
return [];
|
|
6038
|
-
}
|
|
6039
|
-
let records;
|
|
6040
|
-
try {
|
|
6041
|
-
records = parseClaudeCode(text);
|
|
6003
|
+
const s = fs5.statSync(p);
|
|
6004
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
6042
6005
|
} catch {
|
|
6043
|
-
|
|
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)`);
|
|
6006
|
+
return null;
|
|
6055
6007
|
}
|
|
6056
|
-
return fresh;
|
|
6057
6008
|
}
|
|
6058
|
-
function
|
|
6009
|
+
function parseAndFilter(filePath, verbose) {
|
|
6059
6010
|
let text;
|
|
6060
6011
|
try {
|
|
6061
|
-
text =
|
|
6012
|
+
text = fs5.readFileSync(filePath, "utf8");
|
|
6062
6013
|
} catch {
|
|
6014
|
+
if (verbose) warn(`Could not read ${filePath} \u2014 skipping`);
|
|
6063
6015
|
return [];
|
|
6064
6016
|
}
|
|
6065
6017
|
let records;
|
|
6066
6018
|
try {
|
|
6067
|
-
records =
|
|
6019
|
+
records = parseClaudeCode(text);
|
|
6068
6020
|
} catch {
|
|
6021
|
+
if (verbose) warn(`Failed to parse ${filePath} \u2014 skipping`);
|
|
6069
6022
|
return [];
|
|
6070
6023
|
}
|
|
6071
6024
|
const fresh = [];
|
|
6072
6025
|
for (const r of records) {
|
|
6073
|
-
if (seen.has(r.dedupeKey))
|
|
6074
|
-
|
|
6075
|
-
|
|
6026
|
+
if (!seen.has(r.dedupeKey)) {
|
|
6027
|
+
seen.add(r.dedupeKey);
|
|
6028
|
+
fresh.push(r);
|
|
6029
|
+
}
|
|
6076
6030
|
}
|
|
6077
|
-
if (fresh.length > 0) {
|
|
6078
|
-
|
|
6079
|
-
log.debug(`${filePath}: ${fresh.length} new Codex session(s)`);
|
|
6031
|
+
if (verbose && fresh.length > 0) {
|
|
6032
|
+
dim(` ${filePath}: ${fresh.length} new session(s)`);
|
|
6080
6033
|
}
|
|
6081
6034
|
return fresh;
|
|
6082
6035
|
}
|
|
6036
|
+
async function upload(client, records, verbose) {
|
|
6037
|
+
if (records.length === 0) return;
|
|
6038
|
+
try {
|
|
6039
|
+
const res = await client.uploadSessions(records);
|
|
6040
|
+
if (verbose) {
|
|
6041
|
+
dim(` Uploaded ${res.accepted} new, ${res.duplicates} duplicate(s)`);
|
|
6042
|
+
} else {
|
|
6043
|
+
success(`Uploaded ${res.accepted} session(s)`);
|
|
6044
|
+
}
|
|
6045
|
+
} catch (err) {
|
|
6046
|
+
if (err instanceof DeviceRevokedError) {
|
|
6047
|
+
markDisconnected();
|
|
6048
|
+
deleteToken();
|
|
6049
|
+
error(
|
|
6050
|
+
"This device was disconnected from the Token Rats web UI. Daemon will exit; re-run `token-rats login` to reconnect."
|
|
6051
|
+
);
|
|
6052
|
+
process.exit(0);
|
|
6053
|
+
}
|
|
6054
|
+
if (err instanceof ApiError2 && err.status === 401) {
|
|
6055
|
+
error("Session expired. Run `token-rats login` to re-authenticate.");
|
|
6056
|
+
process.exit(1);
|
|
6057
|
+
}
|
|
6058
|
+
warn(`Upload failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6083
6061
|
function makeDebounced(fn, ms) {
|
|
6084
6062
|
const timers = /* @__PURE__ */ new Map();
|
|
6085
|
-
return (
|
|
6086
|
-
const existing = timers.get(
|
|
6063
|
+
return (path5) => {
|
|
6064
|
+
const existing = timers.get(path5);
|
|
6087
6065
|
if (existing) clearTimeout(existing);
|
|
6088
6066
|
timers.set(
|
|
6089
|
-
|
|
6067
|
+
path5,
|
|
6090
6068
|
setTimeout(() => {
|
|
6091
|
-
timers.delete(
|
|
6092
|
-
fn(
|
|
6069
|
+
timers.delete(path5);
|
|
6070
|
+
fn(path5);
|
|
6093
6071
|
}, ms)
|
|
6094
6072
|
);
|
|
6095
6073
|
};
|
|
6096
6074
|
}
|
|
6097
6075
|
function findJsonlFiles2(dir) {
|
|
6098
|
-
const
|
|
6099
|
-
if (!
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6076
|
+
const results = [];
|
|
6077
|
+
if (!fs5.existsSync(dir)) return results;
|
|
6078
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
6079
|
+
for (const entry of entries) {
|
|
6080
|
+
const full = `${dir}/${entry.name}`;
|
|
6081
|
+
if (entry.isDirectory()) {
|
|
6082
|
+
results.push(...findJsonlFiles2(full));
|
|
6083
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
6084
|
+
results.push(full);
|
|
6085
|
+
}
|
|
6104
6086
|
}
|
|
6105
|
-
return
|
|
6087
|
+
return results;
|
|
6106
6088
|
}
|
|
6107
|
-
async function
|
|
6108
|
-
if (
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6089
|
+
async function watchCommand(opts) {
|
|
6090
|
+
if (isDisconnected()) {
|
|
6091
|
+
error(
|
|
6092
|
+
"This device was disconnected from the Token Rats web UI. Run `token-rats login` to reconnect."
|
|
6093
|
+
);
|
|
6094
|
+
process.exit(0);
|
|
6095
|
+
}
|
|
6096
|
+
const token = loadToken();
|
|
6097
|
+
if (!token) {
|
|
6098
|
+
error("Not logged in. Run `token-rats login` first.");
|
|
6099
|
+
process.exit(1);
|
|
6112
6100
|
}
|
|
6113
|
-
const
|
|
6114
|
-
|
|
6101
|
+
const client = new ApiClient({
|
|
6102
|
+
apiUrl: opts.apiUrl,
|
|
6103
|
+
token,
|
|
6104
|
+
deviceId: ensureDeviceId(),
|
|
6105
|
+
cliVersion: CLI_VERSION
|
|
6106
|
+
});
|
|
6107
|
+
const debounceMs = opts.interval ?? 2e3;
|
|
6108
|
+
const dir = claudeCodeProjectsDir();
|
|
6109
|
+
if (!fs5.existsSync(dir)) {
|
|
6110
|
+
warn(`Claude Code projects directory not found: ${dir}`);
|
|
6111
|
+
warn("No files to watch. Exiting.");
|
|
6112
|
+
process.exit(0);
|
|
6113
|
+
}
|
|
6114
|
+
info(`Watching ${dir} (debounce: ${debounceMs}ms)`);
|
|
6115
|
+
info("Press Ctrl-C to stop.\n");
|
|
6116
|
+
const persistedState = loadWatchState();
|
|
6117
|
+
const liveState = /* @__PURE__ */ new Map();
|
|
6118
|
+
const initialFiles = findJsonlFiles2(dir);
|
|
6119
|
+
for (const f of initialFiles) {
|
|
6120
|
+
const cur = statFile(f);
|
|
6121
|
+
if (!cur) continue;
|
|
6122
|
+
liveState.set(f, cur);
|
|
6123
|
+
const prev = persistedState[f];
|
|
6124
|
+
if (prev && cur.size < prev.size) {
|
|
6125
|
+
warn(`Rotation detected on ${f}: size shrank ${prev.size} \u2192 ${cur.size}.`);
|
|
6126
|
+
}
|
|
6127
|
+
parseAndFilter(f, false);
|
|
6128
|
+
}
|
|
6129
|
+
if (opts.verbose) {
|
|
6130
|
+
dim(`Initial snapshot: ${seen.size} session(s) in ${initialFiles.length} file(s)`);
|
|
6131
|
+
}
|
|
6132
|
+
saveWatchState(Object.fromEntries(liveState));
|
|
6133
|
+
const onChanged = makeDebounced(async (filePath) => {
|
|
6134
|
+
if (!filePath.endsWith(".jsonl")) return;
|
|
6135
|
+
const cur = statFile(filePath);
|
|
6136
|
+
if (cur) {
|
|
6137
|
+
const prev = liveState.get(filePath);
|
|
6138
|
+
if (prev && cur.size < prev.size) {
|
|
6139
|
+
warn(`Rotation detected on ${filePath}: size shrank ${prev.size} \u2192 ${cur.size}.`);
|
|
6140
|
+
}
|
|
6141
|
+
liveState.set(filePath, cur);
|
|
6142
|
+
saveWatchState(Object.fromEntries(liveState));
|
|
6143
|
+
}
|
|
6144
|
+
const fresh = parseAndFilter(filePath, opts.verbose ?? false);
|
|
6145
|
+
if (fresh.length > 0) {
|
|
6146
|
+
await upload(client, fresh, opts.verbose ?? false);
|
|
6147
|
+
}
|
|
6115
6148
|
}, debounceMs);
|
|
6149
|
+
const heartbeatTimer = setInterval(async () => {
|
|
6150
|
+
try {
|
|
6151
|
+
await client.heartbeat();
|
|
6152
|
+
} catch (err) {
|
|
6153
|
+
if (err instanceof DeviceRevokedError) {
|
|
6154
|
+
markDisconnected();
|
|
6155
|
+
deleteToken();
|
|
6156
|
+
error(
|
|
6157
|
+
"This device was disconnected from the Token Rats web UI. Daemon exiting; re-run `token-rats login` to reconnect."
|
|
6158
|
+
);
|
|
6159
|
+
cleanup?.();
|
|
6160
|
+
clearInterval(heartbeatTimer);
|
|
6161
|
+
process.exit(0);
|
|
6162
|
+
}
|
|
6163
|
+
if (opts.verbose) {
|
|
6164
|
+
dim(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
}, HEARTBEAT_MS);
|
|
6168
|
+
client.heartbeat().catch(() => {
|
|
6169
|
+
});
|
|
6170
|
+
let cleanup = null;
|
|
6116
6171
|
try {
|
|
6117
|
-
const
|
|
6118
|
-
const
|
|
6119
|
-
const watcher = chokidarMod.watch(`${dir}/**/*.jsonl`, {
|
|
6172
|
+
const chokidar = await new Function("m", "return import(m)")("chokidar");
|
|
6173
|
+
const watcher = chokidar.watch(`${dir}/**/*.jsonl`, {
|
|
6120
6174
|
ignoreInitial: true,
|
|
6121
6175
|
persistent: true,
|
|
6122
6176
|
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
6123
6177
|
});
|
|
6124
|
-
watcher.on("add", (p) =>
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
if (typeof p === "string") debouncedChange(p);
|
|
6129
|
-
});
|
|
6130
|
-
return () => {
|
|
6131
|
-
watcher.close();
|
|
6132
|
-
};
|
|
6178
|
+
watcher.on("add", (p) => onChanged(p));
|
|
6179
|
+
watcher.on("change", (p) => onChanged(p));
|
|
6180
|
+
cleanup = () => watcher.close();
|
|
6181
|
+
if (opts.verbose) dim("Using chokidar for file watching");
|
|
6133
6182
|
} catch {
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
try {
|
|
6139
|
-
lastMtimes.set(f, fs8.statSync(f).mtimeMs);
|
|
6140
|
-
} catch {
|
|
6141
|
-
}
|
|
6142
|
-
}
|
|
6143
|
-
const handle = setInterval(() => {
|
|
6144
|
-
const current = findJsonlFiles2(dir);
|
|
6145
|
-
for (const f of current) {
|
|
6183
|
+
if (opts.verbose) dim("chokidar not available; using Node built-in fs.watch");
|
|
6184
|
+
if (process.platform === "linux") {
|
|
6185
|
+
let lastMtimes = /* @__PURE__ */ new Map();
|
|
6186
|
+
for (const f of initialFiles) {
|
|
6146
6187
|
try {
|
|
6147
|
-
|
|
6148
|
-
const prev = lastMtimes.get(f) ?? 0;
|
|
6149
|
-
if (mtime > prev) {
|
|
6150
|
-
lastMtimes.set(f, mtime);
|
|
6151
|
-
debouncedChange(f);
|
|
6152
|
-
}
|
|
6188
|
+
lastMtimes.set(f, fs5.statSync(f).mtimeMs);
|
|
6153
6189
|
} catch {
|
|
6154
6190
|
}
|
|
6155
6191
|
}
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6192
|
+
const pollInterval = setInterval(() => {
|
|
6193
|
+
const current = findJsonlFiles2(dir);
|
|
6194
|
+
for (const f of current) {
|
|
6195
|
+
try {
|
|
6196
|
+
const mtime = fs5.statSync(f).mtimeMs;
|
|
6197
|
+
const prev = lastMtimes.get(f) ?? 0;
|
|
6198
|
+
if (mtime > prev) {
|
|
6199
|
+
lastMtimes.set(f, mtime);
|
|
6200
|
+
onChanged(f);
|
|
6201
|
+
}
|
|
6202
|
+
} catch {
|
|
6203
|
+
}
|
|
6160
6204
|
}
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
const controller = new AbortController();
|
|
6167
|
-
(async () => {
|
|
6168
|
-
try {
|
|
6169
|
-
const { watch } = await import("node:fs/promises");
|
|
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));
|
|
6205
|
+
for (const f of current) {
|
|
6206
|
+
if (!lastMtimes.has(f)) {
|
|
6207
|
+
lastMtimes.set(f, Date.now());
|
|
6208
|
+
onChanged(f);
|
|
6209
|
+
}
|
|
6174
6210
|
}
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
}
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
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;
|
|
6211
|
+
lastMtimes = new Map(current.map((f) => [f, lastMtimes.get(f) ?? 0]));
|
|
6212
|
+
}, debounceMs);
|
|
6213
|
+
cleanup = () => clearInterval(pollInterval);
|
|
6214
|
+
} else {
|
|
6215
|
+
const controller = new AbortController();
|
|
6216
|
+
(async () => {
|
|
6217
|
+
try {
|
|
6218
|
+
const { watch } = await import("node:fs/promises");
|
|
6219
|
+
const watcher = watch(dir, { recursive: true, signal: controller.signal });
|
|
6220
|
+
for await (const event of watcher) {
|
|
6221
|
+
const filename = event.filename;
|
|
6222
|
+
if (filename?.endsWith(".jsonl")) {
|
|
6223
|
+
const fullPath = `${dir}/${filename}`;
|
|
6224
|
+
onChanged(fullPath);
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
} catch (err) {
|
|
6228
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
6229
|
+
warn(`Watcher error: ${err.message}`);
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
})();
|
|
6233
|
+
cleanup = () => controller.abort();
|
|
6225
6234
|
}
|
|
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
6235
|
}
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
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();
|
|
6236
|
+
function shutdown() {
|
|
6237
|
+
info("Shutting down\u2026");
|
|
6238
|
+
clearInterval(heartbeatTimer);
|
|
6239
|
+
if (cleanup) cleanup();
|
|
6299
6240
|
process.exit(0);
|
|
6300
|
-
}
|
|
6241
|
+
}
|
|
6301
6242
|
process.on("SIGINT", shutdown);
|
|
6302
6243
|
process.on("SIGTERM", shutdown);
|
|
6303
6244
|
}
|
|
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
|
-
);
|
|
6322
|
-
}
|
|
6323
|
-
} catch (err) {
|
|
6324
|
-
error(`Install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6325
|
-
process.exit(1);
|
|
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)"}`);
|
|
6352
|
-
}
|
|
6353
|
-
}
|
|
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
6245
|
|
|
6361
6246
|
// src/commands/whoami.ts
|
|
6362
6247
|
async function whoamiCommand(opts) {
|
|
@@ -6365,11 +6250,25 @@ async function whoamiCommand(opts) {
|
|
|
6365
6250
|
error("Not logged in. Run `token-rats login` first.");
|
|
6366
6251
|
process.exit(1);
|
|
6367
6252
|
}
|
|
6368
|
-
const client = new ApiClient({
|
|
6253
|
+
const client = new ApiClient({
|
|
6254
|
+
apiUrl: opts.apiUrl,
|
|
6255
|
+
token,
|
|
6256
|
+
deviceId: ensureDeviceId(),
|
|
6257
|
+
cliVersion: CLI_VERSION
|
|
6258
|
+
});
|
|
6369
6259
|
try {
|
|
6370
6260
|
const res = await client.getMe();
|
|
6371
6261
|
info(`Signed in as \x1B[1m@${res.user.handle}\x1B[0m`);
|
|
6262
|
+
info(`Device id: ${ensureDeviceId()}`);
|
|
6372
6263
|
} catch (err) {
|
|
6264
|
+
if (err instanceof DeviceRevokedError) {
|
|
6265
|
+
markDisconnected();
|
|
6266
|
+
deleteToken();
|
|
6267
|
+
error(
|
|
6268
|
+
"This device was disconnected from the Token Rats web UI. Run `token-rats login` to reconnect."
|
|
6269
|
+
);
|
|
6270
|
+
process.exit(1);
|
|
6271
|
+
}
|
|
6373
6272
|
if (err instanceof ApiError2 && err.status === 401) {
|
|
6374
6273
|
error("Your session has expired. Run `token-rats login` to re-authenticate.");
|
|
6375
6274
|
process.exit(1);
|
|
@@ -6391,19 +6290,17 @@ function printHelp() {
|
|
|
6391
6290
|
token-rats <command> [flags]
|
|
6392
6291
|
|
|
6393
6292
|
\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
|
|
6293
|
+
login Authenticate with Token Rats (opens browser); installs the background watcher by default
|
|
6294
|
+
sync Read local Claude Code + Cursor logs and upload counts
|
|
6295
|
+
watch Watch logs in real-time; upload new sessions as they appear
|
|
6296
|
+
whoami Show the currently signed-in account + device id
|
|
6297
|
+
logout Clear your stored credentials
|
|
6298
|
+
install-daemon Install the background watcher (runs at logon)
|
|
6299
|
+
uninstall-daemon Remove the background watcher
|
|
6300
|
+
daemon-status Show whether the background watcher is running
|
|
6301
|
+
install-cursor Install better-sqlite3 globally for faster Cursor reads
|
|
6302
|
+
(sql.js works out of the box \u2014 this is opt-in speed-up)
|
|
6303
|
+
help Show this help message
|
|
6407
6304
|
|
|
6408
6305
|
\x1B[1mFlags (all commands):\x1B[0m
|
|
6409
6306
|
--api-url <url> Override API URL (default: https://api.tokenrats.com)
|
|
@@ -6411,15 +6308,8 @@ function printHelp() {
|
|
|
6411
6308
|
\x1B[1mFlags (sync only):\x1B[0m
|
|
6412
6309
|
--dry-run Parse but do not upload; print what would be sent
|
|
6413
6310
|
--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
6311
|
|
|
6417
6312
|
\x1B[1mFlags (watch only):\x1B[0m
|
|
6418
|
-
--install Install the background watcher (launchd / systemd --user)
|
|
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
6313
|
--interval <ms> Debounce window in ms before uploading (default: 2000)
|
|
6424
6314
|
--verbose Print file change events and upload detail
|
|
6425
6315
|
|
|
@@ -6449,10 +6339,6 @@ function parseArgs(argv) {
|
|
|
6449
6339
|
let verbose = false;
|
|
6450
6340
|
let interval;
|
|
6451
6341
|
let noDaemon = false;
|
|
6452
|
-
let daemon = false;
|
|
6453
|
-
let install = false;
|
|
6454
|
-
let uninstall = false;
|
|
6455
|
-
let status = false;
|
|
6456
6342
|
let i = 0;
|
|
6457
6343
|
while (i < argv.length) {
|
|
6458
6344
|
const arg = argv[i];
|
|
@@ -6470,14 +6356,6 @@ function parseArgs(argv) {
|
|
|
6470
6356
|
interval = Number(arg.slice("--interval=".length));
|
|
6471
6357
|
} else if (arg === "--no-daemon") {
|
|
6472
6358
|
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
6359
|
} else if (arg === "--version" || arg === "-V") {
|
|
6482
6360
|
console.log(getVersion());
|
|
6483
6361
|
process.exit(0);
|
|
@@ -6490,56 +6368,24 @@ function parseArgs(argv) {
|
|
|
6490
6368
|
i++;
|
|
6491
6369
|
}
|
|
6492
6370
|
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
|
-
};
|
|
6371
|
+
return { command, apiUrl, dryRun, verbose, interval, noDaemon, rest };
|
|
6506
6372
|
}
|
|
6507
6373
|
async function main() {
|
|
6508
6374
|
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;
|
|
6375
|
+
const { command, apiUrl, dryRun, verbose, interval, noDaemon } = args;
|
|
6521
6376
|
if (!command || command === "help") {
|
|
6522
6377
|
printHelp();
|
|
6523
6378
|
process.exit(0);
|
|
6524
6379
|
}
|
|
6525
6380
|
switch (command) {
|
|
6526
6381
|
case "login":
|
|
6527
|
-
await loginCommand({ apiUrl });
|
|
6382
|
+
await loginCommand({ apiUrl, noDaemon });
|
|
6528
6383
|
break;
|
|
6529
6384
|
case "sync":
|
|
6530
|
-
await syncCommand({ apiUrl, dryRun, verbose
|
|
6385
|
+
await syncCommand({ apiUrl, dryRun, verbose });
|
|
6531
6386
|
break;
|
|
6532
6387
|
case "watch":
|
|
6533
|
-
await watchCommand({
|
|
6534
|
-
apiUrl,
|
|
6535
|
-
verbose,
|
|
6536
|
-
interval,
|
|
6537
|
-
daemon,
|
|
6538
|
-
install,
|
|
6539
|
-
uninstall,
|
|
6540
|
-
status,
|
|
6541
|
-
version: getVersion()
|
|
6542
|
-
});
|
|
6388
|
+
await watchCommand({ apiUrl, verbose, interval });
|
|
6543
6389
|
break;
|
|
6544
6390
|
case "whoami":
|
|
6545
6391
|
await whoamiCommand({ apiUrl });
|
|
@@ -6550,6 +6396,15 @@ async function main() {
|
|
|
6550
6396
|
case "install-cursor":
|
|
6551
6397
|
await installCursorCommand();
|
|
6552
6398
|
break;
|
|
6399
|
+
case "install-daemon":
|
|
6400
|
+
await installDaemonCommand();
|
|
6401
|
+
break;
|
|
6402
|
+
case "uninstall-daemon":
|
|
6403
|
+
await uninstallDaemonCommand();
|
|
6404
|
+
break;
|
|
6405
|
+
case "daemon-status":
|
|
6406
|
+
await daemonStatusCommand();
|
|
6407
|
+
break;
|
|
6553
6408
|
default:
|
|
6554
6409
|
console.error(`\x1B[31mUnknown command: ${command}\x1B[0m`);
|
|
6555
6410
|
console.error("Run \x1B[1mtoken-rats help\x1B[0m for a list of commands.");
|