kaizenai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +246 -0
  3. package/bin/kaizen +15 -0
  4. package/dist/client/apple-touch-icon.png +0 -0
  5. package/dist/client/assets/index-D-ORCGrq.js +603 -0
  6. package/dist/client/assets/index-r28mcHqz.css +32 -0
  7. package/dist/client/favicon.png +0 -0
  8. package/dist/client/fonts/body-medium.woff2 +0 -0
  9. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  10. package/dist/client/fonts/body-regular.woff2 +0 -0
  11. package/dist/client/fonts/body-semibold.woff2 +0 -0
  12. package/dist/client/index.html +22 -0
  13. package/dist/client/manifest-dark.webmanifest +24 -0
  14. package/dist/client/manifest.webmanifest +24 -0
  15. package/dist/client/pwa-192.png +0 -0
  16. package/dist/client/pwa-512.png +0 -0
  17. package/dist/client/pwa-icon.svg +15 -0
  18. package/dist/client/pwa-splash.png +0 -0
  19. package/dist/client/pwa-splash.svg +15 -0
  20. package/package.json +103 -0
  21. package/src/server/acp-shared.ts +315 -0
  22. package/src/server/agent.ts +1120 -0
  23. package/src/server/attachments.ts +133 -0
  24. package/src/server/backgrounds.ts +74 -0
  25. package/src/server/cli-runtime.ts +333 -0
  26. package/src/server/cli-supervisor.ts +81 -0
  27. package/src/server/cli.ts +68 -0
  28. package/src/server/codex-app-server-protocol.ts +453 -0
  29. package/src/server/codex-app-server.ts +1350 -0
  30. package/src/server/cursor-acp.ts +819 -0
  31. package/src/server/discovery.ts +322 -0
  32. package/src/server/event-store.ts +1369 -0
  33. package/src/server/events.ts +244 -0
  34. package/src/server/external-open.ts +272 -0
  35. package/src/server/gemini-acp.ts +844 -0
  36. package/src/server/gemini-cli.ts +525 -0
  37. package/src/server/generate-title.ts +36 -0
  38. package/src/server/git-manager.ts +79 -0
  39. package/src/server/git-repository.ts +101 -0
  40. package/src/server/harness-types.ts +20 -0
  41. package/src/server/keybindings.ts +177 -0
  42. package/src/server/machine-name.ts +22 -0
  43. package/src/server/paths.ts +112 -0
  44. package/src/server/process-utils.ts +22 -0
  45. package/src/server/project-icon.ts +344 -0
  46. package/src/server/project-metadata.ts +10 -0
  47. package/src/server/provider-catalog.ts +85 -0
  48. package/src/server/provider-settings.ts +155 -0
  49. package/src/server/quick-response.ts +153 -0
  50. package/src/server/read-models.ts +275 -0
  51. package/src/server/recovery.ts +507 -0
  52. package/src/server/restart.ts +30 -0
  53. package/src/server/server.ts +244 -0
  54. package/src/server/terminal-manager.ts +350 -0
  55. package/src/server/theme-settings.ts +179 -0
  56. package/src/server/update-manager.ts +230 -0
  57. package/src/server/usage/base-provider-usage.ts +57 -0
  58. package/src/server/usage/claude-usage.ts +558 -0
  59. package/src/server/usage/codex-usage.ts +144 -0
  60. package/src/server/usage/cursor-browser.ts +120 -0
  61. package/src/server/usage/cursor-cookies.ts +390 -0
  62. package/src/server/usage/cursor-usage.ts +490 -0
  63. package/src/server/usage/gemini-usage.ts +24 -0
  64. package/src/server/usage/provider-usage.ts +61 -0
  65. package/src/server/usage/test-helpers.ts +9 -0
  66. package/src/server/usage/types.ts +54 -0
  67. package/src/server/usage/utils.ts +325 -0
  68. package/src/server/ws-router.ts +717 -0
  69. package/src/shared/branding.ts +83 -0
  70. package/src/shared/dev-ports.ts +43 -0
  71. package/src/shared/ports.ts +2 -0
  72. package/src/shared/protocol.ts +152 -0
  73. package/src/shared/tools.ts +251 -0
  74. package/src/shared/types.ts +1028 -0
