token-rats 0.0.4 → 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/dist/index.js +954 -301
- package/package.json +1 -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,12 +4321,30 @@ var z = /* @__PURE__ */ Object.freeze({
|
|
|
3948
4321
|
|
|
3949
4322
|
// ../contracts/src/session.ts
|
|
3950
4323
|
var Source = z.enum(["claude-code", "cursor", "codex"]);
|
|
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"]);
|
|
3951
4333
|
var SessionRecord = z.object({
|
|
3952
4334
|
id: z.string().min(1),
|
|
3953
4335
|
source: Source,
|
|
4336
|
+
provider: Provider.optional(),
|
|
4337
|
+
client: z.string().min(1).max(64).optional(),
|
|
4338
|
+
channel: SessionChannel.optional(),
|
|
3954
4339
|
model: z.string().min(1),
|
|
3955
4340
|
inTokens: z.number().int().nonnegative(),
|
|
3956
4341
|
outTokens: z.number().int().nonnegative(),
|
|
4342
|
+
/** Anthropic cache reads / OpenAI `cached_input_tokens`. Billed cheap-or-free. */
|
|
4343
|
+
cacheReadTokens: z.number().int().nonnegative().optional(),
|
|
4344
|
+
/** Anthropic cache creation tokens. Billed at a premium for first-write only. */
|
|
4345
|
+
cacheWriteTokens: z.number().int().nonnegative().optional(),
|
|
4346
|
+
/** OpenAI reasoning output tokens (o-series / Codex). Billed at output rate. */
|
|
4347
|
+
reasoningTokens: z.number().int().nonnegative().optional(),
|
|
3957
4348
|
costUsdCents: z.number().int().nonnegative(),
|
|
3958
4349
|
startedAt: z.number().int().positive(),
|
|
3959
4350
|
endedAt: z.number().int().positive(),
|
|
@@ -3961,6 +4352,12 @@ var SessionRecord = z.object({
|
|
|
3961
4352
|
});
|
|
3962
4353
|
|
|
3963
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
|
+
});
|
|
3964
4361
|
var User = z.object({
|
|
3965
4362
|
id: z.string(),
|
|
3966
4363
|
handle: z.string(),
|
|
@@ -3968,19 +4365,42 @@ var User = z.object({
|
|
|
3968
4365
|
/** Present only when the caller is the user themselves or the user is public. */
|
|
3969
4366
|
bio: z.string().max(200).nullable().optional(),
|
|
3970
4367
|
twitterHandle: z.string().max(50).nullable().optional(),
|
|
3971
|
-
|
|
4368
|
+
/**
|
|
4369
|
+
* True iff `twitter_handle` came from the OAuth flow (i.e. `twitter_user_id`
|
|
4370
|
+
* is set). Self-only — used by settings UI to distinguish manual legacy
|
|
4371
|
+
* handles from verified ones.
|
|
4372
|
+
*/
|
|
4373
|
+
twitterVerified: z.boolean().optional(),
|
|
4374
|
+
publicProfile: z.boolean().optional(),
|
|
4375
|
+
/**
|
|
4376
|
+
* Primary verified GitHub email, captured at OAuth callback time. Self-only
|
|
4377
|
+
* (other readers never see this field). `null` when GitHub didn't return a
|
|
4378
|
+
* verified email — the user sees a banner asking them to add one.
|
|
4379
|
+
*/
|
|
4380
|
+
email: z.string().email().nullable().optional()
|
|
3972
4381
|
});
|
|
3973
4382
|
var PublicProfileSettings = z.object({
|
|
3974
4383
|
publicProfile: z.boolean().optional(),
|
|
3975
|
-
bio: z.string().max(200).nullable().optional()
|
|
3976
|
-
twitterHandle: z.string().max(50).nullable().optional()
|
|
4384
|
+
bio: z.string().max(200).nullable().optional()
|
|
3977
4385
|
});
|
|
3978
4386
|
var Profile = User.extend({
|
|
3979
4387
|
totals: z.object({
|
|
3980
4388
|
today: z.object({ tokens: z.number().int(), costUsdCents: z.number().int() }),
|
|
3981
4389
|
week: z.object({ tokens: z.number().int(), costUsdCents: z.number().int() }),
|
|
3982
4390
|
allTime: z.object({ tokens: z.number().int(), costUsdCents: z.number().int() })
|
|
3983
|
-
})
|
|
4391
|
+
}),
|
|
4392
|
+
/** Number of users this user has brought in via their referral link. */
|
|
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(),
|
|
4398
|
+
/**
|
|
4399
|
+
* The profile owner's referral code. Self-only — present only when the
|
|
4400
|
+
* caller is viewing their own profile, so the page can render a
|
|
4401
|
+
* copy-able invite link without a separate fetch to /v1/me/referral.
|
|
4402
|
+
*/
|
|
4403
|
+
referralCode: z.string().optional()
|
|
3984
4404
|
});
|
|
3985
4405
|
var AutobiographyStats = z.object({
|
|
3986
4406
|
handle: z.string(),
|
|
@@ -4016,13 +4436,24 @@ var Room = z.object({
|
|
|
4016
4436
|
name: z.string().min(1).max(64),
|
|
4017
4437
|
ownerId: z.string(),
|
|
4018
4438
|
orgId: z.string().nullable(),
|
|
4019
|
-
createdAt: z.number().int().positive()
|
|
4439
|
+
createdAt: z.number().int().positive(),
|
|
4440
|
+
/** v1.2: public country-locked groups. `country` is set iff `isPublic` is true. */
|
|
4441
|
+
isPublic: z.boolean(),
|
|
4442
|
+
/** ISO-3166-1 alpha-2 (`cf-ipcountry` of the creator). Null on private rooms. */
|
|
4443
|
+
country: z.string().min(2).max(2).nullable(),
|
|
4444
|
+
/**
|
|
4445
|
+
* True iff this room is the caller's pinned room. Populated only on
|
|
4446
|
+
* caller-scoped responses (e.g. GET /v1/me/rooms); absent elsewhere.
|
|
4447
|
+
*/
|
|
4448
|
+
isPinned: z.boolean().optional()
|
|
4020
4449
|
});
|
|
4021
4450
|
var RoomMember = z.object({
|
|
4022
4451
|
userId: z.string(),
|
|
4023
4452
|
handle: z.string(),
|
|
4024
4453
|
avatarUrl: z.string().url().nullable(),
|
|
4025
|
-
joinedAt: z.number().int().positive()
|
|
4454
|
+
joinedAt: z.number().int().positive(),
|
|
4455
|
+
/** OAuth-verified X handle, when present. Manual handles are not surfaced. */
|
|
4456
|
+
twitterHandle: z.string().max(50).nullable().optional()
|
|
4026
4457
|
});
|
|
4027
4458
|
|
|
4028
4459
|
// ../contracts/src/leaderboard.ts
|
|
@@ -4039,8 +4470,14 @@ var LeaderboardRow = z.object({
|
|
|
4039
4470
|
tokens: z.number().int().nonnegative(),
|
|
4040
4471
|
costUsdCents: z.number().int().nonnegative(),
|
|
4041
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),
|
|
4042
4475
|
/** Up to 2 dominant sources by token volume, descending. May be empty. */
|
|
4043
|
-
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([])
|
|
4044
4481
|
});
|
|
4045
4482
|
var Leaderboard = z.object({
|
|
4046
4483
|
range: LeaderboardRange,
|
|
@@ -4088,8 +4525,25 @@ var ChallengeWithLeaderboard = Challenge.extend({
|
|
|
4088
4525
|
winnerHandle: z.string().nullable()
|
|
4089
4526
|
});
|
|
4090
4527
|
|
|
4528
|
+
// ../contracts/src/referral.ts
|
|
4529
|
+
var ReferredUser = z.object({
|
|
4530
|
+
handle: z.string(),
|
|
4531
|
+
avatarUrl: z.string().url().nullable(),
|
|
4532
|
+
/** Unix ms — when the referred user signed up. */
|
|
4533
|
+
createdAt: z.number().int()
|
|
4534
|
+
});
|
|
4535
|
+
var ReferralStats = z.object({
|
|
4536
|
+
/** The user's unique referral code (URL-safe, ~8 chars). */
|
|
4537
|
+
code: z.string(),
|
|
4538
|
+
/** Total users referred. */
|
|
4539
|
+
count: z.number().int().nonnegative(),
|
|
4540
|
+
/** Most recent referred users (newest first), capped server-side. */
|
|
4541
|
+
recent: z.array(ReferredUser)
|
|
4542
|
+
});
|
|
4543
|
+
|
|
4091
4544
|
// ../contracts/src/org.ts
|
|
4092
|
-
var OrgPlan = z.enum(["free", "pro"]);
|
|
4545
|
+
var OrgPlan = z.enum(["free", "student", "pro"]);
|
|
4546
|
+
var OrgStatus = z.enum(["pending", "approved"]);
|
|
4093
4547
|
var OrgMemberRole = z.enum(["owner", "admin", "member"]);
|
|
4094
4548
|
var OrgSlug = z.string().min(3).max(48).regex(/^[a-z0-9-]+$/, "Slug must be lowercase letters, numbers, and hyphens only");
|
|
4095
4549
|
var Org = z.object({
|
|
@@ -4099,7 +4553,12 @@ var Org = z.object({
|
|
|
4099
4553
|
plan: OrgPlan,
|
|
4100
4554
|
seatCount: z.number().int().nonnegative(),
|
|
4101
4555
|
githubOrgLogin: z.string().nullable(),
|
|
4102
|
-
createdAt: z.number().int().positive()
|
|
4556
|
+
createdAt: z.number().int().positive(),
|
|
4557
|
+
/** v1.2: soft-create status. Approved orgs are fully usable; pending orgs are gated. */
|
|
4558
|
+
status: OrgStatus,
|
|
4559
|
+
requestedPlan: OrgPlan.nullable(),
|
|
4560
|
+
founderEmail: z.string().email().nullable(),
|
|
4561
|
+
founderName: z.string().nullable()
|
|
4103
4562
|
});
|
|
4104
4563
|
var OrgMember = z.object({
|
|
4105
4564
|
userId: z.string(),
|
|
@@ -4144,8 +4603,17 @@ var OrgDashboard = z.object({
|
|
|
4144
4603
|
var CreateOrgRequest = z.object({
|
|
4145
4604
|
name: z.string().min(1).max(64),
|
|
4146
4605
|
slug: OrgSlug,
|
|
4147
|
-
githubOrgLogin: z.string().optional()
|
|
4606
|
+
githubOrgLogin: z.string().optional(),
|
|
4607
|
+
// v1.2 soft-create — required fields, trust-on-submit.
|
|
4608
|
+
founderEmail: z.string().email(),
|
|
4609
|
+
founderName: z.string().min(1).max(80).optional(),
|
|
4610
|
+
requestedPlan: OrgPlan
|
|
4611
|
+
});
|
|
4612
|
+
var PatchOrgRequest = z.object({
|
|
4613
|
+
founderEmail: z.string().email().optional(),
|
|
4614
|
+
founderName: z.string().min(1).max(80).optional()
|
|
4148
4615
|
});
|
|
4616
|
+
var PatchOrgResponse = z.object({ org: Org });
|
|
4149
4617
|
var CreateOrgResponse = z.object({ org: Org });
|
|
4150
4618
|
var GetOrgResponse = z.object({
|
|
4151
4619
|
org: Org,
|
|
@@ -4160,6 +4628,20 @@ var CreateOrgInviteRequest = z.object({
|
|
|
4160
4628
|
var CreateOrgInviteResponse = z.object({ invite: OrgInvite });
|
|
4161
4629
|
var AcceptOrgInviteResponse = z.object({ ok: z.boolean() });
|
|
4162
4630
|
var GetOrgDashboardResponse = z.object({ dashboard: OrgDashboard });
|
|
4631
|
+
var AdminPendingOrg = z.object({
|
|
4632
|
+
id: z.string(),
|
|
4633
|
+
name: z.string(),
|
|
4634
|
+
slug: OrgSlug.nullable(),
|
|
4635
|
+
requestedPlan: OrgPlan.nullable(),
|
|
4636
|
+
founderEmail: z.string().email().nullable(),
|
|
4637
|
+
founderName: z.string().nullable(),
|
|
4638
|
+
founderHandle: z.string(),
|
|
4639
|
+
createdAt: z.number().int().positive()
|
|
4640
|
+
});
|
|
4641
|
+
var GetPendingOrgsResponse = z.object({
|
|
4642
|
+
orgs: z.array(AdminPendingOrg)
|
|
4643
|
+
});
|
|
4644
|
+
var ApproveOrgResponse = z.object({ org: Org });
|
|
4163
4645
|
|
|
4164
4646
|
// ../contracts/src/api.ts
|
|
4165
4647
|
var GetMeResponse = z.object({ user: User });
|
|
@@ -4171,7 +4653,9 @@ var UploadSessionsResponse = z.object({
|
|
|
4171
4653
|
duplicates: z.number().int().nonnegative()
|
|
4172
4654
|
});
|
|
4173
4655
|
var CreateRoomRequest = z.object({
|
|
4174
|
-
name: z.string().min(1).max(64)
|
|
4656
|
+
name: z.string().min(1).max(64),
|
|
4657
|
+
/** v1.2: when true the room is publicly listed in /groups for its country. */
|
|
4658
|
+
isPublic: z.boolean().default(false)
|
|
4175
4659
|
});
|
|
4176
4660
|
var CreateRoomResponse = z.object({ room: Room });
|
|
4177
4661
|
var JoinRoomResponse = z.object({ room: Room });
|
|
@@ -4191,14 +4675,60 @@ var HeatmapDay = z.object({
|
|
|
4191
4675
|
tokens: z.number().int().nonnegative(),
|
|
4192
4676
|
sessions: z.number().int().nonnegative()
|
|
4193
4677
|
});
|
|
4678
|
+
var HeatmapRange = z.enum(["30d", "52w"]);
|
|
4194
4679
|
var Heatmap = z.object({
|
|
4680
|
+
range: HeatmapRange,
|
|
4195
4681
|
from: z.string(),
|
|
4196
4682
|
// YYYY-MM-DD UTC, inclusive
|
|
4197
4683
|
to: z.string(),
|
|
4198
4684
|
// YYYY-MM-DD UTC, inclusive
|
|
4199
4685
|
days: z.array(HeatmapDay)
|
|
4200
4686
|
});
|
|
4687
|
+
var GetHeatmapQuery = z.object({
|
|
4688
|
+
range: HeatmapRange.default("30d")
|
|
4689
|
+
});
|
|
4201
4690
|
var GetHeatmapResponse = z.object({ heatmap: Heatmap });
|
|
4691
|
+
var RoomSummary = z.object({
|
|
4692
|
+
code: z.string(),
|
|
4693
|
+
name: z.string(),
|
|
4694
|
+
/** Future: true once feature #6 lands. Always false for now. */
|
|
4695
|
+
isPublic: z.boolean(),
|
|
4696
|
+
/** ISO country code (e.g. "DE") when isPublic is true; null otherwise. */
|
|
4697
|
+
country: z.string().nullable(),
|
|
4698
|
+
memberCount: z.number().int().nonnegative(),
|
|
4699
|
+
total30dTokens: z.number().int().nonnegative(),
|
|
4700
|
+
total30dCostUsdCents: z.number().int().nonnegative()
|
|
4701
|
+
});
|
|
4702
|
+
var GetRoomSummaryResponse = z.object({ summary: RoomSummary });
|
|
4703
|
+
var PublicGroupRow = z.object({
|
|
4704
|
+
code: z.string(),
|
|
4705
|
+
name: z.string(),
|
|
4706
|
+
country: z.string().min(2).max(2),
|
|
4707
|
+
memberCount: z.number().int().nonnegative(),
|
|
4708
|
+
total30dTokens: z.number().int().nonnegative(),
|
|
4709
|
+
total30dCostUsdCents: z.number().int().nonnegative()
|
|
4710
|
+
});
|
|
4711
|
+
var CountryBoardRow = z.object({
|
|
4712
|
+
rank: z.number().int().positive(),
|
|
4713
|
+
userId: z.string(),
|
|
4714
|
+
handle: z.string(),
|
|
4715
|
+
avatarUrl: z.string().url().nullable(),
|
|
4716
|
+
tokens: z.number().int().nonnegative(),
|
|
4717
|
+
costUsdCents: z.number().int().nonnegative(),
|
|
4718
|
+
sessions: z.number().int().nonnegative()
|
|
4719
|
+
});
|
|
4720
|
+
var GetGroupsResponse = z.object({
|
|
4721
|
+
country: z.string().min(2).max(2).nullable(),
|
|
4722
|
+
groups: z.array(PublicGroupRow),
|
|
4723
|
+
/** Public users in the viewer's country, ranked by trailing-30d tokens. */
|
|
4724
|
+
userBoard: z.array(CountryBoardRow)
|
|
4725
|
+
});
|
|
4726
|
+
var GroupStreak = z.object({
|
|
4727
|
+
currentStreak: z.number().int().nonnegative(),
|
|
4728
|
+
/** Yesterday in YYYY-MM-DD UTC. */
|
|
4729
|
+
asOf: z.string()
|
|
4730
|
+
});
|
|
4731
|
+
var GetGroupStreakResponse = z.object({ groupStreak: GroupStreak });
|
|
4202
4732
|
var GetMyRoomsResponse = z.object({
|
|
4203
4733
|
rooms: z.array(Room)
|
|
4204
4734
|
});
|
|
@@ -4234,6 +4764,7 @@ var GetTrendingResponse = z.object({
|
|
|
4234
4764
|
range: LeaderboardRange,
|
|
4235
4765
|
generatedAt: z.number().int().positive()
|
|
4236
4766
|
});
|
|
4767
|
+
var GetReferralResponse = z.object({ referral: ReferralStats });
|
|
4237
4768
|
var ReportAbuseRequest = z.object({
|
|
4238
4769
|
targetHandle: z.string().min(1).max(100),
|
|
4239
4770
|
reason: z.string().min(1).max(500)
|
|
@@ -4249,14 +4780,22 @@ var ENDPOINTS = {
|
|
|
4249
4780
|
profile: (handle) => `/v1/u/${handle}`,
|
|
4250
4781
|
autobiography: (handle) => `/v1/u/${handle}/autobiography`,
|
|
4251
4782
|
profileHeatmap: (handle) => `/v1/u/${handle}/heatmap`,
|
|
4783
|
+
// v1.2 room aggregates — auth optional, accessible to non-members.
|
|
4784
|
+
roomSummary: (code) => `/v1/r/${code}/summary`,
|
|
4785
|
+
roomHeatmap: (code) => `/v1/r/${code}/heatmap`,
|
|
4786
|
+
roomGroupStreak: (code) => `/v1/r/${code}/group-streak`,
|
|
4787
|
+
// v1.2 public country-locked groups list
|
|
4788
|
+
groups: "/v1/groups",
|
|
4252
4789
|
authGithubStart: "/v1/auth/github/start",
|
|
4253
4790
|
authGithubCallback: "/v1/auth/github/callback",
|
|
4791
|
+
authLogout: "/v1/auth/logout",
|
|
4254
4792
|
authCliExchange: "/v1/auth/cli/exchange",
|
|
4255
4793
|
authCliPoll: "/v1/auth/cli/poll",
|
|
4256
4794
|
// Phase 2 Track G+H
|
|
4257
4795
|
meRooms: "/v1/me/rooms",
|
|
4258
4796
|
leaveRoom: (code) => `/v1/rooms/${code}/leave`,
|
|
4259
4797
|
renameRoom: (code) => `/v1/rooms/${code}`,
|
|
4798
|
+
pinRoom: (code) => `/v1/rooms/${code}/pin`,
|
|
4260
4799
|
roomActivity: (code) => `/v1/rooms/${code}/activity`,
|
|
4261
4800
|
roomStreaks: (code) => `/v1/rooms/${code}/streaks`,
|
|
4262
4801
|
roomChallenges: (code) => `/v1/rooms/${code}/challenges`,
|
|
@@ -4270,6 +4809,16 @@ var ENDPOINTS = {
|
|
|
4270
4809
|
patchMe: "/v1/me",
|
|
4271
4810
|
trending: "/v1/trending",
|
|
4272
4811
|
reportAbuse: "/v1/abuse/report",
|
|
4812
|
+
// Affiliate / referral tracking
|
|
4813
|
+
meReferral: "/v1/me/referral",
|
|
4814
|
+
// v1.2 Track AD — friends derived from shared private rooms
|
|
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",
|
|
4273
4822
|
// Phase 3 Track O — Org plan
|
|
4274
4823
|
orgs: "/v1/orgs",
|
|
4275
4824
|
org: (slug) => `/v1/orgs/${slug}`,
|
|
@@ -4283,7 +4832,10 @@ var ENDPOINTS = {
|
|
|
4283
4832
|
// Admin analytics (project-owner only)
|
|
4284
4833
|
adminSignups: "/v1/admin/signups",
|
|
4285
4834
|
adminActivity: "/v1/admin/activity",
|
|
4286
|
-
adminReferrers: "/v1/admin/referrers"
|
|
4835
|
+
adminReferrers: "/v1/admin/referrers",
|
|
4836
|
+
// Admin org approval (v1.2)
|
|
4837
|
+
adminOrgsPending: "/v1/admin/orgs/pending",
|
|
4838
|
+
adminOrgApprove: (slug) => `/v1/admin/orgs/${slug}/approve`
|
|
4287
4839
|
};
|
|
4288
4840
|
|
|
4289
4841
|
// ../contracts/src/errors.ts
|
|
@@ -4369,20 +4921,98 @@ var AdminActivityResponse = z.object({
|
|
|
4369
4921
|
generatedAt: z.number().int().positive()
|
|
4370
4922
|
});
|
|
4371
4923
|
var AdminReferrerRow = z.object({
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
/** Number of users
|
|
4924
|
+
handle: z.string(),
|
|
4925
|
+
avatarUrl: z.string().url().nullable(),
|
|
4926
|
+
/** Number of users this referrer has brought in. */
|
|
4375
4927
|
count: z.number().int().nonnegative()
|
|
4376
4928
|
});
|
|
4377
4929
|
var AdminReferrersResponse = z.object({
|
|
4378
|
-
/** True iff a real referral attribution column / table exists. */
|
|
4379
|
-
tracked: z.boolean(),
|
|
4380
|
-
/** Description of what `rows` represents (e.g. "First CLI source"). */
|
|
4381
|
-
signal: z.string(),
|
|
4382
4930
|
rows: z.array(AdminReferrerRow),
|
|
4383
4931
|
generatedAt: z.number().int().positive()
|
|
4384
4932
|
});
|
|
4385
4933
|
|
|
4934
|
+
// ../contracts/src/friends.ts
|
|
4935
|
+
var FriendSharedRoom = z.object({
|
|
4936
|
+
code: z.string(),
|
|
4937
|
+
name: z.string()
|
|
4938
|
+
});
|
|
4939
|
+
var FriendRow = z.object({
|
|
4940
|
+
userId: z.string(),
|
|
4941
|
+
handle: z.string(),
|
|
4942
|
+
avatarUrl: z.string().url().nullable(),
|
|
4943
|
+
twitterHandle: z.string().nullable().optional(),
|
|
4944
|
+
publicProfile: z.boolean(),
|
|
4945
|
+
sharedRooms: z.array(FriendSharedRoom),
|
|
4946
|
+
tokens: z.number().int().nonnegative(),
|
|
4947
|
+
costUsdCents: z.number().int().nonnegative(),
|
|
4948
|
+
sessions: z.number().int().nonnegative()
|
|
4949
|
+
});
|
|
4950
|
+
var FriendsResponse = z.object({
|
|
4951
|
+
range: LeaderboardRange,
|
|
4952
|
+
friends: z.array(FriendRow)
|
|
4953
|
+
});
|
|
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
|
+
|
|
4386
5016
|
// src/lib/api.ts
|
|
4387
5017
|
var DEFAULT_API_URL = "https://api.tokenrats.com";
|
|
4388
5018
|
function isTransient(status) {
|
|
@@ -4399,33 +5029,40 @@ var ApiError2 = class extends Error {
|
|
|
4399
5029
|
this.name = "ApiError";
|
|
4400
5030
|
}
|
|
4401
5031
|
};
|
|
5032
|
+
var DeviceRevokedError = class extends ApiError2 {
|
|
5033
|
+
constructor(body) {
|
|
5034
|
+
super(401, body);
|
|
5035
|
+
this.name = "DeviceRevokedError";
|
|
5036
|
+
}
|
|
5037
|
+
};
|
|
4402
5038
|
var ApiClient = class {
|
|
4403
5039
|
apiUrl;
|
|
4404
5040
|
token;
|
|
5041
|
+
deviceId;
|
|
5042
|
+
cliVersion;
|
|
4405
5043
|
constructor(opts = {}) {
|
|
4406
5044
|
this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
4407
5045
|
this.token = opts.token;
|
|
5046
|
+
this.deviceId = opts.deviceId;
|
|
5047
|
+
this.cliVersion = opts.cliVersion;
|
|
4408
5048
|
}
|
|
4409
5049
|
setToken(token) {
|
|
4410
5050
|
this.token = token;
|
|
4411
5051
|
}
|
|
4412
5052
|
headers() {
|
|
4413
5053
|
const h = { "Content-Type": "application/json" };
|
|
4414
|
-
if (this.token) {
|
|
4415
|
-
|
|
4416
|
-
|
|
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;
|
|
4417
5057
|
return h;
|
|
4418
5058
|
}
|
|
4419
|
-
/** Perform a fetch with retry+backoff. maxRetries=3, delays: 1s, 2s, 4s. */
|
|
4420
5059
|
async fetchWithRetry(url, init, maxRetries = 3) {
|
|
4421
5060
|
let attempt = 0;
|
|
4422
5061
|
let lastErr;
|
|
4423
5062
|
while (attempt <= maxRetries) {
|
|
4424
5063
|
try {
|
|
4425
5064
|
const res = await fetch(url, init);
|
|
4426
|
-
if (res.ok || !isTransient(res.status))
|
|
4427
|
-
return res;
|
|
4428
|
-
}
|
|
5065
|
+
if (res.ok || !isTransient(res.status)) return res;
|
|
4429
5066
|
lastErr = new ApiError2(res.status, await res.text());
|
|
4430
5067
|
} catch (err) {
|
|
4431
5068
|
lastErr = err;
|
|
@@ -4437,34 +5074,37 @@ var ApiClient = class {
|
|
|
4437
5074
|
}
|
|
4438
5075
|
throw lastErr;
|
|
4439
5076
|
}
|
|
4440
|
-
async
|
|
4441
|
-
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}`;
|
|
4442
5086
|
const res = await this.fetchWithRetry(url, {
|
|
4443
5087
|
method: "POST",
|
|
4444
5088
|
headers: this.headers(),
|
|
4445
5089
|
body: JSON.stringify(body)
|
|
4446
5090
|
});
|
|
4447
|
-
if (!res.ok)
|
|
4448
|
-
throw new ApiError2(res.status, await res.text());
|
|
4449
|
-
}
|
|
5091
|
+
if (!res.ok) throw await this.failedResponseToError(res);
|
|
4450
5092
|
return res.json();
|
|
4451
5093
|
}
|
|
4452
|
-
async get(
|
|
4453
|
-
const url = `${this.apiUrl}${
|
|
5094
|
+
async get(path5) {
|
|
5095
|
+
const url = `${this.apiUrl}${path5}`;
|
|
4454
5096
|
const res = await this.fetchWithRetry(url, {
|
|
4455
5097
|
method: "GET",
|
|
4456
5098
|
headers: this.headers()
|
|
4457
5099
|
});
|
|
4458
|
-
if (!res.ok)
|
|
4459
|
-
throw new ApiError2(res.status, await res.text());
|
|
4460
|
-
}
|
|
5100
|
+
if (!res.ok) throw await this.failedResponseToError(res);
|
|
4461
5101
|
return res.json();
|
|
4462
5102
|
}
|
|
4463
5103
|
/** Initiate device-code flow. */
|
|
4464
5104
|
async cliExchange() {
|
|
4465
5105
|
return this.post(ENDPOINTS.authCliExchange, {});
|
|
4466
5106
|
}
|
|
4467
|
-
/** Poll for auth token. Returns token on success, null on pending
|
|
5107
|
+
/** Poll for auth token. Returns token on success, null on pending. */
|
|
4468
5108
|
async cliPoll(pollToken) {
|
|
4469
5109
|
const url = `${this.apiUrl}${ENDPOINTS.authCliPoll}`;
|
|
4470
5110
|
const res = await fetch(url, {
|
|
@@ -4476,12 +5116,8 @@ var ApiClient = class {
|
|
|
4476
5116
|
const data = await res.json();
|
|
4477
5117
|
return data.token;
|
|
4478
5118
|
}
|
|
4479
|
-
if (res.status === 202)
|
|
4480
|
-
|
|
4481
|
-
}
|
|
4482
|
-
if (res.status === 410) {
|
|
4483
|
-
throw new ApiError2(410, "Code expired");
|
|
4484
|
-
}
|
|
5119
|
+
if (res.status === 202) return null;
|
|
5120
|
+
if (res.status === 410) throw new ApiError2(410, "Code expired");
|
|
4485
5121
|
throw new ApiError2(res.status, await res.text());
|
|
4486
5122
|
}
|
|
4487
5123
|
/** GET /v1/me */
|
|
@@ -4492,106 +5128,18 @@ var ApiClient = class {
|
|
|
4492
5128
|
async uploadSessions(sessions) {
|
|
4493
5129
|
return this.post(ENDPOINTS.sessions, { sessions });
|
|
4494
5130
|
}
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
import * as fs from "node:fs";
|
|
4499
|
-
import * as os from "node:os";
|
|
4500
|
-
import * as path from "node:path";
|
|
4501
|
-
function tokenDir() {
|
|
4502
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
4503
|
-
const base = xdgConfig ?? path.join(os.homedir(), ".config");
|
|
4504
|
-
return path.join(base, "token-rats");
|
|
4505
|
-
}
|
|
4506
|
-
function tokenPath() {
|
|
4507
|
-
return path.join(tokenDir(), "token");
|
|
4508
|
-
}
|
|
4509
|
-
function saveToken(token) {
|
|
4510
|
-
const dir = tokenDir();
|
|
4511
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
4512
|
-
const file = tokenPath();
|
|
4513
|
-
fs.writeFileSync(file, token, { encoding: "utf8", mode: 384 });
|
|
4514
|
-
}
|
|
4515
|
-
function loadToken() {
|
|
4516
|
-
const file = tokenPath();
|
|
4517
|
-
try {
|
|
4518
|
-
const token = fs.readFileSync(file, "utf8").trim();
|
|
4519
|
-
return token.length > 0 ? token : null;
|
|
4520
|
-
} catch {
|
|
4521
|
-
return null;
|
|
5131
|
+
/** GET /v1/me/devices */
|
|
5132
|
+
async getDevices() {
|
|
5133
|
+
return this.get(ENDPOINTS.meDevices);
|
|
4522
5134
|
}
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
try {
|
|
4527
|
-
fs.unlinkSync(file);
|
|
4528
|
-
} catch {
|
|
5135
|
+
/** POST /v1/me/devices/heartbeat */
|
|
5136
|
+
async heartbeat() {
|
|
5137
|
+
return this.post(ENDPOINTS.meDeviceHeartbeat, {});
|
|
4529
5138
|
}
|
|
4530
|
-
}
|
|
4531
|
-
function isLoggedIn() {
|
|
4532
|
-
return loadToken() !== null;
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
|
-
// src/lib/log.ts
|
|
4536
|
-
var ESC = "\x1B";
|
|
4537
|
-
var c = {
|
|
4538
|
-
reset: `${ESC}[0m`,
|
|
4539
|
-
bold: `${ESC}[1m`,
|
|
4540
|
-
dim: `${ESC}[2m`,
|
|
4541
|
-
green: `${ESC}[32m`,
|
|
4542
|
-
yellow: `${ESC}[33m`,
|
|
4543
|
-
cyan: `${ESC}[36m`,
|
|
4544
|
-
red: `${ESC}[31m`,
|
|
4545
|
-
gray: `${ESC}[90m`
|
|
4546
5139
|
};
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
function isTTY() {
|
|
4551
|
-
return process.stdout.isTTY === true;
|
|
4552
|
-
}
|
|
4553
|
-
function color(code, text) {
|
|
4554
|
-
return isTTY() ? `${code}${text}${c.reset}` : strip(text);
|
|
4555
|
-
}
|
|
4556
|
-
function info(msg) {
|
|
4557
|
-
console.log(color(c.cyan, ` ${msg}`));
|
|
4558
|
-
}
|
|
4559
|
-
function success(msg) {
|
|
4560
|
-
console.log(color(c.green, `\u2713 ${msg}`));
|
|
4561
|
-
}
|
|
4562
|
-
function warn(msg) {
|
|
4563
|
-
console.warn(color(c.yellow, `\u26A0 ${msg}`));
|
|
4564
|
-
}
|
|
4565
|
-
function error(msg) {
|
|
4566
|
-
console.error(color(c.red, `\u2717 ${msg}`));
|
|
4567
|
-
}
|
|
4568
|
-
function dim(msg) {
|
|
4569
|
-
console.log(color(c.dim, ` ${msg}`));
|
|
4570
|
-
}
|
|
4571
|
-
function spinner(label) {
|
|
4572
|
-
if (!isTTY()) {
|
|
4573
|
-
process.stdout.write(` ${label}...
|
|
4574
|
-
`);
|
|
4575
|
-
return {
|
|
4576
|
-
stop(finalMsg) {
|
|
4577
|
-
if (finalMsg) process.stdout.write(` ${finalMsg}
|
|
4578
|
-
`);
|
|
4579
|
-
}
|
|
4580
|
-
};
|
|
4581
|
-
}
|
|
4582
|
-
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4583
|
-
let i = 0;
|
|
4584
|
-
const interval = setInterval(() => {
|
|
4585
|
-
process.stdout.write(`\r${c.cyan}${frames[i++ % frames.length]}${c.reset} ${label} `);
|
|
4586
|
-
}, 80);
|
|
4587
|
-
return {
|
|
4588
|
-
stop(finalMsg) {
|
|
4589
|
-
clearInterval(interval);
|
|
4590
|
-
process.stdout.write("\r\x1B[K");
|
|
4591
|
-
if (finalMsg) success(finalMsg);
|
|
4592
|
-
}
|
|
4593
|
-
};
|
|
4594
|
-
}
|
|
5140
|
+
|
|
5141
|
+
// src/lib/cli-version.ts
|
|
5142
|
+
var CLI_VERSION = "0.2.0";
|
|
4595
5143
|
|
|
4596
5144
|
// src/commands/login.ts
|
|
4597
5145
|
async function openBrowser(url) {
|
|
@@ -4604,12 +5152,12 @@ async function openBrowser(url) {
|
|
|
4604
5152
|
} catch {
|
|
4605
5153
|
}
|
|
4606
5154
|
try {
|
|
4607
|
-
const { execFile } = await import("node:child_process");
|
|
4608
|
-
const { promisify } = await import("node:util");
|
|
4609
|
-
const
|
|
5155
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
5156
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
5157
|
+
const exec2 = promisify2(execFile2);
|
|
4610
5158
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
4611
5159
|
const args = process.platform === "win32" ? ["/c", "start", url] : [url];
|
|
4612
|
-
await
|
|
5160
|
+
await exec2(cmd, args).catch(() => null);
|
|
4613
5161
|
} catch {
|
|
4614
5162
|
}
|
|
4615
5163
|
}
|
|
@@ -4625,7 +5173,12 @@ async function copyToClipboard(text) {
|
|
|
4625
5173
|
return false;
|
|
4626
5174
|
}
|
|
4627
5175
|
async function loginCommand(opts) {
|
|
4628
|
-
const
|
|
5176
|
+
const deviceId = ensureDeviceId();
|
|
5177
|
+
const client = new ApiClient({
|
|
5178
|
+
apiUrl: opts.apiUrl,
|
|
5179
|
+
deviceId,
|
|
5180
|
+
cliVersion: CLI_VERSION
|
|
5181
|
+
});
|
|
4629
5182
|
info("Authenticating with Token Rats\u2026");
|
|
4630
5183
|
let exchange;
|
|
4631
5184
|
try {
|
|
@@ -4673,7 +5226,16 @@ async function loginCommand(opts) {
|
|
|
4673
5226
|
process.exit(1);
|
|
4674
5227
|
}
|
|
4675
5228
|
saveToken(token);
|
|
5229
|
+
clearDisconnected();
|
|
4676
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();
|
|
4677
5239
|
}
|
|
4678
5240
|
|
|
4679
5241
|
// src/commands/logout.ts
|
|
@@ -4687,68 +5249,7 @@ function logoutCommand() {
|
|
|
4687
5249
|
}
|
|
4688
5250
|
|
|
4689
5251
|
// src/commands/sync.ts
|
|
4690
|
-
import * as
|
|
4691
|
-
|
|
4692
|
-
// ../pricing/src/prices.ts
|
|
4693
|
-
var prices = {
|
|
4694
|
-
models: {
|
|
4695
|
-
// ── Anthropic / Claude ────────────────────────────────────────────────
|
|
4696
|
-
"claude-opus-4-7": { inputPerMTok: 15, outputPerMTok: 75 },
|
|
4697
|
-
"claude-opus-4-6": { inputPerMTok: 15, outputPerMTok: 75 },
|
|
4698
|
-
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4699
|
-
"claude-sonnet-4-5": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4700
|
-
"claude-haiku-4-5": { inputPerMTok: 0.8, outputPerMTok: 4 },
|
|
4701
|
-
"claude-3-5-sonnet-20241022": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4702
|
-
"claude-3-5-sonnet-20240620": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4703
|
-
"claude-3-5-haiku-20241022": { inputPerMTok: 0.8, outputPerMTok: 4 },
|
|
4704
|
-
"claude-3-opus-20240229": { inputPerMTok: 15, outputPerMTok: 75 },
|
|
4705
|
-
"claude-3-sonnet-20240229": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4706
|
-
"claude-3-haiku-20240307": { inputPerMTok: 0.25, outputPerMTok: 1.25 },
|
|
4707
|
-
// ── OpenAI / Codex ────────────────────────────────────────────────────
|
|
4708
|
-
// GPT-5 family. gpt-5-codex is the Codex CLI's public model; OpenAI's
|
|
4709
|
-
// pricing page lists only input ($1.25/MTok). Output uses the same $10
|
|
4710
|
-
// rate the broader GPT-5 base is publicly quoted at — flagged here for
|
|
4711
|
-
// re-verification when OpenAI publishes an explicit output figure.
|
|
4712
|
-
"gpt-5-codex": { inputPerMTok: 1.25, outputPerMTok: 10 },
|
|
4713
|
-
"gpt-5-mini": { inputPerMTok: 0.25, outputPerMTok: 2 },
|
|
4714
|
-
"gpt-5-nano": { inputPerMTok: 0.05, outputPerMTok: 0.4 },
|
|
4715
|
-
"gpt-5-pro": { inputPerMTok: 15, outputPerMTok: 120 },
|
|
4716
|
-
"gpt-5.5": { inputPerMTok: 5, outputPerMTok: 30 },
|
|
4717
|
-
"codex-mini-latest": { inputPerMTok: 1.5, outputPerMTok: 6 },
|
|
4718
|
-
// GPT-4 family.
|
|
4719
|
-
"gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10 },
|
|
4720
|
-
"gpt-4o-mini": { inputPerMTok: 0.15, outputPerMTok: 0.6 },
|
|
4721
|
-
"gpt-4-turbo": { inputPerMTok: 10, outputPerMTok: 30 },
|
|
4722
|
-
"gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8 },
|
|
4723
|
-
"gpt-4.1-mini": { inputPerMTok: 0.4, outputPerMTok: 1.6 },
|
|
4724
|
-
"gpt-4.1-nano": { inputPerMTok: 0.1, outputPerMTok: 0.4 },
|
|
4725
|
-
// Reasoning models (o-series).
|
|
4726
|
-
o1: { inputPerMTok: 15, outputPerMTok: 60 },
|
|
4727
|
-
"o1-mini": { inputPerMTok: 3, outputPerMTok: 12 },
|
|
4728
|
-
// o3 was repriced in 2026; previously $10/$40, now $2/$8.
|
|
4729
|
-
o3: { inputPerMTok: 2, outputPerMTok: 8 },
|
|
4730
|
-
"o3-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4 },
|
|
4731
|
-
"o4-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4 }
|
|
4732
|
-
}
|
|
4733
|
-
};
|
|
4734
|
-
|
|
4735
|
-
// ../pricing/src/index.ts
|
|
4736
|
-
var TABLE = prices.models;
|
|
4737
|
-
function priceOf(model, inTokens, outTokens) {
|
|
4738
|
-
const p = TABLE[model] ?? matchPrefix(model);
|
|
4739
|
-
if (!p) return { costUsdCents: 0, known: false };
|
|
4740
|
-
const dollars = (inTokens * p.inputPerMTok + outTokens * p.outputPerMTok) / 1e6;
|
|
4741
|
-
return { costUsdCents: Math.round(dollars * 100), known: true };
|
|
4742
|
-
}
|
|
4743
|
-
function matchPrefix(model) {
|
|
4744
|
-
let best;
|
|
4745
|
-
for (const [key, price] of Object.entries(TABLE)) {
|
|
4746
|
-
if (model.startsWith(key) && (!best || key.length > best.key.length)) {
|
|
4747
|
-
best = { key, price };
|
|
4748
|
-
}
|
|
4749
|
-
}
|
|
4750
|
-
return best?.price;
|
|
4751
|
-
}
|
|
5252
|
+
import * as fs4 from "node:fs";
|
|
4752
5253
|
|
|
4753
5254
|
// ../parsers/src/hash.ts
|
|
4754
5255
|
var FNV_PRIME = 16777619;
|
|
@@ -4811,6 +5312,8 @@ function parseClaudeCode(input) {
|
|
|
4811
5312
|
endedAt: timestamp,
|
|
4812
5313
|
inTokens: 0,
|
|
4813
5314
|
outTokens: 0,
|
|
5315
|
+
cacheReadTokens: 0,
|
|
5316
|
+
cacheWriteTokens: 0,
|
|
4814
5317
|
model: ""
|
|
4815
5318
|
};
|
|
4816
5319
|
sessions.set(sessionId, acc);
|
|
@@ -4829,10 +5332,10 @@ function parseClaudeCode(input) {
|
|
|
4829
5332
|
const usage = msg.usage;
|
|
4830
5333
|
if (typeof usage === "object" && usage !== null) {
|
|
4831
5334
|
const u = usage;
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
acc.
|
|
4835
|
-
acc.
|
|
5335
|
+
acc.inTokens += toNonNegInt(u.input_tokens);
|
|
5336
|
+
acc.outTokens += toNonNegInt(u.output_tokens);
|
|
5337
|
+
acc.cacheReadTokens += toNonNegInt(u.cache_read_input_tokens);
|
|
5338
|
+
acc.cacheWriteTokens += toNonNegInt(u.cache_creation_input_tokens);
|
|
4836
5339
|
}
|
|
4837
5340
|
}
|
|
4838
5341
|
const results = [];
|
|
@@ -4841,7 +5344,7 @@ function parseClaudeCode(input) {
|
|
|
4841
5344
|
const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
|
|
4842
5345
|
if (startedAt <= 0 || endedAt <= 0) continue;
|
|
4843
5346
|
const model = acc.model.length > 0 ? acc.model : "unknown";
|
|
4844
|
-
const
|
|
5347
|
+
const costUsdCents = 0;
|
|
4845
5348
|
const dedupeKey = computeDedupeKey(
|
|
4846
5349
|
"claude-code",
|
|
4847
5350
|
model,
|
|
@@ -4852,9 +5355,14 @@ function parseClaudeCode(input) {
|
|
|
4852
5355
|
results.push({
|
|
4853
5356
|
id: `claude-code:${acc.sessionId}`,
|
|
4854
5357
|
source: "claude-code",
|
|
5358
|
+
provider: "anthropic",
|
|
5359
|
+
client: "claude-code",
|
|
5360
|
+
channel: "cli",
|
|
4855
5361
|
model,
|
|
4856
5362
|
inTokens: acc.inTokens,
|
|
4857
5363
|
outTokens: acc.outTokens,
|
|
5364
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
5365
|
+
cacheWriteTokens: acc.cacheWriteTokens,
|
|
4858
5366
|
costUsdCents,
|
|
4859
5367
|
startedAt,
|
|
4860
5368
|
endedAt,
|
|
@@ -4898,6 +5406,9 @@ function parseCodex(input) {
|
|
|
4898
5406
|
currentSessionId = id;
|
|
4899
5407
|
const metaTs = parseTimestamp(payload.timestamp) || ts;
|
|
4900
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";
|
|
4901
5412
|
if (metaTs > 0 && (acc2.startedAt === 0 || metaTs < acc2.startedAt)) {
|
|
4902
5413
|
acc2.startedAt = metaTs;
|
|
4903
5414
|
}
|
|
@@ -4926,6 +5437,8 @@ function parseCodex(input) {
|
|
|
4926
5437
|
const reasoning = toNonNegInt2(t.reasoning_output_tokens);
|
|
4927
5438
|
acc.inTokens = Math.max(0, inputTotal - cachedInput);
|
|
4928
5439
|
acc.outTokens = output + reasoning;
|
|
5440
|
+
acc.cacheReadTokens = cachedInput;
|
|
5441
|
+
acc.reasoningTokens = reasoning;
|
|
4929
5442
|
}
|
|
4930
5443
|
}
|
|
4931
5444
|
const results = [];
|
|
@@ -4934,14 +5447,19 @@ function parseCodex(input) {
|
|
|
4934
5447
|
const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
|
|
4935
5448
|
if (startedAt <= 0 || endedAt <= 0) continue;
|
|
4936
5449
|
const model = acc.model.length > 0 ? acc.model : "unknown";
|
|
4937
|
-
const
|
|
5450
|
+
const costUsdCents = 0;
|
|
4938
5451
|
const dedupeKey = computeDedupeKey("codex", model, startedAt, acc.inTokens, acc.outTokens);
|
|
4939
5452
|
results.push({
|
|
4940
5453
|
id: `codex:${acc.sessionId}`,
|
|
4941
5454
|
source: "codex",
|
|
5455
|
+
provider: "openai",
|
|
5456
|
+
client: acc.client || "codex-cli",
|
|
5457
|
+
channel: acc.channel,
|
|
4942
5458
|
model,
|
|
4943
5459
|
inTokens: acc.inTokens,
|
|
4944
5460
|
outTokens: acc.outTokens,
|
|
5461
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
5462
|
+
reasoningTokens: acc.reasoningTokens,
|
|
4945
5463
|
costUsdCents,
|
|
4946
5464
|
startedAt,
|
|
4947
5465
|
endedAt,
|
|
@@ -4959,7 +5477,11 @@ function upsert(map, sessionId) {
|
|
|
4959
5477
|
endedAt: 0,
|
|
4960
5478
|
inTokens: 0,
|
|
4961
5479
|
outTokens: 0,
|
|
4962
|
-
|
|
5480
|
+
cacheReadTokens: 0,
|
|
5481
|
+
reasoningTokens: 0,
|
|
5482
|
+
model: "",
|
|
5483
|
+
client: "codex-cli",
|
|
5484
|
+
channel: "unknown"
|
|
4963
5485
|
};
|
|
4964
5486
|
map.set(sessionId, acc);
|
|
4965
5487
|
}
|
|
@@ -4983,12 +5505,6 @@ var ESTIMATES = {
|
|
|
4983
5505
|
composer: { inTokens: 1e4, outTokens: 2e3, model: "cursor-composer" }
|
|
4984
5506
|
// "tab" deliberately omitted — see file header.
|
|
4985
5507
|
};
|
|
4986
|
-
var INPUT_USD_PER_MTOK = 3;
|
|
4987
|
-
var OUTPUT_USD_PER_MTOK = 15;
|
|
4988
|
-
function estimatedCostCents(inTokens, outTokens) {
|
|
4989
|
-
const dollars = inTokens / 1e6 * INPUT_USD_PER_MTOK + outTokens / 1e6 * OUTPUT_USD_PER_MTOK;
|
|
4990
|
-
return Math.round(dollars * 100);
|
|
4991
|
-
}
|
|
4992
5508
|
function parseCursor(input) {
|
|
4993
5509
|
let text;
|
|
4994
5510
|
if (typeof input === "string") {
|
|
@@ -5007,20 +5523,23 @@ function parseCursor(input) {
|
|
|
5007
5523
|
for (const raw of rows) {
|
|
5008
5524
|
if (typeof raw !== "object" || raw === null) continue;
|
|
5009
5525
|
const row = raw;
|
|
5010
|
-
const id = typeof row
|
|
5526
|
+
const id = typeof row.id === "string" ? row.id : null;
|
|
5011
5527
|
if (!id) continue;
|
|
5012
|
-
const type = typeof row
|
|
5528
|
+
const type = typeof row.type === "string" ? row.type : null;
|
|
5013
5529
|
if (!type) continue;
|
|
5014
|
-
const unixMs = typeof row
|
|
5530
|
+
const unixMs = typeof row.unixMs === "number" && Number.isFinite(row.unixMs) && row.unixMs > 0 ? row.unixMs : null;
|
|
5015
5531
|
if (unixMs === null) continue;
|
|
5016
5532
|
const estimate = ESTIMATES[type];
|
|
5017
5533
|
if (!estimate) continue;
|
|
5018
5534
|
const { inTokens, outTokens, model } = estimate;
|
|
5019
|
-
const costUsdCents =
|
|
5535
|
+
const costUsdCents = 0;
|
|
5020
5536
|
const dedupeKey = computeDedupeKey("cursor", model, unixMs, inTokens, outTokens);
|
|
5021
5537
|
results.push({
|
|
5022
5538
|
id: `cursor:${id}`,
|
|
5023
5539
|
source: "cursor",
|
|
5540
|
+
provider: "cursor",
|
|
5541
|
+
client: "cursor",
|
|
5542
|
+
channel: "ide",
|
|
5024
5543
|
model,
|
|
5025
5544
|
inTokens,
|
|
5026
5545
|
outTokens,
|
|
@@ -5034,31 +5553,31 @@ function parseCursor(input) {
|
|
|
5034
5553
|
}
|
|
5035
5554
|
|
|
5036
5555
|
// src/lib/cursor-extract.ts
|
|
5037
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
5556
|
+
import { existsSync as existsSync3, readdirSync } from "node:fs";
|
|
5038
5557
|
import { readFile } from "node:fs/promises";
|
|
5039
|
-
import { homedir as
|
|
5040
|
-
import { join as
|
|
5558
|
+
import { homedir as homedir3 } from "node:os";
|
|
5559
|
+
import { join as join3 } from "node:path";
|
|
5041
5560
|
function cursorWorkspaceStorageDir() {
|
|
5042
|
-
const home =
|
|
5043
|
-
const rel =
|
|
5561
|
+
const home = homedir3();
|
|
5562
|
+
const rel = join3("Cursor", "User", "workspaceStorage");
|
|
5044
5563
|
if (process.platform === "darwin") {
|
|
5045
|
-
return
|
|
5564
|
+
return join3(home, "Library", "Application Support", rel);
|
|
5046
5565
|
}
|
|
5047
5566
|
if (process.platform === "win32") {
|
|
5048
|
-
const appData = process.env
|
|
5049
|
-
return
|
|
5567
|
+
const appData = process.env.APPDATA ?? join3(home, "AppData", "Roaming");
|
|
5568
|
+
return join3(appData, rel);
|
|
5050
5569
|
}
|
|
5051
|
-
const xdgConfig = process.env
|
|
5052
|
-
return
|
|
5570
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join3(home, ".config");
|
|
5571
|
+
return join3(xdgConfig, rel);
|
|
5053
5572
|
}
|
|
5054
5573
|
function discoverWorkspaceDbs() {
|
|
5055
5574
|
const root = cursorWorkspaceStorageDir();
|
|
5056
|
-
if (!
|
|
5575
|
+
if (!existsSync3(root)) return [];
|
|
5057
5576
|
const out = [];
|
|
5058
5577
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
5059
5578
|
if (!entry.isDirectory()) continue;
|
|
5060
|
-
const candidate =
|
|
5061
|
-
if (
|
|
5579
|
+
const candidate = join3(root, entry.name, "state.vscdb");
|
|
5580
|
+
if (existsSync3(candidate)) out.push(candidate);
|
|
5062
5581
|
}
|
|
5063
5582
|
return out;
|
|
5064
5583
|
}
|
|
@@ -5136,9 +5655,9 @@ async function readGenerationsFromDb(dbPath) {
|
|
|
5136
5655
|
for (const item of parsed) {
|
|
5137
5656
|
if (typeof item !== "object" || item === null) continue;
|
|
5138
5657
|
const r = item;
|
|
5139
|
-
const id = typeof r
|
|
5140
|
-
const type = typeof r
|
|
5141
|
-
const unixMs = typeof r
|
|
5658
|
+
const id = typeof r.generationUUID === "string" ? r.generationUUID : null;
|
|
5659
|
+
const type = typeof r.type === "string" ? r.type : null;
|
|
5660
|
+
const unixMs = typeof r.unixMs === "number" && Number.isFinite(r.unixMs) && r.unixMs > 0 ? r.unixMs : null;
|
|
5142
5661
|
if (!id || !type || unixMs === null) continue;
|
|
5143
5662
|
out.push({ id, type, unixMs });
|
|
5144
5663
|
}
|
|
@@ -5189,15 +5708,15 @@ async function extractCursorGenerations() {
|
|
|
5189
5708
|
}
|
|
5190
5709
|
|
|
5191
5710
|
// src/lib/discover.ts
|
|
5192
|
-
import * as
|
|
5193
|
-
import * as
|
|
5194
|
-
import * as
|
|
5711
|
+
import * as fs3 from "node:fs";
|
|
5712
|
+
import * as os3 from "node:os";
|
|
5713
|
+
import * as path3 from "node:path";
|
|
5195
5714
|
function findJsonlFiles(dir) {
|
|
5196
5715
|
const results = [];
|
|
5197
|
-
if (!
|
|
5198
|
-
const entries =
|
|
5716
|
+
if (!fs3.existsSync(dir)) return results;
|
|
5717
|
+
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
5199
5718
|
for (const entry of entries) {
|
|
5200
|
-
const full =
|
|
5719
|
+
const full = path3.join(dir, entry.name);
|
|
5201
5720
|
if (entry.isDirectory()) {
|
|
5202
5721
|
results.push(...findJsonlFiles(full));
|
|
5203
5722
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -5207,32 +5726,32 @@ function findJsonlFiles(dir) {
|
|
|
5207
5726
|
return results;
|
|
5208
5727
|
}
|
|
5209
5728
|
function claudeCodeProjectsDir() {
|
|
5210
|
-
const home =
|
|
5729
|
+
const home = os3.homedir();
|
|
5211
5730
|
if (process.platform === "win32") {
|
|
5212
5731
|
const profile = process.env.USERPROFILE ?? home;
|
|
5213
|
-
return
|
|
5732
|
+
return path3.join(profile, ".claude", "projects");
|
|
5214
5733
|
}
|
|
5215
|
-
return
|
|
5734
|
+
return path3.join(home, ".claude", "projects");
|
|
5216
5735
|
}
|
|
5217
5736
|
function discoverClaudeCodeFiles() {
|
|
5218
5737
|
const dir = claudeCodeProjectsDir();
|
|
5219
5738
|
return findJsonlFiles(dir);
|
|
5220
5739
|
}
|
|
5221
5740
|
function codexSessionsDirs() {
|
|
5222
|
-
const home =
|
|
5741
|
+
const home = os3.homedir();
|
|
5223
5742
|
const candidates = [];
|
|
5224
5743
|
if (process.platform === "win32") {
|
|
5225
5744
|
const profile = process.env.USERPROFILE ?? home;
|
|
5226
|
-
candidates.push(
|
|
5227
|
-
const appData = process.env.APPDATA ??
|
|
5228
|
-
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"));
|
|
5229
5748
|
} else {
|
|
5230
|
-
candidates.push(
|
|
5231
|
-
const snapRoot =
|
|
5232
|
-
if (
|
|
5233
|
-
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 })) {
|
|
5234
5753
|
if (entry.isDirectory()) {
|
|
5235
|
-
candidates.push(
|
|
5754
|
+
candidates.push(path3.join(snapRoot, entry.name, "sessions"));
|
|
5236
5755
|
}
|
|
5237
5756
|
}
|
|
5238
5757
|
}
|
|
@@ -5241,7 +5760,7 @@ function codexSessionsDirs() {
|
|
|
5241
5760
|
const dirs = [];
|
|
5242
5761
|
for (const c2 of candidates) {
|
|
5243
5762
|
try {
|
|
5244
|
-
const real =
|
|
5763
|
+
const real = fs3.existsSync(c2) ? fs3.realpathSync(c2) : null;
|
|
5245
5764
|
if (real && !seen2.has(real)) {
|
|
5246
5765
|
seen2.add(real);
|
|
5247
5766
|
dirs.push(c2);
|
|
@@ -5285,8 +5804,7 @@ function mergeBySessionId(records) {
|
|
|
5285
5804
|
}
|
|
5286
5805
|
}
|
|
5287
5806
|
for (const rec of byId.values()) {
|
|
5288
|
-
|
|
5289
|
-
rec.costUsdCents = costUsdCents;
|
|
5807
|
+
rec.costUsdCents = 0;
|
|
5290
5808
|
rec.dedupeKey = computeDedupeKey(
|
|
5291
5809
|
rec.source,
|
|
5292
5810
|
rec.model,
|
|
@@ -5303,7 +5821,12 @@ async function syncCommand(opts) {
|
|
|
5303
5821
|
error("Not logged in. Run `token-rats login` first.");
|
|
5304
5822
|
process.exit(1);
|
|
5305
5823
|
}
|
|
5306
|
-
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
|
+
});
|
|
5307
5830
|
const claudeFiles = discoverClaudeCodeFiles();
|
|
5308
5831
|
if (opts.verbose) {
|
|
5309
5832
|
info(`Found ${claudeFiles.length} Claude Code file(s) in ~/.claude/projects/`);
|
|
@@ -5313,7 +5836,7 @@ async function syncCommand(opts) {
|
|
|
5313
5836
|
for (const file of claudeFiles) {
|
|
5314
5837
|
let text;
|
|
5315
5838
|
try {
|
|
5316
|
-
text =
|
|
5839
|
+
text = fs4.readFileSync(file, "utf8");
|
|
5317
5840
|
} catch {
|
|
5318
5841
|
if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
|
|
5319
5842
|
continue;
|
|
@@ -5335,7 +5858,7 @@ async function syncCommand(opts) {
|
|
|
5335
5858
|
for (const file of codexFiles) {
|
|
5336
5859
|
let text;
|
|
5337
5860
|
try {
|
|
5338
|
-
text =
|
|
5861
|
+
text = fs4.readFileSync(file, "utf8");
|
|
5339
5862
|
} catch {
|
|
5340
5863
|
if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
|
|
5341
5864
|
continue;
|
|
@@ -5396,7 +5919,9 @@ async function syncCommand(opts) {
|
|
|
5396
5919
|
}
|
|
5397
5920
|
info(`Discovered ${allSessions.length} session(s) from ${sourceStr}.`);
|
|
5398
5921
|
if (opts.dryRun) {
|
|
5399
|
-
info(
|
|
5922
|
+
info(
|
|
5923
|
+
`[dry-run] Would upload ${allSessions.length} session(s) in ${Math.ceil(allSessions.length / BATCH_SIZE)} batch(es).`
|
|
5924
|
+
);
|
|
5400
5925
|
if (opts.verbose) {
|
|
5401
5926
|
for (const s of allSessions.slice(0, 10)) {
|
|
5402
5927
|
dim(
|
|
@@ -5419,6 +5944,14 @@ async function syncCommand(opts) {
|
|
|
5419
5944
|
totalDuplicates += res.duplicates;
|
|
5420
5945
|
} catch (err) {
|
|
5421
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
|
+
}
|
|
5422
5955
|
if (err instanceof ApiError2 && err.status === 401) {
|
|
5423
5956
|
error("Session expired. Run `token-rats login` to re-authenticate.");
|
|
5424
5957
|
process.exit(1);
|
|
@@ -5436,12 +5969,47 @@ async function syncCommand(opts) {
|
|
|
5436
5969
|
}
|
|
5437
5970
|
|
|
5438
5971
|
// src/commands/watch.ts
|
|
5439
|
-
import * as
|
|
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;
|
|
5440
5976
|
var seen = /* @__PURE__ */ new Set();
|
|
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 {};
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
function saveWatchState(state) {
|
|
5991
|
+
try {
|
|
5992
|
+
const file = stateFilePath();
|
|
5993
|
+
fs5.mkdirSync(path4.dirname(file), { recursive: true });
|
|
5994
|
+
fs5.writeFileSync(file, JSON.stringify(state), { mode: 384 });
|
|
5995
|
+
} catch (err) {
|
|
5996
|
+
if (err instanceof Error) {
|
|
5997
|
+
console.warn(`watch-state write failed: ${err.message}`);
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
function statFile(p) {
|
|
6002
|
+
try {
|
|
6003
|
+
const s = fs5.statSync(p);
|
|
6004
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
6005
|
+
} catch {
|
|
6006
|
+
return null;
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
5441
6009
|
function parseAndFilter(filePath, verbose) {
|
|
5442
6010
|
let text;
|
|
5443
6011
|
try {
|
|
5444
|
-
text =
|
|
6012
|
+
text = fs5.readFileSync(filePath, "utf8");
|
|
5445
6013
|
} catch {
|
|
5446
6014
|
if (verbose) warn(`Could not read ${filePath} \u2014 skipping`);
|
|
5447
6015
|
return [];
|
|
@@ -5475,6 +6043,14 @@ async function upload(client, records, verbose) {
|
|
|
5475
6043
|
success(`Uploaded ${res.accepted} session(s)`);
|
|
5476
6044
|
}
|
|
5477
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
|
+
}
|
|
5478
6054
|
if (err instanceof ApiError2 && err.status === 401) {
|
|
5479
6055
|
error("Session expired. Run `token-rats login` to re-authenticate.");
|
|
5480
6056
|
process.exit(1);
|
|
@@ -5484,22 +6060,22 @@ async function upload(client, records, verbose) {
|
|
|
5484
6060
|
}
|
|
5485
6061
|
function makeDebounced(fn, ms) {
|
|
5486
6062
|
const timers = /* @__PURE__ */ new Map();
|
|
5487
|
-
return (
|
|
5488
|
-
const existing = timers.get(
|
|
6063
|
+
return (path5) => {
|
|
6064
|
+
const existing = timers.get(path5);
|
|
5489
6065
|
if (existing) clearTimeout(existing);
|
|
5490
6066
|
timers.set(
|
|
5491
|
-
|
|
6067
|
+
path5,
|
|
5492
6068
|
setTimeout(() => {
|
|
5493
|
-
timers.delete(
|
|
5494
|
-
fn(
|
|
6069
|
+
timers.delete(path5);
|
|
6070
|
+
fn(path5);
|
|
5495
6071
|
}, ms)
|
|
5496
6072
|
);
|
|
5497
6073
|
};
|
|
5498
6074
|
}
|
|
5499
6075
|
function findJsonlFiles2(dir) {
|
|
5500
6076
|
const results = [];
|
|
5501
|
-
if (!
|
|
5502
|
-
const entries =
|
|
6077
|
+
if (!fs5.existsSync(dir)) return results;
|
|
6078
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
5503
6079
|
for (const entry of entries) {
|
|
5504
6080
|
const full = `${dir}/${entry.name}`;
|
|
5505
6081
|
if (entry.isDirectory()) {
|
|
@@ -5511,40 +6087,89 @@ function findJsonlFiles2(dir) {
|
|
|
5511
6087
|
return results;
|
|
5512
6088
|
}
|
|
5513
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
|
+
}
|
|
5514
6096
|
const token = loadToken();
|
|
5515
6097
|
if (!token) {
|
|
5516
6098
|
error("Not logged in. Run `token-rats login` first.");
|
|
5517
6099
|
process.exit(1);
|
|
5518
6100
|
}
|
|
5519
|
-
const client = new ApiClient({
|
|
6101
|
+
const client = new ApiClient({
|
|
6102
|
+
apiUrl: opts.apiUrl,
|
|
6103
|
+
token,
|
|
6104
|
+
deviceId: ensureDeviceId(),
|
|
6105
|
+
cliVersion: CLI_VERSION
|
|
6106
|
+
});
|
|
5520
6107
|
const debounceMs = opts.interval ?? 2e3;
|
|
5521
6108
|
const dir = claudeCodeProjectsDir();
|
|
5522
|
-
if (!
|
|
6109
|
+
if (!fs5.existsSync(dir)) {
|
|
5523
6110
|
warn(`Claude Code projects directory not found: ${dir}`);
|
|
5524
6111
|
warn("No files to watch. Exiting.");
|
|
5525
6112
|
process.exit(0);
|
|
5526
6113
|
}
|
|
5527
6114
|
info(`Watching ${dir} (debounce: ${debounceMs}ms)`);
|
|
5528
6115
|
info("Press Ctrl-C to stop.\n");
|
|
6116
|
+
const persistedState = loadWatchState();
|
|
6117
|
+
const liveState = /* @__PURE__ */ new Map();
|
|
5529
6118
|
const initialFiles = findJsonlFiles2(dir);
|
|
5530
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
|
+
}
|
|
5531
6127
|
parseAndFilter(f, false);
|
|
5532
6128
|
}
|
|
5533
6129
|
if (opts.verbose) {
|
|
5534
6130
|
dim(`Initial snapshot: ${seen.size} session(s) in ${initialFiles.length} file(s)`);
|
|
5535
6131
|
}
|
|
6132
|
+
saveWatchState(Object.fromEntries(liveState));
|
|
5536
6133
|
const onChanged = makeDebounced(async (filePath) => {
|
|
5537
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
|
+
}
|
|
5538
6144
|
const fresh = parseAndFilter(filePath, opts.verbose ?? false);
|
|
5539
6145
|
if (fresh.length > 0) {
|
|
5540
6146
|
await upload(client, fresh, opts.verbose ?? false);
|
|
5541
6147
|
}
|
|
5542
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
|
+
});
|
|
5543
6170
|
let cleanup = null;
|
|
5544
6171
|
try {
|
|
5545
|
-
const chokidar = await new Function("m", "return import(m)")(
|
|
5546
|
-
"chokidar"
|
|
5547
|
-
);
|
|
6172
|
+
const chokidar = await new Function("m", "return import(m)")("chokidar");
|
|
5548
6173
|
const watcher = chokidar.watch(`${dir}/**/*.jsonl`, {
|
|
5549
6174
|
ignoreInitial: true,
|
|
5550
6175
|
persistent: true,
|
|
@@ -5552,9 +6177,7 @@ async function watchCommand(opts) {
|
|
|
5552
6177
|
});
|
|
5553
6178
|
watcher.on("add", (p) => onChanged(p));
|
|
5554
6179
|
watcher.on("change", (p) => onChanged(p));
|
|
5555
|
-
cleanup = () =>
|
|
5556
|
-
watcher.close();
|
|
5557
|
-
};
|
|
6180
|
+
cleanup = () => watcher.close();
|
|
5558
6181
|
if (opts.verbose) dim("Using chokidar for file watching");
|
|
5559
6182
|
} catch {
|
|
5560
6183
|
if (opts.verbose) dim("chokidar not available; using Node built-in fs.watch");
|
|
@@ -5562,7 +6185,7 @@ async function watchCommand(opts) {
|
|
|
5562
6185
|
let lastMtimes = /* @__PURE__ */ new Map();
|
|
5563
6186
|
for (const f of initialFiles) {
|
|
5564
6187
|
try {
|
|
5565
|
-
lastMtimes.set(f,
|
|
6188
|
+
lastMtimes.set(f, fs5.statSync(f).mtimeMs);
|
|
5566
6189
|
} catch {
|
|
5567
6190
|
}
|
|
5568
6191
|
}
|
|
@@ -5570,7 +6193,7 @@ async function watchCommand(opts) {
|
|
|
5570
6193
|
const current = findJsonlFiles2(dir);
|
|
5571
6194
|
for (const f of current) {
|
|
5572
6195
|
try {
|
|
5573
|
-
const mtime =
|
|
6196
|
+
const mtime = fs5.statSync(f).mtimeMs;
|
|
5574
6197
|
const prev = lastMtimes.get(f) ?? 0;
|
|
5575
6198
|
if (mtime > prev) {
|
|
5576
6199
|
lastMtimes.set(f, mtime);
|
|
@@ -5612,6 +6235,7 @@ async function watchCommand(opts) {
|
|
|
5612
6235
|
}
|
|
5613
6236
|
function shutdown() {
|
|
5614
6237
|
info("Shutting down\u2026");
|
|
6238
|
+
clearInterval(heartbeatTimer);
|
|
5615
6239
|
if (cleanup) cleanup();
|
|
5616
6240
|
process.exit(0);
|
|
5617
6241
|
}
|
|
@@ -5626,11 +6250,25 @@ async function whoamiCommand(opts) {
|
|
|
5626
6250
|
error("Not logged in. Run `token-rats login` first.");
|
|
5627
6251
|
process.exit(1);
|
|
5628
6252
|
}
|
|
5629
|
-
const client = new ApiClient({
|
|
6253
|
+
const client = new ApiClient({
|
|
6254
|
+
apiUrl: opts.apiUrl,
|
|
6255
|
+
token,
|
|
6256
|
+
deviceId: ensureDeviceId(),
|
|
6257
|
+
cliVersion: CLI_VERSION
|
|
6258
|
+
});
|
|
5630
6259
|
try {
|
|
5631
6260
|
const res = await client.getMe();
|
|
5632
6261
|
info(`Signed in as \x1B[1m@${res.user.handle}\x1B[0m`);
|
|
6262
|
+
info(`Device id: ${ensureDeviceId()}`);
|
|
5633
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
|
+
}
|
|
5634
6272
|
if (err instanceof ApiError2 && err.status === 401) {
|
|
5635
6273
|
error("Your session has expired. Run `token-rats login` to re-authenticate.");
|
|
5636
6274
|
process.exit(1);
|
|
@@ -5642,7 +6280,7 @@ async function whoamiCommand(opts) {
|
|
|
5642
6280
|
|
|
5643
6281
|
// src/index.ts
|
|
5644
6282
|
function getVersion() {
|
|
5645
|
-
return
|
|
6283
|
+
return CLI_VERSION;
|
|
5646
6284
|
}
|
|
5647
6285
|
function printHelp() {
|
|
5648
6286
|
console.log(`
|
|
@@ -5652,14 +6290,17 @@ function printHelp() {
|
|
|
5652
6290
|
token-rats <command> [flags]
|
|
5653
6291
|
|
|
5654
6292
|
\x1B[1mCommands:\x1B[0m
|
|
5655
|
-
login
|
|
5656
|
-
sync
|
|
5657
|
-
watch
|
|
5658
|
-
whoami
|
|
5659
|
-
logout
|
|
5660
|
-
install-
|
|
5661
|
-
|
|
5662
|
-
|
|
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
|
|
5663
6304
|
|
|
5664
6305
|
\x1B[1mFlags (all commands):\x1B[0m
|
|
5665
6306
|
--api-url <url> Override API URL (default: https://api.tokenrats.com)
|
|
@@ -5697,6 +6338,7 @@ function parseArgs(argv) {
|
|
|
5697
6338
|
let dryRun = false;
|
|
5698
6339
|
let verbose = false;
|
|
5699
6340
|
let interval;
|
|
6341
|
+
let noDaemon = false;
|
|
5700
6342
|
let i = 0;
|
|
5701
6343
|
while (i < argv.length) {
|
|
5702
6344
|
const arg = argv[i];
|
|
@@ -5712,6 +6354,8 @@ function parseArgs(argv) {
|
|
|
5712
6354
|
interval = Number(argv[++i]);
|
|
5713
6355
|
} else if (arg.startsWith("--interval=")) {
|
|
5714
6356
|
interval = Number(arg.slice("--interval=".length));
|
|
6357
|
+
} else if (arg === "--no-daemon") {
|
|
6358
|
+
noDaemon = true;
|
|
5715
6359
|
} else if (arg === "--version" || arg === "-V") {
|
|
5716
6360
|
console.log(getVersion());
|
|
5717
6361
|
process.exit(0);
|
|
@@ -5724,18 +6368,18 @@ function parseArgs(argv) {
|
|
|
5724
6368
|
i++;
|
|
5725
6369
|
}
|
|
5726
6370
|
const [command = null, ...rest] = positional;
|
|
5727
|
-
return { command, apiUrl, dryRun, verbose, interval, rest };
|
|
6371
|
+
return { command, apiUrl, dryRun, verbose, interval, noDaemon, rest };
|
|
5728
6372
|
}
|
|
5729
6373
|
async function main() {
|
|
5730
6374
|
const args = parseArgs(process.argv.slice(2));
|
|
5731
|
-
const { command, apiUrl, dryRun, verbose, interval } = args;
|
|
6375
|
+
const { command, apiUrl, dryRun, verbose, interval, noDaemon } = args;
|
|
5732
6376
|
if (!command || command === "help") {
|
|
5733
6377
|
printHelp();
|
|
5734
6378
|
process.exit(0);
|
|
5735
6379
|
}
|
|
5736
6380
|
switch (command) {
|
|
5737
6381
|
case "login":
|
|
5738
|
-
await loginCommand({ apiUrl });
|
|
6382
|
+
await loginCommand({ apiUrl, noDaemon });
|
|
5739
6383
|
break;
|
|
5740
6384
|
case "sync":
|
|
5741
6385
|
await syncCommand({ apiUrl, dryRun, verbose });
|
|
@@ -5752,6 +6396,15 @@ async function main() {
|
|
|
5752
6396
|
case "install-cursor":
|
|
5753
6397
|
await installCursorCommand();
|
|
5754
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;
|
|
5755
6408
|
default:
|
|
5756
6409
|
console.error(`\x1B[31mUnknown command: ${command}\x1B[0m`);
|
|
5757
6410
|
console.error("Run \x1B[1mtoken-rats help\x1B[0m for a list of commands.");
|