token-rats 0.0.4 → 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 +1064 -266
- package/package.json +2 -3
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
|
|
4212
|
+
});
|
|
4213
|
+
var PatchOrgRequest = z.object({
|
|
4214
|
+
founderEmail: z.string().email().optional(),
|
|
4215
|
+
founderName: z.string().min(1).max(80).optional()
|
|
4148
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}`,
|
|
@@ -4283,7 +4427,10 @@ var ENDPOINTS = {
|
|
|
4283
4427
|
// Admin analytics (project-owner only)
|
|
4284
4428
|
adminSignups: "/v1/admin/signups",
|
|
4285
4429
|
adminActivity: "/v1/admin/activity",
|
|
4286
|
-
adminReferrers: "/v1/admin/referrers"
|
|
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`
|
|
4287
4434
|
};
|
|
4288
4435
|
|
|
4289
4436
|
// ../contracts/src/errors.ts
|
|
@@ -4369,20 +4516,37 @@ var AdminActivityResponse = z.object({
|
|
|
4369
4516
|
generatedAt: z.number().int().positive()
|
|
4370
4517
|
});
|
|
4371
4518
|
var AdminReferrerRow = z.object({
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
/** Number of users
|
|
4519
|
+
handle: z.string(),
|
|
4520
|
+
avatarUrl: z.string().url().nullable(),
|
|
4521
|
+
/** Number of users this referrer has brought in. */
|
|
4375
4522
|
count: z.number().int().nonnegative()
|
|
4376
4523
|
});
|
|
4377
4524
|
var AdminReferrersResponse = z.object({
|
|
4378
|
-
/** True iff a real referral attribution column / table exists. */
|
|
4379
|
-
tracked: z.boolean(),
|
|
4380
|
-
/** Description of what `rows` represents (e.g. "First CLI source"). */
|
|
4381
|
-
signal: z.string(),
|
|
4382
4525
|
rows: z.array(AdminReferrerRow),
|
|
4383
4526
|
generatedAt: z.number().int().positive()
|
|
4384
4527
|
});
|
|
4385
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
|
+
|
|
4386
4550
|
// src/lib/api.ts
|
|
4387
4551
|
var DEFAULT_API_URL = "https://api.tokenrats.com";
|
|
4388
4552
|
function isTransient(status) {
|
|
@@ -4437,8 +4601,8 @@ var ApiClient = class {
|
|
|
4437
4601
|
}
|
|
4438
4602
|
throw lastErr;
|
|
4439
4603
|
}
|
|
4440
|
-
async post(
|
|
4441
|
-
const url = `${this.apiUrl}${
|
|
4604
|
+
async post(path7, body) {
|
|
4605
|
+
const url = `${this.apiUrl}${path7}`;
|
|
4442
4606
|
const res = await this.fetchWithRetry(url, {
|
|
4443
4607
|
method: "POST",
|
|
4444
4608
|
headers: this.headers(),
|
|
@@ -4449,8 +4613,8 @@ var ApiClient = class {
|
|
|
4449
4613
|
}
|
|
4450
4614
|
return res.json();
|
|
4451
4615
|
}
|
|
4452
|
-
async get(
|
|
4453
|
-
const url = `${this.apiUrl}${
|
|
4616
|
+
async get(path7) {
|
|
4617
|
+
const url = `${this.apiUrl}${path7}`;
|
|
4454
4618
|
const res = await this.fetchWithRetry(url, {
|
|
4455
4619
|
method: "GET",
|
|
4456
4620
|
headers: this.headers()
|
|
@@ -4687,68 +4851,8 @@ function logoutCommand() {
|
|
|
4687
4851
|
}
|
|
4688
4852
|
|
|
4689
4853
|
// src/commands/sync.ts
|
|
4690
|
-
import * as
|
|
4691
|
-
|
|
4692
|
-
// ../pricing/src/prices.ts
|
|
4693
|
-
var prices = {
|
|
4694
|
-
models: {
|
|
4695
|
-
// ── Anthropic / Claude ────────────────────────────────────────────────
|
|
4696
|
-
"claude-opus-4-7": { inputPerMTok: 15, outputPerMTok: 75 },
|
|
4697
|
-
"claude-opus-4-6": { inputPerMTok: 15, outputPerMTok: 75 },
|
|
4698
|
-
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4699
|
-
"claude-sonnet-4-5": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4700
|
-
"claude-haiku-4-5": { inputPerMTok: 0.8, outputPerMTok: 4 },
|
|
4701
|
-
"claude-3-5-sonnet-20241022": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4702
|
-
"claude-3-5-sonnet-20240620": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4703
|
-
"claude-3-5-haiku-20241022": { inputPerMTok: 0.8, outputPerMTok: 4 },
|
|
4704
|
-
"claude-3-opus-20240229": { inputPerMTok: 15, outputPerMTok: 75 },
|
|
4705
|
-
"claude-3-sonnet-20240229": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
4706
|
-
"claude-3-haiku-20240307": { inputPerMTok: 0.25, outputPerMTok: 1.25 },
|
|
4707
|
-
// ── OpenAI / Codex ────────────────────────────────────────────────────
|
|
4708
|
-
// GPT-5 family. gpt-5-codex is the Codex CLI's public model; OpenAI's
|
|
4709
|
-
// pricing page lists only input ($1.25/MTok). Output uses the same $10
|
|
4710
|
-
// rate the broader GPT-5 base is publicly quoted at — flagged here for
|
|
4711
|
-
// re-verification when OpenAI publishes an explicit output figure.
|
|
4712
|
-
"gpt-5-codex": { inputPerMTok: 1.25, outputPerMTok: 10 },
|
|
4713
|
-
"gpt-5-mini": { inputPerMTok: 0.25, outputPerMTok: 2 },
|
|
4714
|
-
"gpt-5-nano": { inputPerMTok: 0.05, outputPerMTok: 0.4 },
|
|
4715
|
-
"gpt-5-pro": { inputPerMTok: 15, outputPerMTok: 120 },
|
|
4716
|
-
"gpt-5.5": { inputPerMTok: 5, outputPerMTok: 30 },
|
|
4717
|
-
"codex-mini-latest": { inputPerMTok: 1.5, outputPerMTok: 6 },
|
|
4718
|
-
// GPT-4 family.
|
|
4719
|
-
"gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10 },
|
|
4720
|
-
"gpt-4o-mini": { inputPerMTok: 0.15, outputPerMTok: 0.6 },
|
|
4721
|
-
"gpt-4-turbo": { inputPerMTok: 10, outputPerMTok: 30 },
|
|
4722
|
-
"gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8 },
|
|
4723
|
-
"gpt-4.1-mini": { inputPerMTok: 0.4, outputPerMTok: 1.6 },
|
|
4724
|
-
"gpt-4.1-nano": { inputPerMTok: 0.1, outputPerMTok: 0.4 },
|
|
4725
|
-
// Reasoning models (o-series).
|
|
4726
|
-
o1: { inputPerMTok: 15, outputPerMTok: 60 },
|
|
4727
|
-
"o1-mini": { inputPerMTok: 3, outputPerMTok: 12 },
|
|
4728
|
-
// o3 was repriced in 2026; previously $10/$40, now $2/$8.
|
|
4729
|
-
o3: { inputPerMTok: 2, outputPerMTok: 8 },
|
|
4730
|
-
"o3-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4 },
|
|
4731
|
-
"o4-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4 }
|
|
4732
|
-
}
|
|
4733
|
-
};
|
|
4734
|
-
|
|
4735
|
-
// ../pricing/src/index.ts
|
|
4736
|
-
var TABLE = prices.models;
|
|
4737
|
-
function priceOf(model, inTokens, outTokens) {
|
|
4738
|
-
const p = TABLE[model] ?? matchPrefix(model);
|
|
4739
|
-
if (!p) return { costUsdCents: 0, known: false };
|
|
4740
|
-
const dollars = (inTokens * p.inputPerMTok + outTokens * p.outputPerMTok) / 1e6;
|
|
4741
|
-
return { costUsdCents: Math.round(dollars * 100), known: true };
|
|
4742
|
-
}
|
|
4743
|
-
function matchPrefix(model) {
|
|
4744
|
-
let best;
|
|
4745
|
-
for (const [key, price] of Object.entries(TABLE)) {
|
|
4746
|
-
if (model.startsWith(key) && (!best || key.length > best.key.length)) {
|
|
4747
|
-
best = { key, price };
|
|
4748
|
-
}
|
|
4749
|
-
}
|
|
4750
|
-
return best?.price;
|
|
4751
|
-
}
|
|
4854
|
+
import * as fs7 from "node:fs";
|
|
4855
|
+
import * as readline from "node:readline/promises";
|
|
4752
4856
|
|
|
4753
4857
|
// ../parsers/src/hash.ts
|
|
4754
4858
|
var FNV_PRIME = 16777619;
|
|
@@ -4811,6 +4915,8 @@ function parseClaudeCode(input) {
|
|
|
4811
4915
|
endedAt: timestamp,
|
|
4812
4916
|
inTokens: 0,
|
|
4813
4917
|
outTokens: 0,
|
|
4918
|
+
cacheReadTokens: 0,
|
|
4919
|
+
cacheWriteTokens: 0,
|
|
4814
4920
|
model: ""
|
|
4815
4921
|
};
|
|
4816
4922
|
sessions.set(sessionId, acc);
|
|
@@ -4829,10 +4935,10 @@ function parseClaudeCode(input) {
|
|
|
4829
4935
|
const usage = msg.usage;
|
|
4830
4936
|
if (typeof usage === "object" && usage !== null) {
|
|
4831
4937
|
const u = usage;
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
acc.
|
|
4835
|
-
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);
|
|
4836
4942
|
}
|
|
4837
4943
|
}
|
|
4838
4944
|
const results = [];
|
|
@@ -4841,7 +4947,7 @@ function parseClaudeCode(input) {
|
|
|
4841
4947
|
const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
|
|
4842
4948
|
if (startedAt <= 0 || endedAt <= 0) continue;
|
|
4843
4949
|
const model = acc.model.length > 0 ? acc.model : "unknown";
|
|
4844
|
-
const
|
|
4950
|
+
const costUsdCents = 0;
|
|
4845
4951
|
const dedupeKey = computeDedupeKey(
|
|
4846
4952
|
"claude-code",
|
|
4847
4953
|
model,
|
|
@@ -4852,9 +4958,12 @@ function parseClaudeCode(input) {
|
|
|
4852
4958
|
results.push({
|
|
4853
4959
|
id: `claude-code:${acc.sessionId}`,
|
|
4854
4960
|
source: "claude-code",
|
|
4961
|
+
provider: "anthropic",
|
|
4855
4962
|
model,
|
|
4856
4963
|
inTokens: acc.inTokens,
|
|
4857
4964
|
outTokens: acc.outTokens,
|
|
4965
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
4966
|
+
cacheWriteTokens: acc.cacheWriteTokens,
|
|
4858
4967
|
costUsdCents,
|
|
4859
4968
|
startedAt,
|
|
4860
4969
|
endedAt,
|
|
@@ -4926,6 +5035,8 @@ function parseCodex(input) {
|
|
|
4926
5035
|
const reasoning = toNonNegInt2(t.reasoning_output_tokens);
|
|
4927
5036
|
acc.inTokens = Math.max(0, inputTotal - cachedInput);
|
|
4928
5037
|
acc.outTokens = output + reasoning;
|
|
5038
|
+
acc.cacheReadTokens = cachedInput;
|
|
5039
|
+
acc.reasoningTokens = reasoning;
|
|
4929
5040
|
}
|
|
4930
5041
|
}
|
|
4931
5042
|
const results = [];
|
|
@@ -4934,14 +5045,17 @@ function parseCodex(input) {
|
|
|
4934
5045
|
const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
|
|
4935
5046
|
if (startedAt <= 0 || endedAt <= 0) continue;
|
|
4936
5047
|
const model = acc.model.length > 0 ? acc.model : "unknown";
|
|
4937
|
-
const
|
|
5048
|
+
const costUsdCents = 0;
|
|
4938
5049
|
const dedupeKey = computeDedupeKey("codex", model, startedAt, acc.inTokens, acc.outTokens);
|
|
4939
5050
|
results.push({
|
|
4940
5051
|
id: `codex:${acc.sessionId}`,
|
|
4941
5052
|
source: "codex",
|
|
5053
|
+
provider: "openai",
|
|
4942
5054
|
model,
|
|
4943
5055
|
inTokens: acc.inTokens,
|
|
4944
5056
|
outTokens: acc.outTokens,
|
|
5057
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
5058
|
+
reasoningTokens: acc.reasoningTokens,
|
|
4945
5059
|
costUsdCents,
|
|
4946
5060
|
startedAt,
|
|
4947
5061
|
endedAt,
|
|
@@ -4959,6 +5073,8 @@ function upsert(map, sessionId) {
|
|
|
4959
5073
|
endedAt: 0,
|
|
4960
5074
|
inTokens: 0,
|
|
4961
5075
|
outTokens: 0,
|
|
5076
|
+
cacheReadTokens: 0,
|
|
5077
|
+
reasoningTokens: 0,
|
|
4962
5078
|
model: ""
|
|
4963
5079
|
};
|
|
4964
5080
|
map.set(sessionId, acc);
|
|
@@ -4983,12 +5099,6 @@ var ESTIMATES = {
|
|
|
4983
5099
|
composer: { inTokens: 1e4, outTokens: 2e3, model: "cursor-composer" }
|
|
4984
5100
|
// "tab" deliberately omitted — see file header.
|
|
4985
5101
|
};
|
|
4986
|
-
var INPUT_USD_PER_MTOK = 3;
|
|
4987
|
-
var OUTPUT_USD_PER_MTOK = 15;
|
|
4988
|
-
function estimatedCostCents(inTokens, outTokens) {
|
|
4989
|
-
const dollars = inTokens / 1e6 * INPUT_USD_PER_MTOK + outTokens / 1e6 * OUTPUT_USD_PER_MTOK;
|
|
4990
|
-
return Math.round(dollars * 100);
|
|
4991
|
-
}
|
|
4992
5102
|
function parseCursor(input) {
|
|
4993
5103
|
let text;
|
|
4994
5104
|
if (typeof input === "string") {
|
|
@@ -5007,20 +5117,21 @@ function parseCursor(input) {
|
|
|
5007
5117
|
for (const raw of rows) {
|
|
5008
5118
|
if (typeof raw !== "object" || raw === null) continue;
|
|
5009
5119
|
const row = raw;
|
|
5010
|
-
const id = typeof row
|
|
5120
|
+
const id = typeof row.id === "string" ? row.id : null;
|
|
5011
5121
|
if (!id) continue;
|
|
5012
|
-
const type = typeof row
|
|
5122
|
+
const type = typeof row.type === "string" ? row.type : null;
|
|
5013
5123
|
if (!type) continue;
|
|
5014
|
-
const unixMs = typeof row
|
|
5124
|
+
const unixMs = typeof row.unixMs === "number" && Number.isFinite(row.unixMs) && row.unixMs > 0 ? row.unixMs : null;
|
|
5015
5125
|
if (unixMs === null) continue;
|
|
5016
5126
|
const estimate = ESTIMATES[type];
|
|
5017
5127
|
if (!estimate) continue;
|
|
5018
5128
|
const { inTokens, outTokens, model } = estimate;
|
|
5019
|
-
const costUsdCents =
|
|
5129
|
+
const costUsdCents = 0;
|
|
5020
5130
|
const dedupeKey = computeDedupeKey("cursor", model, unixMs, inTokens, outTokens);
|
|
5021
5131
|
results.push({
|
|
5022
5132
|
id: `cursor:${id}`,
|
|
5023
5133
|
source: "cursor",
|
|
5134
|
+
provider: "cursor",
|
|
5024
5135
|
model,
|
|
5025
5136
|
inTokens,
|
|
5026
5137
|
outTokens,
|
|
@@ -5034,7 +5145,7 @@ function parseCursor(input) {
|
|
|
5034
5145
|
}
|
|
5035
5146
|
|
|
5036
5147
|
// src/lib/cursor-extract.ts
|
|
5037
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
5148
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
5038
5149
|
import { readFile } from "node:fs/promises";
|
|
5039
5150
|
import { homedir as homedir2 } from "node:os";
|
|
5040
5151
|
import { join as join2 } from "node:path";
|
|
@@ -5045,10 +5156,10 @@ function cursorWorkspaceStorageDir() {
|
|
|
5045
5156
|
return join2(home, "Library", "Application Support", rel);
|
|
5046
5157
|
}
|
|
5047
5158
|
if (process.platform === "win32") {
|
|
5048
|
-
const appData = process.env
|
|
5159
|
+
const appData = process.env.APPDATA ?? join2(home, "AppData", "Roaming");
|
|
5049
5160
|
return join2(appData, rel);
|
|
5050
5161
|
}
|
|
5051
|
-
const xdgConfig = process.env
|
|
5162
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join2(home, ".config");
|
|
5052
5163
|
return join2(xdgConfig, rel);
|
|
5053
5164
|
}
|
|
5054
5165
|
function discoverWorkspaceDbs() {
|
|
@@ -5136,9 +5247,9 @@ async function readGenerationsFromDb(dbPath) {
|
|
|
5136
5247
|
for (const item of parsed) {
|
|
5137
5248
|
if (typeof item !== "object" || item === null) continue;
|
|
5138
5249
|
const r = item;
|
|
5139
|
-
const id = typeof r
|
|
5140
|
-
const type = typeof r
|
|
5141
|
-
const unixMs = typeof r
|
|
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;
|
|
5142
5253
|
if (!id || !type || unixMs === null) continue;
|
|
5143
5254
|
out.push({ id, type, unixMs });
|
|
5144
5255
|
}
|
|
@@ -5187,17 +5298,373 @@ async function extractCursorGenerations() {
|
|
|
5187
5298
|
}
|
|
5188
5299
|
return { rows, dbCount: dbPaths.length, skipped: null };
|
|
5189
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 });
|
|
5310
|
+
} catch {
|
|
5311
|
+
}
|
|
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
|
+
}
|
|
5190
5331
|
|
|
5191
|
-
// src/lib/
|
|
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
|
|
5192
5339
|
import * as fs2 from "node:fs";
|
|
5193
5340
|
import * as os2 from "node:os";
|
|
5194
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());
|
|
5441
|
+
} catch {
|
|
5442
|
+
}
|
|
5443
|
+
systemctl(["daemon-reload"]);
|
|
5444
|
+
}
|
|
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]);
|
|
5533
|
+
try {
|
|
5534
|
+
fs4.rmSync(file);
|
|
5535
|
+
} catch {
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
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 {};
|
|
5557
|
+
}
|
|
5558
|
+
}
|
|
5559
|
+
function writeDaemonState(state) {
|
|
5560
|
+
ensureDaemonDirs();
|
|
5561
|
+
fs5.writeFileSync(daemonStateFile(), `${JSON.stringify(state, null, 2)}
|
|
5562
|
+
`, {
|
|
5563
|
+
encoding: "utf8",
|
|
5564
|
+
mode: 384
|
|
5565
|
+
});
|
|
5566
|
+
}
|
|
5567
|
+
function markDeclined() {
|
|
5568
|
+
const s = readDaemonState();
|
|
5569
|
+
s.declinedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5570
|
+
writeDaemonState(s);
|
|
5571
|
+
}
|
|
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";
|
|
5602
|
+
}
|
|
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
|
+
}
|
|
5629
|
+
}
|
|
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
|
|
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 };
|
|
5656
|
+
}
|
|
5657
|
+
|
|
5658
|
+
// src/lib/discover.ts
|
|
5659
|
+
import * as fs6 from "node:fs";
|
|
5660
|
+
import * as os5 from "node:os";
|
|
5661
|
+
import * as path5 from "node:path";
|
|
5195
5662
|
function findJsonlFiles(dir) {
|
|
5196
5663
|
const results = [];
|
|
5197
|
-
if (!
|
|
5198
|
-
const entries =
|
|
5664
|
+
if (!fs6.existsSync(dir)) return results;
|
|
5665
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
5199
5666
|
for (const entry of entries) {
|
|
5200
|
-
const full =
|
|
5667
|
+
const full = path5.join(dir, entry.name);
|
|
5201
5668
|
if (entry.isDirectory()) {
|
|
5202
5669
|
results.push(...findJsonlFiles(full));
|
|
5203
5670
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -5207,32 +5674,32 @@ function findJsonlFiles(dir) {
|
|
|
5207
5674
|
return results;
|
|
5208
5675
|
}
|
|
5209
5676
|
function claudeCodeProjectsDir() {
|
|
5210
|
-
const home =
|
|
5677
|
+
const home = os5.homedir();
|
|
5211
5678
|
if (process.platform === "win32") {
|
|
5212
5679
|
const profile = process.env.USERPROFILE ?? home;
|
|
5213
|
-
return
|
|
5680
|
+
return path5.join(profile, ".claude", "projects");
|
|
5214
5681
|
}
|
|
5215
|
-
return
|
|
5682
|
+
return path5.join(home, ".claude", "projects");
|
|
5216
5683
|
}
|
|
5217
5684
|
function discoverClaudeCodeFiles() {
|
|
5218
5685
|
const dir = claudeCodeProjectsDir();
|
|
5219
5686
|
return findJsonlFiles(dir);
|
|
5220
5687
|
}
|
|
5221
5688
|
function codexSessionsDirs() {
|
|
5222
|
-
const home =
|
|
5689
|
+
const home = os5.homedir();
|
|
5223
5690
|
const candidates = [];
|
|
5224
5691
|
if (process.platform === "win32") {
|
|
5225
5692
|
const profile = process.env.USERPROFILE ?? home;
|
|
5226
|
-
candidates.push(
|
|
5227
|
-
const appData = process.env.APPDATA ??
|
|
5228
|
-
candidates.push(
|
|
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"));
|
|
5229
5696
|
} else {
|
|
5230
|
-
candidates.push(
|
|
5231
|
-
const snapRoot =
|
|
5232
|
-
if (
|
|
5233
|
-
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 })) {
|
|
5234
5701
|
if (entry.isDirectory()) {
|
|
5235
|
-
candidates.push(
|
|
5702
|
+
candidates.push(path5.join(snapRoot, entry.name, "sessions"));
|
|
5236
5703
|
}
|
|
5237
5704
|
}
|
|
5238
5705
|
}
|
|
@@ -5241,7 +5708,7 @@ function codexSessionsDirs() {
|
|
|
5241
5708
|
const dirs = [];
|
|
5242
5709
|
for (const c2 of candidates) {
|
|
5243
5710
|
try {
|
|
5244
|
-
const real =
|
|
5711
|
+
const real = fs6.existsSync(c2) ? fs6.realpathSync(c2) : null;
|
|
5245
5712
|
if (real && !seen2.has(real)) {
|
|
5246
5713
|
seen2.add(real);
|
|
5247
5714
|
dirs.push(c2);
|
|
@@ -5259,6 +5726,9 @@ function discoverCodexFiles() {
|
|
|
5259
5726
|
return out;
|
|
5260
5727
|
}
|
|
5261
5728
|
|
|
5729
|
+
// src/lib/version.ts
|
|
5730
|
+
var CLI_VERSION = "0.1.0";
|
|
5731
|
+
|
|
5262
5732
|
// src/commands/sync.ts
|
|
5263
5733
|
var BATCH_SIZE = 500;
|
|
5264
5734
|
function chunk(arr, size) {
|
|
@@ -5285,8 +5755,7 @@ function mergeBySessionId(records) {
|
|
|
5285
5755
|
}
|
|
5286
5756
|
}
|
|
5287
5757
|
for (const rec of byId.values()) {
|
|
5288
|
-
|
|
5289
|
-
rec.costUsdCents = costUsdCents;
|
|
5758
|
+
rec.costUsdCents = 0;
|
|
5290
5759
|
rec.dedupeKey = computeDedupeKey(
|
|
5291
5760
|
rec.source,
|
|
5292
5761
|
rec.model,
|
|
@@ -5313,7 +5782,7 @@ async function syncCommand(opts) {
|
|
|
5313
5782
|
for (const file of claudeFiles) {
|
|
5314
5783
|
let text;
|
|
5315
5784
|
try {
|
|
5316
|
-
text =
|
|
5785
|
+
text = fs7.readFileSync(file, "utf8");
|
|
5317
5786
|
} catch {
|
|
5318
5787
|
if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
|
|
5319
5788
|
continue;
|
|
@@ -5335,7 +5804,7 @@ async function syncCommand(opts) {
|
|
|
5335
5804
|
for (const file of codexFiles) {
|
|
5336
5805
|
let text;
|
|
5337
5806
|
try {
|
|
5338
|
-
text =
|
|
5807
|
+
text = fs7.readFileSync(file, "utf8");
|
|
5339
5808
|
} catch {
|
|
5340
5809
|
if (opts.verbose) warn(`Could not read ${file} \u2014 skipping`);
|
|
5341
5810
|
continue;
|
|
@@ -5396,7 +5865,9 @@ async function syncCommand(opts) {
|
|
|
5396
5865
|
}
|
|
5397
5866
|
info(`Discovered ${allSessions.length} session(s) from ${sourceStr}.`);
|
|
5398
5867
|
if (opts.dryRun) {
|
|
5399
|
-
info(
|
|
5868
|
+
info(
|
|
5869
|
+
`[dry-run] Would upload ${allSessions.length} session(s) in ${Math.ceil(allSessions.length / BATCH_SIZE)} batch(es).`
|
|
5870
|
+
);
|
|
5400
5871
|
if (opts.verbose) {
|
|
5401
5872
|
for (const s of allSessions.slice(0, 10)) {
|
|
5402
5873
|
dim(
|
|
@@ -5433,191 +5904,459 @@ async function syncCommand(opts) {
|
|
|
5433
5904
|
success(
|
|
5434
5905
|
`Synced ${allSessions.length} sessions (${totalAccepted} new, ${totalDuplicates} already on server) from ${sourceStr}`
|
|
5435
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
|
+
}
|
|
5436
5962
|
}
|
|
5437
5963
|
|
|
5438
5964
|
// src/commands/watch.ts
|
|
5439
|
-
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
|
+
}
|
|
5440
5995
|
var seen = /* @__PURE__ */ new Set();
|
|
5441
|
-
|
|
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) {
|
|
5442
6032
|
let text;
|
|
5443
6033
|
try {
|
|
5444
|
-
text =
|
|
6034
|
+
text = fs8.readFileSync(filePath, "utf8");
|
|
5445
6035
|
} catch {
|
|
5446
|
-
|
|
6036
|
+
log.debug(`Could not read ${filePath} \u2014 skipping`);
|
|
5447
6037
|
return [];
|
|
5448
6038
|
}
|
|
5449
6039
|
let records;
|
|
5450
6040
|
try {
|
|
5451
6041
|
records = parseClaudeCode(text);
|
|
5452
6042
|
} catch {
|
|
5453
|
-
|
|
6043
|
+
log.debug(`Failed to parse ${filePath} \u2014 skipping`);
|
|
5454
6044
|
return [];
|
|
5455
6045
|
}
|
|
5456
6046
|
const fresh = [];
|
|
5457
6047
|
for (const r of records) {
|
|
5458
|
-
if (
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
}
|
|
6048
|
+
if (seen.has(r.dedupeKey)) continue;
|
|
6049
|
+
seen.add(r.dedupeKey);
|
|
6050
|
+
fresh.push(r);
|
|
5462
6051
|
}
|
|
5463
|
-
if (
|
|
5464
|
-
|
|
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)`);
|
|
5465
6055
|
}
|
|
5466
6056
|
return fresh;
|
|
5467
6057
|
}
|
|
5468
|
-
|
|
5469
|
-
|
|
6058
|
+
function freshCodexRecords(filePath, log) {
|
|
6059
|
+
let text;
|
|
5470
6060
|
try {
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
process.exit(1);
|
|
5481
|
-
}
|
|
5482
|
-
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 [];
|
|
5483
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;
|
|
5484
6082
|
}
|
|
5485
6083
|
function makeDebounced(fn, ms) {
|
|
5486
6084
|
const timers = /* @__PURE__ */ new Map();
|
|
5487
|
-
return (
|
|
5488
|
-
const existing = timers.get(
|
|
6085
|
+
return (p) => {
|
|
6086
|
+
const existing = timers.get(p);
|
|
5489
6087
|
if (existing) clearTimeout(existing);
|
|
5490
6088
|
timers.set(
|
|
5491
|
-
|
|
6089
|
+
p,
|
|
5492
6090
|
setTimeout(() => {
|
|
5493
|
-
timers.delete(
|
|
5494
|
-
fn(
|
|
6091
|
+
timers.delete(p);
|
|
6092
|
+
fn(p);
|
|
5495
6093
|
}, ms)
|
|
5496
6094
|
);
|
|
5497
6095
|
};
|
|
5498
6096
|
}
|
|
5499
6097
|
function findJsonlFiles2(dir) {
|
|
5500
|
-
const
|
|
5501
|
-
if (!
|
|
5502
|
-
const
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
if (entry.
|
|
5506
|
-
results.push(...findJsonlFiles2(full));
|
|
5507
|
-
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
5508
|
-
results.push(full);
|
|
5509
|
-
}
|
|
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);
|
|
5510
6104
|
}
|
|
5511
|
-
return
|
|
6105
|
+
return out;
|
|
5512
6106
|
}
|
|
5513
|
-
async function
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
}
|
|
5519
|
-
const client = new ApiClient({ apiUrl: opts.apiUrl, token });
|
|
5520
|
-
const debounceMs = opts.interval ?? 2e3;
|
|
5521
|
-
const dir = claudeCodeProjectsDir();
|
|
5522
|
-
if (!fs4.existsSync(dir)) {
|
|
5523
|
-
warn(`Claude Code projects directory not found: ${dir}`);
|
|
5524
|
-
warn("No files to watch. Exiting.");
|
|
5525
|
-
process.exit(0);
|
|
5526
|
-
}
|
|
5527
|
-
info(`Watching ${dir} (debounce: ${debounceMs}ms)`);
|
|
5528
|
-
info("Press Ctrl-C to stop.\n");
|
|
5529
|
-
const initialFiles = findJsonlFiles2(dir);
|
|
5530
|
-
for (const f of initialFiles) {
|
|
5531
|
-
parseAndFilter(f, false);
|
|
5532
|
-
}
|
|
5533
|
-
if (opts.verbose) {
|
|
5534
|
-
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
|
+
};
|
|
5535
6112
|
}
|
|
5536
|
-
const
|
|
5537
|
-
if (
|
|
5538
|
-
const fresh = parseAndFilter(filePath, opts.verbose ?? false);
|
|
5539
|
-
if (fresh.length > 0) {
|
|
5540
|
-
await upload(client, fresh, opts.verbose ?? false);
|
|
5541
|
-
}
|
|
6113
|
+
const debouncedChange = makeDebounced((p) => {
|
|
6114
|
+
if (p.endsWith(".jsonl")) onChange(p);
|
|
5542
6115
|
}, debounceMs);
|
|
5543
|
-
let cleanup = null;
|
|
5544
6116
|
try {
|
|
5545
|
-
const
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
const watcher = chokidar.watch(`${dir}/**/*.jsonl`, {
|
|
6117
|
+
const dynImport = new Function("m", "return import(m)");
|
|
6118
|
+
const chokidarMod = await dynImport("chokidar");
|
|
6119
|
+
const watcher = chokidarMod.watch(`${dir}/**/*.jsonl`, {
|
|
5549
6120
|
ignoreInitial: true,
|
|
5550
6121
|
persistent: true,
|
|
5551
6122
|
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
5552
6123
|
});
|
|
5553
|
-
watcher.on("add", (p) =>
|
|
5554
|
-
|
|
5555
|
-
|
|
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 () => {
|
|
5556
6131
|
watcher.close();
|
|
5557
6132
|
};
|
|
5558
|
-
if (opts.verbose) dim("Using chokidar for file watching");
|
|
5559
6133
|
} catch {
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
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) {
|
|
5564
6146
|
try {
|
|
5565
|
-
|
|
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
|
+
}
|
|
5566
6153
|
} catch {
|
|
5567
6154
|
}
|
|
5568
6155
|
}
|
|
5569
|
-
const
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
const mtime = fs4.statSync(f).mtimeMs;
|
|
5574
|
-
const prev = lastMtimes.get(f) ?? 0;
|
|
5575
|
-
if (mtime > prev) {
|
|
5576
|
-
lastMtimes.set(f, mtime);
|
|
5577
|
-
onChanged(f);
|
|
5578
|
-
}
|
|
5579
|
-
} catch {
|
|
5580
|
-
}
|
|
6156
|
+
for (const f of current) {
|
|
6157
|
+
if (!lastMtimes.has(f)) {
|
|
6158
|
+
lastMtimes.set(f, Date.now());
|
|
6159
|
+
debouncedChange(f);
|
|
5581
6160
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
const
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
const { watch } = await import("node:fs/promises");
|
|
5596
|
-
const watcher = watch(dir, { recursive: true, signal: controller.signal });
|
|
5597
|
-
for await (const event of watcher) {
|
|
5598
|
-
const filename = event.filename;
|
|
5599
|
-
if (filename?.endsWith(".jsonl")) {
|
|
5600
|
-
const fullPath = `${dir}/${filename}`;
|
|
5601
|
-
onChanged(fullPath);
|
|
5602
|
-
}
|
|
5603
|
-
}
|
|
5604
|
-
} catch (err) {
|
|
5605
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
5606
|
-
warn(`Watcher error: ${err.message}`);
|
|
5607
|
-
}
|
|
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));
|
|
5608
6174
|
}
|
|
5609
|
-
}
|
|
5610
|
-
|
|
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;
|
|
5611
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;
|
|
5612
6244
|
}
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
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
|
+
);
|
|
5617
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
|
+
};
|
|
5618
6301
|
process.on("SIGINT", shutdown);
|
|
5619
6302
|
process.on("SIGTERM", shutdown);
|
|
5620
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
|
+
}
|
|
5621
6360
|
|
|
5622
6361
|
// src/commands/whoami.ts
|
|
5623
6362
|
async function whoamiCommand(opts) {
|
|
@@ -5642,7 +6381,7 @@ async function whoamiCommand(opts) {
|
|
|
5642
6381
|
|
|
5643
6382
|
// src/index.ts
|
|
5644
6383
|
function getVersion() {
|
|
5645
|
-
return
|
|
6384
|
+
return CLI_VERSION;
|
|
5646
6385
|
}
|
|
5647
6386
|
function printHelp() {
|
|
5648
6387
|
console.log(`
|
|
@@ -5653,8 +6392,13 @@ function printHelp() {
|
|
|
5653
6392
|
|
|
5654
6393
|
\x1B[1mCommands:\x1B[0m
|
|
5655
6394
|
login Authenticate with Token Rats (opens browser)
|
|
5656
|
-
sync
|
|
5657
|
-
|
|
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.
|
|
5658
6402
|
whoami Show the currently signed-in account
|
|
5659
6403
|
logout Clear your stored credentials
|
|
5660
6404
|
install-cursor Install better-sqlite3 globally for faster Cursor reads
|
|
@@ -5667,8 +6411,15 @@ function printHelp() {
|
|
|
5667
6411
|
\x1B[1mFlags (sync only):\x1B[0m
|
|
5668
6412
|
--dry-run Parse but do not upload; print what would be sent
|
|
5669
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)
|
|
5670
6416
|
|
|
5671
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.
|
|
5672
6423
|
--interval <ms> Debounce window in ms before uploading (default: 2000)
|
|
5673
6424
|
--verbose Print file change events and upload detail
|
|
5674
6425
|
|
|
@@ -5697,6 +6448,11 @@ function parseArgs(argv) {
|
|
|
5697
6448
|
let dryRun = false;
|
|
5698
6449
|
let verbose = false;
|
|
5699
6450
|
let interval;
|
|
6451
|
+
let noDaemon = false;
|
|
6452
|
+
let daemon = false;
|
|
6453
|
+
let install = false;
|
|
6454
|
+
let uninstall = false;
|
|
6455
|
+
let status = false;
|
|
5700
6456
|
let i = 0;
|
|
5701
6457
|
while (i < argv.length) {
|
|
5702
6458
|
const arg = argv[i];
|
|
@@ -5712,6 +6468,16 @@ function parseArgs(argv) {
|
|
|
5712
6468
|
interval = Number(argv[++i]);
|
|
5713
6469
|
} else if (arg.startsWith("--interval=")) {
|
|
5714
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;
|
|
5715
6481
|
} else if (arg === "--version" || arg === "-V") {
|
|
5716
6482
|
console.log(getVersion());
|
|
5717
6483
|
process.exit(0);
|
|
@@ -5724,11 +6490,34 @@ function parseArgs(argv) {
|
|
|
5724
6490
|
i++;
|
|
5725
6491
|
}
|
|
5726
6492
|
const [command = null, ...rest] = positional;
|
|
5727
|
-
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
|
+
};
|
|
5728
6506
|
}
|
|
5729
6507
|
async function main() {
|
|
5730
6508
|
const args = parseArgs(process.argv.slice(2));
|
|
5731
|
-
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;
|
|
5732
6521
|
if (!command || command === "help") {
|
|
5733
6522
|
printHelp();
|
|
5734
6523
|
process.exit(0);
|
|
@@ -5738,10 +6527,19 @@ async function main() {
|
|
|
5738
6527
|
await loginCommand({ apiUrl });
|
|
5739
6528
|
break;
|
|
5740
6529
|
case "sync":
|
|
5741
|
-
await syncCommand({ apiUrl, dryRun, verbose });
|
|
6530
|
+
await syncCommand({ apiUrl, dryRun, verbose, noDaemon });
|
|
5742
6531
|
break;
|
|
5743
6532
|
case "watch":
|
|
5744
|
-
await watchCommand({
|
|
6533
|
+
await watchCommand({
|
|
6534
|
+
apiUrl,
|
|
6535
|
+
verbose,
|
|
6536
|
+
interval,
|
|
6537
|
+
daemon,
|
|
6538
|
+
install,
|
|
6539
|
+
uninstall,
|
|
6540
|
+
status,
|
|
6541
|
+
version: getVersion()
|
|
6542
|
+
});
|
|
5745
6543
|
break;
|
|
5746
6544
|
case "whoami":
|
|
5747
6545
|
await whoamiCommand({ apiUrl });
|