@@ -0,0 +1,490 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { mkdtempSync, rmSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import process from "node:process"
6
+ import puppeteer from "puppeteer-core"
7
+ import type { ProviderUsageAvailability, ProviderUsageEntry } from "../../shared/types"
8
+ import { BaseProviderUsage } from "./base-provider-usage"
9
+ import {
10
+ attemptCursorUsageFetch,
11
+ CURSOR_BROWSER_LOGIN_TIMEOUT_MS,
12
+ CURSOR_DASHBOARD_URL,
13
+ refreshCursorSessionFromDashboard,
14
+ resolveBrowserExecutable,
15
+ sessionFromBrowserCookies,
16
+ } from "./cursor-browser"
17
+ import {
18
+ bootstrapCursorSessionFromBrowser,
19
+ CURSOR_SESSION_COOKIE_NAME,
20
+ importCursorSessionFromCurl,
21
+ mergeCursorCookies,
22
+ } from "./cursor-cookies"
23
+ import type { CursorSessionCache, CursorSessionCookie, CursorUsagePayload } from "./types"
24
+ import { asRecord, deriveAvailability, toNumber, usageWarnings } from "./utils"
25
+
26
+ const PROVIDER_CACHE_TTL_MS = 30_000
27
+ const PROVIDER_USAGE_REQUEST_MIN_INTERVAL_MS = 30 * 60 * 1000
28
+
29
+ function cursorSessionPath(dataDir: string) {
30
+ return path.join(dataDir, "cursor-session.json")
31
+ }
32
+
33
+ function cursorUsagePath(dataDir: string) {
34
+ return path.join(dataDir, "cursor-usage.json")
35
+ }
36
+
37
+ function cursorEntryFromSuccess(payload: CursorUsagePayload, updatedAt = Date.now(), lastRequestedAt = updatedAt): ProviderUsageEntry {
38
+ return {
39
+ provider: "cursor",
40
+ sessionLimitUsedPercent: payload.sessionLimitUsedPercent,
41
+ apiLimitUsedPercent: payload.apiPercentUsed,
42
+ rateLimitResetAt: payload.rateLimitResetAt,
43
+ rateLimitResetLabel: payload.rateLimitResetLabel,
44
+ weeklyLimitUsedPercent: null,
45
+ weeklyRateLimitResetAt: null,
46
+ weeklyRateLimitResetLabel: null,
47
+ statusDetail: null,
48
+ availability: "available",
49
+ lastRequestedAt,
50
+ updatedAt,
51
+ warnings: usageWarnings({
52
+ contextUsedPercent: null,
53
+ sessionLimitUsedPercent: payload.sessionLimitUsedPercent,
54
+ updatedAt,
55
+ }),
56
+ }
57
+ }
58
+
59
+ function cursorStatusEntry(args: {
60
+ availability: ProviderUsageAvailability
61
+ statusDetail: string | null
62
+ lastRequestedAt?: number | null
63
+ updatedAt?: number | null
64
+ }): ProviderUsageEntry {
65
+ return {
66
+ provider: "cursor",
67
+ sessionLimitUsedPercent: null,
68
+ apiLimitUsedPercent: null,
69
+ rateLimitResetAt: null,
70
+ rateLimitResetLabel: null,
71
+ weeklyLimitUsedPercent: null,
72
+ weeklyRateLimitResetAt: null,
73
+ weeklyRateLimitResetLabel: null,
74
+ statusDetail: args.statusDetail,
75
+ availability: args.availability,
76
+ lastRequestedAt: args.lastRequestedAt ?? null,
77
+ updatedAt: args.updatedAt ?? null,
78
+ warnings: args.availability === "stale" && args.updatedAt
79
+ ? ["stale"]
80
+ : [],
81
+ }
82
+ }
83
+
84
+ function normalizeCursorResetLabel(label: unknown): string | null {
85
+ if (typeof label !== "string") return null
86
+ const trimmed = label.trim()
87
+ return trimmed || null
88
+ }
89
+
90
+ function parseCursorTimestamp(value: unknown): number | null {
91
+ if (typeof value === "number" && Number.isFinite(value)) {
92
+ return value > 10_000_000_000 ? value : value * 1000
93
+ }
94
+ if (typeof value === "string") {
95
+ const numeric = Number(value)
96
+ if (Number.isFinite(numeric)) {
97
+ return numeric > 10_000_000_000 ? numeric : numeric * 1000
98
+ }
99
+ const parsed = Date.parse(value)
100
+ return Number.isFinite(parsed) ? parsed : null
101
+ }
102
+ return null
103
+ }
104
+
105
+ function findFirstValue(value: unknown, matchers: RegExp[], seen = new Set<unknown>()): unknown {
106
+ if (!value || typeof value !== "object") return null
107
+ if (seen.has(value)) return null
108
+ seen.add(value)
109
+
110
+ if (Array.isArray(value)) {
111
+ for (const entry of value) {
112
+ const found = findFirstValue(entry, matchers, seen)
113
+ if (found !== null && found !== undefined) return found
114
+ }
115
+ return null
116
+ }
117
+
118
+ for (const [key, entry] of Object.entries(value)) {
119
+ if (matchers.some((matcher) => matcher.test(key)) && entry !== null && entry !== undefined) {
120
+ return entry
121
+ }
122
+ }
123
+
124
+ for (const entry of Object.values(value)) {
125
+ const found = findFirstValue(entry, matchers, seen)
126
+ if (found !== null && found !== undefined) return found
127
+ }
128
+
129
+ return null
130
+ }
131
+
132
+ export function parseCursorUsagePayload(payload: unknown): CursorUsagePayload | null {
133
+ const record = asRecord(payload)
134
+ if (!record) return null
135
+
136
+ const planUsage = asRecord(record.planUsage)
137
+ const autoPercentUsed = toNumber(planUsage?.autoPercentUsed)
138
+ const apiPercentRaw = toNumber(planUsage?.apiPercentUsed)
139
+ if (autoPercentUsed !== null) {
140
+ return {
141
+ sessionLimitUsedPercent: Math.max(0, Math.min(100, autoPercentUsed)),
142
+ apiPercentUsed: apiPercentRaw === null ? null : Math.max(0, Math.min(100, apiPercentRaw)),
143
+ rateLimitResetAt: null,
144
+ rateLimitResetLabel: null,
145
+ }
146
+ }
147
+
148
+ const percentValue = findFirstValue(record, [
149
+ /^used_?percent$/i,
150
+ /^session_?limit_?used_?percent$/i,
151
+ /^percent_?used$/i,
152
+ /^usage_?percent(age)?$/i,
153
+ ])
154
+ const resetValue = findFirstValue(record, [
155
+ /^reset(s|_at|At)?$/i,
156
+ /^current_?period_?(end|reset)(s|_at|At)?$/i,
157
+ /^period_?(end|reset)(s|_at|At)?$/i,
158
+ /^next_?reset(_at|At)?$/i,
159
+ ])
160
+ const resetLabelValue = findFirstValue(record, [
161
+ /^reset_?label$/i,
162
+ /^reset_?text$/i,
163
+ /^next_?reset_?label$/i,
164
+ ])
165
+
166
+ const percent = typeof percentValue === "number"
167
+ ? percentValue
168
+ : typeof percentValue === "string"
169
+ ? Number(percentValue)
170
+ : null
171
+
172
+ if (!Number.isFinite(percent)) return null
173
+ const normalizedPercent = percent as number
174
+
175
+ return {
176
+ sessionLimitUsedPercent: Math.max(0, Math.min(100, normalizedPercent)),
177
+ apiPercentUsed: null,
178
+ rateLimitResetAt: parseCursorTimestamp(resetValue),
179
+ rateLimitResetLabel: normalizeCursorResetLabel(resetLabelValue),
180
+ }
181
+ }
182
+
183
+ export class CursorUsage extends BaseProviderUsage {
184
+ readonly provider = "cursor" as const
185
+ private fileCache: { filePath: string; entry: ProviderUsageEntry | null; cachedAt: number } | null = null
186
+
187
+ private persistSession(session: CursorSessionCache) {
188
+ const prunedCookies = session.cookies
189
+ .filter((cookie) => cookie.domain === "cursor.com" || cookie.domain.endsWith(".cursor.com"))
190
+ .map((cookie) => ({
191
+ ...cookie,
192
+ value: String(cookie.value),
193
+ }))
194
+
195
+ writeFileSync(cursorSessionPath(this.dataDir), JSON.stringify({
196
+ cookies: prunedCookies,
197
+ updatedAt: session.updatedAt,
198
+ lastSuccessAt: session.lastSuccessAt,
199
+ }))
200
+ }
201
+
202
+ private loadPersistedSession(): CursorSessionCache | null {
203
+ try {
204
+ const filePath = cursorSessionPath(this.dataDir)
205
+ if (!existsSync(filePath)) return null
206
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"))
207
+ const cookies = Array.isArray(parsed.cookies)
208
+ ? parsed.cookies
209
+ .map((cookie: unknown) => asRecord(cookie))
210
+ .filter((cookie: Record<string, unknown> | null): cookie is Record<string, unknown> => Boolean(cookie))
211
+ .map((cookie: Record<string, unknown>) => ({
212
+ name: typeof cookie.name === "string" ? cookie.name : "",
213
+ value: typeof cookie.value === "string" ? cookie.value : "",
214
+ domain: typeof cookie.domain === "string" ? cookie.domain : "cursor.com",
215
+ path: typeof cookie.path === "string" ? cookie.path : "/",
216
+ expiresAt: typeof cookie.expiresAt === "number" ? cookie.expiresAt : null,
217
+ secure: cookie.secure !== false,
218
+ httpOnly: cookie.httpOnly !== false,
219
+ }))
220
+ .filter((cookie: CursorSessionCookie) => cookie.name && cookie.value)
221
+ : []
222
+
223
+ if (cookies.length === 0) return null
224
+
225
+ return {
226
+ cookies,
227
+ updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
228
+ lastSuccessAt: typeof parsed.lastSuccessAt === "number" ? parsed.lastSuccessAt : null,
229
+ }
230
+ } catch {
231
+ return null
232
+ }
233
+ }
234
+
235
+ private persistUsageEntry(entry: ProviderUsageEntry) {
236
+ const filePath = cursorUsagePath(this.dataDir)
237
+ writeFileSync(filePath, JSON.stringify(entry))
238
+ this.fileCache = { filePath, entry, cachedAt: Date.now() }
239
+ }
240
+
241
+ loadPersistedEntry(): ProviderUsageEntry | null {
242
+ const now = Date.now()
243
+ const filePath = cursorUsagePath(this.dataDir)
244
+ if (this.fileCache && this.fileCache.filePath === filePath && now - this.fileCache.cachedAt < PROVIDER_CACHE_TTL_MS) {
245
+ return this.fileCache.entry
246
+ }
247
+
248
+ try {
249
+ if (!existsSync(filePath)) return null
250
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"))
251
+ const entry = asRecord(parsed)
252
+ if (!entry) return null
253
+ const availability = typeof entry.availability === "string" ? entry.availability as ProviderUsageAvailability : "unavailable"
254
+ const hasApiLimitUsedPercent = Object.prototype.hasOwnProperty.call(entry, "apiLimitUsedPercent")
255
+ const normalized: ProviderUsageEntry = {
256
+ provider: "cursor",
257
+ sessionLimitUsedPercent: typeof entry.sessionLimitUsedPercent === "number" ? entry.sessionLimitUsedPercent : null,
258
+ apiLimitUsedPercent: hasApiLimitUsedPercent
259
+ ? (typeof entry.apiLimitUsedPercent === "number" ? entry.apiLimitUsedPercent : null)
260
+ : undefined,
261
+ rateLimitResetAt: typeof entry.rateLimitResetAt === "number" ? entry.rateLimitResetAt : null,
262
+ rateLimitResetLabel: typeof entry.rateLimitResetLabel === "string" ? entry.rateLimitResetLabel : null,
263
+ weeklyLimitUsedPercent: typeof entry.weeklyLimitUsedPercent === "number" ? entry.weeklyLimitUsedPercent : null,
264
+ weeklyRateLimitResetAt: typeof entry.weeklyRateLimitResetAt === "number" ? entry.weeklyRateLimitResetAt : null,
265
+ weeklyRateLimitResetLabel: typeof entry.weeklyRateLimitResetLabel === "string" ? entry.weeklyRateLimitResetLabel : null,
266
+ statusDetail: typeof entry.statusDetail === "string" ? entry.statusDetail : null,
267
+ lastRequestedAt: typeof entry.lastRequestedAt === "number" ? entry.lastRequestedAt : null,
268
+ availability: availability === "available" || availability === "unavailable" || availability === "stale" || availability === "login_required"
269
+ ? availability
270
+ : "unavailable",
271
+ updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : null,
272
+ warnings: Array.isArray(entry.warnings)
273
+ ? entry.warnings.filter((warning): warning is ProviderUsageEntry["warnings"][number] => typeof warning === "string")
274
+ : [],
275
+ }
276
+
277
+ if (normalized.availability === "available" || normalized.availability === "stale") {
278
+ normalized.availability = deriveAvailability(normalized.updatedAt)
279
+ normalized.warnings = usageWarnings({
280
+ contextUsedPercent: null,
281
+ sessionLimitUsedPercent: normalized.sessionLimitUsedPercent,
282
+ updatedAt: normalized.updatedAt,
283
+ })
284
+ }
285
+
286
+ this.fileCache = { filePath, entry: normalized, cachedAt: now }
287
+ return normalized
288
+ } catch {
289
+ this.fileCache = { filePath, entry: null, cachedAt: now }
290
+ return null
291
+ }
292
+ }
293
+
294
+ async refresh(platform = process.platform, force = false): Promise<ProviderUsageEntry> {
295
+ const now = Date.now()
296
+ const persistedEntry = this.loadPersistedEntry()
297
+ const lastRequestedAt = persistedEntry?.lastRequestedAt ?? this.readLastRequestedAt(this.provider)
298
+ const needsCursorUsageSplitRefresh = persistedEntry?.availability === "available"
299
+ && persistedEntry.sessionLimitUsedPercent !== null
300
+ && persistedEntry.apiLimitUsedPercent === undefined
301
+
302
+ if (!force && now - lastRequestedAt < PROVIDER_USAGE_REQUEST_MIN_INTERVAL_MS && persistedEntry && !needsCursorUsageSplitRefresh) {
303
+ return persistedEntry
304
+ }
305
+
306
+ this.recordRequestTime(this.provider, now)
307
+
308
+ let session = this.loadPersistedSession()
309
+
310
+ if (!session) {
311
+ session = bootstrapCursorSessionFromBrowser(platform)
312
+ if (session) {
313
+ this.persistSession(session)
314
+ }
315
+ }
316
+
317
+ if (!session) {
318
+ const entry = cursorStatusEntry({
319
+ availability: platform === "linux" || platform === "darwin" ? "login_required" : "unavailable",
320
+ statusDetail: platform === "linux" || platform === "darwin" ? "browser_cookie_import_failed" : "unsupported_platform",
321
+ lastRequestedAt: now,
322
+ })
323
+ this.persistUsageEntry(entry)
324
+ return entry
325
+ }
326
+
327
+ let result = await attemptCursorUsageFetch(session)
328
+ session = result.session
329
+
330
+ if (result.authFailed) {
331
+ const refreshed = await refreshCursorSessionFromDashboard(session)
332
+ session = refreshed.session
333
+ result = await attemptCursorUsageFetch(session)
334
+ session = result.session
335
+ }
336
+
337
+ if (!result.ok && result.authFailed) {
338
+ const bootstrapped = bootstrapCursorSessionFromBrowser(platform)
339
+ if (bootstrapped) {
340
+ session = {
341
+ cookies: mergeCursorCookies(session.cookies, bootstrapped.cookies),
342
+ updatedAt: Date.now(),
343
+ lastSuccessAt: session.lastSuccessAt,
344
+ }
345
+ result = await attemptCursorUsageFetch(session)
346
+ session = result.session
347
+ }
348
+ }
349
+
350
+ if (session.cookies.length > 0) {
351
+ this.persistSession(session)
352
+ }
353
+
354
+ if (!result.ok || !result.payload) {
355
+ const entry = cursorStatusEntry({
356
+ availability: result.authFailed ? "login_required" : "unavailable",
357
+ statusDetail: result.authFailed ? "session_refresh_failed" : "fetch_failed",
358
+ lastRequestedAt: now,
359
+ updatedAt: session.lastSuccessAt,
360
+ })
361
+ this.persistUsageEntry(entry)
362
+ return entry
363
+ }
364
+
365
+ session.lastSuccessAt = Date.now()
366
+ session.updatedAt = session.lastSuccessAt
367
+ this.persistSession(session)
368
+
369
+ const entry = cursorEntryFromSuccess(result.payload, session.lastSuccessAt, now)
370
+ this.persistUsageEntry(entry)
371
+ return entry
372
+ }
373
+
374
+ async importFromCurl(curlCommand: string, platform = process.platform) {
375
+ const imported = importCursorSessionFromCurl(curlCommand)
376
+ if (!imported) {
377
+ return cursorStatusEntry({
378
+ availability: "login_required",
379
+ statusDetail: "invalid_curl_import",
380
+ lastRequestedAt: Date.now(),
381
+ })
382
+ }
383
+
384
+ const existing = this.loadPersistedSession()
385
+ const session: CursorSessionCache = {
386
+ cookies: mergeCursorCookies(existing?.cookies ?? [], imported.cookies),
387
+ updatedAt: Date.now(),
388
+ lastSuccessAt: existing?.lastSuccessAt ?? null,
389
+ }
390
+ this.persistSession(session)
391
+ return this.refresh(platform)
392
+ }
393
+
394
+ async signInWithBrowser(platform = process.platform) {
395
+ const executablePath = resolveBrowserExecutable(platform)
396
+ if (!executablePath) {
397
+ return cursorStatusEntry({
398
+ availability: "login_required",
399
+ statusDetail: "browser_launch_failed",
400
+ lastRequestedAt: Date.now(),
401
+ })
402
+ }
403
+
404
+ const userDataDir = mkdtempSync(path.join(tmpdir(), "kaizen-cursor-login-"))
405
+ let browser: Awaited<ReturnType<typeof puppeteer.launch>> | null = null
406
+
407
+ try {
408
+ browser = await puppeteer.launch({
409
+ executablePath,
410
+ headless: false,
411
+ userDataDir,
412
+ defaultViewport: null,
413
+ ignoreDefaultArgs: ["--enable-automation"],
414
+ args: [
415
+ "--no-first-run",
416
+ "--no-default-browser-check",
417
+ "--disable-blink-features=AutomationControlled",
418
+ ],
419
+ })
420
+
421
+ const page = await browser.newPage()
422
+ await page.evaluateOnNewDocument(() => {
423
+ Object.defineProperty(navigator, "webdriver", {
424
+ get: () => undefined,
425
+ })
426
+ })
427
+ await page.goto(CURSOR_DASHBOARD_URL, { waitUntil: "domcontentloaded" })
428
+
429
+ const start = Date.now()
430
+ let session: CursorSessionCache | null = null
431
+ while (Date.now() - start < CURSOR_BROWSER_LOGIN_TIMEOUT_MS) {
432
+ const cookies = await page.cookies(CURSOR_DASHBOARD_URL)
433
+ if (cookies.some((cookie) => cookie.name === CURSOR_SESSION_COOKIE_NAME && cookie.value)) {
434
+ session = sessionFromBrowserCookies(cookies)
435
+ break
436
+ }
437
+ await Bun.sleep(1000)
438
+ }
439
+
440
+ if (!session) {
441
+ return cursorStatusEntry({
442
+ availability: "login_required",
443
+ statusDetail: "browser_login_failed",
444
+ lastRequestedAt: Date.now(),
445
+ })
446
+ }
447
+
448
+ this.persistSession(session)
449
+ return this.refresh(platform)
450
+ } catch {
451
+ return cursorStatusEntry({
452
+ availability: "login_required",
453
+ statusDetail: "browser_login_failed",
454
+ lastRequestedAt: Date.now(),
455
+ })
456
+ } finally {
457
+ try {
458
+ await browser?.close()
459
+ } catch {
460
+ // noop
461
+ }
462
+ rmSync(userDataDir, { recursive: true, force: true })
463
+ }
464
+ }
465
+ }
466
+
467
+ const _instances = new Map<string, CursorUsage>()
468
+
469
+ export function getCursorUsage(dataDir: string): CursorUsage {
470
+ if (!_instances.has(dataDir)) {
471
+ _instances.set(dataDir, new CursorUsage(dataDir))
472
+ }
473
+ return _instances.get(dataDir)!
474
+ }
475
+
476
+ export async function refreshCursorUsage(dataDir: string, platform = process.platform, force = false): Promise<ProviderUsageEntry> {
477
+ return getCursorUsage(dataDir).refresh(platform, force)
478
+ }
479
+
480
+ export async function importCursorUsageFromCurl(dataDir: string, curlCommand: string, platform = process.platform) {
481
+ return getCursorUsage(dataDir).importFromCurl(curlCommand, platform)
482
+ }
483
+
484
+ export async function signInToCursorWithBrowser(dataDir: string, platform = process.platform) {
485
+ return getCursorUsage(dataDir).signInWithBrowser(platform)
486
+ }
487
+
488
+ export function resetCursorUsageCaches() {
489
+ _instances.clear()
490
+ }
@@ -0,0 +1,24 @@
1
+ import type { ProviderUsageEntry } from "../../shared/types"
2
+ import { BaseProviderUsage } from "./base-provider-usage"
3
+ import { unavailableEntry } from "./utils"
4
+
5
+ export class GeminiUsage extends BaseProviderUsage {
6
+ readonly provider = "gemini" as const
7
+
8
+ loadPersistedEntry(): ProviderUsageEntry | null {
9
+ return unavailableEntry(this.provider)
10
+ }
11
+
12
+ deriveEntry(): ProviderUsageEntry {
13
+ return unavailableEntry(this.provider)
14
+ }
15
+ }
16
+
17
+ const _instances = new Map<string, GeminiUsage>()
18
+
19
+ export function getGeminiUsage(dataDir: string): GeminiUsage {
20
+ if (!_instances.has(dataDir)) {
21
+ _instances.set(dataDir, new GeminiUsage(dataDir))
22
+ }
23
+ return _instances.get(dataDir)!
24
+ }
@@ -0,0 +1,61 @@
1
+ import { PROVIDERS, type AgentProvider, type ChatUsageSnapshot, type ProviderUsageMap } from "../../shared/types"
2
+ import type { EventStore } from "../event-store"
3
+ import type { BaseProviderUsage } from "./base-provider-usage"
4
+ import { getClaudeUsage } from "./claude-usage"
5
+ import { getCodexUsage } from "./codex-usage"
6
+ import { getCursorUsage } from "./cursor-usage"
7
+ import { getGeminiUsage } from "./gemini-usage"
8
+ import { unavailableEntry } from "./utils"
9
+
10
+ export function getProviderUsage(provider: AgentProvider, dataDir: string): BaseProviderUsage {
11
+ if (provider === "claude") return getClaudeUsage(dataDir)
12
+ if (provider === "codex") return getCodexUsage(dataDir)
13
+ if (provider === "cursor") return getCursorUsage(dataDir)
14
+ return getGeminiUsage(dataDir)
15
+ }
16
+
17
+ function latestLiveSnapshotForProvider(
18
+ provider: AgentProvider,
19
+ liveUsage: Map<string, ChatUsageSnapshot>
20
+ ): ChatUsageSnapshot | null {
21
+ let latest: ChatUsageSnapshot | null = null
22
+
23
+ for (const snapshot of liveUsage.values()) {
24
+ if (snapshot.provider !== provider) continue
25
+ if (!latest || (snapshot.updatedAt ?? 0) > (latest.updatedAt ?? 0)) {
26
+ latest = snapshot
27
+ }
28
+ }
29
+
30
+ return latest
31
+ }
32
+
33
+ export function deriveProviderUsage(
34
+ liveUsage: Map<string, ChatUsageSnapshot>,
35
+ store: EventStore
36
+ ): ProviderUsageMap {
37
+ const result: ProviderUsageMap = {}
38
+
39
+ for (const provider of PROVIDERS) {
40
+ const liveSnapshot = latestLiveSnapshotForProvider(provider.id, liveUsage)
41
+
42
+ if (provider.id === "claude") {
43
+ result[provider.id] = getClaudeUsage(store.dataDir).deriveEntry(liveSnapshot)
44
+ continue
45
+ }
46
+
47
+ if (provider.id === "codex") {
48
+ result[provider.id] = getCodexUsage(store.dataDir).deriveEntry(liveSnapshot, store)
49
+ continue
50
+ }
51
+
52
+ if (provider.id === "cursor") {
53
+ result[provider.id] = getCursorUsage(store.dataDir).loadPersistedEntry() ?? unavailableEntry("cursor")
54
+ continue
55
+ }
56
+
57
+ result[provider.id] = getGeminiUsage(store.dataDir).deriveEntry()
58
+ }
59
+
60
+ return result
61
+ }
@@ -0,0 +1,9 @@
1
+ import type { TranscriptEntry } from "../../shared/types"
2
+
3
+ export function transcriptEntry(overrides: Partial<TranscriptEntry> & Pick<TranscriptEntry, "kind">): TranscriptEntry {
4
+ return {
5
+ _id: crypto.randomUUID(),
6
+ createdAt: 1,
7
+ ...overrides,
8
+ } as TranscriptEntry
9
+ }
@@ -0,0 +1,54 @@
1
+ import type { ChatUsageSnapshot } from "../../shared/types"
2
+
3
+ export interface NumericUsage {
4
+ inputTokens: number
5
+ outputTokens: number
6
+ cachedInputTokens: number
7
+ reasoningOutputTokens: number
8
+ }
9
+
10
+ export interface ClaudeRateLimitInfo {
11
+ percent: number | null
12
+ resetsAt: number | null
13
+ }
14
+
15
+ export interface CursorSessionCookie {
16
+ name: string
17
+ value: string
18
+ domain: string
19
+ path: string
20
+ expiresAt: number | null
21
+ secure: boolean
22
+ httpOnly: boolean
23
+ }
24
+
25
+ export interface CursorSessionCache {
26
+ cookies: CursorSessionCookie[]
27
+ updatedAt: number
28
+ lastSuccessAt: number | null
29
+ }
30
+
31
+ export interface CursorUsagePayload {
32
+ sessionLimitUsedPercent: number | null
33
+ apiPercentUsed: number | null
34
+ rateLimitResetAt: number | null
35
+ rateLimitResetLabel: string | null
36
+ }
37
+
38
+ export interface CursorCurlImportResult {
39
+ cookies: CursorSessionCookie[]
40
+ }
41
+
42
+ export interface ClaudeRateLimitCacheSnapshot extends ChatUsageSnapshot {
43
+ rateLimitResetLabel?: string | null
44
+ weeklyLimitUsedPercent?: number | null
45
+ weeklyRateLimitResetAt?: number | null
46
+ weeklyRateLimitResetLabel?: string | null
47
+ }
48
+
49
+ export interface ProviderRateLimitSnapshot extends ChatUsageSnapshot {
50
+ rateLimitResetLabel?: string | null
51
+ weeklyLimitUsedPercent?: number | null
52
+ weeklyRateLimitResetAt?: number | null
53
+ weeklyRateLimitResetLabel?: string | null
54
+ }