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.
- package/dist/index.js +257 -232
- 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
|
|
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 *
|
|
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
|
|
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(
|
|
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
|
|
4749
|
-
const sidSnake = ev
|
|
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
|
|
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
|
|
4777
|
-
const message = ev
|
|
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
|
|
4781
|
-
acc.model = msg
|
|
4826
|
+
if (typeof msg.model === "string" && msg.model.length > 0) {
|
|
4827
|
+
acc.model = msg.model;
|
|
4782
4828
|
}
|
|
4783
|
-
const usage = msg
|
|
4829
|
+
const usage = msg.usage;
|
|
4784
4830
|
if (typeof usage === "object" && usage !== null) {
|
|
4785
4831
|
const u = usage;
|
|
4786
|
-
const inputTokens = toNonNegInt(u
|
|
4787
|
-
const outputTokens = toNonNegInt(u
|
|
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
|
|
4847
|
-
const type = typeof ev
|
|
4848
|
-
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;
|
|
4849
4895
|
if (type === "session_meta" && payload) {
|
|
4850
|
-
const id = typeof payload
|
|
4896
|
+
const id = typeof payload.id === "string" ? payload.id : null;
|
|
4851
4897
|
if (!id) continue;
|
|
4852
4898
|
currentSessionId = id;
|
|
4853
|
-
const metaTs = parseTimestamp(payload
|
|
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
|
|
4868
|
-
acc.model = payload
|
|
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
|
|
4872
|
-
const info2 = payload
|
|
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
|
|
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
|
|
4878
|
-
const cachedInput = toNonNegInt2(t
|
|
4879
|
-
const output = toNonNegInt2(t
|
|
4880
|
-
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);
|
|
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
|
|
4943
|
-
|
|
4944
|
-
"
|
|
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
|
|
4978
|
-
|
|
4979
|
-
const
|
|
4980
|
-
|
|
4981
|
-
const
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
const
|
|
4985
|
-
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);
|
|
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
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
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
|
|
5063
|
+
return out;
|
|
5017
5064
|
}
|
|
5018
|
-
async function
|
|
5019
|
-
let initSqlJs;
|
|
5065
|
+
async function openDb(dbPath) {
|
|
5020
5066
|
try {
|
|
5021
|
-
const mod = await import("
|
|
5022
|
-
|
|
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
|
-
|
|
5030
|
-
|
|
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
|
|
5065
|
-
|
|
5120
|
+
async function readGenerationsFromDb(dbPath) {
|
|
5121
|
+
const db = await openDb(dbPath);
|
|
5122
|
+
if (!db) return [];
|
|
5066
5123
|
try {
|
|
5067
|
-
db =
|
|
5068
|
-
|
|
5069
|
-
|
|
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
|
|
5147
|
+
return [];
|
|
5072
5148
|
} finally {
|
|
5073
5149
|
try {
|
|
5074
|
-
db
|
|
5150
|
+
db.close();
|
|
5075
5151
|
} catch {
|
|
5076
5152
|
}
|
|
5077
5153
|
}
|
|
5078
5154
|
}
|
|
5079
|
-
function
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
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
|
-
|
|
5170
|
+
perDb = await readGenerationsFromDb(p);
|
|
5089
5171
|
} catch {
|
|
5090
|
-
|
|
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
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
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
|
|
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
|
|
5225
|
+
const profile = process.env.USERPROFILE ?? home;
|
|
5193
5226
|
candidates.push(path2.join(profile, ".codex", "sessions"));
|
|
5194
|
-
const appData = process.env
|
|
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
|
|
5337
|
-
if (
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
if (
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
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
|
-
}
|
|
5351
|
-
|
|
5367
|
+
} catch {
|
|
5368
|
+
if (opts.verbose) warn("Failed to parse Cursor rows \u2014 skipping");
|
|
5352
5369
|
}
|
|
5353
|
-
} else {
|
|
5354
|
-
|
|
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)")(
|
|
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
|
|
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.
|
|
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(
|
|
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
|
+
"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
|
},
|