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.
- package/README.md +54 -11
- package/dist/index.js +1280 -457
- 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:
|
|
389
|
-
const fullPath = [...
|
|
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,
|
|
511
|
+
constructor(parent, value, path7, key) {
|
|
512
512
|
this._cachedPath = [];
|
|
513
513
|
this.parent = parent;
|
|
514
514
|
this.data = value;
|
|
515
|
-
this._path =
|
|
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
|
-
|
|
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
|
|
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 *
|
|
4599
|
+
await sleep(1e3 * 2 ** (attempt - 1));
|
|
4390
4600
|
}
|
|
4391
4601
|
}
|
|
4392
4602
|
throw lastErr;
|
|
4393
4603
|
}
|
|
4394
|
-
async post(
|
|
4395
|
-
const url = `${this.apiUrl}${
|
|
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(
|
|
4407
|
-
const url = `${this.apiUrl}${
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
4749
|
-
const sidSnake = ev
|
|
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
|
|
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
|
|
4777
|
-
const message = ev
|
|
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
|
|
4781
|
-
acc.model = msg
|
|
4932
|
+
if (typeof msg.model === "string" && msg.model.length > 0) {
|
|
4933
|
+
acc.model = msg.model;
|
|
4782
4934
|
}
|
|
4783
|
-
const usage = msg
|
|
4935
|
+
const usage = msg.usage;
|
|
4784
4936
|
if (typeof usage === "object" && usage !== null) {
|
|
4785
4937
|
const u = usage;
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
acc.
|
|
4789
|
-
acc.
|
|
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
|
|
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
|
|
4847
|
-
const type = typeof ev
|
|
4848
|
-
const payload = typeof ev
|
|
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
|
|
5005
|
+
const id = typeof payload.id === "string" ? payload.id : null;
|
|
4851
5006
|
if (!id) continue;
|
|
4852
5007
|
currentSessionId = id;
|
|
4853
|
-
const metaTs = parseTimestamp(payload
|
|
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
|
|
4868
|
-
acc.model = payload
|
|
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
|
|
4872
|
-
const info2 = payload
|
|
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
|
|
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
|
|
4878
|
-
const cachedInput = toNonNegInt2(t
|
|
4879
|
-
const output = toNonNegInt2(t
|
|
4880
|
-
const reasoning = toNonNegInt2(t
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
5120
|
+
const id = typeof row.id === "string" ? row.id : null;
|
|
4976
5121
|
if (!id) continue;
|
|
4977
|
-
const
|
|
4978
|
-
|
|
4979
|
-
const
|
|
4980
|
-
|
|
4981
|
-
const
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
const
|
|
4985
|
-
const dedupeKey = computeDedupeKey("cursor", model,
|
|
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
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
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
|
|
5174
|
+
return out;
|
|
5017
5175
|
}
|
|
5018
|
-
async function
|
|
5019
|
-
let initSqlJs;
|
|
5176
|
+
async function openDb(dbPath) {
|
|
5020
5177
|
try {
|
|
5021
|
-
const mod = await import("
|
|
5022
|
-
|
|
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
|
-
|
|
5030
|
-
|
|
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
|
|
5065
|
-
|
|
5231
|
+
async function readGenerationsFromDb(dbPath) {
|
|
5232
|
+
const db = await openDb(dbPath);
|
|
5233
|
+
if (!db) return [];
|
|
5066
5234
|
try {
|
|
5067
|
-
db =
|
|
5068
|
-
|
|
5069
|
-
|
|
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
|
|
5258
|
+
return [];
|
|
5072
5259
|
} finally {
|
|
5073
5260
|
try {
|
|
5074
|
-
db
|
|
5261
|
+
db.close();
|
|
5075
5262
|
} catch {
|
|
5076
5263
|
}
|
|
5077
5264
|
}
|
|
5078
5265
|
}
|
|
5079
|
-
function
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5093
|
-
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
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
|
-
|
|
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
|
|
5115
|
-
const
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
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
|
|
5132
|
-
|
|
5133
|
-
|
|
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
|
|
5136
|
-
|
|
5137
|
-
|
|
5567
|
+
function markDeclined() {
|
|
5568
|
+
const s = readDaemonState();
|
|
5569
|
+
s.declinedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5570
|
+
writeDaemonState(s);
|
|
5138
5571
|
}
|
|
5139
|
-
|
|
5140
|
-
const
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
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
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
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
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
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
|
|
5160
|
-
import * as
|
|
5161
|
-
import * as
|
|
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 (!
|
|
5165
|
-
const entries =
|
|
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 =
|
|
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 =
|
|
5677
|
+
const home = os5.homedir();
|
|
5178
5678
|
if (process.platform === "win32") {
|
|
5179
|
-
const profile = process.env
|
|
5180
|
-
return
|
|
5679
|
+
const profile = process.env.USERPROFILE ?? home;
|
|
5680
|
+
return path5.join(profile, ".claude", "projects");
|
|
5181
5681
|
}
|
|
5182
|
-
return
|
|
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 =
|
|
5689
|
+
const home = os5.homedir();
|
|
5190
5690
|
const candidates = [];
|
|
5191
5691
|
if (process.platform === "win32") {
|
|
5192
|
-
const profile = process.env
|
|
5193
|
-
candidates.push(
|
|
5194
|
-
const appData = process.env
|
|
5195
|
-
candidates.push(
|
|
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(
|
|
5198
|
-
const snapRoot =
|
|
5199
|
-
if (
|
|
5200
|
-
for (const entry of
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
5337
|
-
if (
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
if (
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
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
|
-
}
|
|
5351
|
-
|
|
5836
|
+
} catch {
|
|
5837
|
+
if (opts.verbose) warn("Failed to parse Cursor rows \u2014 skipping");
|
|
5352
5838
|
}
|
|
5353
|
-
} else {
|
|
5354
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
6034
|
+
text = fs8.readFileSync(filePath, "utf8");
|
|
5428
6035
|
} catch {
|
|
5429
|
-
|
|
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
|
-
|
|
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 (
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
}
|
|
6048
|
+
if (seen.has(r.dedupeKey)) continue;
|
|
6049
|
+
seen.add(r.dedupeKey);
|
|
6050
|
+
fresh.push(r);
|
|
5445
6051
|
}
|
|
5446
|
-
if (
|
|
5447
|
-
|
|
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
|
-
|
|
5452
|
-
|
|
6058
|
+
function freshCodexRecords(filePath, log) {
|
|
6059
|
+
let text;
|
|
5453
6060
|
try {
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
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 (
|
|
5471
|
-
const existing = timers.get(
|
|
6085
|
+
return (p) => {
|
|
6086
|
+
const existing = timers.get(p);
|
|
5472
6087
|
if (existing) clearTimeout(existing);
|
|
5473
6088
|
timers.set(
|
|
5474
|
-
|
|
6089
|
+
p,
|
|
5475
6090
|
setTimeout(() => {
|
|
5476
|
-
timers.delete(
|
|
5477
|
-
fn(
|
|
6091
|
+
timers.delete(p);
|
|
6092
|
+
fn(p);
|
|
5478
6093
|
}, ms)
|
|
5479
6094
|
);
|
|
5480
6095
|
};
|
|
5481
6096
|
}
|
|
5482
6097
|
function findJsonlFiles2(dir) {
|
|
5483
|
-
const
|
|
5484
|
-
if (!
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
if (entry.
|
|
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
|
|
6105
|
+
return out;
|
|
5495
6106
|
}
|
|
5496
|
-
async function
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
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
|
|
5520
|
-
if (
|
|
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
|
|
5529
|
-
const
|
|
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) =>
|
|
5535
|
-
|
|
5536
|
-
|
|
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
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
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
|
-
|
|
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
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
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
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
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
|
|
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
|
|
5638
|
-
|
|
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 {
|
|
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 {
|
|
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({
|
|
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(
|
|
6555
|
+
console.error("Run \x1B[1mtoken-rats help\x1B[0m for a list of commands.");
|
|
5733
6556
|
process.exit(1);
|
|
5734
6557
|
}
|
|
5735
6558
|
}
|