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.
Files changed (3) hide show
  1. package/README.md +0 -4
  2. package/dist/index.js +305 -202
  3. 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["Authorization"] = `Bearer ${this.token}`;
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 * Math.pow(2, attempt - 1));
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["XDG_CONFIG_HOME"];
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(` Open this URL to sign in:`);
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["sessionId"];
4717
- const sidSnake = ev["session_id"];
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["timestamp"];
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["type"] !== "assistant") continue;
4745
- const message = ev["message"];
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["model"] === "string" && msg["model"].length > 0) {
4749
- acc.model = msg["model"];
4826
+ if (typeof msg.model === "string" && msg.model.length > 0) {
4827
+ acc.model = msg.model;
4750
4828
  }
4751
- const usage = msg["usage"];
4829
+ const usage = msg.usage;
4752
4830
  if (typeof usage === "object" && usage !== null) {
4753
4831
  const u = usage;
4754
- const inputTokens = toNonNegInt(u["input_tokens"]);
4755
- const outputTokens = toNonNegInt(u["output_tokens"]);
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["timestamp"]);
4815
- const type = typeof ev["type"] === "string" ? ev["type"] : null;
4816
- const payload = typeof ev["payload"] === "object" && ev["payload"] !== null ? ev["payload"] : null;
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["id"] === "string" ? payload["id"] : null;
4896
+ const id = typeof payload.id === "string" ? payload.id : null;
4819
4897
  if (!id) continue;
4820
4898
  currentSessionId = id;
4821
- const metaTs = parseTimestamp(payload["timestamp"]) || ts;
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["model"] === "string") {
4836
- acc.model = payload["model"];
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["type"] === "token_count") {
4840
- const info2 = payload["info"];
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["total_token_usage"];
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["input_tokens"]);
4846
- const cachedInput = toNonNegInt2(t["cached_input_tokens"]);
4847
- const output = toNonNegInt2(t["output_tokens"]);
4848
- const reasoning = toNonNegInt2(t["reasoning_output_tokens"]);
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 CURSOR_MODEL_MAP = {
4911
- // Claude models Cursor uses abbreviated names without date suffixes
4912
- "claude-3.5-sonnet": "claude-3-5-sonnet-20241022",
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 rawModel = typeof row["model"] === "string" ? row["model"] : "";
4946
- const model = rawModel.length > 0 ? CURSOR_MODEL_MAP[rawModel] ?? rawModel : "unknown";
4947
- const inTokens = toNonNegInt3(row["promptTokens"]);
4948
- const outTokens = toNonNegInt3(row["completionTokens"]);
4949
- const startedAt = typeof row["startedAt"] === "number" && isFinite(row["startedAt"]) && row["startedAt"] > 0 ? row["startedAt"] : null;
4950
- const endedAt = typeof row["endedAt"] === "number" && isFinite(row["endedAt"]) && row["endedAt"] > 0 ? row["endedAt"] : null;
4951
- if (startedAt === null || endedAt === null) continue;
4952
- const { costUsdCents } = priceOf(model, inTokens, outTokens);
4953
- const dedupeKey = computeDedupeKey("cursor", model, startedAt, inTokens, outTokens);
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
- async function tryNodeSqlite(dbPath) {
4975
- let DatabaseConstructor;
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
- DatabaseConstructor = mod.DatabaseSync;
4979
- if (!DatabaseConstructor) return null;
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 mod = await import("better-sqlite3");
4989
- Database = mod.default ?? mod;
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
- db = factory();
4999
- const rows = tryItemTable(db) ?? tryDirectTable(db) ?? [];
5000
- return rows;
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 tryItemTable(db) {
5120
+ async function readGenerationsFromDb(dbPath) {
5121
+ const db = await openDb(dbPath);
5122
+ if (!db) return [];
5011
5123
  try {
5012
- const stmt = db.prepare("SELECT value FROM ItemTable WHERE key = 'aiRequests'");
5013
- const rows = stmt.all();
5014
- if (rows.length === 0) return null;
5015
- const raw = rows[0]?.value;
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 null;
5022
- }
5023
- if (!Array.isArray(parsed)) return null;
5024
- return normalizeRows(parsed);
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 null;
5027
- }
5028
- }
5029
- function tryDirectTable(db) {
5030
- try {
5031
- const stmt = db.prepare(
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 normalizeRows(rows) {
5046
- const results = [];
5047
- for (const raw of rows) {
5048
- if (typeof raw !== "object" || raw === null) continue;
5049
- const r = raw;
5050
- const id = typeof r["id"] === "string" ? r["id"] : null;
5051
- if (!id) continue;
5052
- const model = typeof r["model"] === "string" ? r["model"] : "unknown";
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
- return results;
5061
- }
5062
- function toNonNegInt4(v) {
5063
- if (typeof v !== "number" || !isFinite(v)) return 0;
5064
- return Math.max(0, Math.floor(v));
5065
- }
5066
- function toPositiveMs(v) {
5067
- if (typeof v !== "number" || !isFinite(v) || v <= 0) return null;
5068
- return v;
5069
- }
5070
- async function readCursorDb(dbPath) {
5071
- const fromNodeSqlite = await tryNodeSqlite(dbPath);
5072
- if (fromNodeSqlite !== null) {
5073
- return { rows: fromNodeSqlite, skipped: null };
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
- const fromBetter = await tryBetterSqlite3(dbPath);
5076
- if (fromBetter !== null) {
5077
- return { rows: fromBetter, skipped: null };
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["USERPROFILE"] ?? home;
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["USERPROFILE"] ?? home;
5225
+ const profile = process.env.USERPROFILE ?? home;
5120
5226
  candidates.push(path2.join(profile, ".codex", "sessions"));
5121
- const appData = process.env["APPDATA"] ?? path2.join(profile, "AppData", "Roaming");
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 cursorDbPath2 = discoverCursorDb();
5264
- if (cursorDbPath2) {
5265
- if (opts.verbose) info(`Found Cursor DB at ${cursorDbPath2}`);
5266
- const { rows, skipped } = await readCursorDb(cursorDbPath2);
5267
- if (skipped) {
5268
- warn(skipped);
5269
- } else if (rows.length > 0) {
5270
- try {
5271
- const records = parseCursor(JSON.stringify(rows));
5272
- cursorSessions.push(...records);
5273
- if (opts.verbose) dim(` Cursor DB: ${records.length} session(s)`);
5274
- } catch {
5275
- if (opts.verbose) warn("Failed to parse Cursor rows \u2014 skipping");
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
- } else if (opts.verbose) {
5278
- dim(" Cursor DB: 0 rows found");
5367
+ } catch {
5368
+ if (opts.verbose) warn("Failed to parse Cursor rows \u2014 skipping");
5279
5369
  }
5280
- } else {
5281
- if (opts.verbose) info("Cursor DB not found \u2014 skipping Cursor source");
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)")("chokidar");
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 && filename.endsWith(".jsonl")) {
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.1";
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 Authenticate with Token Rats (opens browser)
5564
- sync Read local Claude Code + Cursor logs and upload counts
5565
- watch Watch logs in real-time; upload new sessions as they appear
5566
- whoami Show the currently signed-in account
5567
- logout Clear your stored credentials
5568
- help Show this help message
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(`Run \x1B[1mtoken-rats help\x1B[0m for a list of commands.`);
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.2",
3
+ "version": "0.0.4",
4
4
  "description": "Sync your Claude Code + Cursor token usage to your Token Rats leaderboard.",
5
- "license": "MIT",
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
  }