token-rats 0.0.2 → 0.0.4
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 +0 -4
- package/dist/index.js +305 -202
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -86,7 +86,3 @@ Authentication uses a device-code flow:
|
|
|
86
86
|
Token Rats is open source. The CLI source is in [`packages/cli/`](.) and the parsers are in [`packages/parsers/`](../parsers/). You can inspect exactly what is read from your disk and what is sent to the server.
|
|
87
87
|
|
|
88
88
|
**Privacy posture:** Token Rats reads usage counts only — never prompts or completions. The parser source is in `packages/parsers/`. We literally can't read what you typed.
|
|
89
|
-
|
|
90
|
-
## License
|
|
91
|
-
|
|
92
|
-
MIT
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/commands/install-cursor.ts
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
async function installCursorCommand(opts = {}) {
|
|
6
|
+
const pm = opts.packageManager ?? "npm";
|
|
7
|
+
console.log("\x1B[1mtoken-rats install-cursor\x1B[0m");
|
|
8
|
+
console.log(
|
|
9
|
+
"Installs better-sqlite3 globally for faster Cursor extraction.\nYou don't need this for Cursor support to work \u2014 sql.js (already\nbundled) handles it. This is purely a speed upgrade for large DBs.\n"
|
|
10
|
+
);
|
|
11
|
+
const args = ["install", "-g", "better-sqlite3@^9.4.3"];
|
|
12
|
+
console.log(`\x1B[2m$ ${pm} ${args.join(" ")}\x1B[0m
|
|
13
|
+
`);
|
|
14
|
+
const exitCode = await new Promise((resolve) => {
|
|
15
|
+
const child = spawn(pm, args, { stdio: "inherit" });
|
|
16
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
17
|
+
child.on("error", (err) => {
|
|
18
|
+
console.error(`\x1B[31mFailed to launch ${pm}: ${err.message}\x1B[0m`);
|
|
19
|
+
resolve(1);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
if (exitCode === 0) {
|
|
23
|
+
console.log(
|
|
24
|
+
"\n\x1B[32m\u2713\x1B[0m Done. Future `token-rats sync` runs will prefer better-sqlite3."
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.error(
|
|
29
|
+
`
|
|
30
|
+
\x1B[31mInstall failed (exit ${exitCode}). Cursor still works via sql.js \u2014 no action required.\x1B[0m`
|
|
31
|
+
);
|
|
32
|
+
process.exit(exitCode);
|
|
33
|
+
}
|
|
34
|
+
|
|
3
35
|
// ../../node_modules/.pnpm/zod@3.23.8/node_modules/zod/lib/index.mjs
|
|
4
36
|
var util;
|
|
5
37
|
(function(util2) {
|
|
@@ -4247,7 +4279,11 @@ var ENDPOINTS = {
|
|
|
4247
4279
|
stripeWebhook: "/webhooks/stripe",
|
|
4248
4280
|
// Phase 3 Track M
|
|
4249
4281
|
proxyAnthropicMessages: "/v1/proxy/anthropic/v1/messages",
|
|
4250
|
-
proxyAnthropicKey: "/v1/proxy/keys/anthropic"
|
|
4282
|
+
proxyAnthropicKey: "/v1/proxy/keys/anthropic",
|
|
4283
|
+
// Admin analytics (project-owner only)
|
|
4284
|
+
adminSignups: "/v1/admin/signups",
|
|
4285
|
+
adminActivity: "/v1/admin/activity",
|
|
4286
|
+
adminReferrers: "/v1/admin/referrers"
|
|
4251
4287
|
};
|
|
4252
4288
|
|
|
4253
4289
|
// ../contracts/src/errors.ts
|
|
@@ -4305,6 +4341,48 @@ var LiveEvent = z.discriminatedUnion("kind", [
|
|
|
4305
4341
|
})
|
|
4306
4342
|
]);
|
|
4307
4343
|
|
|
4344
|
+
// ../contracts/src/admin.ts
|
|
4345
|
+
var AdminSignupsDay = z.object({
|
|
4346
|
+
day: z.string(),
|
|
4347
|
+
// YYYY-MM-DD UTC
|
|
4348
|
+
/** Newly created users on this day. */
|
|
4349
|
+
newUsers: z.number().int().nonnegative(),
|
|
4350
|
+
/** Running total of users created up to and including this day. */
|
|
4351
|
+
cumulative: z.number().int().nonnegative()
|
|
4352
|
+
});
|
|
4353
|
+
var AdminSignupsResponse = z.object({
|
|
4354
|
+
totalUsers: z.number().int().nonnegative(),
|
|
4355
|
+
/** Length = 30, ordered oldest -> newest. */
|
|
4356
|
+
series: z.array(AdminSignupsDay),
|
|
4357
|
+
generatedAt: z.number().int().positive()
|
|
4358
|
+
});
|
|
4359
|
+
var AdminActivityDay = z.object({
|
|
4360
|
+
day: z.string(),
|
|
4361
|
+
// YYYY-MM-DD UTC
|
|
4362
|
+
activeUsers: z.number().int().nonnegative()
|
|
4363
|
+
});
|
|
4364
|
+
var AdminActivityResponse = z.object({
|
|
4365
|
+
/** Length = 30, ordered oldest -> newest. */
|
|
4366
|
+
series: z.array(AdminActivityDay),
|
|
4367
|
+
/** Distinct users active in the full 30-day window. */
|
|
4368
|
+
activeUsers30d: z.number().int().nonnegative(),
|
|
4369
|
+
generatedAt: z.number().int().positive()
|
|
4370
|
+
});
|
|
4371
|
+
var AdminReferrerRow = z.object({
|
|
4372
|
+
/** A label describing the source of the signal. */
|
|
4373
|
+
label: z.string(),
|
|
4374
|
+
/** Number of users (or events) attributed to this label. */
|
|
4375
|
+
count: z.number().int().nonnegative()
|
|
4376
|
+
});
|
|
4377
|
+
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
|
+
rows: z.array(AdminReferrerRow),
|
|
4383
|
+
generatedAt: z.number().int().positive()
|
|
4384
|
+
});
|
|
4385
|
+
|
|
4308
4386
|
// src/lib/api.ts
|
|
4309
4387
|
var DEFAULT_API_URL = "https://api.tokenrats.com";
|
|
4310
4388
|
function isTransient(status) {
|
|
@@ -4334,7 +4412,7 @@ var ApiClient = class {
|
|
|
4334
4412
|
headers() {
|
|
4335
4413
|
const h = { "Content-Type": "application/json" };
|
|
4336
4414
|
if (this.token) {
|
|
4337
|
-
h
|
|
4415
|
+
h.Authorization = `Bearer ${this.token}`;
|
|
4338
4416
|
}
|
|
4339
4417
|
return h;
|
|
4340
4418
|
}
|
|
@@ -4354,7 +4432,7 @@ var ApiClient = class {
|
|
|
4354
4432
|
}
|
|
4355
4433
|
attempt++;
|
|
4356
4434
|
if (attempt <= maxRetries) {
|
|
4357
|
-
await sleep(1e3 *
|
|
4435
|
+
await sleep(1e3 * 2 ** (attempt - 1));
|
|
4358
4436
|
}
|
|
4359
4437
|
}
|
|
4360
4438
|
throw lastErr;
|
|
@@ -4421,7 +4499,7 @@ import * as fs from "node:fs";
|
|
|
4421
4499
|
import * as os from "node:os";
|
|
4422
4500
|
import * as path from "node:path";
|
|
4423
4501
|
function tokenDir() {
|
|
4424
|
-
const xdgConfig = process.env
|
|
4502
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
4425
4503
|
const base = xdgConfig ?? path.join(os.homedir(), ".config");
|
|
4426
4504
|
return path.join(base, "token-rats");
|
|
4427
4505
|
}
|
|
@@ -4560,7 +4638,7 @@ async function loginCommand(opts) {
|
|
|
4560
4638
|
const codeMatch = verificationUrl.match(/[?&]code=([A-Z0-9-]+)/);
|
|
4561
4639
|
const code = codeMatch?.[1] ?? "";
|
|
4562
4640
|
console.log("");
|
|
4563
|
-
console.log(
|
|
4641
|
+
console.log(" Open this URL to sign in:");
|
|
4564
4642
|
console.log(` \x1B[1m\x1B[36m${verificationUrl}\x1B[0m`);
|
|
4565
4643
|
if (code) {
|
|
4566
4644
|
console.log(` Code: \x1B[1m${code}\x1B[0m`);
|
|
@@ -4713,13 +4791,13 @@ function parseClaudeCode(input) {
|
|
|
4713
4791
|
}
|
|
4714
4792
|
if (typeof event !== "object" || event === null) continue;
|
|
4715
4793
|
const ev = event;
|
|
4716
|
-
const sidCamel = ev
|
|
4717
|
-
const sidSnake = ev
|
|
4794
|
+
const sidCamel = ev.sessionId;
|
|
4795
|
+
const sidSnake = ev.session_id;
|
|
4718
4796
|
const sessionId = typeof sidCamel === "string" ? sidCamel : typeof sidSnake === "string" ? sidSnake : null;
|
|
4719
4797
|
if (!sessionId) continue;
|
|
4720
|
-
const rawTs = ev
|
|
4798
|
+
const rawTs = ev.timestamp;
|
|
4721
4799
|
let timestamp = 0;
|
|
4722
|
-
if (typeof rawTs === "number" && isFinite(rawTs)) {
|
|
4800
|
+
if (typeof rawTs === "number" && Number.isFinite(rawTs)) {
|
|
4723
4801
|
timestamp = rawTs;
|
|
4724
4802
|
} else if (typeof rawTs === "string") {
|
|
4725
4803
|
const parsed = Date.parse(rawTs);
|
|
@@ -4741,18 +4819,18 @@ function parseClaudeCode(input) {
|
|
|
4741
4819
|
if (acc.startedAt === 0 || timestamp < acc.startedAt) acc.startedAt = timestamp;
|
|
4742
4820
|
if (timestamp > acc.endedAt) acc.endedAt = timestamp;
|
|
4743
4821
|
}
|
|
4744
|
-
if (ev
|
|
4745
|
-
const message = ev
|
|
4822
|
+
if (ev.type !== "assistant") continue;
|
|
4823
|
+
const message = ev.message;
|
|
4746
4824
|
if (typeof message !== "object" || message === null) continue;
|
|
4747
4825
|
const msg = message;
|
|
4748
|
-
if (typeof msg
|
|
4749
|
-
acc.model = msg
|
|
4826
|
+
if (typeof msg.model === "string" && msg.model.length > 0) {
|
|
4827
|
+
acc.model = msg.model;
|
|
4750
4828
|
}
|
|
4751
|
-
const usage = msg
|
|
4829
|
+
const usage = msg.usage;
|
|
4752
4830
|
if (typeof usage === "object" && usage !== null) {
|
|
4753
4831
|
const u = usage;
|
|
4754
|
-
const inputTokens = toNonNegInt(u
|
|
4755
|
-
const outputTokens = toNonNegInt(u
|
|
4832
|
+
const inputTokens = toNonNegInt(u.input_tokens);
|
|
4833
|
+
const outputTokens = toNonNegInt(u.output_tokens);
|
|
4756
4834
|
acc.inTokens += inputTokens;
|
|
4757
4835
|
acc.outTokens += outputTokens;
|
|
4758
4836
|
}
|
|
@@ -4786,7 +4864,7 @@ function parseClaudeCode(input) {
|
|
|
4786
4864
|
return results;
|
|
4787
4865
|
}
|
|
4788
4866
|
function toNonNegInt(v) {
|
|
4789
|
-
if (typeof v !== "number" || !isFinite(v)) return 0;
|
|
4867
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return 0;
|
|
4790
4868
|
return Math.max(0, Math.floor(v));
|
|
4791
4869
|
}
|
|
4792
4870
|
|
|
@@ -4811,14 +4889,14 @@ function parseCodex(input) {
|
|
|
4811
4889
|
}
|
|
4812
4890
|
if (typeof event !== "object" || event === null) continue;
|
|
4813
4891
|
const ev = event;
|
|
4814
|
-
const ts = parseTimestamp(ev
|
|
4815
|
-
const type = typeof ev
|
|
4816
|
-
const payload = typeof ev
|
|
4892
|
+
const ts = parseTimestamp(ev.timestamp);
|
|
4893
|
+
const type = typeof ev.type === "string" ? ev.type : null;
|
|
4894
|
+
const payload = typeof ev.payload === "object" && ev.payload !== null ? ev.payload : null;
|
|
4817
4895
|
if (type === "session_meta" && payload) {
|
|
4818
|
-
const id = typeof payload
|
|
4896
|
+
const id = typeof payload.id === "string" ? payload.id : null;
|
|
4819
4897
|
if (!id) continue;
|
|
4820
4898
|
currentSessionId = id;
|
|
4821
|
-
const metaTs = parseTimestamp(payload
|
|
4899
|
+
const metaTs = parseTimestamp(payload.timestamp) || ts;
|
|
4822
4900
|
const acc2 = upsert(sessions, id);
|
|
4823
4901
|
if (metaTs > 0 && (acc2.startedAt === 0 || metaTs < acc2.startedAt)) {
|
|
4824
4902
|
acc2.startedAt = metaTs;
|
|
@@ -4832,20 +4910,20 @@ function parseCodex(input) {
|
|
|
4832
4910
|
if (acc.startedAt === 0) acc.startedAt = ts;
|
|
4833
4911
|
if (ts > acc.endedAt) acc.endedAt = ts;
|
|
4834
4912
|
}
|
|
4835
|
-
if (type === "turn_context" && payload && typeof payload
|
|
4836
|
-
acc.model = payload
|
|
4913
|
+
if (type === "turn_context" && payload && typeof payload.model === "string") {
|
|
4914
|
+
acc.model = payload.model;
|
|
4837
4915
|
continue;
|
|
4838
4916
|
}
|
|
4839
|
-
if (type === "event_msg" && payload && payload
|
|
4840
|
-
const info2 = payload
|
|
4917
|
+
if (type === "event_msg" && payload && payload.type === "token_count") {
|
|
4918
|
+
const info2 = payload.info;
|
|
4841
4919
|
if (typeof info2 !== "object" || info2 === null) continue;
|
|
4842
|
-
const total = info2
|
|
4920
|
+
const total = info2.total_token_usage;
|
|
4843
4921
|
if (typeof total !== "object" || total === null) continue;
|
|
4844
4922
|
const t = total;
|
|
4845
|
-
const inputTotal = toNonNegInt2(t
|
|
4846
|
-
const cachedInput = toNonNegInt2(t
|
|
4847
|
-
const output = toNonNegInt2(t
|
|
4848
|
-
const reasoning = toNonNegInt2(t
|
|
4923
|
+
const inputTotal = toNonNegInt2(t.input_tokens);
|
|
4924
|
+
const cachedInput = toNonNegInt2(t.cached_input_tokens);
|
|
4925
|
+
const output = toNonNegInt2(t.output_tokens);
|
|
4926
|
+
const reasoning = toNonNegInt2(t.reasoning_output_tokens);
|
|
4849
4927
|
acc.inTokens = Math.max(0, inputTotal - cachedInput);
|
|
4850
4928
|
acc.outTokens = output + reasoning;
|
|
4851
4929
|
}
|
|
@@ -4857,13 +4935,7 @@ function parseCodex(input) {
|
|
|
4857
4935
|
if (startedAt <= 0 || endedAt <= 0) continue;
|
|
4858
4936
|
const model = acc.model.length > 0 ? acc.model : "unknown";
|
|
4859
4937
|
const { costUsdCents } = priceOf(model, acc.inTokens, acc.outTokens);
|
|
4860
|
-
const dedupeKey = computeDedupeKey(
|
|
4861
|
-
"codex",
|
|
4862
|
-
model,
|
|
4863
|
-
startedAt,
|
|
4864
|
-
acc.inTokens,
|
|
4865
|
-
acc.outTokens
|
|
4866
|
-
);
|
|
4938
|
+
const dedupeKey = computeDedupeKey("codex", model, startedAt, acc.inTokens, acc.outTokens);
|
|
4867
4939
|
results.push({
|
|
4868
4940
|
id: `codex:${acc.sessionId}`,
|
|
4869
4941
|
source: "codex",
|
|
@@ -4894,7 +4966,7 @@ function upsert(map, sessionId) {
|
|
|
4894
4966
|
return acc;
|
|
4895
4967
|
}
|
|
4896
4968
|
function parseTimestamp(v) {
|
|
4897
|
-
if (typeof v === "number" && isFinite(v)) return v;
|
|
4969
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
4898
4970
|
if (typeof v === "string") {
|
|
4899
4971
|
const parsed = Date.parse(v);
|
|
4900
4972
|
if (!Number.isNaN(parsed)) return parsed;
|
|
@@ -4902,26 +4974,21 @@ function parseTimestamp(v) {
|
|
|
4902
4974
|
return 0;
|
|
4903
4975
|
}
|
|
4904
4976
|
function toNonNegInt2(v) {
|
|
4905
|
-
if (typeof v !== "number" || !isFinite(v)) return 0;
|
|
4977
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return 0;
|
|
4906
4978
|
return Math.max(0, Math.floor(v));
|
|
4907
4979
|
}
|
|
4908
4980
|
|
|
4909
4981
|
// ../parsers/src/cursor.ts
|
|
4910
|
-
var
|
|
4911
|
-
|
|
4912
|
-
"
|
|
4913
|
-
"claude-3-5-sonnet": "claude-3-5-sonnet-20241022",
|
|
4914
|
-
"claude-3.5-haiku": "claude-3-5-haiku-20241022",
|
|
4915
|
-
"claude-3-5-haiku": "claude-3-5-haiku-20241022",
|
|
4916
|
-
"claude-3.5-opus": "claude-3-opus-20240229",
|
|
4917
|
-
"claude-3-opus": "claude-3-opus-20240229",
|
|
4918
|
-
"claude-3-sonnet": "claude-3-sonnet-20240229",
|
|
4919
|
-
"claude-3-haiku": "claude-3-haiku-20240307",
|
|
4920
|
-
// GPT models — Cursor may omit date suffixes
|
|
4921
|
-
"gpt-4o-mini": "gpt-4o-mini",
|
|
4922
|
-
"gpt-4o": "gpt-4o",
|
|
4923
|
-
"gpt-4-turbo": "gpt-4-turbo"
|
|
4982
|
+
var ESTIMATES = {
|
|
4983
|
+
composer: { inTokens: 1e4, outTokens: 2e3, model: "cursor-composer" }
|
|
4984
|
+
// "tab" deliberately omitted — see file header.
|
|
4924
4985
|
};
|
|
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
|
+
}
|
|
4925
4992
|
function parseCursor(input) {
|
|
4926
4993
|
let text;
|
|
4927
4994
|
if (typeof input === "string") {
|
|
@@ -4942,15 +5009,15 @@ function parseCursor(input) {
|
|
|
4942
5009
|
const row = raw;
|
|
4943
5010
|
const id = typeof row["id"] === "string" ? row["id"] : null;
|
|
4944
5011
|
if (!id) continue;
|
|
4945
|
-
const
|
|
4946
|
-
|
|
4947
|
-
const
|
|
4948
|
-
|
|
4949
|
-
const
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
const
|
|
4953
|
-
const dedupeKey = computeDedupeKey("cursor", model,
|
|
5012
|
+
const type = typeof row["type"] === "string" ? row["type"] : null;
|
|
5013
|
+
if (!type) continue;
|
|
5014
|
+
const unixMs = typeof row["unixMs"] === "number" && isFinite(row["unixMs"]) && row["unixMs"] > 0 ? row["unixMs"] : null;
|
|
5015
|
+
if (unixMs === null) continue;
|
|
5016
|
+
const estimate = ESTIMATES[type];
|
|
5017
|
+
if (!estimate) continue;
|
|
5018
|
+
const { inTokens, outTokens, model } = estimate;
|
|
5019
|
+
const costUsdCents = estimatedCostCents(inTokens, outTokens);
|
|
5020
|
+
const dedupeKey = computeDedupeKey("cursor", model, unixMs, inTokens, outTokens);
|
|
4954
5021
|
results.push({
|
|
4955
5022
|
id: `cursor:${id}`,
|
|
4956
5023
|
source: "cursor",
|
|
@@ -4958,128 +5025,167 @@ function parseCursor(input) {
|
|
|
4958
5025
|
inTokens,
|
|
4959
5026
|
outTokens,
|
|
4960
5027
|
costUsdCents,
|
|
4961
|
-
startedAt,
|
|
4962
|
-
endedAt,
|
|
5028
|
+
startedAt: unixMs,
|
|
5029
|
+
endedAt: unixMs,
|
|
4963
5030
|
dedupeKey
|
|
4964
5031
|
});
|
|
4965
5032
|
}
|
|
4966
5033
|
return results;
|
|
4967
5034
|
}
|
|
4968
|
-
function toNonNegInt3(v) {
|
|
4969
|
-
if (typeof v !== "number" || !isFinite(v)) return 0;
|
|
4970
|
-
return Math.max(0, Math.floor(v));
|
|
4971
|
-
}
|
|
4972
5035
|
|
|
4973
5036
|
// src/lib/cursor-extract.ts
|
|
4974
|
-
|
|
4975
|
-
|
|
5037
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
5038
|
+
import { readFile } from "node:fs/promises";
|
|
5039
|
+
import { homedir as homedir2 } from "node:os";
|
|
5040
|
+
import { join as join2 } from "node:path";
|
|
5041
|
+
function cursorWorkspaceStorageDir() {
|
|
5042
|
+
const home = homedir2();
|
|
5043
|
+
const rel = join2("Cursor", "User", "workspaceStorage");
|
|
5044
|
+
if (process.platform === "darwin") {
|
|
5045
|
+
return join2(home, "Library", "Application Support", rel);
|
|
5046
|
+
}
|
|
5047
|
+
if (process.platform === "win32") {
|
|
5048
|
+
const appData = process.env["APPDATA"] ?? join2(home, "AppData", "Roaming");
|
|
5049
|
+
return join2(appData, rel);
|
|
5050
|
+
}
|
|
5051
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"] ?? join2(home, ".config");
|
|
5052
|
+
return join2(xdgConfig, rel);
|
|
5053
|
+
}
|
|
5054
|
+
function discoverWorkspaceDbs() {
|
|
5055
|
+
const root = cursorWorkspaceStorageDir();
|
|
5056
|
+
if (!existsSync(root)) return [];
|
|
5057
|
+
const out = [];
|
|
5058
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
5059
|
+
if (!entry.isDirectory()) continue;
|
|
5060
|
+
const candidate = join2(root, entry.name, "state.vscdb");
|
|
5061
|
+
if (existsSync(candidate)) out.push(candidate);
|
|
5062
|
+
}
|
|
5063
|
+
return out;
|
|
5064
|
+
}
|
|
5065
|
+
async function openDb(dbPath) {
|
|
4976
5066
|
try {
|
|
4977
5067
|
const mod = await import("node:sqlite");
|
|
4978
|
-
|
|
4979
|
-
if (
|
|
5068
|
+
const DatabaseSync = mod.DatabaseSync;
|
|
5069
|
+
if (DatabaseSync) {
|
|
5070
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
5071
|
+
return {
|
|
5072
|
+
exec: (sql) => {
|
|
5073
|
+
const stmt = db.prepare(sql);
|
|
5074
|
+
const rows = stmt.all();
|
|
5075
|
+
if (rows.length === 0) return [];
|
|
5076
|
+
const columns = Object.keys(rows[0]);
|
|
5077
|
+
return [
|
|
5078
|
+
{
|
|
5079
|
+
columns,
|
|
5080
|
+
values: rows.map((r) => columns.map((c2) => r[c2]))
|
|
5081
|
+
}
|
|
5082
|
+
];
|
|
5083
|
+
},
|
|
5084
|
+
close: () => db.close()
|
|
5085
|
+
};
|
|
5086
|
+
}
|
|
4980
5087
|
} catch {
|
|
4981
|
-
return null;
|
|
4982
5088
|
}
|
|
4983
|
-
return readWithDb(() => new DatabaseConstructor(dbPath, { readOnly: true }));
|
|
4984
|
-
}
|
|
4985
|
-
async function tryBetterSqlite3(dbPath) {
|
|
4986
|
-
let Database;
|
|
4987
5089
|
try {
|
|
4988
|
-
const
|
|
4989
|
-
|
|
5090
|
+
const sqlJs = await import("sql.js");
|
|
5091
|
+
const initSqlJs = sqlJs.default ?? sqlJs;
|
|
5092
|
+
const SQL = await initSqlJs();
|
|
5093
|
+
const bytes = await readFile(dbPath);
|
|
5094
|
+
return new SQL.Database(new Uint8Array(bytes));
|
|
4990
5095
|
} catch {
|
|
4991
|
-
return null;
|
|
4992
5096
|
}
|
|
4993
|
-
return readWithDb(() => new Database(dbPath, { readonly: true, fileMustExist: true }));
|
|
4994
|
-
}
|
|
4995
|
-
function readWithDb(factory) {
|
|
4996
|
-
let db = null;
|
|
4997
5097
|
try {
|
|
4998
|
-
|
|
4999
|
-
const
|
|
5000
|
-
|
|
5098
|
+
const mod = await import("better-sqlite3");
|
|
5099
|
+
const Database = mod.default ?? mod;
|
|
5100
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
5101
|
+
return {
|
|
5102
|
+
exec: (sql) => {
|
|
5103
|
+
const stmt = db.prepare(sql);
|
|
5104
|
+
const rows = stmt.all();
|
|
5105
|
+
if (rows.length === 0) return [];
|
|
5106
|
+
const columns = Object.keys(rows[0]);
|
|
5107
|
+
return [
|
|
5108
|
+
{
|
|
5109
|
+
columns,
|
|
5110
|
+
values: rows.map((r) => columns.map((c2) => r[c2]))
|
|
5111
|
+
}
|
|
5112
|
+
];
|
|
5113
|
+
},
|
|
5114
|
+
close: () => db.close()
|
|
5115
|
+
};
|
|
5001
5116
|
} catch {
|
|
5002
5117
|
return null;
|
|
5003
|
-
} finally {
|
|
5004
|
-
try {
|
|
5005
|
-
db?.close();
|
|
5006
|
-
} catch {
|
|
5007
|
-
}
|
|
5008
5118
|
}
|
|
5009
5119
|
}
|
|
5010
|
-
function
|
|
5120
|
+
async function readGenerationsFromDb(dbPath) {
|
|
5121
|
+
const db = await openDb(dbPath);
|
|
5122
|
+
if (!db) return [];
|
|
5011
5123
|
try {
|
|
5012
|
-
const
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
if (typeof raw !== "string") return null;
|
|
5124
|
+
const res = db.exec(`SELECT value FROM ItemTable WHERE key = 'aiService.generations'`);
|
|
5125
|
+
if (res.length === 0 || res[0].values.length === 0) return [];
|
|
5126
|
+
const raw = res[0].values[0][0];
|
|
5127
|
+
if (typeof raw !== "string") return [];
|
|
5017
5128
|
let parsed;
|
|
5018
5129
|
try {
|
|
5019
5130
|
parsed = JSON.parse(raw);
|
|
5020
5131
|
} catch {
|
|
5021
|
-
return
|
|
5022
|
-
}
|
|
5023
|
-
if (!Array.isArray(parsed)) return
|
|
5024
|
-
|
|
5132
|
+
return [];
|
|
5133
|
+
}
|
|
5134
|
+
if (!Array.isArray(parsed)) return [];
|
|
5135
|
+
const out = [];
|
|
5136
|
+
for (const item of parsed) {
|
|
5137
|
+
if (typeof item !== "object" || item === null) continue;
|
|
5138
|
+
const r = item;
|
|
5139
|
+
const id = typeof r["generationUUID"] === "string" ? r["generationUUID"] : null;
|
|
5140
|
+
const type = typeof r["type"] === "string" ? r["type"] : null;
|
|
5141
|
+
const unixMs = typeof r["unixMs"] === "number" && isFinite(r["unixMs"]) && r["unixMs"] > 0 ? r["unixMs"] : null;
|
|
5142
|
+
if (!id || !type || unixMs === null) continue;
|
|
5143
|
+
out.push({ id, type, unixMs });
|
|
5144
|
+
}
|
|
5145
|
+
return out;
|
|
5025
5146
|
} catch {
|
|
5026
|
-
return
|
|
5027
|
-
}
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
`SELECT id, model,
|
|
5033
|
-
prompt_tokens as promptTokens, completion_tokens as completionTokens,
|
|
5034
|
-
started_at as startedAt, ended_at as endedAt
|
|
5035
|
-
FROM cursor_requests
|
|
5036
|
-
ORDER BY started_at DESC
|
|
5037
|
-
LIMIT 50000`
|
|
5038
|
-
);
|
|
5039
|
-
const rows = stmt.all();
|
|
5040
|
-
return normalizeRows(rows);
|
|
5041
|
-
} catch {
|
|
5042
|
-
return null;
|
|
5147
|
+
return [];
|
|
5148
|
+
} finally {
|
|
5149
|
+
try {
|
|
5150
|
+
db.close();
|
|
5151
|
+
} catch {
|
|
5152
|
+
}
|
|
5043
5153
|
}
|
|
5044
5154
|
}
|
|
5045
|
-
function
|
|
5046
|
-
const
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
const promptTokens = toNonNegInt4(r["promptTokens"] ?? r["prompt_tokens"]);
|
|
5054
|
-
const completionTokens = toNonNegInt4(r["completionTokens"] ?? r["completion_tokens"]);
|
|
5055
|
-
const startedAt = toPositiveMs(r["startedAt"] ?? r["started_at"]);
|
|
5056
|
-
const endedAt = toPositiveMs(r["endedAt"] ?? r["ended_at"]);
|
|
5057
|
-
if (startedAt === null || endedAt === null) continue;
|
|
5058
|
-
results.push({ id, model, promptTokens, completionTokens, startedAt, endedAt });
|
|
5155
|
+
async function extractCursorGenerations() {
|
|
5156
|
+
const dbPaths = discoverWorkspaceDbs();
|
|
5157
|
+
if (dbPaths.length === 0) {
|
|
5158
|
+
return {
|
|
5159
|
+
rows: [],
|
|
5160
|
+
skipped: null,
|
|
5161
|
+
dbCount: 0
|
|
5162
|
+
};
|
|
5059
5163
|
}
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5164
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
5165
|
+
const rows = [];
|
|
5166
|
+
let openFailures = 0;
|
|
5167
|
+
for (const p of dbPaths) {
|
|
5168
|
+
let perDb = [];
|
|
5169
|
+
try {
|
|
5170
|
+
perDb = await readGenerationsFromDb(p);
|
|
5171
|
+
} catch {
|
|
5172
|
+
openFailures++;
|
|
5173
|
+
continue;
|
|
5174
|
+
}
|
|
5175
|
+
for (const r of perDb) {
|
|
5176
|
+
if (seen2.has(r.id)) continue;
|
|
5177
|
+
seen2.add(r.id);
|
|
5178
|
+
rows.push(r);
|
|
5179
|
+
}
|
|
5074
5180
|
}
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5181
|
+
if (rows.length === 0 && openFailures === dbPaths.length) {
|
|
5182
|
+
return {
|
|
5183
|
+
rows: [],
|
|
5184
|
+
dbCount: dbPaths.length,
|
|
5185
|
+
skipped: "Could not open any Cursor workspace DB (sqlite drivers unavailable). Run `npx token-rats install-cursor` for native speed, or upgrade to Node \u226522.5."
|
|
5186
|
+
};
|
|
5078
5187
|
}
|
|
5079
|
-
return {
|
|
5080
|
-
rows: [],
|
|
5081
|
-
skipped: "Could not open Cursor DB (no sqlite driver available). Skipping Cursor source."
|
|
5082
|
-
};
|
|
5188
|
+
return { rows, dbCount: dbPaths.length, skipped: null };
|
|
5083
5189
|
}
|
|
5084
5190
|
|
|
5085
5191
|
// src/lib/discover.ts
|
|
@@ -5103,7 +5209,7 @@ function findJsonlFiles(dir) {
|
|
|
5103
5209
|
function claudeCodeProjectsDir() {
|
|
5104
5210
|
const home = os2.homedir();
|
|
5105
5211
|
if (process.platform === "win32") {
|
|
5106
|
-
const profile = process.env
|
|
5212
|
+
const profile = process.env.USERPROFILE ?? home;
|
|
5107
5213
|
return path2.join(profile, ".claude", "projects");
|
|
5108
5214
|
}
|
|
5109
5215
|
return path2.join(home, ".claude", "projects");
|
|
@@ -5116,9 +5222,9 @@ function codexSessionsDirs() {
|
|
|
5116
5222
|
const home = os2.homedir();
|
|
5117
5223
|
const candidates = [];
|
|
5118
5224
|
if (process.platform === "win32") {
|
|
5119
|
-
const profile = process.env
|
|
5225
|
+
const profile = process.env.USERPROFILE ?? home;
|
|
5120
5226
|
candidates.push(path2.join(profile, ".codex", "sessions"));
|
|
5121
|
-
const appData = process.env
|
|
5227
|
+
const appData = process.env.APPDATA ?? path2.join(profile, "AppData", "Roaming");
|
|
5122
5228
|
candidates.push(path2.join(appData, "Codex", "sessions"));
|
|
5123
5229
|
} else {
|
|
5124
5230
|
candidates.push(path2.join(home, ".codex", "sessions"));
|
|
@@ -5152,23 +5258,6 @@ function discoverCodexFiles() {
|
|
|
5152
5258
|
}
|
|
5153
5259
|
return out;
|
|
5154
5260
|
}
|
|
5155
|
-
function cursorDbPath() {
|
|
5156
|
-
const home = os2.homedir();
|
|
5157
|
-
const rel = path2.join("Cursor", "User", "globalStorage", "state.vscdb");
|
|
5158
|
-
if (process.platform === "darwin") {
|
|
5159
|
-
return path2.join(home, "Library", "Application Support", rel);
|
|
5160
|
-
}
|
|
5161
|
-
if (process.platform === "win32") {
|
|
5162
|
-
const appData = process.env["APPDATA"] ?? path2.join(home, "AppData", "Roaming");
|
|
5163
|
-
return path2.join(appData, rel);
|
|
5164
|
-
}
|
|
5165
|
-
const xdgConfig = process.env["XDG_CONFIG_HOME"] ?? path2.join(home, ".config");
|
|
5166
|
-
return path2.join(xdgConfig, rel);
|
|
5167
|
-
}
|
|
5168
|
-
function discoverCursorDb() {
|
|
5169
|
-
const p = cursorDbPath();
|
|
5170
|
-
return fs2.existsSync(p) ? p : null;
|
|
5171
|
-
}
|
|
5172
5261
|
|
|
5173
5262
|
// src/commands/sync.ts
|
|
5174
5263
|
var BATCH_SIZE = 500;
|
|
@@ -5260,25 +5349,26 @@ async function syncCommand(opts) {
|
|
|
5260
5349
|
}
|
|
5261
5350
|
}
|
|
5262
5351
|
const cursorSessions = [];
|
|
5263
|
-
const
|
|
5264
|
-
if (
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
if (
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5352
|
+
const { rows, skipped, dbCount } = await extractCursorGenerations();
|
|
5353
|
+
if (skipped) {
|
|
5354
|
+
warn(skipped);
|
|
5355
|
+
} else if (dbCount === 0) {
|
|
5356
|
+
if (opts.verbose) info("No Cursor workspace storage found \u2014 skipping Cursor source");
|
|
5357
|
+
} else if (rows.length > 0) {
|
|
5358
|
+
if (opts.verbose) info(`Scanned ${dbCount} Cursor workspace DB(s)`);
|
|
5359
|
+
try {
|
|
5360
|
+
const records = parseCursor(JSON.stringify(rows));
|
|
5361
|
+
cursorSessions.push(...records);
|
|
5362
|
+
if (opts.verbose) {
|
|
5363
|
+
dim(
|
|
5364
|
+
` Cursor: ${rows.length} generation event(s) \u2192 ${records.length} session(s) (tokens estimated, see help)`
|
|
5365
|
+
);
|
|
5276
5366
|
}
|
|
5277
|
-
}
|
|
5278
|
-
|
|
5367
|
+
} catch {
|
|
5368
|
+
if (opts.verbose) warn("Failed to parse Cursor rows \u2014 skipping");
|
|
5279
5369
|
}
|
|
5280
|
-
} else {
|
|
5281
|
-
|
|
5370
|
+
} else if (opts.verbose) {
|
|
5371
|
+
dim(` Scanned ${dbCount} Cursor workspace DB(s): no AI generations found`);
|
|
5282
5372
|
}
|
|
5283
5373
|
const mergedClaude = mergeBySessionId(claudeSessions);
|
|
5284
5374
|
if (opts.verbose && mergedClaude.length !== claudeSessions.length) {
|
|
@@ -5452,7 +5542,9 @@ async function watchCommand(opts) {
|
|
|
5452
5542
|
}, debounceMs);
|
|
5453
5543
|
let cleanup = null;
|
|
5454
5544
|
try {
|
|
5455
|
-
const chokidar = await new Function("m", "return import(m)")(
|
|
5545
|
+
const chokidar = await new Function("m", "return import(m)")(
|
|
5546
|
+
"chokidar"
|
|
5547
|
+
);
|
|
5456
5548
|
const watcher = chokidar.watch(`${dir}/**/*.jsonl`, {
|
|
5457
5549
|
ignoreInitial: true,
|
|
5458
5550
|
persistent: true,
|
|
@@ -5504,7 +5596,7 @@ async function watchCommand(opts) {
|
|
|
5504
5596
|
const watcher = watch(dir, { recursive: true, signal: controller.signal });
|
|
5505
5597
|
for await (const event of watcher) {
|
|
5506
5598
|
const filename = event.filename;
|
|
5507
|
-
if (filename
|
|
5599
|
+
if (filename?.endsWith(".jsonl")) {
|
|
5508
5600
|
const fullPath = `${dir}/${filename}`;
|
|
5509
5601
|
onChanged(fullPath);
|
|
5510
5602
|
}
|
|
@@ -5550,7 +5642,7 @@ async function whoamiCommand(opts) {
|
|
|
5550
5642
|
|
|
5551
5643
|
// src/index.ts
|
|
5552
5644
|
function getVersion() {
|
|
5553
|
-
return "0.0.
|
|
5645
|
+
return "0.0.4";
|
|
5554
5646
|
}
|
|
5555
5647
|
function printHelp() {
|
|
5556
5648
|
console.log(`
|
|
@@ -5560,12 +5652,14 @@ function printHelp() {
|
|
|
5560
5652
|
token-rats <command> [flags]
|
|
5561
5653
|
|
|
5562
5654
|
\x1B[1mCommands:\x1B[0m
|
|
5563
|
-
login
|
|
5564
|
-
sync
|
|
5565
|
-
watch
|
|
5566
|
-
whoami
|
|
5567
|
-
logout
|
|
5568
|
-
|
|
5655
|
+
login Authenticate with Token Rats (opens browser)
|
|
5656
|
+
sync Read local Claude Code + Cursor logs and upload counts
|
|
5657
|
+
watch Watch logs in real-time; upload new sessions as they appear
|
|
5658
|
+
whoami Show the currently signed-in account
|
|
5659
|
+
logout Clear your stored credentials
|
|
5660
|
+
install-cursor Install better-sqlite3 globally for faster Cursor reads
|
|
5661
|
+
(sql.js works out of the box \u2014 this is opt-in speed-up)
|
|
5662
|
+
help Show this help message
|
|
5569
5663
|
|
|
5570
5664
|
\x1B[1mFlags (all commands):\x1B[0m
|
|
5571
5665
|
--api-url <url> Override API URL (default: https://api.tokenrats.com)
|
|
@@ -5583,6 +5677,12 @@ function printHelp() {
|
|
|
5583
5677
|
The parser source is in packages/parsers/. We literally can't read
|
|
5584
5678
|
what you typed.
|
|
5585
5679
|
|
|
5680
|
+
\x1B[1mCursor notes:\x1B[0m
|
|
5681
|
+
Cursor doesn't store token counts locally, so per-request tokens
|
|
5682
|
+
are *estimated* (10k in / 2k out per composer turn, claude-3-5-sonnet
|
|
5683
|
+
rates). Tab autocomplete is excluded. Numbers are comparable across
|
|
5684
|
+
Token Rats users but won't match cursor.com to the token.
|
|
5685
|
+
|
|
5586
5686
|
\x1B[1mExamples:\x1B[0m
|
|
5587
5687
|
npx token-rats login
|
|
5588
5688
|
npx token-rats sync
|
|
@@ -5649,9 +5749,12 @@ async function main() {
|
|
|
5649
5749
|
case "logout":
|
|
5650
5750
|
logoutCommand();
|
|
5651
5751
|
break;
|
|
5752
|
+
case "install-cursor":
|
|
5753
|
+
await installCursorCommand();
|
|
5754
|
+
break;
|
|
5652
5755
|
default:
|
|
5653
5756
|
console.error(`\x1B[31mUnknown command: ${command}\x1B[0m`);
|
|
5654
|
-
console.error(
|
|
5757
|
+
console.error("Run \x1B[1mtoken-rats help\x1B[0m for a list of commands.");
|
|
5655
5758
|
process.exit(1);
|
|
5656
5759
|
}
|
|
5657
5760
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-rats",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Sync your Claude Code + Cursor token usage to your Token Rats leaderboard.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"token-rats": "./dist/index.js"
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"prepublishOnly": "node build.mjs"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"better-sqlite3": "^9.4.3",
|
|
30
29
|
"chokidar": "^3.6.0",
|
|
31
30
|
"clipboardy": "^4.0.0",
|
|
32
31
|
"open": "^10.1.0"
|
|
@@ -37,9 +36,13 @@
|
|
|
37
36
|
"@token-rats/pricing": "workspace:*",
|
|
38
37
|
"@types/better-sqlite3": "^7.6.12",
|
|
39
38
|
"@types/node": "22.10.2",
|
|
39
|
+
"@types/sql.js": "^1.4.11",
|
|
40
40
|
"esbuild": "^0.24.2",
|
|
41
41
|
"tsx": "4.19.2",
|
|
42
42
|
"typescript": "5.7.2",
|
|
43
43
|
"vitest": "2.1.8"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"sql.js": "^1.14.1"
|
|
44
47
|
}
|
|
45
48
|
}
|