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.
- package/LICENSE +22 -0
- package/README.md +246 -0
- package/bin/kaizen +15 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/index-D-ORCGrq.js +603 -0
- package/dist/client/assets/index-r28mcHqz.css +32 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +22 -0
- package/dist/client/manifest-dark.webmanifest +24 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/client/pwa-192.png +0 -0
- package/dist/client/pwa-512.png +0 -0
- package/dist/client/pwa-icon.svg +15 -0
- package/dist/client/pwa-splash.png +0 -0
- package/dist/client/pwa-splash.svg +15 -0
- package/package.json +103 -0
- package/src/server/acp-shared.ts +315 -0
- package/src/server/agent.ts +1120 -0
- package/src/server/attachments.ts +133 -0
- package/src/server/backgrounds.ts +74 -0
- package/src/server/cli-runtime.ts +333 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +68 -0
- package/src/server/codex-app-server-protocol.ts +453 -0
- package/src/server/codex-app-server.ts +1350 -0
- package/src/server/cursor-acp.ts +819 -0
- package/src/server/discovery.ts +322 -0
- package/src/server/event-store.ts +1369 -0
- package/src/server/events.ts +244 -0
- package/src/server/external-open.ts +272 -0
- package/src/server/gemini-acp.ts +844 -0
- package/src/server/gemini-cli.ts +525 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-manager.ts +79 -0
- package/src/server/git-repository.ts +101 -0
- package/src/server/harness-types.ts +20 -0
- package/src/server/keybindings.ts +177 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths.ts +112 -0
- package/src/server/process-utils.ts +22 -0
- package/src/server/project-icon.ts +344 -0
- package/src/server/project-metadata.ts +10 -0
- package/src/server/provider-catalog.ts +85 -0
- package/src/server/provider-settings.ts +155 -0
- package/src/server/quick-response.ts +153 -0
- package/src/server/read-models.ts +275 -0
- package/src/server/recovery.ts +507 -0
- package/src/server/restart.ts +30 -0
- package/src/server/server.ts +244 -0
- package/src/server/terminal-manager.ts +350 -0
- package/src/server/theme-settings.ts +179 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/usage/base-provider-usage.ts +57 -0
- package/src/server/usage/claude-usage.ts +558 -0
- package/src/server/usage/codex-usage.ts +144 -0
- package/src/server/usage/cursor-browser.ts +120 -0
- package/src/server/usage/cursor-cookies.ts +390 -0
- package/src/server/usage/cursor-usage.ts +490 -0
- package/src/server/usage/gemini-usage.ts +24 -0
- package/src/server/usage/provider-usage.ts +61 -0
- package/src/server/usage/test-helpers.ts +9 -0
- package/src/server/usage/types.ts +54 -0
- package/src/server/usage/utils.ts +325 -0
- package/src/server/ws-router.ts +717 -0
- package/src/shared/branding.ts +83 -0
- package/src/shared/dev-ports.ts +43 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +152 -0
- package/src/shared/tools.ts +251 -0
- 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
|
+
}
|