token-rats 0.0.3 → 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 (2) hide show
  1. package/dist/index.js +257 -232
  2. package/package.json +3 -13
package/dist/index.js CHANGED
@@ -4279,7 +4279,11 @@ var ENDPOINTS = {
4279
4279
  stripeWebhook: "/webhooks/stripe",
4280
4280
  // Phase 3 Track M
4281
4281
  proxyAnthropicMessages: "/v1/proxy/anthropic/v1/messages",
4282
- 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"
4283
4287
  };
4284
4288
 
4285
4289
  // ../contracts/src/errors.ts
@@ -4337,6 +4341,48 @@ var LiveEvent = z.discriminatedUnion("kind", [
4337
4341
  })
4338
4342
  ]);
4339
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
+
4340
4386
  // src/lib/api.ts
4341
4387
  var DEFAULT_API_URL = "https://api.tokenrats.com";
4342
4388
  function isTransient(status) {
@@ -4366,7 +4412,7 @@ var ApiClient = class {
4366
4412
  headers() {
4367
4413
  const h = { "Content-Type": "application/json" };
4368
4414
  if (this.token) {
4369
- h["Authorization"] = `Bearer ${this.token}`;
4415
+ h.Authorization = `Bearer ${this.token}`;
4370
4416
  }
4371
4417
  return h;
4372
4418
  }
@@ -4386,7 +4432,7 @@ var ApiClient = class {
4386
4432
  }
4387
4433
  attempt++;
4388
4434
  if (attempt <= maxRetries) {
4389
- await sleep(1e3 * Math.pow(2, attempt - 1));
4435
+ await sleep(1e3 * 2 ** (attempt - 1));
4390
4436
  }
4391
4437
  }
4392
4438
  throw lastErr;
@@ -4453,7 +4499,7 @@ import * as fs from "node:fs";
4453
4499
  import * as os from "node:os";
4454
4500
  import * as path from "node:path";
4455
4501
  function tokenDir() {
4456
- const xdgConfig = process.env["XDG_CONFIG_HOME"];
4502
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
4457
4503
  const base = xdgConfig ?? path.join(os.homedir(), ".config");
4458
4504
  return path.join(base, "token-rats");
4459
4505
  }
@@ -4592,7 +4638,7 @@ async function loginCommand(opts) {
4592
4638
  const codeMatch = verificationUrl.match(/[?&]code=([A-Z0-9-]+)/);
4593
4639
  const code = codeMatch?.[1] ?? "";
4594
4640
  console.log("");
4595
- console.log(` Open this URL to sign in:`);
4641
+ console.log(" Open this URL to sign in:");
4596
4642
  console.log(` \x1B[1m\x1B[36m${verificationUrl}\x1B[0m`);
4597
4643
  if (code) {
4598
4644
  console.log(` Code: \x1B[1m${code}\x1B[0m`);
@@ -4745,13 +4791,13 @@ function parseClaudeCode(input) {
4745
4791
  }
4746
4792
  if (typeof event !== "object" || event === null) continue;
4747
4793
  const ev = event;
4748
- const sidCamel = ev["sessionId"];
4749
- const sidSnake = ev["session_id"];
4794
+ const sidCamel = ev.sessionId;
4795
+ const sidSnake = ev.session_id;
4750
4796
  const sessionId = typeof sidCamel === "string" ? sidCamel : typeof sidSnake === "string" ? sidSnake : null;
4751
4797
  if (!sessionId) continue;
4752
- const rawTs = ev["timestamp"];
4798
+ const rawTs = ev.timestamp;
4753
4799
  let timestamp = 0;
4754
- if (typeof rawTs === "number" && isFinite(rawTs)) {
4800
+ if (typeof rawTs === "number" && Number.isFinite(rawTs)) {
4755
4801
  timestamp = rawTs;
4756
4802
  } else if (typeof rawTs === "string") {
4757
4803
  const parsed = Date.parse(rawTs);
@@ -4773,18 +4819,18 @@ function parseClaudeCode(input) {
4773
4819
  if (acc.startedAt === 0 || timestamp < acc.startedAt) acc.startedAt = timestamp;
4774
4820
  if (timestamp > acc.endedAt) acc.endedAt = timestamp;
4775
4821
  }
4776
- if (ev["type"] !== "assistant") continue;
4777
- const message = ev["message"];
4822
+ if (ev.type !== "assistant") continue;
4823
+ const message = ev.message;
4778
4824
  if (typeof message !== "object" || message === null) continue;
4779
4825
  const msg = message;
4780
- if (typeof msg["model"] === "string" && msg["model"].length > 0) {
4781
- acc.model = msg["model"];
4826
+ if (typeof msg.model === "string" && msg.model.length > 0) {
4827
+ acc.model = msg.model;
4782
4828
  }
4783
- const usage = msg["usage"];
4829
+ const usage = msg.usage;
4784
4830
  if (typeof usage === "object" && usage !== null) {
4785
4831
  const u = usage;
4786
- const inputTokens = toNonNegInt(u["input_tokens"]);
4787
- const outputTokens = toNonNegInt(u["output_tokens"]);
4832
+ const inputTokens = toNonNegInt(u.input_tokens);
4833
+ const outputTokens = toNonNegInt(u.output_tokens);
4788
4834
  acc.inTokens += inputTokens;
4789
4835
  acc.outTokens += outputTokens;
4790
4836
  }
@@ -4818,7 +4864,7 @@ function parseClaudeCode(input) {
4818
4864
  return results;
4819
4865
  }
4820
4866
  function toNonNegInt(v) {
4821
- if (typeof v !== "number" || !isFinite(v)) return 0;
4867
+ if (typeof v !== "number" || !Number.isFinite(v)) return 0;
4822
4868
  return Math.max(0, Math.floor(v));
4823
4869
  }
4824
4870
 
@@ -4843,14 +4889,14 @@ function parseCodex(input) {
4843
4889
  }
4844
4890
  if (typeof event !== "object" || event === null) continue;
4845
4891
  const ev = event;
4846
- const ts = parseTimestamp(ev["timestamp"]);
4847
- const type = typeof ev["type"] === "string" ? ev["type"] : null;
4848
- 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;
4849
4895
  if (type === "session_meta" && payload) {
4850
- const id = typeof payload["id"] === "string" ? payload["id"] : null;
4896
+ const id = typeof payload.id === "string" ? payload.id : null;
4851
4897
  if (!id) continue;
4852
4898
  currentSessionId = id;
4853
- const metaTs = parseTimestamp(payload["timestamp"]) || ts;
4899
+ const metaTs = parseTimestamp(payload.timestamp) || ts;
4854
4900
  const acc2 = upsert(sessions, id);
4855
4901
  if (metaTs > 0 && (acc2.startedAt === 0 || metaTs < acc2.startedAt)) {
4856
4902
  acc2.startedAt = metaTs;
@@ -4864,20 +4910,20 @@ function parseCodex(input) {
4864
4910
  if (acc.startedAt === 0) acc.startedAt = ts;
4865
4911
  if (ts > acc.endedAt) acc.endedAt = ts;
4866
4912
  }
4867
- if (type === "turn_context" && payload && typeof payload["model"] === "string") {
4868
- acc.model = payload["model"];
4913
+ if (type === "turn_context" && payload && typeof payload.model === "string") {
4914
+ acc.model = payload.model;
4869
4915
  continue;
4870
4916
  }
4871
- if (type === "event_msg" && payload && payload["type"] === "token_count") {
4872
- const info2 = payload["info"];
4917
+ if (type === "event_msg" && payload && payload.type === "token_count") {
4918
+ const info2 = payload.info;
4873
4919
  if (typeof info2 !== "object" || info2 === null) continue;
4874
- const total = info2["total_token_usage"];
4920
+ const total = info2.total_token_usage;
4875
4921
  if (typeof total !== "object" || total === null) continue;
4876
4922
  const t = total;
4877
- const inputTotal = toNonNegInt2(t["input_tokens"]);
4878
- const cachedInput = toNonNegInt2(t["cached_input_tokens"]);
4879
- const output = toNonNegInt2(t["output_tokens"]);
4880
- 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);
4881
4927
  acc.inTokens = Math.max(0, inputTotal - cachedInput);
4882
4928
  acc.outTokens = output + reasoning;
4883
4929
  }
@@ -4889,13 +4935,7 @@ function parseCodex(input) {
4889
4935
  if (startedAt <= 0 || endedAt <= 0) continue;
4890
4936
  const model = acc.model.length > 0 ? acc.model : "unknown";
4891
4937
  const { costUsdCents } = priceOf(model, acc.inTokens, acc.outTokens);
4892
- const dedupeKey = computeDedupeKey(
4893
- "codex",
4894
- model,
4895
- startedAt,
4896
- acc.inTokens,
4897
- acc.outTokens
4898
- );
4938
+ const dedupeKey = computeDedupeKey("codex", model, startedAt, acc.inTokens, acc.outTokens);
4899
4939
  results.push({
4900
4940
  id: `codex:${acc.sessionId}`,
4901
4941
  source: "codex",
@@ -4926,7 +4966,7 @@ function upsert(map, sessionId) {
4926
4966
  return acc;
4927
4967
  }
4928
4968
  function parseTimestamp(v) {
4929
- if (typeof v === "number" && isFinite(v)) return v;
4969
+ if (typeof v === "number" && Number.isFinite(v)) return v;
4930
4970
  if (typeof v === "string") {
4931
4971
  const parsed = Date.parse(v);
4932
4972
  if (!Number.isNaN(parsed)) return parsed;
@@ -4934,26 +4974,21 @@ function parseTimestamp(v) {
4934
4974
  return 0;
4935
4975
  }
4936
4976
  function toNonNegInt2(v) {
4937
- if (typeof v !== "number" || !isFinite(v)) return 0;
4977
+ if (typeof v !== "number" || !Number.isFinite(v)) return 0;
4938
4978
  return Math.max(0, Math.floor(v));
4939
4979
  }
4940
4980
 
4941
4981
  // ../parsers/src/cursor.ts
4942
- var CURSOR_MODEL_MAP = {
4943
- // Claude models Cursor uses abbreviated names without date suffixes
4944
- "claude-3.5-sonnet": "claude-3-5-sonnet-20241022",
4945
- "claude-3-5-sonnet": "claude-3-5-sonnet-20241022",
4946
- "claude-3.5-haiku": "claude-3-5-haiku-20241022",
4947
- "claude-3-5-haiku": "claude-3-5-haiku-20241022",
4948
- "claude-3.5-opus": "claude-3-opus-20240229",
4949
- "claude-3-opus": "claude-3-opus-20240229",
4950
- "claude-3-sonnet": "claude-3-sonnet-20240229",
4951
- "claude-3-haiku": "claude-3-haiku-20240307",
4952
- // GPT models — Cursor may omit date suffixes
4953
- "gpt-4o-mini": "gpt-4o-mini",
4954
- "gpt-4o": "gpt-4o",
4955
- "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.
4956
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
+ }
4957
4992
  function parseCursor(input) {
4958
4993
  let text;
4959
4994
  if (typeof input === "string") {
@@ -4974,15 +5009,15 @@ function parseCursor(input) {
4974
5009
  const row = raw;
4975
5010
  const id = typeof row["id"] === "string" ? row["id"] : null;
4976
5011
  if (!id) continue;
4977
- const rawModel = typeof row["model"] === "string" ? row["model"] : "";
4978
- const model = rawModel.length > 0 ? CURSOR_MODEL_MAP[rawModel] ?? rawModel : "unknown";
4979
- const inTokens = toNonNegInt3(row["promptTokens"]);
4980
- const outTokens = toNonNegInt3(row["completionTokens"]);
4981
- const startedAt = typeof row["startedAt"] === "number" && isFinite(row["startedAt"]) && row["startedAt"] > 0 ? row["startedAt"] : null;
4982
- const endedAt = typeof row["endedAt"] === "number" && isFinite(row["endedAt"]) && row["endedAt"] > 0 ? row["endedAt"] : null;
4983
- if (startedAt === null || endedAt === null) continue;
4984
- const { costUsdCents } = priceOf(model, inTokens, outTokens);
4985
- 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);
4986
5021
  results.push({
4987
5022
  id: `cursor:${id}`,
4988
5023
  source: "cursor",
@@ -4990,169 +5025,167 @@ function parseCursor(input) {
4990
5025
  inTokens,
4991
5026
  outTokens,
4992
5027
  costUsdCents,
4993
- startedAt,
4994
- endedAt,
5028
+ startedAt: unixMs,
5029
+ endedAt: unixMs,
4995
5030
  dedupeKey
4996
5031
  });
4997
5032
  }
4998
5033
  return results;
4999
5034
  }
5000
- function toNonNegInt3(v) {
5001
- if (typeof v !== "number" || !isFinite(v)) return 0;
5002
- return Math.max(0, Math.floor(v));
5003
- }
5004
5035
 
5005
5036
  // src/lib/cursor-extract.ts
5037
+ import { existsSync, readdirSync } from "node:fs";
5006
5038
  import { readFile } from "node:fs/promises";
5007
- async function tryNodeSqlite(dbPath) {
5008
- let DatabaseConstructor;
5009
- try {
5010
- const mod = await import("node:sqlite");
5011
- DatabaseConstructor = mod.DatabaseSync;
5012
- if (!DatabaseConstructor) return null;
5013
- } catch {
5014
- return null;
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);
5015
5062
  }
5016
- return readWithDb(() => new DatabaseConstructor(dbPath, { readOnly: true }));
5063
+ return out;
5017
5064
  }
5018
- async function trySqlJs(dbPath) {
5019
- let initSqlJs;
5065
+ async function openDb(dbPath) {
5020
5066
  try {
5021
- const mod = await import("sql.js");
5022
- initSqlJs = mod.default ?? mod;
5067
+ const mod = await import("node:sqlite");
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
+ }
5023
5087
  } catch {
5024
- return null;
5025
5088
  }
5026
- let SQL;
5027
- let fileBytes;
5028
5089
  try {
5029
- SQL = await initSqlJs();
5030
- fileBytes = await readFile(dbPath);
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));
5031
5095
  } catch {
5032
- return null;
5033
5096
  }
5034
- const sqlDb = new SQL.Database(new Uint8Array(fileBytes));
5035
- const adapter = {
5036
- prepare: (sql) => ({
5037
- all: () => {
5038
- const results = sqlDb.exec(sql);
5039
- if (results.length === 0) return [];
5040
- const { columns, values } = results[0];
5041
- return values.map((row) => {
5042
- const obj = {};
5043
- for (let i = 0; i < columns.length; i++) {
5044
- obj[columns[i]] = row[i];
5045
- }
5046
- return obj;
5047
- });
5048
- }
5049
- }),
5050
- close: () => sqlDb.close()
5051
- };
5052
- return readWithDb(() => adapter);
5053
- }
5054
- async function tryBetterSqlite3(dbPath) {
5055
- let Database;
5056
5097
  try {
5057
5098
  const mod = await import("better-sqlite3");
5058
- Database = mod.default ?? mod;
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
+ };
5059
5116
  } catch {
5060
5117
  return null;
5061
5118
  }
5062
- return readWithDb(() => new Database(dbPath, { readonly: true, fileMustExist: true }));
5063
5119
  }
5064
- function readWithDb(factory) {
5065
- let db = null;
5120
+ async function readGenerationsFromDb(dbPath) {
5121
+ const db = await openDb(dbPath);
5122
+ if (!db) return [];
5066
5123
  try {
5067
- db = factory();
5068
- const rows = tryItemTable(db) ?? tryDirectTable(db) ?? [];
5069
- return rows;
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 [];
5128
+ let parsed;
5129
+ try {
5130
+ parsed = JSON.parse(raw);
5131
+ } catch {
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;
5070
5146
  } catch {
5071
- return null;
5147
+ return [];
5072
5148
  } finally {
5073
5149
  try {
5074
- db?.close();
5150
+ db.close();
5075
5151
  } catch {
5076
5152
  }
5077
5153
  }
5078
5154
  }
5079
- function tryItemTable(db) {
5080
- try {
5081
- const stmt = db.prepare("SELECT value FROM ItemTable WHERE key = 'aiRequests'");
5082
- const rows = stmt.all();
5083
- if (rows.length === 0) return null;
5084
- const raw = rows[0]?.value;
5085
- if (typeof raw !== "string") return null;
5086
- let parsed;
5155
+ async function extractCursorGenerations() {
5156
+ const dbPaths = discoverWorkspaceDbs();
5157
+ if (dbPaths.length === 0) {
5158
+ return {
5159
+ rows: [],
5160
+ skipped: null,
5161
+ dbCount: 0
5162
+ };
5163
+ }
5164
+ const seen2 = /* @__PURE__ */ new Set();
5165
+ const rows = [];
5166
+ let openFailures = 0;
5167
+ for (const p of dbPaths) {
5168
+ let perDb = [];
5087
5169
  try {
5088
- parsed = JSON.parse(raw);
5170
+ perDb = await readGenerationsFromDb(p);
5089
5171
  } catch {
5090
- return null;
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);
5091
5179
  }
5092
- if (!Array.isArray(parsed)) return null;
5093
- return normalizeRows(parsed);
5094
- } catch {
5095
- return null;
5096
- }
5097
- }
5098
- function tryDirectTable(db) {
5099
- try {
5100
- const stmt = db.prepare(
5101
- `SELECT id, model,
5102
- prompt_tokens as promptTokens, completion_tokens as completionTokens,
5103
- started_at as startedAt, ended_at as endedAt
5104
- FROM cursor_requests
5105
- ORDER BY started_at DESC
5106
- LIMIT 50000`
5107
- );
5108
- const rows = stmt.all();
5109
- return normalizeRows(rows);
5110
- } catch {
5111
- return null;
5112
- }
5113
- }
5114
- function normalizeRows(rows) {
5115
- const results = [];
5116
- for (const raw of rows) {
5117
- if (typeof raw !== "object" || raw === null) continue;
5118
- const r = raw;
5119
- const id = typeof r["id"] === "string" ? r["id"] : null;
5120
- if (!id) continue;
5121
- const model = typeof r["model"] === "string" ? r["model"] : "unknown";
5122
- const promptTokens = toNonNegInt4(r["promptTokens"] ?? r["prompt_tokens"]);
5123
- const completionTokens = toNonNegInt4(r["completionTokens"] ?? r["completion_tokens"]);
5124
- const startedAt = toPositiveMs(r["startedAt"] ?? r["started_at"]);
5125
- const endedAt = toPositiveMs(r["endedAt"] ?? r["ended_at"]);
5126
- if (startedAt === null || endedAt === null) continue;
5127
- results.push({ id, model, promptTokens, completionTokens, startedAt, endedAt });
5128
- }
5129
- return results;
5130
- }
5131
- function toNonNegInt4(v) {
5132
- if (typeof v !== "number" || !isFinite(v)) return 0;
5133
- return Math.max(0, Math.floor(v));
5134
- }
5135
- function toPositiveMs(v) {
5136
- if (typeof v !== "number" || !isFinite(v) || v <= 0) return null;
5137
- return v;
5138
- }
5139
- async function readCursorDb(dbPath) {
5140
- const fromNodeSqlite = await tryNodeSqlite(dbPath);
5141
- if (fromNodeSqlite !== null) {
5142
- return { rows: fromNodeSqlite, skipped: null };
5143
- }
5144
- const fromSqlJs = await trySqlJs(dbPath);
5145
- if (fromSqlJs !== null) {
5146
- return { rows: fromSqlJs, skipped: null };
5147
5180
  }
5148
- const fromBetter = await tryBetterSqlite3(dbPath);
5149
- if (fromBetter !== null) {
5150
- 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
+ };
5151
5187
  }
5152
- return {
5153
- rows: [],
5154
- skipped: "Could not open Cursor DB (sqlite drivers unavailable). Run `npx token-rats install-cursor` for native speed, or upgrade to Node \u226522.5."
5155
- };
5188
+ return { rows, dbCount: dbPaths.length, skipped: null };
5156
5189
  }
5157
5190
 
5158
5191
  // src/lib/discover.ts
@@ -5176,7 +5209,7 @@ function findJsonlFiles(dir) {
5176
5209
  function claudeCodeProjectsDir() {
5177
5210
  const home = os2.homedir();
5178
5211
  if (process.platform === "win32") {
5179
- const profile = process.env["USERPROFILE"] ?? home;
5212
+ const profile = process.env.USERPROFILE ?? home;
5180
5213
  return path2.join(profile, ".claude", "projects");
5181
5214
  }
5182
5215
  return path2.join(home, ".claude", "projects");
@@ -5189,9 +5222,9 @@ function codexSessionsDirs() {
5189
5222
  const home = os2.homedir();
5190
5223
  const candidates = [];
5191
5224
  if (process.platform === "win32") {
5192
- const profile = process.env["USERPROFILE"] ?? home;
5225
+ const profile = process.env.USERPROFILE ?? home;
5193
5226
  candidates.push(path2.join(profile, ".codex", "sessions"));
5194
- const appData = process.env["APPDATA"] ?? path2.join(profile, "AppData", "Roaming");
5227
+ const appData = process.env.APPDATA ?? path2.join(profile, "AppData", "Roaming");
5195
5228
  candidates.push(path2.join(appData, "Codex", "sessions"));
5196
5229
  } else {
5197
5230
  candidates.push(path2.join(home, ".codex", "sessions"));
@@ -5225,23 +5258,6 @@ function discoverCodexFiles() {
5225
5258
  }
5226
5259
  return out;
5227
5260
  }
5228
- function cursorDbPath() {
5229
- const home = os2.homedir();
5230
- const rel = path2.join("Cursor", "User", "globalStorage", "state.vscdb");
5231
- if (process.platform === "darwin") {
5232
- return path2.join(home, "Library", "Application Support", rel);
5233
- }
5234
- if (process.platform === "win32") {
5235
- const appData = process.env["APPDATA"] ?? path2.join(home, "AppData", "Roaming");
5236
- return path2.join(appData, rel);
5237
- }
5238
- const xdgConfig = process.env["XDG_CONFIG_HOME"] ?? path2.join(home, ".config");
5239
- return path2.join(xdgConfig, rel);
5240
- }
5241
- function discoverCursorDb() {
5242
- const p = cursorDbPath();
5243
- return fs2.existsSync(p) ? p : null;
5244
- }
5245
5261
 
5246
5262
  // src/commands/sync.ts
5247
5263
  var BATCH_SIZE = 500;
@@ -5333,25 +5349,26 @@ async function syncCommand(opts) {
5333
5349
  }
5334
5350
  }
5335
5351
  const cursorSessions = [];
5336
- const cursorDbPath2 = discoverCursorDb();
5337
- if (cursorDbPath2) {
5338
- if (opts.verbose) info(`Found Cursor DB at ${cursorDbPath2}`);
5339
- const { rows, skipped } = await readCursorDb(cursorDbPath2);
5340
- if (skipped) {
5341
- warn(skipped);
5342
- } else if (rows.length > 0) {
5343
- try {
5344
- const records = parseCursor(JSON.stringify(rows));
5345
- cursorSessions.push(...records);
5346
- if (opts.verbose) dim(` Cursor DB: ${records.length} session(s)`);
5347
- } catch {
5348
- 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
+ );
5349
5366
  }
5350
- } else if (opts.verbose) {
5351
- dim(" Cursor DB: 0 rows found");
5367
+ } catch {
5368
+ if (opts.verbose) warn("Failed to parse Cursor rows \u2014 skipping");
5352
5369
  }
5353
- } else {
5354
- 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`);
5355
5372
  }
5356
5373
  const mergedClaude = mergeBySessionId(claudeSessions);
5357
5374
  if (opts.verbose && mergedClaude.length !== claudeSessions.length) {
@@ -5525,7 +5542,9 @@ async function watchCommand(opts) {
5525
5542
  }, debounceMs);
5526
5543
  let cleanup = null;
5527
5544
  try {
5528
- const chokidar = await new Function("m", "return import(m)")("chokidar");
5545
+ const chokidar = await new Function("m", "return import(m)")(
5546
+ "chokidar"
5547
+ );
5529
5548
  const watcher = chokidar.watch(`${dir}/**/*.jsonl`, {
5530
5549
  ignoreInitial: true,
5531
5550
  persistent: true,
@@ -5577,7 +5596,7 @@ async function watchCommand(opts) {
5577
5596
  const watcher = watch(dir, { recursive: true, signal: controller.signal });
5578
5597
  for await (const event of watcher) {
5579
5598
  const filename = event.filename;
5580
- if (filename && filename.endsWith(".jsonl")) {
5599
+ if (filename?.endsWith(".jsonl")) {
5581
5600
  const fullPath = `${dir}/${filename}`;
5582
5601
  onChanged(fullPath);
5583
5602
  }
@@ -5623,7 +5642,7 @@ async function whoamiCommand(opts) {
5623
5642
 
5624
5643
  // src/index.ts
5625
5644
  function getVersion() {
5626
- return "0.0.3";
5645
+ return "0.0.4";
5627
5646
  }
5628
5647
  function printHelp() {
5629
5648
  console.log(`
@@ -5658,6 +5677,12 @@ function printHelp() {
5658
5677
  The parser source is in packages/parsers/. We literally can't read
5659
5678
  what you typed.
5660
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
+
5661
5686
  \x1B[1mExamples:\x1B[0m
5662
5687
  npx token-rats login
5663
5688
  npx token-rats sync
@@ -5729,7 +5754,7 @@ async function main() {
5729
5754
  break;
5730
5755
  default:
5731
5756
  console.error(`\x1B[31mUnknown command: ${command}\x1B[0m`);
5732
- 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.");
5733
5758
  process.exit(1);
5734
5759
  }
5735
5760
  }
package/package.json CHANGED
@@ -1,30 +1,20 @@
1
1
  {
2
2
  "name": "token-rats",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Sync your Claude Code + Cursor token usage to your Token Rats leaderboard.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "token-rats": "./dist/index.js"
9
9
  },
10
- "files": [
11
- "dist",
12
- "README.md"
13
- ],
10
+ "files": ["dist", "README.md"],
14
11
  "repository": {
15
12
  "type": "git",
16
13
  "url": "git+https://github.com/hsalberti/token-rats.git",
17
14
  "directory": "packages/cli"
18
15
  },
19
16
  "homepage": "https://tokenrats.com",
20
- "keywords": [
21
- "claude",
22
- "claude-code",
23
- "cursor",
24
- "tokens",
25
- "leaderboard",
26
- "ai-usage"
27
- ],
17
+ "keywords": ["claude", "claude-code", "cursor", "tokens", "leaderboard", "ai-usage"],
28
18
  "publishConfig": {
29
19
  "access": "public"
30
20
  },