token-rats 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +54 -11
  2. package/dist/index.js +1280 -457
  3. package/package.json +4 -15
package/dist/index.js CHANGED
@@ -385,8 +385,8 @@ function getErrorMap() {
385
385
  return overrideErrorMap;
386
386
  }
387
387
  var makeIssue = (params) => {
388
- const { data, path: path3, errorMaps, issueData } = params;
389
- const fullPath = [...path3, ...issueData.path || []];
388
+ const { data, path: path7, errorMaps, issueData } = params;
389
+ const fullPath = [...path7, ...issueData.path || []];
390
390
  const fullIssue = {
391
391
  ...issueData,
392
392
  path: fullPath
@@ -508,11 +508,11 @@ var errorUtil;
508
508
  var _ZodEnum_cache;
509
509
  var _ZodNativeEnum_cache;
510
510
  var ParseInputLazyPath = class {
511
- constructor(parent, value, path3, key) {
511
+ constructor(parent, value, path7, key) {
512
512
  this._cachedPath = [];
513
513
  this.parent = parent;
514
514
  this.data = value;
515
- this._path = path3;
515
+ this._path = path7;
516
516
  this._key = key;
517
517
  }
518
518
  get path() {
@@ -3948,12 +3948,20 @@ var z = /* @__PURE__ */ Object.freeze({
3948
3948
 
3949
3949
  // ../contracts/src/session.ts
3950
3950
  var Source = z.enum(["claude-code", "cursor", "codex"]);
3951
+ var Provider = z.enum(["anthropic", "openai", "cursor", "unknown"]);
3951
3952
  var SessionRecord = z.object({
3952
3953
  id: z.string().min(1),
3953
3954
  source: Source,
3955
+ provider: Provider.optional(),
3954
3956
  model: z.string().min(1),
3955
3957
  inTokens: z.number().int().nonnegative(),
3956
3958
  outTokens: z.number().int().nonnegative(),
3959
+ /** Anthropic cache reads / OpenAI `cached_input_tokens`. Billed cheap-or-free. */
3960
+ cacheReadTokens: z.number().int().nonnegative().optional(),
3961
+ /** Anthropic cache creation tokens. Billed at a premium for first-write only. */
3962
+ cacheWriteTokens: z.number().int().nonnegative().optional(),
3963
+ /** OpenAI reasoning output tokens (o-series / Codex). Billed at output rate. */
3964
+ reasoningTokens: z.number().int().nonnegative().optional(),
3957
3965
  costUsdCents: z.number().int().nonnegative(),
3958
3966
  startedAt: z.number().int().positive(),
3959
3967
  endedAt: z.number().int().positive(),
@@ -3968,19 +3976,38 @@ var User = z.object({
3968
3976
  /** Present only when the caller is the user themselves or the user is public. */
3969
3977
  bio: z.string().max(200).nullable().optional(),
3970
3978
  twitterHandle: z.string().max(50).nullable().optional(),
3971
- publicProfile: z.boolean().optional()
3979
+ /**
3980
+ * True iff `twitter_handle` came from the OAuth flow (i.e. `twitter_user_id`
3981
+ * is set). Self-only — used by settings UI to distinguish manual legacy
3982
+ * handles from verified ones.
3983
+ */
3984
+ twitterVerified: z.boolean().optional(),
3985
+ publicProfile: z.boolean().optional(),
3986
+ /**
3987
+ * Primary verified GitHub email, captured at OAuth callback time. Self-only
3988
+ * (other readers never see this field). `null` when GitHub didn't return a
3989
+ * verified email — the user sees a banner asking them to add one.
3990
+ */
3991
+ email: z.string().email().nullable().optional()
3972
3992
  });
3973
3993
  var PublicProfileSettings = z.object({
3974
3994
  publicProfile: z.boolean().optional(),
3975
- bio: z.string().max(200).nullable().optional(),
3976
- twitterHandle: z.string().max(50).nullable().optional()
3995
+ bio: z.string().max(200).nullable().optional()
3977
3996
  });
3978
3997
  var Profile = User.extend({
3979
3998
  totals: z.object({
3980
3999
  today: z.object({ tokens: z.number().int(), costUsdCents: z.number().int() }),
3981
4000
  week: z.object({ tokens: z.number().int(), costUsdCents: z.number().int() }),
3982
4001
  allTime: z.object({ tokens: z.number().int(), costUsdCents: z.number().int() })
3983
- })
4002
+ }),
4003
+ /** Number of users this user has brought in via their referral link. */
4004
+ referredCount: z.number().int().nonnegative().optional(),
4005
+ /**
4006
+ * The profile owner's referral code. Self-only — present only when the
4007
+ * caller is viewing their own profile, so the page can render a
4008
+ * copy-able invite link without a separate fetch to /v1/me/referral.
4009
+ */
4010
+ referralCode: z.string().optional()
3984
4011
  });
3985
4012
  var AutobiographyStats = z.object({
3986
4013
  handle: z.string(),
@@ -4016,13 +4043,24 @@ var Room = z.object({
4016
4043
  name: z.string().min(1).max(64),
4017
4044
  ownerId: z.string(),
4018
4045
  orgId: z.string().nullable(),
4019
- createdAt: z.number().int().positive()
4046
+ createdAt: z.number().int().positive(),
4047
+ /** v1.2: public country-locked groups. `country` is set iff `isPublic` is true. */
4048
+ isPublic: z.boolean(),
4049
+ /** ISO-3166-1 alpha-2 (`cf-ipcountry` of the creator). Null on private rooms. */
4050
+ country: z.string().min(2).max(2).nullable(),
4051
+ /**
4052
+ * True iff this room is the caller's pinned room. Populated only on
4053
+ * caller-scoped responses (e.g. GET /v1/me/rooms); absent elsewhere.
4054
+ */
4055
+ isPinned: z.boolean().optional()
4020
4056
  });
4021
4057
  var RoomMember = z.object({
4022
4058
  userId: z.string(),
4023
4059
  handle: z.string(),
4024
4060
  avatarUrl: z.string().url().nullable(),
4025
- joinedAt: z.number().int().positive()
4061
+ joinedAt: z.number().int().positive(),
4062
+ /** OAuth-verified X handle, when present. Manual handles are not surfaced. */
4063
+ twitterHandle: z.string().max(50).nullable().optional()
4026
4064
  });
4027
4065
 
4028
4066
  // ../contracts/src/leaderboard.ts
@@ -4088,8 +4126,25 @@ var ChallengeWithLeaderboard = Challenge.extend({
4088
4126
  winnerHandle: z.string().nullable()
4089
4127
  });
4090
4128
 
4129
+ // ../contracts/src/referral.ts
4130
+ var ReferredUser = z.object({
4131
+ handle: z.string(),
4132
+ avatarUrl: z.string().url().nullable(),
4133
+ /** Unix ms — when the referred user signed up. */
4134
+ createdAt: z.number().int()
4135
+ });
4136
+ var ReferralStats = z.object({
4137
+ /** The user's unique referral code (URL-safe, ~8 chars). */
4138
+ code: z.string(),
4139
+ /** Total users referred. */
4140
+ count: z.number().int().nonnegative(),
4141
+ /** Most recent referred users (newest first), capped server-side. */
4142
+ recent: z.array(ReferredUser)
4143
+ });
4144
+
4091
4145
  // ../contracts/src/org.ts
4092
- var OrgPlan = z.enum(["free", "pro"]);
4146
+ var OrgPlan = z.enum(["free", "student", "pro"]);
4147
+ var OrgStatus = z.enum(["pending", "approved"]);
4093
4148
  var OrgMemberRole = z.enum(["owner", "admin", "member"]);
4094
4149
  var OrgSlug = z.string().min(3).max(48).regex(/^[a-z0-9-]+$/, "Slug must be lowercase letters, numbers, and hyphens only");
4095
4150
  var Org = z.object({
@@ -4099,7 +4154,12 @@ var Org = z.object({
4099
4154
  plan: OrgPlan,
4100
4155
  seatCount: z.number().int().nonnegative(),
4101
4156
  githubOrgLogin: z.string().nullable(),
4102
- createdAt: z.number().int().positive()
4157
+ createdAt: z.number().int().positive(),
4158
+ /** v1.2: soft-create status. Approved orgs are fully usable; pending orgs are gated. */
4159
+ status: OrgStatus,
4160
+ requestedPlan: OrgPlan.nullable(),
4161
+ founderEmail: z.string().email().nullable(),
4162
+ founderName: z.string().nullable()
4103
4163
  });
4104
4164
  var OrgMember = z.object({
4105
4165
  userId: z.string(),
@@ -4144,8 +4204,17 @@ var OrgDashboard = z.object({
4144
4204
  var CreateOrgRequest = z.object({
4145
4205
  name: z.string().min(1).max(64),
4146
4206
  slug: OrgSlug,
4147
- githubOrgLogin: z.string().optional()
4207
+ githubOrgLogin: z.string().optional(),
4208
+ // v1.2 soft-create — required fields, trust-on-submit.
4209
+ founderEmail: z.string().email(),
4210
+ founderName: z.string().min(1).max(80).optional(),
4211
+ requestedPlan: OrgPlan
4148
4212
  });
4213
+ var PatchOrgRequest = z.object({
4214
+ founderEmail: z.string().email().optional(),
4215
+ founderName: z.string().min(1).max(80).optional()
4216
+ });
4217
+ var PatchOrgResponse = z.object({ org: Org });
4149
4218
  var CreateOrgResponse = z.object({ org: Org });
4150
4219
  var GetOrgResponse = z.object({
4151
4220
  org: Org,
@@ -4160,6 +4229,20 @@ var CreateOrgInviteRequest = z.object({
4160
4229
  var CreateOrgInviteResponse = z.object({ invite: OrgInvite });
4161
4230
  var AcceptOrgInviteResponse = z.object({ ok: z.boolean() });
4162
4231
  var GetOrgDashboardResponse = z.object({ dashboard: OrgDashboard });
4232
+ var AdminPendingOrg = z.object({
4233
+ id: z.string(),
4234
+ name: z.string(),
4235
+ slug: OrgSlug.nullable(),
4236
+ requestedPlan: OrgPlan.nullable(),
4237
+ founderEmail: z.string().email().nullable(),
4238
+ founderName: z.string().nullable(),
4239
+ founderHandle: z.string(),
4240
+ createdAt: z.number().int().positive()
4241
+ });
4242
+ var GetPendingOrgsResponse = z.object({
4243
+ orgs: z.array(AdminPendingOrg)
4244
+ });
4245
+ var ApproveOrgResponse = z.object({ org: Org });
4163
4246
 
4164
4247
  // ../contracts/src/api.ts
4165
4248
  var GetMeResponse = z.object({ user: User });
@@ -4171,7 +4254,9 @@ var UploadSessionsResponse = z.object({
4171
4254
  duplicates: z.number().int().nonnegative()
4172
4255
  });
4173
4256
  var CreateRoomRequest = z.object({
4174
- name: z.string().min(1).max(64)
4257
+ name: z.string().min(1).max(64),
4258
+ /** v1.2: when true the room is publicly listed in /groups for its country. */
4259
+ isPublic: z.boolean().default(false)
4175
4260
  });
4176
4261
  var CreateRoomResponse = z.object({ room: Room });
4177
4262
  var JoinRoomResponse = z.object({ room: Room });
@@ -4191,14 +4276,60 @@ var HeatmapDay = z.object({
4191
4276
  tokens: z.number().int().nonnegative(),
4192
4277
  sessions: z.number().int().nonnegative()
4193
4278
  });
4279
+ var HeatmapRange = z.enum(["30d", "52w"]);
4194
4280
  var Heatmap = z.object({
4281
+ range: HeatmapRange,
4195
4282
  from: z.string(),
4196
4283
  // YYYY-MM-DD UTC, inclusive
4197
4284
  to: z.string(),
4198
4285
  // YYYY-MM-DD UTC, inclusive
4199
4286
  days: z.array(HeatmapDay)
4200
4287
  });
4288
+ var GetHeatmapQuery = z.object({
4289
+ range: HeatmapRange.default("30d")
4290
+ });
4201
4291
  var GetHeatmapResponse = z.object({ heatmap: Heatmap });
4292
+ var RoomSummary = z.object({
4293
+ code: z.string(),
4294
+ name: z.string(),
4295
+ /** Future: true once feature #6 lands. Always false for now. */
4296
+ isPublic: z.boolean(),
4297
+ /** ISO country code (e.g. "DE") when isPublic is true; null otherwise. */
4298
+ country: z.string().nullable(),
4299
+ memberCount: z.number().int().nonnegative(),
4300
+ total30dTokens: z.number().int().nonnegative(),
4301
+ total30dCostUsdCents: z.number().int().nonnegative()
4302
+ });
4303
+ var GetRoomSummaryResponse = z.object({ summary: RoomSummary });
4304
+ var PublicGroupRow = z.object({
4305
+ code: z.string(),
4306
+ name: z.string(),
4307
+ country: z.string().min(2).max(2),
4308
+ memberCount: z.number().int().nonnegative(),
4309
+ total30dTokens: z.number().int().nonnegative(),
4310
+ total30dCostUsdCents: z.number().int().nonnegative()
4311
+ });
4312
+ var CountryBoardRow = z.object({
4313
+ rank: z.number().int().positive(),
4314
+ userId: z.string(),
4315
+ handle: z.string(),
4316
+ avatarUrl: z.string().url().nullable(),
4317
+ tokens: z.number().int().nonnegative(),
4318
+ costUsdCents: z.number().int().nonnegative(),
4319
+ sessions: z.number().int().nonnegative()
4320
+ });
4321
+ var GetGroupsResponse = z.object({
4322
+ country: z.string().min(2).max(2).nullable(),
4323
+ groups: z.array(PublicGroupRow),
4324
+ /** Public users in the viewer's country, ranked by trailing-30d tokens. */
4325
+ userBoard: z.array(CountryBoardRow)
4326
+ });
4327
+ var GroupStreak = z.object({
4328
+ currentStreak: z.number().int().nonnegative(),
4329
+ /** Yesterday in YYYY-MM-DD UTC. */
4330
+ asOf: z.string()
4331
+ });
4332
+ var GetGroupStreakResponse = z.object({ groupStreak: GroupStreak });
4202
4333
  var GetMyRoomsResponse = z.object({
4203
4334
  rooms: z.array(Room)
4204
4335
  });
@@ -4234,6 +4365,7 @@ var GetTrendingResponse = z.object({
4234
4365
  range: LeaderboardRange,
4235
4366
  generatedAt: z.number().int().positive()
4236
4367
  });
4368
+ var GetReferralResponse = z.object({ referral: ReferralStats });
4237
4369
  var ReportAbuseRequest = z.object({
4238
4370
  targetHandle: z.string().min(1).max(100),
4239
4371
  reason: z.string().min(1).max(500)
@@ -4249,14 +4381,22 @@ var ENDPOINTS = {
4249
4381
  profile: (handle) => `/v1/u/${handle}`,
4250
4382
  autobiography: (handle) => `/v1/u/${handle}/autobiography`,
4251
4383
  profileHeatmap: (handle) => `/v1/u/${handle}/heatmap`,
4384
+ // v1.2 room aggregates — auth optional, accessible to non-members.
4385
+ roomSummary: (code) => `/v1/r/${code}/summary`,
4386
+ roomHeatmap: (code) => `/v1/r/${code}/heatmap`,
4387
+ roomGroupStreak: (code) => `/v1/r/${code}/group-streak`,
4388
+ // v1.2 public country-locked groups list
4389
+ groups: "/v1/groups",
4252
4390
  authGithubStart: "/v1/auth/github/start",
4253
4391
  authGithubCallback: "/v1/auth/github/callback",
4392
+ authLogout: "/v1/auth/logout",
4254
4393
  authCliExchange: "/v1/auth/cli/exchange",
4255
4394
  authCliPoll: "/v1/auth/cli/poll",
4256
4395
  // Phase 2 Track G+H
4257
4396
  meRooms: "/v1/me/rooms",
4258
4397
  leaveRoom: (code) => `/v1/rooms/${code}/leave`,
4259
4398
  renameRoom: (code) => `/v1/rooms/${code}`,
4399
+ pinRoom: (code) => `/v1/rooms/${code}/pin`,
4260
4400
  roomActivity: (code) => `/v1/rooms/${code}/activity`,
4261
4401
  roomStreaks: (code) => `/v1/rooms/${code}/streaks`,
4262
4402
  roomChallenges: (code) => `/v1/rooms/${code}/challenges`,
@@ -4270,6 +4410,10 @@ var ENDPOINTS = {
4270
4410
  patchMe: "/v1/me",
4271
4411
  trending: "/v1/trending",
4272
4412
  reportAbuse: "/v1/abuse/report",
4413
+ // Affiliate / referral tracking
4414
+ meReferral: "/v1/me/referral",
4415
+ // v1.2 Track AD — friends derived from shared private rooms
4416
+ meFriends: "/v1/me/friends",
4273
4417
  // Phase 3 Track O — Org plan
4274
4418
  orgs: "/v1/orgs",
4275
4419
  org: (slug) => `/v1/orgs/${slug}`,
@@ -4279,7 +4423,14 @@ var ENDPOINTS = {
4279
4423
  stripeWebhook: "/webhooks/stripe",
4280
4424
  // Phase 3 Track M
4281
4425
  proxyAnthropicMessages: "/v1/proxy/anthropic/v1/messages",
4282
- proxyAnthropicKey: "/v1/proxy/keys/anthropic"
4426
+ proxyAnthropicKey: "/v1/proxy/keys/anthropic",
4427
+ // Admin analytics (project-owner only)
4428
+ adminSignups: "/v1/admin/signups",
4429
+ adminActivity: "/v1/admin/activity",
4430
+ adminReferrers: "/v1/admin/referrers",
4431
+ // Admin org approval (v1.2)
4432
+ adminOrgsPending: "/v1/admin/orgs/pending",
4433
+ adminOrgApprove: (slug) => `/v1/admin/orgs/${slug}/approve`
4283
4434
  };
4284
4435
 
4285
4436
  // ../contracts/src/errors.ts
@@ -4337,6 +4488,65 @@ var LiveEvent = z.discriminatedUnion("kind", [
4337
4488
  })
4338
4489
  ]);
4339
4490
 
4491
+ // ../contracts/src/admin.ts
4492
+ var AdminSignupsDay = z.object({
4493
+ day: z.string(),
4494
+ // YYYY-MM-DD UTC
4495
+ /** Newly created users on this day. */
4496
+ newUsers: z.number().int().nonnegative(),
4497
+ /** Running total of users created up to and including this day. */
4498
+ cumulative: z.number().int().nonnegative()
4499
+ });
4500
+ var AdminSignupsResponse = z.object({
4501
+ totalUsers: z.number().int().nonnegative(),
4502
+ /** Length = 30, ordered oldest -> newest. */
4503
+ series: z.array(AdminSignupsDay),
4504
+ generatedAt: z.number().int().positive()
4505
+ });
4506
+ var AdminActivityDay = z.object({
4507
+ day: z.string(),
4508
+ // YYYY-MM-DD UTC
4509
+ activeUsers: z.number().int().nonnegative()
4510
+ });
4511
+ var AdminActivityResponse = z.object({
4512
+ /** Length = 30, ordered oldest -> newest. */
4513
+ series: z.array(AdminActivityDay),
4514
+ /** Distinct users active in the full 30-day window. */
4515
+ activeUsers30d: z.number().int().nonnegative(),
4516
+ generatedAt: z.number().int().positive()
4517
+ });
4518
+ var AdminReferrerRow = z.object({
4519
+ handle: z.string(),
4520
+ avatarUrl: z.string().url().nullable(),
4521
+ /** Number of users this referrer has brought in. */
4522
+ count: z.number().int().nonnegative()
4523
+ });
4524
+ var AdminReferrersResponse = z.object({
4525
+ rows: z.array(AdminReferrerRow),
4526
+ generatedAt: z.number().int().positive()
4527
+ });
4528
+
4529
+ // ../contracts/src/friends.ts
4530
+ var FriendSharedRoom = z.object({
4531
+ code: z.string(),
4532
+ name: z.string()
4533
+ });
4534
+ var FriendRow = z.object({
4535
+ userId: z.string(),
4536
+ handle: z.string(),
4537
+ avatarUrl: z.string().url().nullable(),
4538
+ twitterHandle: z.string().nullable().optional(),
4539
+ publicProfile: z.boolean(),
4540
+ sharedRooms: z.array(FriendSharedRoom),
4541
+ tokens: z.number().int().nonnegative(),
4542
+ costUsdCents: z.number().int().nonnegative(),
4543
+ sessions: z.number().int().nonnegative()
4544
+ });
4545
+ var FriendsResponse = z.object({
4546
+ range: LeaderboardRange,
4547
+ friends: z.array(FriendRow)
4548
+ });
4549
+
4340
4550
  // src/lib/api.ts
4341
4551
  var DEFAULT_API_URL = "https://api.tokenrats.com";
4342
4552
  function isTransient(status) {
@@ -4366,7 +4576,7 @@ var ApiClient = class {
4366
4576
  headers() {
4367
4577
  const h = { "Content-Type": "application/json" };
4368
4578
  if (this.token) {
4369
- h["Authorization"] = `Bearer ${this.token}`;
4579
+ h.Authorization = `Bearer ${this.token}`;
4370
4580
  }
4371
4581
  return h;
4372
4582
  }
@@ -4386,13 +4596,13 @@ var ApiClient = class {
4386
4596
  }
4387
4597
  attempt++;
4388
4598
  if (attempt <= maxRetries) {
4389
- await sleep(1e3 * Math.pow(2, attempt - 1));
4599
+ await sleep(1e3 * 2 ** (attempt - 1));
4390
4600
  }
4391
4601
  }
4392
4602
  throw lastErr;
4393
4603
  }
4394
- async post(path3, body) {
4395
- const url = `${this.apiUrl}${path3}`;
4604
+ async post(path7, body) {
4605
+ const url = `${this.apiUrl}${path7}`;
4396
4606
  const res = await this.fetchWithRetry(url, {
4397
4607
  method: "POST",
4398
4608
  headers: this.headers(),
@@ -4403,8 +4613,8 @@ var ApiClient = class {
4403
4613
  }
4404
4614
  return res.json();
4405
4615
  }
4406
- async get(path3) {
4407
- const url = `${this.apiUrl}${path3}`;
4616
+ async get(path7) {
4617
+ const url = `${this.apiUrl}${path7}`;
4408
4618
  const res = await this.fetchWithRetry(url, {
4409
4619
  method: "GET",
4410
4620
  headers: this.headers()
@@ -4453,7 +4663,7 @@ import * as fs from "node:fs";
4453
4663
  import * as os from "node:os";
4454
4664
  import * as path from "node:path";
4455
4665
  function tokenDir() {
4456
- const xdgConfig = process.env["XDG_CONFIG_HOME"];
4666
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
4457
4667
  const base = xdgConfig ?? path.join(os.homedir(), ".config");
4458
4668
  return path.join(base, "token-rats");
4459
4669
  }
@@ -4592,7 +4802,7 @@ async function loginCommand(opts) {
4592
4802
  const codeMatch = verificationUrl.match(/[?&]code=([A-Z0-9-]+)/);
4593
4803
  const code = codeMatch?.[1] ?? "";
4594
4804
  console.log("");
4595
- console.log(` Open this URL to sign in:`);
4805
+ console.log(" Open this URL to sign in:");
4596
4806
  console.log(` \x1B[1m\x1B[36m${verificationUrl}\x1B[0m`);
4597
4807
  if (code) {
4598
4808
  console.log(` Code: \x1B[1m${code}\x1B[0m`);
@@ -4641,68 +4851,8 @@ function logoutCommand() {
4641
4851
  }
4642
4852
 
4643
4853
  // src/commands/sync.ts
4644
- import * as fs3 from "node:fs";
4645
-
4646
- // ../pricing/src/prices.ts
4647
- var prices = {
4648
- models: {
4649
- // ── Anthropic / Claude ────────────────────────────────────────────────
4650
- "claude-opus-4-7": { inputPerMTok: 15, outputPerMTok: 75 },
4651
- "claude-opus-4-6": { inputPerMTok: 15, outputPerMTok: 75 },
4652
- "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
4653
- "claude-sonnet-4-5": { inputPerMTok: 3, outputPerMTok: 15 },
4654
- "claude-haiku-4-5": { inputPerMTok: 0.8, outputPerMTok: 4 },
4655
- "claude-3-5-sonnet-20241022": { inputPerMTok: 3, outputPerMTok: 15 },
4656
- "claude-3-5-sonnet-20240620": { inputPerMTok: 3, outputPerMTok: 15 },
4657
- "claude-3-5-haiku-20241022": { inputPerMTok: 0.8, outputPerMTok: 4 },
4658
- "claude-3-opus-20240229": { inputPerMTok: 15, outputPerMTok: 75 },
4659
- "claude-3-sonnet-20240229": { inputPerMTok: 3, outputPerMTok: 15 },
4660
- "claude-3-haiku-20240307": { inputPerMTok: 0.25, outputPerMTok: 1.25 },
4661
- // ── OpenAI / Codex ────────────────────────────────────────────────────
4662
- // GPT-5 family. gpt-5-codex is the Codex CLI's public model; OpenAI's
4663
- // pricing page lists only input ($1.25/MTok). Output uses the same $10
4664
- // rate the broader GPT-5 base is publicly quoted at — flagged here for
4665
- // re-verification when OpenAI publishes an explicit output figure.
4666
- "gpt-5-codex": { inputPerMTok: 1.25, outputPerMTok: 10 },
4667
- "gpt-5-mini": { inputPerMTok: 0.25, outputPerMTok: 2 },
4668
- "gpt-5-nano": { inputPerMTok: 0.05, outputPerMTok: 0.4 },
4669
- "gpt-5-pro": { inputPerMTok: 15, outputPerMTok: 120 },
4670
- "gpt-5.5": { inputPerMTok: 5, outputPerMTok: 30 },
4671
- "codex-mini-latest": { inputPerMTok: 1.5, outputPerMTok: 6 },
4672
- // GPT-4 family.
4673
- "gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10 },
4674
- "gpt-4o-mini": { inputPerMTok: 0.15, outputPerMTok: 0.6 },
4675
- "gpt-4-turbo": { inputPerMTok: 10, outputPerMTok: 30 },
4676
- "gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8 },
4677
- "gpt-4.1-mini": { inputPerMTok: 0.4, outputPerMTok: 1.6 },
4678
- "gpt-4.1-nano": { inputPerMTok: 0.1, outputPerMTok: 0.4 },
4679
- // Reasoning models (o-series).
4680
- o1: { inputPerMTok: 15, outputPerMTok: 60 },
4681
- "o1-mini": { inputPerMTok: 3, outputPerMTok: 12 },
4682
- // o3 was repriced in 2026; previously $10/$40, now $2/$8.
4683
- o3: { inputPerMTok: 2, outputPerMTok: 8 },
4684
- "o3-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4 },
4685
- "o4-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4 }
4686
- }
4687
- };
4688
-
4689
- // ../pricing/src/index.ts
4690
- var TABLE = prices.models;
4691
- function priceOf(model, inTokens, outTokens) {
4692
- const p = TABLE[model] ?? matchPrefix(model);
4693
- if (!p) return { costUsdCents: 0, known: false };
4694
- const dollars = (inTokens * p.inputPerMTok + outTokens * p.outputPerMTok) / 1e6;
4695
- return { costUsdCents: Math.round(dollars * 100), known: true };
4696
- }
4697
- function matchPrefix(model) {
4698
- let best;
4699
- for (const [key, price] of Object.entries(TABLE)) {
4700
- if (model.startsWith(key) && (!best || key.length > best.key.length)) {
4701
- best = { key, price };
4702
- }
4703
- }
4704
- return best?.price;
4705
- }
4854
+ import * as fs7 from "node:fs";
4855
+ import * as readline from "node:readline/promises";
4706
4856
 
4707
4857
  // ../parsers/src/hash.ts
4708
4858
  var FNV_PRIME = 16777619;
@@ -4745,13 +4895,13 @@ function parseClaudeCode(input) {
4745
4895
  }
4746
4896
  if (typeof event !== "object" || event === null) continue;
4747
4897
  const ev = event;
4748
- const sidCamel = ev["sessionId"];
4749
- const sidSnake = ev["session_id"];
4898
+ const sidCamel = ev.sessionId;
4899
+ const sidSnake = ev.session_id;
4750
4900
  const sessionId = typeof sidCamel === "string" ? sidCamel : typeof sidSnake === "string" ? sidSnake : null;
4751
4901
  if (!sessionId) continue;
4752
- const rawTs = ev["timestamp"];
4902
+ const rawTs = ev.timestamp;
4753
4903
  let timestamp = 0;
4754
- if (typeof rawTs === "number" && isFinite(rawTs)) {
4904
+ if (typeof rawTs === "number" && Number.isFinite(rawTs)) {
4755
4905
  timestamp = rawTs;
4756
4906
  } else if (typeof rawTs === "string") {
4757
4907
  const parsed = Date.parse(rawTs);
@@ -4765,6 +4915,8 @@ function parseClaudeCode(input) {
4765
4915
  endedAt: timestamp,
4766
4916
  inTokens: 0,
4767
4917
  outTokens: 0,
4918
+ cacheReadTokens: 0,
4919
+ cacheWriteTokens: 0,
4768
4920
  model: ""
4769
4921
  };
4770
4922
  sessions.set(sessionId, acc);
@@ -4773,20 +4925,20 @@ function parseClaudeCode(input) {
4773
4925
  if (acc.startedAt === 0 || timestamp < acc.startedAt) acc.startedAt = timestamp;
4774
4926
  if (timestamp > acc.endedAt) acc.endedAt = timestamp;
4775
4927
  }
4776
- if (ev["type"] !== "assistant") continue;
4777
- const message = ev["message"];
4928
+ if (ev.type !== "assistant") continue;
4929
+ const message = ev.message;
4778
4930
  if (typeof message !== "object" || message === null) continue;
4779
4931
  const msg = message;
4780
- if (typeof msg["model"] === "string" && msg["model"].length > 0) {
4781
- acc.model = msg["model"];
4932
+ if (typeof msg.model === "string" && msg.model.length > 0) {
4933
+ acc.model = msg.model;
4782
4934
  }
4783
- const usage = msg["usage"];
4935
+ const usage = msg.usage;
4784
4936
  if (typeof usage === "object" && usage !== null) {
4785
4937
  const u = usage;
4786
- const inputTokens = toNonNegInt(u["input_tokens"]);
4787
- const outputTokens = toNonNegInt(u["output_tokens"]);
4788
- acc.inTokens += inputTokens;
4789
- acc.outTokens += outputTokens;
4938
+ acc.inTokens += toNonNegInt(u.input_tokens);
4939
+ acc.outTokens += toNonNegInt(u.output_tokens);
4940
+ acc.cacheReadTokens += toNonNegInt(u.cache_read_input_tokens);
4941
+ acc.cacheWriteTokens += toNonNegInt(u.cache_creation_input_tokens);
4790
4942
  }
4791
4943
  }
4792
4944
  const results = [];
@@ -4795,7 +4947,7 @@ function parseClaudeCode(input) {
4795
4947
  const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
4796
4948
  if (startedAt <= 0 || endedAt <= 0) continue;
4797
4949
  const model = acc.model.length > 0 ? acc.model : "unknown";
4798
- const { costUsdCents } = priceOf(model, acc.inTokens, acc.outTokens);
4950
+ const costUsdCents = 0;
4799
4951
  const dedupeKey = computeDedupeKey(
4800
4952
  "claude-code",
4801
4953
  model,
@@ -4806,9 +4958,12 @@ function parseClaudeCode(input) {
4806
4958
  results.push({
4807
4959
  id: `claude-code:${acc.sessionId}`,
4808
4960
  source: "claude-code",
4961
+ provider: "anthropic",
4809
4962
  model,
4810
4963
  inTokens: acc.inTokens,
4811
4964
  outTokens: acc.outTokens,
4965
+ cacheReadTokens: acc.cacheReadTokens,
4966
+ cacheWriteTokens: acc.cacheWriteTokens,
4812
4967
  costUsdCents,
4813
4968
  startedAt,
4814
4969
  endedAt,
@@ -4818,7 +4973,7 @@ function parseClaudeCode(input) {
4818
4973
  return results;
4819
4974
  }
4820
4975
  function toNonNegInt(v) {
4821
- if (typeof v !== "number" || !isFinite(v)) return 0;
4976
+ if (typeof v !== "number" || !Number.isFinite(v)) return 0;
4822
4977
  return Math.max(0, Math.floor(v));
4823
4978
  }
4824
4979
 
@@ -4843,14 +4998,14 @@ function parseCodex(input) {
4843
4998
  }
4844
4999
  if (typeof event !== "object" || event === null) continue;
4845
5000
  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;
5001
+ const ts = parseTimestamp(ev.timestamp);
5002
+ const type = typeof ev.type === "string" ? ev.type : null;
5003
+ const payload = typeof ev.payload === "object" && ev.payload !== null ? ev.payload : null;
4849
5004
  if (type === "session_meta" && payload) {
4850
- const id = typeof payload["id"] === "string" ? payload["id"] : null;
5005
+ const id = typeof payload.id === "string" ? payload.id : null;
4851
5006
  if (!id) continue;
4852
5007
  currentSessionId = id;
4853
- const metaTs = parseTimestamp(payload["timestamp"]) || ts;
5008
+ const metaTs = parseTimestamp(payload.timestamp) || ts;
4854
5009
  const acc2 = upsert(sessions, id);
4855
5010
  if (metaTs > 0 && (acc2.startedAt === 0 || metaTs < acc2.startedAt)) {
4856
5011
  acc2.startedAt = metaTs;
@@ -4864,22 +5019,24 @@ function parseCodex(input) {
4864
5019
  if (acc.startedAt === 0) acc.startedAt = ts;
4865
5020
  if (ts > acc.endedAt) acc.endedAt = ts;
4866
5021
  }
4867
- if (type === "turn_context" && payload && typeof payload["model"] === "string") {
4868
- acc.model = payload["model"];
5022
+ if (type === "turn_context" && payload && typeof payload.model === "string") {
5023
+ acc.model = payload.model;
4869
5024
  continue;
4870
5025
  }
4871
- if (type === "event_msg" && payload && payload["type"] === "token_count") {
4872
- const info2 = payload["info"];
5026
+ if (type === "event_msg" && payload && payload.type === "token_count") {
5027
+ const info2 = payload.info;
4873
5028
  if (typeof info2 !== "object" || info2 === null) continue;
4874
- const total = info2["total_token_usage"];
5029
+ const total = info2.total_token_usage;
4875
5030
  if (typeof total !== "object" || total === null) continue;
4876
5031
  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"]);
5032
+ const inputTotal = toNonNegInt2(t.input_tokens);
5033
+ const cachedInput = toNonNegInt2(t.cached_input_tokens);
5034
+ const output = toNonNegInt2(t.output_tokens);
5035
+ const reasoning = toNonNegInt2(t.reasoning_output_tokens);
4881
5036
  acc.inTokens = Math.max(0, inputTotal - cachedInput);
4882
5037
  acc.outTokens = output + reasoning;
5038
+ acc.cacheReadTokens = cachedInput;
5039
+ acc.reasoningTokens = reasoning;
4883
5040
  }
4884
5041
  }
4885
5042
  const results = [];
@@ -4888,20 +5045,17 @@ function parseCodex(input) {
4888
5045
  const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
4889
5046
  if (startedAt <= 0 || endedAt <= 0) continue;
4890
5047
  const model = acc.model.length > 0 ? acc.model : "unknown";
4891
- 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
- );
5048
+ const costUsdCents = 0;
5049
+ const dedupeKey = computeDedupeKey("codex", model, startedAt, acc.inTokens, acc.outTokens);
4899
5050
  results.push({
4900
5051
  id: `codex:${acc.sessionId}`,
4901
5052
  source: "codex",
5053
+ provider: "openai",
4902
5054
  model,
4903
5055
  inTokens: acc.inTokens,
4904
5056
  outTokens: acc.outTokens,
5057
+ cacheReadTokens: acc.cacheReadTokens,
5058
+ reasoningTokens: acc.reasoningTokens,
4905
5059
  costUsdCents,
4906
5060
  startedAt,
4907
5061
  endedAt,
@@ -4919,6 +5073,8 @@ function upsert(map, sessionId) {
4919
5073
  endedAt: 0,
4920
5074
  inTokens: 0,
4921
5075
  outTokens: 0,
5076
+ cacheReadTokens: 0,
5077
+ reasoningTokens: 0,
4922
5078
  model: ""
4923
5079
  };
4924
5080
  map.set(sessionId, acc);
@@ -4926,7 +5082,7 @@ function upsert(map, sessionId) {
4926
5082
  return acc;
4927
5083
  }
4928
5084
  function parseTimestamp(v) {
4929
- if (typeof v === "number" && isFinite(v)) return v;
5085
+ if (typeof v === "number" && Number.isFinite(v)) return v;
4930
5086
  if (typeof v === "string") {
4931
5087
  const parsed = Date.parse(v);
4932
5088
  if (!Number.isNaN(parsed)) return parsed;
@@ -4934,25 +5090,14 @@ function parseTimestamp(v) {
4934
5090
  return 0;
4935
5091
  }
4936
5092
  function toNonNegInt2(v) {
4937
- if (typeof v !== "number" || !isFinite(v)) return 0;
5093
+ if (typeof v !== "number" || !Number.isFinite(v)) return 0;
4938
5094
  return Math.max(0, Math.floor(v));
4939
5095
  }
4940
5096
 
4941
5097
  // ../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"
5098
+ var ESTIMATES = {
5099
+ composer: { inTokens: 1e4, outTokens: 2e3, model: "cursor-composer" }
5100
+ // "tab" deliberately omitted — see file header.
4956
5101
  };
4957
5102
  function parseCursor(input) {
4958
5103
  let text;
@@ -4972,199 +5117,554 @@ function parseCursor(input) {
4972
5117
  for (const raw of rows) {
4973
5118
  if (typeof raw !== "object" || raw === null) continue;
4974
5119
  const row = raw;
4975
- const id = typeof row["id"] === "string" ? row["id"] : null;
5120
+ const id = typeof row.id === "string" ? row.id : null;
4976
5121
  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);
5122
+ const type = typeof row.type === "string" ? row.type : null;
5123
+ if (!type) continue;
5124
+ const unixMs = typeof row.unixMs === "number" && Number.isFinite(row.unixMs) && row.unixMs > 0 ? row.unixMs : null;
5125
+ if (unixMs === null) continue;
5126
+ const estimate = ESTIMATES[type];
5127
+ if (!estimate) continue;
5128
+ const { inTokens, outTokens, model } = estimate;
5129
+ const costUsdCents = 0;
5130
+ const dedupeKey = computeDedupeKey("cursor", model, unixMs, inTokens, outTokens);
4986
5131
  results.push({
4987
5132
  id: `cursor:${id}`,
4988
5133
  source: "cursor",
5134
+ provider: "cursor",
4989
5135
  model,
4990
5136
  inTokens,
4991
5137
  outTokens,
4992
5138
  costUsdCents,
4993
- startedAt,
4994
- endedAt,
5139
+ startedAt: unixMs,
5140
+ endedAt: unixMs,
4995
5141
  dedupeKey
4996
5142
  });
4997
5143
  }
4998
5144
  return results;
4999
5145
  }
5000
- function toNonNegInt3(v) {
5001
- if (typeof v !== "number" || !isFinite(v)) return 0;
5002
- return Math.max(0, Math.floor(v));
5003
- }
5004
5146
 
5005
5147
  // src/lib/cursor-extract.ts
5148
+ import { existsSync, readdirSync, statSync } from "node:fs";
5006
5149
  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;
5150
+ import { homedir as homedir2 } from "node:os";
5151
+ import { join as join2 } from "node:path";
5152
+ function cursorWorkspaceStorageDir() {
5153
+ const home = homedir2();
5154
+ const rel = join2("Cursor", "User", "workspaceStorage");
5155
+ if (process.platform === "darwin") {
5156
+ return join2(home, "Library", "Application Support", rel);
5157
+ }
5158
+ if (process.platform === "win32") {
5159
+ const appData = process.env.APPDATA ?? join2(home, "AppData", "Roaming");
5160
+ return join2(appData, rel);
5161
+ }
5162
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join2(home, ".config");
5163
+ return join2(xdgConfig, rel);
5164
+ }
5165
+ function discoverWorkspaceDbs() {
5166
+ const root = cursorWorkspaceStorageDir();
5167
+ if (!existsSync(root)) return [];
5168
+ const out = [];
5169
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
5170
+ if (!entry.isDirectory()) continue;
5171
+ const candidate = join2(root, entry.name, "state.vscdb");
5172
+ if (existsSync(candidate)) out.push(candidate);
5015
5173
  }
5016
- return readWithDb(() => new DatabaseConstructor(dbPath, { readOnly: true }));
5174
+ return out;
5017
5175
  }
5018
- async function trySqlJs(dbPath) {
5019
- let initSqlJs;
5176
+ async function openDb(dbPath) {
5020
5177
  try {
5021
- const mod = await import("sql.js");
5022
- initSqlJs = mod.default ?? mod;
5178
+ const mod = await import("node:sqlite");
5179
+ const DatabaseSync = mod.DatabaseSync;
5180
+ if (DatabaseSync) {
5181
+ const db = new DatabaseSync(dbPath, { readOnly: true });
5182
+ return {
5183
+ exec: (sql) => {
5184
+ const stmt = db.prepare(sql);
5185
+ const rows = stmt.all();
5186
+ if (rows.length === 0) return [];
5187
+ const columns = Object.keys(rows[0]);
5188
+ return [
5189
+ {
5190
+ columns,
5191
+ values: rows.map((r) => columns.map((c2) => r[c2]))
5192
+ }
5193
+ ];
5194
+ },
5195
+ close: () => db.close()
5196
+ };
5197
+ }
5023
5198
  } catch {
5024
- return null;
5025
5199
  }
5026
- let SQL;
5027
- let fileBytes;
5028
5200
  try {
5029
- SQL = await initSqlJs();
5030
- fileBytes = await readFile(dbPath);
5201
+ const sqlJs = await import("sql.js");
5202
+ const initSqlJs = sqlJs.default ?? sqlJs;
5203
+ const SQL = await initSqlJs();
5204
+ const bytes = await readFile(dbPath);
5205
+ return new SQL.Database(new Uint8Array(bytes));
5031
5206
  } catch {
5032
- return null;
5033
5207
  }
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
5208
  try {
5057
5209
  const mod = await import("better-sqlite3");
5058
- Database = mod.default ?? mod;
5210
+ const Database = mod.default ?? mod;
5211
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
5212
+ return {
5213
+ exec: (sql) => {
5214
+ const stmt = db.prepare(sql);
5215
+ const rows = stmt.all();
5216
+ if (rows.length === 0) return [];
5217
+ const columns = Object.keys(rows[0]);
5218
+ return [
5219
+ {
5220
+ columns,
5221
+ values: rows.map((r) => columns.map((c2) => r[c2]))
5222
+ }
5223
+ ];
5224
+ },
5225
+ close: () => db.close()
5226
+ };
5059
5227
  } catch {
5060
5228
  return null;
5061
5229
  }
5062
- return readWithDb(() => new Database(dbPath, { readonly: true, fileMustExist: true }));
5063
5230
  }
5064
- function readWithDb(factory) {
5065
- let db = null;
5231
+ async function readGenerationsFromDb(dbPath) {
5232
+ const db = await openDb(dbPath);
5233
+ if (!db) return [];
5066
5234
  try {
5067
- db = factory();
5068
- const rows = tryItemTable(db) ?? tryDirectTable(db) ?? [];
5069
- return rows;
5235
+ const res = db.exec(`SELECT value FROM ItemTable WHERE key = 'aiService.generations'`);
5236
+ if (res.length === 0 || res[0].values.length === 0) return [];
5237
+ const raw = res[0].values[0][0];
5238
+ if (typeof raw !== "string") return [];
5239
+ let parsed;
5240
+ try {
5241
+ parsed = JSON.parse(raw);
5242
+ } catch {
5243
+ return [];
5244
+ }
5245
+ if (!Array.isArray(parsed)) return [];
5246
+ const out = [];
5247
+ for (const item of parsed) {
5248
+ if (typeof item !== "object" || item === null) continue;
5249
+ const r = item;
5250
+ const id = typeof r.generationUUID === "string" ? r.generationUUID : null;
5251
+ const type = typeof r.type === "string" ? r.type : null;
5252
+ const unixMs = typeof r.unixMs === "number" && Number.isFinite(r.unixMs) && r.unixMs > 0 ? r.unixMs : null;
5253
+ if (!id || !type || unixMs === null) continue;
5254
+ out.push({ id, type, unixMs });
5255
+ }
5256
+ return out;
5070
5257
  } catch {
5071
- return null;
5258
+ return [];
5072
5259
  } finally {
5073
5260
  try {
5074
- db?.close();
5261
+ db.close();
5075
5262
  } catch {
5076
5263
  }
5077
5264
  }
5078
5265
  }
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;
5266
+ async function extractCursorGenerations() {
5267
+ const dbPaths = discoverWorkspaceDbs();
5268
+ if (dbPaths.length === 0) {
5269
+ return {
5270
+ rows: [],
5271
+ skipped: null,
5272
+ dbCount: 0
5273
+ };
5274
+ }
5275
+ const seen2 = /* @__PURE__ */ new Set();
5276
+ const rows = [];
5277
+ let openFailures = 0;
5278
+ for (const p of dbPaths) {
5279
+ let perDb = [];
5087
5280
  try {
5088
- parsed = JSON.parse(raw);
5281
+ perDb = await readGenerationsFromDb(p);
5282
+ } catch {
5283
+ openFailures++;
5284
+ continue;
5285
+ }
5286
+ for (const r of perDb) {
5287
+ if (seen2.has(r.id)) continue;
5288
+ seen2.add(r.id);
5289
+ rows.push(r);
5290
+ }
5291
+ }
5292
+ if (rows.length === 0 && openFailures === dbPaths.length) {
5293
+ return {
5294
+ rows: [],
5295
+ dbCount: dbPaths.length,
5296
+ 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."
5297
+ };
5298
+ }
5299
+ return { rows, dbCount: dbPaths.length, skipped: null };
5300
+ }
5301
+ async function extractCursorGenerationsDelta(lastMtimes, cap = 10) {
5302
+ const dbPaths = discoverWorkspaceDbs();
5303
+ if (dbPaths.length === 0) {
5304
+ return { rows: [], newMtimes: /* @__PURE__ */ new Map(), scanned: 0, opened: 0 };
5305
+ }
5306
+ const stamped = [];
5307
+ for (const p of dbPaths) {
5308
+ try {
5309
+ stamped.push({ path: p, mtimeMs: statSync(p).mtimeMs });
5089
5310
  } catch {
5090
- return null;
5091
5311
  }
5092
- if (!Array.isArray(parsed)) return null;
5093
- return normalizeRows(parsed);
5312
+ }
5313
+ stamped.sort((a, b) => b.mtimeMs - a.mtimeMs);
5314
+ const head = stamped.slice(0, cap);
5315
+ const newMtimes = new Map(lastMtimes);
5316
+ const rows = [];
5317
+ let opened = 0;
5318
+ for (const { path: path7, mtimeMs } of head) {
5319
+ const prev = lastMtimes.get(path7) ?? 0;
5320
+ if (mtimeMs <= prev) continue;
5321
+ opened++;
5322
+ try {
5323
+ const perDb = await readGenerationsFromDb(path7);
5324
+ rows.push(...perDb);
5325
+ newMtimes.set(path7, mtimeMs);
5326
+ } catch {
5327
+ }
5328
+ }
5329
+ return { rows, newMtimes, scanned: stamped.length, opened };
5330
+ }
5331
+
5332
+ // src/lib/daemon/linux.ts
5333
+ import { spawnSync } from "node:child_process";
5334
+ import * as fs3 from "node:fs";
5335
+ import * as os3 from "node:os";
5336
+ import * as path3 from "node:path";
5337
+
5338
+ // src/lib/daemon/paths.ts
5339
+ import * as fs2 from "node:fs";
5340
+ import * as os2 from "node:os";
5341
+ import * as path2 from "node:path";
5342
+ function configBase() {
5343
+ if (process.platform === "darwin") {
5344
+ return path2.join(os2.homedir(), "Library", "Application Support");
5345
+ }
5346
+ const xdg = process.env.XDG_CONFIG_HOME;
5347
+ return xdg ?? path2.join(os2.homedir(), ".config");
5348
+ }
5349
+ function daemonStateDir() {
5350
+ return path2.join(configBase(), "token-rats");
5351
+ }
5352
+ function daemonLogDir() {
5353
+ return path2.join(daemonStateDir(), "logs");
5354
+ }
5355
+ function daemonStateFile() {
5356
+ return path2.join(daemonStateDir(), "daemon-state.json");
5357
+ }
5358
+ function cliEntrypoint() {
5359
+ const node = process.execPath;
5360
+ const script = fs2.realpathSync(process.argv[1] ?? "");
5361
+ return { node, script };
5362
+ }
5363
+ function entrypointIsEphemeral() {
5364
+ const { script } = cliEntrypoint();
5365
+ return script.includes("_npx") || script.includes(".npm/_cacache");
5366
+ }
5367
+ function ensureDaemonDirs() {
5368
+ fs2.mkdirSync(daemonStateDir(), { recursive: true });
5369
+ fs2.mkdirSync(daemonLogDir(), { recursive: true });
5370
+ }
5371
+
5372
+ // src/lib/daemon/linux.ts
5373
+ var UNIT = "token-rats-watch.service";
5374
+ function unitPath() {
5375
+ const xdg = process.env.XDG_CONFIG_HOME ?? path3.join(os3.homedir(), ".config");
5376
+ return path3.join(xdg, "systemd", "user", UNIT);
5377
+ }
5378
+ function renderUnit(node, script, logDir) {
5379
+ return `[Unit]
5380
+ Description=Token Rats background watcher
5381
+ After=default.target
5382
+
5383
+ [Service]
5384
+ Type=simple
5385
+ ExecStart=${node} ${script} watch --daemon
5386
+ Restart=on-failure
5387
+ RestartSec=30
5388
+ StandardOutput=append:${path3.join(logDir, "watch.log")}
5389
+ StandardError=append:${path3.join(logDir, "watch.err.log")}
5390
+ Nice=10
5391
+
5392
+ [Install]
5393
+ WantedBy=default.target
5394
+ `;
5395
+ }
5396
+ function systemctl(args) {
5397
+ const r = spawnSync("systemctl", ["--user", ...args], { encoding: "utf8" });
5398
+ return {
5399
+ ok: r.status === 0,
5400
+ stdout: r.stdout ?? "",
5401
+ stderr: r.stderr ?? ""
5402
+ };
5403
+ }
5404
+ var SystemdUnavailableError = class extends Error {
5405
+ constructor() {
5406
+ super("systemctl --user is unavailable on this system");
5407
+ this.name = "SystemdUnavailableError";
5408
+ }
5409
+ };
5410
+ function assertSystemd() {
5411
+ const r = spawnSync("systemctl", ["--user", "--version"], { encoding: "utf8" });
5412
+ if (r.status !== 0) throw new SystemdUnavailableError();
5413
+ }
5414
+ function lingerEnabled() {
5415
+ const user = os3.userInfo().username;
5416
+ const r = spawnSync("loginctl", ["show-user", user], { encoding: "utf8" });
5417
+ if (r.status !== 0) return false;
5418
+ return /Linger=yes/.test(r.stdout);
5419
+ }
5420
+ function installLinux(node, script) {
5421
+ assertSystemd();
5422
+ ensureDaemonDirs();
5423
+ const file = unitPath();
5424
+ fs3.mkdirSync(path3.dirname(file), { recursive: true });
5425
+ const logDir = daemonLogDir();
5426
+ fs3.writeFileSync(file, renderUnit(node, script, logDir), { encoding: "utf8", mode: 420 });
5427
+ const reload = systemctl(["daemon-reload"]);
5428
+ if (!reload.ok) {
5429
+ throw new Error(`systemctl daemon-reload failed: ${reload.stderr.trim()}`);
5430
+ }
5431
+ const enable = systemctl(["enable", "--now", UNIT]);
5432
+ if (!enable.ok) {
5433
+ throw new Error(`systemctl enable --now failed: ${enable.stderr.trim()}`);
5434
+ }
5435
+ return { unit: file, logDir };
5436
+ }
5437
+ function uninstallLinux() {
5438
+ systemctl(["disable", "--now", UNIT]);
5439
+ try {
5440
+ fs3.rmSync(unitPath());
5094
5441
  } catch {
5095
- return null;
5096
5442
  }
5443
+ systemctl(["daemon-reload"]);
5097
5444
  }
5098
- function tryDirectTable(db) {
5445
+ function statusLinux() {
5446
+ const installed = fs3.existsSync(unitPath());
5447
+ if (!installed) return { installed: false, running: false };
5448
+ const r = systemctl(["show", UNIT, "--property=ActiveState,MainPID"]);
5449
+ const active = /ActiveState=active/.test(r.stdout);
5450
+ const pidMatch = r.stdout.match(/MainPID=(\d+)/);
5451
+ const pid = pidMatch?.[1] ? Number(pidMatch[1]) : void 0;
5452
+ return {
5453
+ installed: true,
5454
+ running: active && pid !== void 0 && pid > 0,
5455
+ ...pid && pid > 0 ? { pid } : {}
5456
+ };
5457
+ }
5458
+
5459
+ // src/lib/daemon/macos.ts
5460
+ import { spawnSync as spawnSync2 } from "node:child_process";
5461
+ import * as fs4 from "node:fs";
5462
+ import * as os4 from "node:os";
5463
+ import * as path4 from "node:path";
5464
+ var LABEL = "com.tokenrats.watch";
5465
+ function plistPath() {
5466
+ return path4.join(os4.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
5467
+ }
5468
+ function xmlEscape(s) {
5469
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
5470
+ }
5471
+ function renderPlist(node, script, logDir) {
5472
+ const stdout = xmlEscape(path4.join(logDir, "watch.log"));
5473
+ const stderr = xmlEscape(path4.join(logDir, "watch.err.log"));
5474
+ return `<?xml version="1.0" encoding="UTF-8"?>
5475
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
5476
+ <plist version="1.0">
5477
+ <dict>
5478
+ <key>Label</key><string>${LABEL}</string>
5479
+ <key>ProgramArguments</key>
5480
+ <array>
5481
+ <string>${xmlEscape(node)}</string>
5482
+ <string>${xmlEscape(script)}</string>
5483
+ <string>watch</string>
5484
+ <string>--daemon</string>
5485
+ </array>
5486
+ <key>RunAtLoad</key><true/>
5487
+ <key>KeepAlive</key>
5488
+ <dict>
5489
+ <key>SuccessfulExit</key><false/>
5490
+ <key>Crashed</key><true/>
5491
+ </dict>
5492
+ <key>ThrottleInterval</key><integer>30</integer>
5493
+ <key>StandardOutPath</key><string>${stdout}</string>
5494
+ <key>StandardErrorPath</key><string>${stderr}</string>
5495
+ <key>ProcessType</key><string>Background</string>
5496
+ </dict>
5497
+ </plist>
5498
+ `;
5499
+ }
5500
+ function uid() {
5501
+ const fn = process.getuid;
5502
+ if (typeof fn !== "function") throw new Error("process.getuid unavailable");
5503
+ return fn();
5504
+ }
5505
+ function launchctl(args) {
5506
+ const r = spawnSync2("launchctl", args, { encoding: "utf8" });
5507
+ return {
5508
+ ok: r.status === 0,
5509
+ stdout: r.stdout ?? "",
5510
+ stderr: r.stderr ?? ""
5511
+ };
5512
+ }
5513
+ function installMacos(node, script) {
5514
+ ensureDaemonDirs();
5515
+ const dir = path4.dirname(plistPath());
5516
+ fs4.mkdirSync(dir, { recursive: true });
5517
+ const logDir = daemonLogDir();
5518
+ const file = plistPath();
5519
+ fs4.writeFileSync(file, renderPlist(node, script, logDir), { encoding: "utf8", mode: 420 });
5520
+ const target = `gui/${uid()}`;
5521
+ launchctl(["bootout", target, file]);
5522
+ const r = launchctl(["bootstrap", target, file]);
5523
+ if (!r.ok) {
5524
+ throw new Error(`launchctl bootstrap failed: ${r.stderr.trim() || r.stdout.trim()}`);
5525
+ }
5526
+ launchctl(["kickstart", "-k", `${target}/${LABEL}`]);
5527
+ return { plist: file, logDir };
5528
+ }
5529
+ function uninstallMacos() {
5530
+ const file = plistPath();
5531
+ const target = `gui/${uid()}`;
5532
+ launchctl(["bootout", target, file]);
5099
5533
  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);
5534
+ fs4.rmSync(file);
5110
5535
  } catch {
5111
- return null;
5112
5536
  }
5113
5537
  }
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 });
5538
+ function statusMacos() {
5539
+ const file = plistPath();
5540
+ const installed = fs4.existsSync(file);
5541
+ if (!installed) return { installed: false, running: false };
5542
+ const r = launchctl(["print", `gui/${uid()}/${LABEL}`]);
5543
+ const match = r.stdout.match(/\bpid\s*=\s*(\d+)/);
5544
+ if (match?.[1]) return { installed: true, running: true, pid: Number(match[1]) };
5545
+ return { installed: true, running: false };
5546
+ }
5547
+
5548
+ // src/lib/daemon/state.ts
5549
+ import * as fs5 from "node:fs";
5550
+ function readDaemonState() {
5551
+ try {
5552
+ const raw = fs5.readFileSync(daemonStateFile(), "utf8");
5553
+ const parsed = JSON.parse(raw);
5554
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
5555
+ } catch {
5556
+ return {};
5128
5557
  }
5129
- return results;
5130
5558
  }
5131
- function toNonNegInt4(v) {
5132
- if (typeof v !== "number" || !isFinite(v)) return 0;
5133
- return Math.max(0, Math.floor(v));
5559
+ function writeDaemonState(state) {
5560
+ ensureDaemonDirs();
5561
+ fs5.writeFileSync(daemonStateFile(), `${JSON.stringify(state, null, 2)}
5562
+ `, {
5563
+ encoding: "utf8",
5564
+ mode: 384
5565
+ });
5134
5566
  }
5135
- function toPositiveMs(v) {
5136
- if (typeof v !== "number" || !isFinite(v) || v <= 0) return null;
5137
- return v;
5567
+ function markDeclined() {
5568
+ const s = readDaemonState();
5569
+ s.declinedAt = (/* @__PURE__ */ new Date()).toISOString();
5570
+ writeDaemonState(s);
5138
5571
  }
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 };
5572
+ function markInstalled(method, script, version) {
5573
+ const s = readDaemonState();
5574
+ s.installedAt = (/* @__PURE__ */ new Date()).toISOString();
5575
+ s.installedScript = script;
5576
+ s.method = method;
5577
+ s.cliVersion = version;
5578
+ s.declinedAt = void 0;
5579
+ writeDaemonState(s);
5580
+ }
5581
+ function markUninstalled() {
5582
+ const s = readDaemonState();
5583
+ s.method = "none";
5584
+ s.installedAt = void 0;
5585
+ s.installedScript = void 0;
5586
+ writeDaemonState(s);
5587
+ }
5588
+
5589
+ // src/lib/daemon/install.ts
5590
+ function platformMethod() {
5591
+ if (process.platform === "darwin") return "launchd";
5592
+ if (process.platform === "linux") return "systemd";
5593
+ return "unsupported";
5594
+ }
5595
+ function isDaemonSupported() {
5596
+ return platformMethod() !== "unsupported";
5597
+ }
5598
+ var DaemonUnsupportedError = class extends Error {
5599
+ constructor(platform) {
5600
+ super(`Background daemon is not yet supported on ${platform}`);
5601
+ this.name = "DaemonUnsupportedError";
5147
5602
  }
5148
- const fromBetter = await tryBetterSqlite3(dbPath);
5149
- if (fromBetter !== null) {
5150
- return { rows: fromBetter, skipped: null };
5603
+ };
5604
+ async function installDaemon(version) {
5605
+ const method = platformMethod();
5606
+ const { node, script } = cliEntrypoint();
5607
+ const ephemeralWarning = entrypointIsEphemeral();
5608
+ if (method === "launchd") {
5609
+ const r = installMacos(node, script);
5610
+ markInstalled("launchd", script, version);
5611
+ return { method: "launchd", logDir: r.logDir, ephemeralWarning };
5612
+ }
5613
+ if (method === "systemd") {
5614
+ try {
5615
+ const r = installLinux(node, script);
5616
+ markInstalled("systemd", script, version);
5617
+ return {
5618
+ method: "systemd",
5619
+ logDir: r.logDir,
5620
+ ephemeralWarning,
5621
+ lingerSet: lingerEnabled()
5622
+ };
5623
+ } catch (err) {
5624
+ if (err instanceof SystemdUnavailableError) {
5625
+ throw new DaemonUnsupportedError(process.platform);
5626
+ }
5627
+ throw err;
5628
+ }
5151
5629
  }
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."
5630
+ throw new DaemonUnsupportedError(process.platform);
5631
+ }
5632
+ async function uninstallDaemon() {
5633
+ const method = platformMethod();
5634
+ if (method === "launchd") uninstallMacos();
5635
+ else if (method === "systemd") uninstallLinux();
5636
+ markUninstalled();
5637
+ }
5638
+ async function daemonStatus() {
5639
+ const method = platformMethod();
5640
+ const state = readDaemonState();
5641
+ const base = {
5642
+ method,
5643
+ installedScript: state.installedScript,
5644
+ installedAt: state.installedAt,
5645
+ declinedAt: state.declinedAt
5155
5646
  };
5647
+ if (method === "launchd") {
5648
+ const s = statusMacos();
5649
+ return { ...base, installed: s.installed, running: s.running, ...s.pid && { pid: s.pid } };
5650
+ }
5651
+ if (method === "systemd") {
5652
+ const s = statusLinux();
5653
+ return { ...base, installed: s.installed, running: s.running, ...s.pid && { pid: s.pid } };
5654
+ }
5655
+ return { ...base, installed: false, running: false };
5156
5656
  }
5157
5657
 
5158
5658
  // src/lib/discover.ts
5159
- import * as fs2 from "node:fs";
5160
- import * as os2 from "node:os";
5161
- import * as path2 from "node:path";
5659
+ import * as fs6 from "node:fs";
5660
+ import * as os5 from "node:os";
5661
+ import * as path5 from "node:path";
5162
5662
  function findJsonlFiles(dir) {
5163
5663
  const results = [];
5164
- if (!fs2.existsSync(dir)) return results;
5165
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
5664
+ if (!fs6.existsSync(dir)) return results;
5665
+ const entries = fs6.readdirSync(dir, { withFileTypes: true });
5166
5666
  for (const entry of entries) {
5167
- const full = path2.join(dir, entry.name);
5667
+ const full = path5.join(dir, entry.name);
5168
5668
  if (entry.isDirectory()) {
5169
5669
  results.push(...findJsonlFiles(full));
5170
5670
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -5174,32 +5674,32 @@ function findJsonlFiles(dir) {
5174
5674
  return results;
5175
5675
  }
5176
5676
  function claudeCodeProjectsDir() {
5177
- const home = os2.homedir();
5677
+ const home = os5.homedir();
5178
5678
  if (process.platform === "win32") {
5179
- const profile = process.env["USERPROFILE"] ?? home;
5180
- return path2.join(profile, ".claude", "projects");
5679
+ const profile = process.env.USERPROFILE ?? home;
5680
+ return path5.join(profile, ".claude", "projects");
5181
5681
  }
5182
- return path2.join(home, ".claude", "projects");
5682
+ return path5.join(home, ".claude", "projects");
5183
5683
  }
5184
5684
  function discoverClaudeCodeFiles() {
5185
5685
  const dir = claudeCodeProjectsDir();
5186
5686
  return findJsonlFiles(dir);
5187
5687
  }
5188
5688
  function codexSessionsDirs() {
5189
- const home = os2.homedir();
5689
+ const home = os5.homedir();
5190
5690
  const candidates = [];
5191
5691
  if (process.platform === "win32") {
5192
- const profile = process.env["USERPROFILE"] ?? home;
5193
- candidates.push(path2.join(profile, ".codex", "sessions"));
5194
- const appData = process.env["APPDATA"] ?? path2.join(profile, "AppData", "Roaming");
5195
- candidates.push(path2.join(appData, "Codex", "sessions"));
5692
+ const profile = process.env.USERPROFILE ?? home;
5693
+ candidates.push(path5.join(profile, ".codex", "sessions"));
5694
+ const appData = process.env.APPDATA ?? path5.join(profile, "AppData", "Roaming");
5695
+ candidates.push(path5.join(appData, "Codex", "sessions"));
5196
5696
  } else {
5197
- candidates.push(path2.join(home, ".codex", "sessions"));
5198
- const snapRoot = path2.join(home, "snap", "codex");
5199
- if (fs2.existsSync(snapRoot)) {
5200
- for (const entry of fs2.readdirSync(snapRoot, { withFileTypes: true })) {
5697
+ candidates.push(path5.join(home, ".codex", "sessions"));
5698
+ const snapRoot = path5.join(home, "snap", "codex");
5699
+ if (fs6.existsSync(snapRoot)) {
5700
+ for (const entry of fs6.readdirSync(snapRoot, { withFileTypes: true })) {
5201
5701
  if (entry.isDirectory()) {
5202
- candidates.push(path2.join(snapRoot, entry.name, "sessions"));
5702
+ candidates.push(path5.join(snapRoot, entry.name, "sessions"));
5203
5703
  }
5204
5704
  }
5205
5705
  }
@@ -5208,7 +5708,7 @@ function codexSessionsDirs() {
5208
5708
  const dirs = [];
5209
5709
  for (const c2 of candidates) {
5210
5710
  try {
5211
- const real = fs2.existsSync(c2) ? fs2.realpathSync(c2) : null;
5711
+ const real = fs6.existsSync(c2) ? fs6.realpathSync(c2) : null;
5212
5712
  if (real && !seen2.has(real)) {
5213
5713
  seen2.add(real);
5214
5714
  dirs.push(c2);
@@ -5225,23 +5725,9 @@ function discoverCodexFiles() {
5225
5725
  }
5226
5726
  return out;
5227
5727
  }
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
- }
5728
+
5729
+ // src/lib/version.ts
5730
+ var CLI_VERSION = "0.1.0";
5245
5731
 
5246
5732
  // src/commands/sync.ts
5247
5733
  var BATCH_SIZE = 500;
@@ -5269,8 +5755,7 @@ function mergeBySessionId(records) {
5269
5755
  }
5270
5756
  }
5271
5757
  for (const rec of byId.values()) {
5272
- const { costUsdCents } = priceOf(rec.model, rec.inTokens, rec.outTokens);
5273
- rec.costUsdCents = costUsdCents;
5758
+ rec.costUsdCents = 0;
5274
5759
  rec.dedupeKey = computeDedupeKey(
5275
5760
  rec.source,
5276
5761
  rec.model,
@@ -5297,7 +5782,7 @@ async function syncCommand(opts) {
5297
5782
  for (const file of claudeFiles) {
5298
5783
  let text;
5299
5784
  try {
5300
- text = fs3.readFileSync(file, "utf8");
5785
+ text = fs7.readFileSync(file, "utf8");
5301
5786
  } catch {
5302
5787
  if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
5303
5788
  continue;
@@ -5319,7 +5804,7 @@ async function syncCommand(opts) {
5319
5804
  for (const file of codexFiles) {
5320
5805
  let text;
5321
5806
  try {
5322
- text = fs3.readFileSync(file, "utf8");
5807
+ text = fs7.readFileSync(file, "utf8");
5323
5808
  } catch {
5324
5809
  if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
5325
5810
  continue;
@@ -5333,25 +5818,26 @@ async function syncCommand(opts) {
5333
5818
  }
5334
5819
  }
5335
5820
  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");
5821
+ const { rows, skipped, dbCount } = await extractCursorGenerations();
5822
+ if (skipped) {
5823
+ warn(skipped);
5824
+ } else if (dbCount === 0) {
5825
+ if (opts.verbose) info("No Cursor workspace storage found \u2014 skipping Cursor source");
5826
+ } else if (rows.length > 0) {
5827
+ if (opts.verbose) info(`Scanned ${dbCount} Cursor workspace DB(s)`);
5828
+ try {
5829
+ const records = parseCursor(JSON.stringify(rows));
5830
+ cursorSessions.push(...records);
5831
+ if (opts.verbose) {
5832
+ dim(
5833
+ ` Cursor: ${rows.length} generation event(s) \u2192 ${records.length} session(s) (tokens estimated, see help)`
5834
+ );
5349
5835
  }
5350
- } else if (opts.verbose) {
5351
- dim(" Cursor DB: 0 rows found");
5836
+ } catch {
5837
+ if (opts.verbose) warn("Failed to parse Cursor rows \u2014 skipping");
5352
5838
  }
5353
- } else {
5354
- if (opts.verbose) info("Cursor DB not found \u2014 skipping Cursor source");
5839
+ } else if (opts.verbose) {
5840
+ dim(` Scanned ${dbCount} Cursor workspace DB(s): no AI generations found`);
5355
5841
  }
5356
5842
  const mergedClaude = mergeBySessionId(claudeSessions);
5357
5843
  if (opts.verbose && mergedClaude.length !== claudeSessions.length) {
@@ -5379,7 +5865,9 @@ async function syncCommand(opts) {
5379
5865
  }
5380
5866
  info(`Discovered ${allSessions.length} session(s) from ${sourceStr}.`);
5381
5867
  if (opts.dryRun) {
5382
- info(`[dry-run] Would upload ${allSessions.length} session(s) in ${Math.ceil(allSessions.length / BATCH_SIZE)} batch(es).`);
5868
+ info(
5869
+ `[dry-run] Would upload ${allSessions.length} session(s) in ${Math.ceil(allSessions.length / BATCH_SIZE)} batch(es).`
5870
+ );
5383
5871
  if (opts.verbose) {
5384
5872
  for (const s of allSessions.slice(0, 10)) {
5385
5873
  dim(
@@ -5416,189 +5904,459 @@ async function syncCommand(opts) {
5416
5904
  success(
5417
5905
  `Synced ${allSessions.length} sessions (${totalAccepted} new, ${totalDuplicates} already on server) from ${sourceStr}`
5418
5906
  );
5907
+ await maybeWireDaemon(opts);
5908
+ }
5909
+ async function maybeWireDaemon(opts) {
5910
+ if (opts.noDaemon) return;
5911
+ if (process.env.TOKEN_RATS_NO_DAEMON === "1") return;
5912
+ if (!isDaemonSupported()) return;
5913
+ const status = await daemonStatus();
5914
+ const state = readDaemonState();
5915
+ if (status.installed) {
5916
+ try {
5917
+ await installDaemon(CLI_VERSION);
5918
+ if (opts.verbose) dim("Refreshed daemon registration (self-heal).");
5919
+ } catch (err) {
5920
+ warn(`Could not refresh daemon: ${err instanceof Error ? err.message : String(err)}`);
5921
+ }
5922
+ return;
5923
+ }
5924
+ if (state.declinedAt) {
5925
+ if (opts.verbose) dim(`Daemon prompt skipped \u2014 declined on ${state.declinedAt}`);
5926
+ return;
5927
+ }
5928
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return;
5929
+ info("");
5930
+ info("Background sync watches Claude Code + Cursor + Codex and uploads new");
5931
+ info("sessions automatically \u2014 no more re-running `token-rats sync`.");
5932
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5933
+ let answer;
5934
+ try {
5935
+ answer = (await rl.question("Install background sync? [Y/n] ")).trim().toLowerCase();
5936
+ } finally {
5937
+ rl.close();
5938
+ }
5939
+ if (answer === "n" || answer === "no") {
5940
+ markDeclined();
5941
+ info("Skipped. Enable later with `token-rats watch --install`.");
5942
+ return;
5943
+ }
5944
+ try {
5945
+ const r = await installDaemon(CLI_VERSION);
5946
+ success(`Background sync enabled (${r.method}). Logs: ${r.logDir}/watch.log`);
5947
+ info("Disable any time with `token-rats watch --uninstall`.");
5948
+ if (r.ephemeralWarning) {
5949
+ warn(
5950
+ "You ran sync via npx \u2014 the daemon may break when the npx cache clears.\nRun `npm i -g token-rats` for a durable install."
5951
+ );
5952
+ }
5953
+ if (r.method === "systemd" && r.lingerSet === false) {
5954
+ warn(
5955
+ "For the watcher to survive logout, run once:\n sudo loginctl enable-linger $USER"
5956
+ );
5957
+ }
5958
+ } catch (err) {
5959
+ error(`Install failed: ${err instanceof Error ? err.message : String(err)}`);
5960
+ info("You can retry later with `token-rats watch --install`.");
5961
+ }
5419
5962
  }
5420
5963
 
5421
5964
  // src/commands/watch.ts
5422
- import * as fs4 from "node:fs";
5965
+ import * as fs8 from "node:fs";
5966
+ import * as path6 from "node:path";
5967
+ var CURSOR_POLL_MS = 9e4;
5968
+ var CURSOR_DB_CAP = 10;
5969
+ var REAUTH_BACKOFF_MS = 10 * 60 * 1e3;
5970
+ function makeLogger(daemon, verbose) {
5971
+ if (daemon) {
5972
+ const emit = (level, msg, extra) => {
5973
+ const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra });
5974
+ process.stderr.write(`${line}
5975
+ `);
5976
+ };
5977
+ return {
5978
+ info: (m, e) => emit("info", m, e),
5979
+ warn: (m, e) => emit("warn", m, e),
5980
+ error: (m, e) => emit("error", m, e),
5981
+ debug: (m, e) => {
5982
+ if (verbose) emit("debug", m, e);
5983
+ }
5984
+ };
5985
+ }
5986
+ return {
5987
+ info: (m) => info(m),
5988
+ warn: (m) => warn(m),
5989
+ error: (m) => error(m),
5990
+ debug: (m) => {
5991
+ if (verbose) dim(m);
5992
+ }
5993
+ };
5994
+ }
5423
5995
  var seen = /* @__PURE__ */ new Set();
5424
- function parseAndFilter(filePath, verbose) {
5996
+ var reauthBackoffUntil = 0;
5997
+ var health = {
5998
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5999
+ uploaded: 0,
6000
+ duplicates: 0,
6001
+ errors: 0,
6002
+ lastSourceAt: {}
6003
+ };
6004
+ async function upload(client, records, log, daemon) {
6005
+ if (records.length === 0) return;
6006
+ if (Date.now() < reauthBackoffUntil) {
6007
+ log.debug("Skipping upload \u2014 in reauth backoff", { records: records.length });
6008
+ return;
6009
+ }
6010
+ try {
6011
+ const res = await client.uploadSessions(records);
6012
+ health.uploaded += res.accepted;
6013
+ health.duplicates += res.duplicates;
6014
+ log.info("upload", { accepted: res.accepted, duplicates: res.duplicates });
6015
+ } catch (err) {
6016
+ health.errors++;
6017
+ if (err instanceof ApiError2 && err.status === 401) {
6018
+ if (daemon) {
6019
+ reauthBackoffUntil = Date.now() + REAUTH_BACKOFF_MS;
6020
+ log.error("Auth expired. Run `token-rats login` to recover.", {
6021
+ backoffUntil: new Date(reauthBackoffUntil).toISOString()
6022
+ });
6023
+ return;
6024
+ }
6025
+ error("Session expired. Run `token-rats login` to re-authenticate.");
6026
+ process.exit(1);
6027
+ }
6028
+ log.warn(`Upload failed: ${err instanceof Error ? err.message : String(err)}`);
6029
+ }
6030
+ }
6031
+ function freshClaudeRecords(filePath, log) {
5425
6032
  let text;
5426
6033
  try {
5427
- text = fs4.readFileSync(filePath, "utf8");
6034
+ text = fs8.readFileSync(filePath, "utf8");
5428
6035
  } catch {
5429
- if (verbose) warn(`Could not read ${filePath} \u2014 skipping`);
6036
+ log.debug(`Could not read ${filePath} \u2014 skipping`);
5430
6037
  return [];
5431
6038
  }
5432
6039
  let records;
5433
6040
  try {
5434
6041
  records = parseClaudeCode(text);
5435
6042
  } catch {
5436
- if (verbose) warn(`Failed to parse ${filePath} \u2014 skipping`);
6043
+ log.debug(`Failed to parse ${filePath} \u2014 skipping`);
5437
6044
  return [];
5438
6045
  }
5439
6046
  const fresh = [];
5440
6047
  for (const r of records) {
5441
- if (!seen.has(r.dedupeKey)) {
5442
- seen.add(r.dedupeKey);
5443
- fresh.push(r);
5444
- }
6048
+ if (seen.has(r.dedupeKey)) continue;
6049
+ seen.add(r.dedupeKey);
6050
+ fresh.push(r);
5445
6051
  }
5446
- if (verbose && fresh.length > 0) {
5447
- dim(` ${filePath}: ${fresh.length} new session(s)`);
6052
+ if (fresh.length > 0) {
6053
+ health.lastSourceAt["claude-code"] = (/* @__PURE__ */ new Date()).toISOString();
6054
+ log.debug(`${filePath}: ${fresh.length} new Claude Code session(s)`);
5448
6055
  }
5449
6056
  return fresh;
5450
6057
  }
5451
- async function upload(client, records, verbose) {
5452
- if (records.length === 0) return;
6058
+ function freshCodexRecords(filePath, log) {
6059
+ let text;
5453
6060
  try {
5454
- const res = await client.uploadSessions(records);
5455
- if (verbose) {
5456
- dim(` Uploaded ${res.accepted} new, ${res.duplicates} duplicate(s)`);
5457
- } else {
5458
- success(`Uploaded ${res.accepted} session(s)`);
5459
- }
5460
- } catch (err) {
5461
- if (err instanceof ApiError2 && err.status === 401) {
5462
- error("Session expired. Run `token-rats login` to re-authenticate.");
5463
- process.exit(1);
5464
- }
5465
- warn(`Upload failed: ${err instanceof Error ? err.message : String(err)}`);
6061
+ text = fs8.readFileSync(filePath, "utf8");
6062
+ } catch {
6063
+ return [];
6064
+ }
6065
+ let records;
6066
+ try {
6067
+ records = parseCodex(text);
6068
+ } catch {
6069
+ return [];
5466
6070
  }
6071
+ const fresh = [];
6072
+ for (const r of records) {
6073
+ if (seen.has(r.dedupeKey)) continue;
6074
+ seen.add(r.dedupeKey);
6075
+ fresh.push(r);
6076
+ }
6077
+ if (fresh.length > 0) {
6078
+ health.lastSourceAt.codex = (/* @__PURE__ */ new Date()).toISOString();
6079
+ log.debug(`${filePath}: ${fresh.length} new Codex session(s)`);
6080
+ }
6081
+ return fresh;
5467
6082
  }
5468
6083
  function makeDebounced(fn, ms) {
5469
6084
  const timers = /* @__PURE__ */ new Map();
5470
- return (path3) => {
5471
- const existing = timers.get(path3);
6085
+ return (p) => {
6086
+ const existing = timers.get(p);
5472
6087
  if (existing) clearTimeout(existing);
5473
6088
  timers.set(
5474
- path3,
6089
+ p,
5475
6090
  setTimeout(() => {
5476
- timers.delete(path3);
5477
- fn(path3);
6091
+ timers.delete(p);
6092
+ fn(p);
5478
6093
  }, ms)
5479
6094
  );
5480
6095
  };
5481
6096
  }
5482
6097
  function findJsonlFiles2(dir) {
5483
- const results = [];
5484
- if (!fs4.existsSync(dir)) return results;
5485
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
5486
- for (const entry of entries) {
5487
- const full = `${dir}/${entry.name}`;
5488
- if (entry.isDirectory()) {
5489
- results.push(...findJsonlFiles2(full));
5490
- } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
5491
- results.push(full);
5492
- }
6098
+ const out = [];
6099
+ if (!fs8.existsSync(dir)) return out;
6100
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
6101
+ const full = path6.join(dir, entry.name);
6102
+ if (entry.isDirectory()) out.push(...findJsonlFiles2(full));
6103
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(full);
5493
6104
  }
5494
- return results;
6105
+ return out;
5495
6106
  }
5496
- async function watchCommand(opts) {
5497
- const token = loadToken();
5498
- if (!token) {
5499
- error("Not logged in. Run `token-rats login` first.");
5500
- process.exit(1);
5501
- }
5502
- const client = new ApiClient({ apiUrl: opts.apiUrl, token });
5503
- const debounceMs = opts.interval ?? 2e3;
5504
- const dir = claudeCodeProjectsDir();
5505
- if (!fs4.existsSync(dir)) {
5506
- warn(`Claude Code projects directory not found: ${dir}`);
5507
- warn("No files to watch. Exiting.");
5508
- process.exit(0);
5509
- }
5510
- info(`Watching ${dir} (debounce: ${debounceMs}ms)`);
5511
- info("Press Ctrl-C to stop.\n");
5512
- const initialFiles = findJsonlFiles2(dir);
5513
- for (const f of initialFiles) {
5514
- parseAndFilter(f, false);
5515
- }
5516
- if (opts.verbose) {
5517
- dim(`Initial snapshot: ${seen.size} session(s) in ${initialFiles.length} file(s)`);
6107
+ async function watchJsonlDir(dir, debounceMs, onChange, log) {
6108
+ if (!fs8.existsSync(dir)) {
6109
+ log.debug(`Directory missing \u2014 not watching: ${dir}`);
6110
+ return () => {
6111
+ };
5518
6112
  }
5519
- const onChanged = makeDebounced(async (filePath) => {
5520
- if (!filePath.endsWith(".jsonl")) return;
5521
- const fresh = parseAndFilter(filePath, opts.verbose ?? false);
5522
- if (fresh.length > 0) {
5523
- await upload(client, fresh, opts.verbose ?? false);
5524
- }
6113
+ const debouncedChange = makeDebounced((p) => {
6114
+ if (p.endsWith(".jsonl")) onChange(p);
5525
6115
  }, debounceMs);
5526
- let cleanup = null;
5527
6116
  try {
5528
- const chokidar = await new Function("m", "return import(m)")("chokidar");
5529
- const watcher = chokidar.watch(`${dir}/**/*.jsonl`, {
6117
+ const dynImport = new Function("m", "return import(m)");
6118
+ const chokidarMod = await dynImport("chokidar");
6119
+ const watcher = chokidarMod.watch(`${dir}/**/*.jsonl`, {
5530
6120
  ignoreInitial: true,
5531
6121
  persistent: true,
5532
6122
  awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
5533
6123
  });
5534
- watcher.on("add", (p) => onChanged(p));
5535
- watcher.on("change", (p) => onChanged(p));
5536
- cleanup = () => {
6124
+ watcher.on("add", (p) => {
6125
+ if (typeof p === "string") debouncedChange(p);
6126
+ });
6127
+ watcher.on("change", (p) => {
6128
+ if (typeof p === "string") debouncedChange(p);
6129
+ });
6130
+ return () => {
5537
6131
  watcher.close();
5538
6132
  };
5539
- if (opts.verbose) dim("Using chokidar for file watching");
5540
6133
  } catch {
5541
- if (opts.verbose) dim("chokidar not available; using Node built-in fs.watch");
5542
- if (process.platform === "linux") {
5543
- let lastMtimes = /* @__PURE__ */ new Map();
5544
- for (const f of initialFiles) {
6134
+ }
6135
+ if (process.platform === "linux") {
6136
+ let lastMtimes = /* @__PURE__ */ new Map();
6137
+ for (const f of findJsonlFiles2(dir)) {
6138
+ try {
6139
+ lastMtimes.set(f, fs8.statSync(f).mtimeMs);
6140
+ } catch {
6141
+ }
6142
+ }
6143
+ const handle = setInterval(() => {
6144
+ const current = findJsonlFiles2(dir);
6145
+ for (const f of current) {
5545
6146
  try {
5546
- lastMtimes.set(f, fs4.statSync(f).mtimeMs);
6147
+ const mtime = fs8.statSync(f).mtimeMs;
6148
+ const prev = lastMtimes.get(f) ?? 0;
6149
+ if (mtime > prev) {
6150
+ lastMtimes.set(f, mtime);
6151
+ debouncedChange(f);
6152
+ }
5547
6153
  } catch {
5548
6154
  }
5549
6155
  }
5550
- const pollInterval = setInterval(() => {
5551
- const current = findJsonlFiles2(dir);
5552
- for (const f of current) {
5553
- try {
5554
- const mtime = fs4.statSync(f).mtimeMs;
5555
- const prev = lastMtimes.get(f) ?? 0;
5556
- if (mtime > prev) {
5557
- lastMtimes.set(f, mtime);
5558
- onChanged(f);
5559
- }
5560
- } catch {
5561
- }
5562
- }
5563
- for (const f of current) {
5564
- if (!lastMtimes.has(f)) {
5565
- lastMtimes.set(f, Date.now());
5566
- onChanged(f);
5567
- }
6156
+ for (const f of current) {
6157
+ if (!lastMtimes.has(f)) {
6158
+ lastMtimes.set(f, Date.now());
6159
+ debouncedChange(f);
5568
6160
  }
5569
- lastMtimes = new Map(current.map((f) => [f, lastMtimes.get(f) ?? 0]));
5570
- }, debounceMs);
5571
- cleanup = () => clearInterval(pollInterval);
5572
- } else {
5573
- const controller = new AbortController();
5574
- (async () => {
5575
- try {
5576
- const { watch } = await import("node:fs/promises");
5577
- const watcher = watch(dir, { recursive: true, signal: controller.signal });
5578
- for await (const event of watcher) {
5579
- const filename = event.filename;
5580
- if (filename && filename.endsWith(".jsonl")) {
5581
- const fullPath = `${dir}/${filename}`;
5582
- onChanged(fullPath);
5583
- }
5584
- }
5585
- } catch (err) {
5586
- if (err instanceof Error && err.name !== "AbortError") {
5587
- warn(`Watcher error: ${err.message}`);
5588
- }
6161
+ }
6162
+ lastMtimes = new Map(current.map((f) => [f, lastMtimes.get(f) ?? 0]));
6163
+ }, debounceMs);
6164
+ return () => clearInterval(handle);
6165
+ }
6166
+ const controller = new AbortController();
6167
+ (async () => {
6168
+ try {
6169
+ const { watch } = await import("node:fs/promises");
6170
+ const watcher = watch(dir, { recursive: true, signal: controller.signal });
6171
+ for await (const event of watcher) {
6172
+ if (event.filename?.endsWith(".jsonl")) {
6173
+ debouncedChange(path6.join(dir, event.filename));
5589
6174
  }
5590
- })();
5591
- cleanup = () => controller.abort();
6175
+ }
6176
+ } catch (err) {
6177
+ if (err instanceof Error && err.name !== "AbortError") {
6178
+ log.warn(`Watcher error on ${dir}: ${err.message}`);
6179
+ }
6180
+ }
6181
+ })();
6182
+ return () => controller.abort();
6183
+ }
6184
+ function startCursorLoop(client, log, daemon) {
6185
+ let lastMtimes = /* @__PURE__ */ new Map();
6186
+ let running = false;
6187
+ let stopped = false;
6188
+ const tick = async () => {
6189
+ if (running || stopped) return;
6190
+ running = true;
6191
+ try {
6192
+ const { rows, newMtimes, scanned, opened } = await extractCursorGenerationsDelta(
6193
+ lastMtimes,
6194
+ CURSOR_DB_CAP
6195
+ );
6196
+ lastMtimes = newMtimes;
6197
+ if (rows.length === 0) {
6198
+ if (opened > 0) log.debug(`Cursor poll: ${scanned} DB(s), opened ${opened}, no new rows`);
6199
+ return;
6200
+ }
6201
+ let records;
6202
+ try {
6203
+ records = parseCursor(JSON.stringify(rows));
6204
+ } catch {
6205
+ log.warn("Failed to parse Cursor rows");
6206
+ return;
6207
+ }
6208
+ const fresh = [];
6209
+ for (const r of records) {
6210
+ if (seen.has(r.dedupeKey)) continue;
6211
+ seen.add(r.dedupeKey);
6212
+ fresh.push(r);
6213
+ }
6214
+ if (fresh.length === 0) {
6215
+ log.debug(`Cursor poll: ${rows.length} row(s), all duplicates`);
6216
+ return;
6217
+ }
6218
+ health.lastSourceAt.cursor = (/* @__PURE__ */ new Date()).toISOString();
6219
+ log.debug(
6220
+ `Cursor poll: ${fresh.length} fresh session(s) (scanned ${scanned}, opened ${opened})`
6221
+ );
6222
+ await upload(client, fresh, log, daemon);
6223
+ } finally {
6224
+ running = false;
5592
6225
  }
6226
+ };
6227
+ const initial = setTimeout(tick, 5e3);
6228
+ const handle = setInterval(tick, CURSOR_POLL_MS);
6229
+ return () => {
6230
+ stopped = true;
6231
+ clearTimeout(initial);
6232
+ clearInterval(handle);
6233
+ };
6234
+ }
6235
+ async function runWatcher(opts) {
6236
+ const daemon = opts.daemon === true;
6237
+ const verbose = opts.verbose === true;
6238
+ const log = makeLogger(daemon, verbose);
6239
+ const token = loadToken();
6240
+ if (!token) {
6241
+ log.error("Not logged in. Run `token-rats login` first.");
6242
+ if (!daemon) process.exit(1);
6243
+ reauthBackoffUntil = Date.now() + REAUTH_BACKOFF_MS;
5593
6244
  }
5594
- function shutdown() {
5595
- info("Shutting down\u2026");
5596
- if (cleanup) cleanup();
5597
- process.exit(0);
6245
+ const client = new ApiClient({ apiUrl: opts.apiUrl, token: token ?? void 0 });
6246
+ const debounceMs = opts.interval ?? 2e3;
6247
+ if (!daemon) {
6248
+ info("Watching Claude Code + Cursor + Codex");
6249
+ info("Press Ctrl-C to stop.\n");
6250
+ } else {
6251
+ log.info("watch starting", { debounceMs, cursorPollMs: CURSOR_POLL_MS });
6252
+ }
6253
+ const ccDir = claudeCodeProjectsDir();
6254
+ for (const f of findJsonlFiles2(ccDir)) freshClaudeRecords(f, log);
6255
+ for (const d of codexSessionsDirs()) {
6256
+ for (const f of findJsonlFiles2(d)) freshCodexRecords(f, log);
6257
+ }
6258
+ log.debug(`Seeded ${seen.size} dedupe key(s) from initial snapshot`);
6259
+ const cleanups = [];
6260
+ cleanups.push(
6261
+ await watchJsonlDir(
6262
+ ccDir,
6263
+ debounceMs,
6264
+ async (filePath) => {
6265
+ const fresh = freshClaudeRecords(filePath, log);
6266
+ await upload(client, fresh, log, daemon);
6267
+ },
6268
+ log
6269
+ )
6270
+ );
6271
+ for (const codexDir of codexSessionsDirs()) {
6272
+ cleanups.push(
6273
+ await watchJsonlDir(
6274
+ codexDir,
6275
+ debounceMs,
6276
+ async (filePath) => {
6277
+ const fresh = freshCodexRecords(filePath, log);
6278
+ await upload(client, fresh, log, daemon);
6279
+ },
6280
+ log
6281
+ )
6282
+ );
5598
6283
  }
6284
+ cleanups.push(startCursorLoop(client, log, daemon));
6285
+ process.on("SIGUSR1", () => {
6286
+ log.info("health", {
6287
+ uptimeSec: Math.round(process.uptime()),
6288
+ uploaded: health.uploaded,
6289
+ duplicates: health.duplicates,
6290
+ errors: health.errors,
6291
+ lastSourceAt: health.lastSourceAt,
6292
+ seenKeys: seen.size,
6293
+ reauthBackoffUntil: reauthBackoffUntil > Date.now() ? new Date(reauthBackoffUntil).toISOString() : null
6294
+ });
6295
+ });
6296
+ const shutdown = () => {
6297
+ log.info("watch stopping");
6298
+ for (const c2 of cleanups) c2();
6299
+ process.exit(0);
6300
+ };
5599
6301
  process.on("SIGINT", shutdown);
5600
6302
  process.on("SIGTERM", shutdown);
5601
6303
  }
6304
+ async function handleInstall(opts) {
6305
+ if (!isDaemonSupported()) {
6306
+ error(`Background daemon not supported on ${process.platform} yet.`);
6307
+ process.exit(1);
6308
+ }
6309
+ try {
6310
+ const r = await installDaemon(opts.version ?? "0.0.0");
6311
+ success(`Background watcher installed (${r.method}).`);
6312
+ info(`Logs: ${r.logDir}/watch.log`);
6313
+ if (r.ephemeralWarning) {
6314
+ warn(
6315
+ "You ran this via npx \u2014 the daemon may break when the npx cache is cleared.\nRun `npm i -g token-rats` (or `pnpm add -g token-rats`) for a durable install."
6316
+ );
6317
+ }
6318
+ if (r.method === "systemd" && r.lingerSet === false) {
6319
+ warn(
6320
+ "For the watcher to survive logout, run once:\n sudo loginctl enable-linger $USER\nWithout it, the watcher resumes at your next login."
6321
+ );
6322
+ }
6323
+ } catch (err) {
6324
+ error(`Install failed: ${err instanceof Error ? err.message : String(err)}`);
6325
+ process.exit(1);
6326
+ }
6327
+ }
6328
+ async function handleUninstall() {
6329
+ try {
6330
+ await uninstallDaemon();
6331
+ success("Background watcher uninstalled.");
6332
+ } catch (err) {
6333
+ error(`Uninstall failed: ${err instanceof Error ? err.message : String(err)}`);
6334
+ process.exit(1);
6335
+ }
6336
+ }
6337
+ async function handleStatus() {
6338
+ const s = await daemonStatus();
6339
+ if (s.method === "unsupported") {
6340
+ info(`Daemon not supported on ${process.platform}.`);
6341
+ return;
6342
+ }
6343
+ info(`Method: ${s.method}`);
6344
+ info(`Installed: ${s.installed ? "yes" : "no"}`);
6345
+ info(`Running: ${s.running ? `yes (pid ${s.pid ?? "?"})` : "no"}`);
6346
+ if (s.installedAt) info(`Installed at: ${s.installedAt}`);
6347
+ if (s.installedScript) info(`Script: ${s.installedScript}`);
6348
+ if (s.declinedAt) info(`Declined at: ${s.declinedAt}`);
6349
+ info(`Log dir: ${daemonLogDir()}`);
6350
+ if (process.platform === "linux") {
6351
+ info(`Linger: ${lingerEnabled() ? "yes (survives logout)" : "no (dies on logout)"}`);
6352
+ }
6353
+ }
6354
+ async function watchCommand(opts) {
6355
+ if (opts.install) return handleInstall(opts);
6356
+ if (opts.uninstall) return handleUninstall();
6357
+ if (opts.status) return handleStatus();
6358
+ return runWatcher(opts);
6359
+ }
5602
6360
 
5603
6361
  // src/commands/whoami.ts
5604
6362
  async function whoamiCommand(opts) {
@@ -5623,7 +6381,7 @@ async function whoamiCommand(opts) {
5623
6381
 
5624
6382
  // src/index.ts
5625
6383
  function getVersion() {
5626
- return "0.0.3";
6384
+ return CLI_VERSION;
5627
6385
  }
5628
6386
  function printHelp() {
5629
6387
  console.log(`
@@ -5634,8 +6392,13 @@ function printHelp() {
5634
6392
 
5635
6393
  \x1B[1mCommands:\x1B[0m
5636
6394
  login Authenticate with Token Rats (opens browser)
5637
- sync Read local Claude Code + Cursor logs and upload counts
5638
- watch Watch logs in real-time; upload new sessions as they appear
6395
+ sync Upload local Claude Code + Cursor + Codex usage. On first
6396
+ successful sync, asks to install a background watcher
6397
+ (launchd on macOS, systemd --user on Linux) that keeps
6398
+ your leaderboard live \u2014 no more re-running sync.
6399
+ watch Run the foreground watcher (Ctrl-C to stop), OR manage
6400
+ the background watcher with --install / --uninstall /
6401
+ --status.
5639
6402
  whoami Show the currently signed-in account
5640
6403
  logout Clear your stored credentials
5641
6404
  install-cursor Install better-sqlite3 globally for faster Cursor reads
@@ -5648,8 +6411,15 @@ function printHelp() {
5648
6411
  \x1B[1mFlags (sync only):\x1B[0m
5649
6412
  --dry-run Parse but do not upload; print what would be sent
5650
6413
  --verbose Print discovered files and per-file record counts
6414
+ --no-daemon Skip the post-sync prompt to install background watcher
6415
+ (TOKEN_RATS_NO_DAEMON=1 does the same globally)
5651
6416
 
5652
6417
  \x1B[1mFlags (watch only):\x1B[0m
6418
+ --install Install the background watcher (launchd / systemd --user)
6419
+ --uninstall Remove the background watcher
6420
+ --status Show whether the watcher is installed and running
6421
+ --daemon Internal: structured logs for launchd/systemd. Not for
6422
+ interactive use.
5653
6423
  --interval <ms> Debounce window in ms before uploading (default: 2000)
5654
6424
  --verbose Print file change events and upload detail
5655
6425
 
@@ -5658,6 +6428,12 @@ function printHelp() {
5658
6428
  The parser source is in packages/parsers/. We literally can't read
5659
6429
  what you typed.
5660
6430
 
6431
+ \x1B[1mCursor notes:\x1B[0m
6432
+ Cursor doesn't store token counts locally, so per-request tokens
6433
+ are *estimated* (10k in / 2k out per composer turn, claude-3-5-sonnet
6434
+ rates). Tab autocomplete is excluded. Numbers are comparable across
6435
+ Token Rats users but won't match cursor.com to the token.
6436
+
5661
6437
  \x1B[1mExamples:\x1B[0m
5662
6438
  npx token-rats login
5663
6439
  npx token-rats sync
@@ -5672,6 +6448,11 @@ function parseArgs(argv) {
5672
6448
  let dryRun = false;
5673
6449
  let verbose = false;
5674
6450
  let interval;
6451
+ let noDaemon = false;
6452
+ let daemon = false;
6453
+ let install = false;
6454
+ let uninstall = false;
6455
+ let status = false;
5675
6456
  let i = 0;
5676
6457
  while (i < argv.length) {
5677
6458
  const arg = argv[i];
@@ -5687,6 +6468,16 @@ function parseArgs(argv) {
5687
6468
  interval = Number(argv[++i]);
5688
6469
  } else if (arg.startsWith("--interval=")) {
5689
6470
  interval = Number(arg.slice("--interval=".length));
6471
+ } else if (arg === "--no-daemon") {
6472
+ noDaemon = true;
6473
+ } else if (arg === "--daemon") {
6474
+ daemon = true;
6475
+ } else if (arg === "--install") {
6476
+ install = true;
6477
+ } else if (arg === "--uninstall") {
6478
+ uninstall = true;
6479
+ } else if (arg === "--status") {
6480
+ status = true;
5690
6481
  } else if (arg === "--version" || arg === "-V") {
5691
6482
  console.log(getVersion());
5692
6483
  process.exit(0);
@@ -5699,11 +6490,34 @@ function parseArgs(argv) {
5699
6490
  i++;
5700
6491
  }
5701
6492
  const [command = null, ...rest] = positional;
5702
- return { command, apiUrl, dryRun, verbose, interval, rest };
6493
+ return {
6494
+ command,
6495
+ apiUrl,
6496
+ dryRun,
6497
+ verbose,
6498
+ interval,
6499
+ noDaemon,
6500
+ daemon,
6501
+ install,
6502
+ uninstall,
6503
+ status,
6504
+ rest
6505
+ };
5703
6506
  }
5704
6507
  async function main() {
5705
6508
  const args = parseArgs(process.argv.slice(2));
5706
- const { command, apiUrl, dryRun, verbose, interval } = args;
6509
+ const {
6510
+ command,
6511
+ apiUrl,
6512
+ dryRun,
6513
+ verbose,
6514
+ interval,
6515
+ noDaemon,
6516
+ daemon,
6517
+ install,
6518
+ uninstall,
6519
+ status
6520
+ } = args;
5707
6521
  if (!command || command === "help") {
5708
6522
  printHelp();
5709
6523
  process.exit(0);
@@ -5713,10 +6527,19 @@ async function main() {
5713
6527
  await loginCommand({ apiUrl });
5714
6528
  break;
5715
6529
  case "sync":
5716
- await syncCommand({ apiUrl, dryRun, verbose });
6530
+ await syncCommand({ apiUrl, dryRun, verbose, noDaemon });
5717
6531
  break;
5718
6532
  case "watch":
5719
- await watchCommand({ apiUrl, verbose, interval });
6533
+ await watchCommand({
6534
+ apiUrl,
6535
+ verbose,
6536
+ interval,
6537
+ daemon,
6538
+ install,
6539
+ uninstall,
6540
+ status,
6541
+ version: getVersion()
6542
+ });
5720
6543
  break;
5721
6544
  case "whoami":
5722
6545
  await whoamiCommand({ apiUrl });
@@ -5729,7 +6552,7 @@ async function main() {
5729
6552
  break;
5730
6553
  default:
5731
6554
  console.error(`\x1B[31mUnknown command: ${command}\x1B[0m`);
5732
- console.error(`Run \x1B[1mtoken-rats help\x1B[0m for a list of commands.`);
6555
+ console.error("Run \x1B[1mtoken-rats help\x1B[0m for a list of commands.");
5733
6556
  process.exit(1);
5734
6557
  }
5735
6558
  }