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,120 @@
|
|
|
1
|
+
import { canOpenMacApp, resolveCommandPath } from "../process-utils"
|
|
2
|
+
import { buildCursorCookieHeader, mergeCursorCookies, responseSetCookies } from "./cursor-cookies"
|
|
3
|
+
import { parseCursorUsagePayload } from "./cursor-usage"
|
|
4
|
+
import type { CursorSessionCache } from "./types"
|
|
5
|
+
|
|
6
|
+
export const CURSOR_DASHBOARD_URL = "https://cursor.com/dashboard/spending"
|
|
7
|
+
export const CURSOR_USAGE_URL = "https://cursor.com/api/dashboard/get-current-period-usage"
|
|
8
|
+
export const CURSOR_BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000
|
|
9
|
+
|
|
10
|
+
export async function fetchCursorEndpoint(args: {
|
|
11
|
+
url: string
|
|
12
|
+
method?: "GET" | "POST"
|
|
13
|
+
session: CursorSessionCache
|
|
14
|
+
body?: unknown
|
|
15
|
+
}) {
|
|
16
|
+
const response = await fetch(args.url, {
|
|
17
|
+
method: args.method ?? "GET",
|
|
18
|
+
headers: {
|
|
19
|
+
accept: "*/*",
|
|
20
|
+
"content-type": "application/json",
|
|
21
|
+
origin: "https://cursor.com",
|
|
22
|
+
referer: CURSOR_DASHBOARD_URL,
|
|
23
|
+
cookie: buildCursorCookieHeader(args.session.cookies),
|
|
24
|
+
"user-agent": "Kaizen/1.0",
|
|
25
|
+
},
|
|
26
|
+
body: args.body === undefined ? undefined : JSON.stringify(args.body),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const mergedSession: CursorSessionCache = {
|
|
30
|
+
cookies: mergeCursorCookies(args.session.cookies, responseSetCookies(response)),
|
|
31
|
+
updatedAt: Date.now(),
|
|
32
|
+
lastSuccessAt: args.session.lastSuccessAt,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { response, session: mergedSession }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isCursorAuthFailure(response: Response) {
|
|
39
|
+
return response.status === 401 || response.status === 403
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function attemptCursorUsageFetch(session: CursorSessionCache) {
|
|
43
|
+
const { response, session: nextSession } = await fetchCursorEndpoint({
|
|
44
|
+
url: CURSOR_USAGE_URL,
|
|
45
|
+
method: "POST",
|
|
46
|
+
session,
|
|
47
|
+
body: {},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (isCursorAuthFailure(response)) {
|
|
51
|
+
return { ok: false as const, authFailed: true, session: nextSession, payload: null }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
return { ok: false as const, authFailed: false, session: nextSession, payload: null }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const payload = parseCursorUsagePayload(await response.json().catch(() => null))
|
|
59
|
+
if (!payload) {
|
|
60
|
+
return { ok: false as const, authFailed: false, session: nextSession, payload: null }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { ok: true as const, authFailed: false, session: nextSession, payload }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function refreshCursorSessionFromDashboard(session: CursorSessionCache) {
|
|
67
|
+
const { response, session: nextSession } = await fetchCursorEndpoint({
|
|
68
|
+
url: CURSOR_DASHBOARD_URL,
|
|
69
|
+
method: "GET",
|
|
70
|
+
session,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (!response.ok && !isCursorAuthFailure(response)) {
|
|
74
|
+
return { ok: false as const, session: nextSession }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { ok: true as const, session: nextSession }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveBrowserExecutable(platform = process.platform) {
|
|
81
|
+
const resolvedCommand = resolveCommandPath("google-chrome")
|
|
82
|
+
?? resolveCommandPath("chromium")
|
|
83
|
+
?? resolveCommandPath("chromium-browser")
|
|
84
|
+
?? resolveCommandPath("brave-browser")
|
|
85
|
+
if (resolvedCommand) return resolvedCommand
|
|
86
|
+
|
|
87
|
+
if (platform === "darwin") {
|
|
88
|
+
if (canOpenMacApp("Google Chrome")) return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
89
|
+
if (canOpenMacApp("Chromium")) return "/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
90
|
+
if (canOpenMacApp("Brave Browser")) return "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function sessionFromBrowserCookies(cookies: Array<{
|
|
97
|
+
name: string
|
|
98
|
+
value: string
|
|
99
|
+
domain?: string
|
|
100
|
+
path?: string
|
|
101
|
+
expires?: number
|
|
102
|
+
secure?: boolean
|
|
103
|
+
httpOnly?: boolean
|
|
104
|
+
}>): CursorSessionCache {
|
|
105
|
+
return {
|
|
106
|
+
cookies: cookies
|
|
107
|
+
.filter((cookie) => (cookie.domain ?? "cursor.com").replace(/^\./, "").endsWith("cursor.com"))
|
|
108
|
+
.map((cookie) => ({
|
|
109
|
+
name: cookie.name,
|
|
110
|
+
value: cookie.value,
|
|
111
|
+
domain: (cookie.domain ?? "cursor.com").replace(/^\./, ""),
|
|
112
|
+
path: cookie.path ?? "/",
|
|
113
|
+
expiresAt: typeof cookie.expires === "number" && Number.isFinite(cookie.expires) ? cookie.expires * 1000 : null,
|
|
114
|
+
secure: cookie.secure !== false,
|
|
115
|
+
httpOnly: cookie.httpOnly === true,
|
|
116
|
+
})),
|
|
117
|
+
updatedAt: Date.now(),
|
|
118
|
+
lastSuccessAt: null,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { copyFileSync, existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs"
|
|
3
|
+
import { createDecipheriv, pbkdf2Sync } from "node:crypto"
|
|
4
|
+
import { homedir, tmpdir } from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import process from "node:process"
|
|
7
|
+
import type { CursorCurlImportResult, CursorSessionCache, CursorSessionCookie } from "./types"
|
|
8
|
+
|
|
9
|
+
const CHROME_EPOCH_OFFSET_MS = Date.UTC(1601, 0, 1)
|
|
10
|
+
export const CURSOR_SESSION_COOKIE_NAME = "WorkosCursorSessionToken"
|
|
11
|
+
|
|
12
|
+
function normalizeCookieDomain(domain: string) {
|
|
13
|
+
return domain.startsWith(".") ? domain.slice(1) : domain
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isExpiredCookie(cookie: CursorSessionCookie, now = Date.now()) {
|
|
17
|
+
return cookie.expiresAt !== null && cookie.expiresAt <= now
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mergeCursorCookies(
|
|
21
|
+
existing: CursorSessionCookie[],
|
|
22
|
+
incoming: CursorSessionCookie[],
|
|
23
|
+
now = Date.now()
|
|
24
|
+
) {
|
|
25
|
+
const merged = new Map<string, CursorSessionCookie>()
|
|
26
|
+
for (const cookie of existing) {
|
|
27
|
+
if (isExpiredCookie(cookie, now)) continue
|
|
28
|
+
merged.set(`${normalizeCookieDomain(cookie.domain)}|${cookie.path}|${cookie.name}`, cookie)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const cookie of incoming) {
|
|
32
|
+
const key = `${normalizeCookieDomain(cookie.domain)}|${cookie.path}|${cookie.name}`
|
|
33
|
+
if (!cookie.value || isExpiredCookie(cookie, now)) {
|
|
34
|
+
merged.delete(key)
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
merged.set(key, {
|
|
38
|
+
...cookie,
|
|
39
|
+
domain: normalizeCookieDomain(cookie.domain),
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [...merged.values()]
|
|
44
|
+
.filter((cookie) => cookie.value)
|
|
45
|
+
.filter((cookie) => !isExpiredCookie(cookie, now))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildCursorCookieHeader(cookies: CursorSessionCookie[]) {
|
|
49
|
+
return cookies
|
|
50
|
+
.filter((cookie) => !isExpiredCookie(cookie))
|
|
51
|
+
.filter((cookie) => cookie.value)
|
|
52
|
+
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
53
|
+
.join("; ")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseSetCookie(header: string): CursorSessionCookie | null {
|
|
57
|
+
const segments = header.split(";").map((part) => part.trim()).filter(Boolean)
|
|
58
|
+
const [cookiePair, ...attributes] = segments
|
|
59
|
+
if (!cookiePair) return null
|
|
60
|
+
const equalsIndex = cookiePair.indexOf("=")
|
|
61
|
+
if (equalsIndex <= 0) return null
|
|
62
|
+
const name = cookiePair.slice(0, equalsIndex).trim()
|
|
63
|
+
const value = cookiePair.slice(equalsIndex + 1).trim()
|
|
64
|
+
if (!name) return null
|
|
65
|
+
|
|
66
|
+
let domain = "cursor.com"
|
|
67
|
+
let cookiePath = "/"
|
|
68
|
+
let expiresAt: number | null = null
|
|
69
|
+
let secure = false
|
|
70
|
+
let httpOnly = false
|
|
71
|
+
|
|
72
|
+
for (const attribute of attributes) {
|
|
73
|
+
const [rawKey, ...rawRest] = attribute.split("=")
|
|
74
|
+
const key = rawKey.trim().toLowerCase()
|
|
75
|
+
const rawValue = rawRest.join("=").trim()
|
|
76
|
+
if (key === "domain" && rawValue) {
|
|
77
|
+
domain = normalizeCookieDomain(rawValue)
|
|
78
|
+
} else if (key === "path" && rawValue) {
|
|
79
|
+
cookiePath = rawValue
|
|
80
|
+
} else if (key === "expires" && rawValue) {
|
|
81
|
+
const parsed = Date.parse(rawValue)
|
|
82
|
+
expiresAt = Number.isFinite(parsed) ? parsed : null
|
|
83
|
+
} else if (key === "max-age" && rawValue) {
|
|
84
|
+
const seconds = Number(rawValue)
|
|
85
|
+
if (Number.isFinite(seconds)) {
|
|
86
|
+
expiresAt = Date.now() + seconds * 1000
|
|
87
|
+
}
|
|
88
|
+
} else if (key === "secure") {
|
|
89
|
+
secure = true
|
|
90
|
+
} else if (key === "httponly") {
|
|
91
|
+
httpOnly = true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
name,
|
|
97
|
+
value,
|
|
98
|
+
domain,
|
|
99
|
+
path: cookiePath,
|
|
100
|
+
expiresAt,
|
|
101
|
+
secure,
|
|
102
|
+
httpOnly,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseCookieHeaderValue(cookieHeader: string): CursorSessionCookie[] {
|
|
107
|
+
return cookieHeader
|
|
108
|
+
.split(";")
|
|
109
|
+
.map((segment) => segment.trim())
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.map((segment): CursorSessionCookie | null => {
|
|
112
|
+
const equalsIndex = segment.indexOf("=")
|
|
113
|
+
if (equalsIndex <= 0) return null
|
|
114
|
+
return {
|
|
115
|
+
name: segment.slice(0, equalsIndex).trim(),
|
|
116
|
+
value: segment.slice(equalsIndex + 1).trim(),
|
|
117
|
+
domain: "cursor.com",
|
|
118
|
+
path: "/",
|
|
119
|
+
expiresAt: null,
|
|
120
|
+
secure: true,
|
|
121
|
+
httpOnly: true,
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
.filter((cookie): cookie is CursorSessionCookie => cookie !== null)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function extractCurlArguments(source: string, patterns: string[]) {
|
|
128
|
+
const values: string[] = []
|
|
129
|
+
for (const pattern of patterns) {
|
|
130
|
+
const regex = new RegExp(`${pattern}\\s+(?:'([^']*)'|"([^"]*)"|(\\S+))`, "ig")
|
|
131
|
+
for (const match of source.matchAll(regex)) {
|
|
132
|
+
values.push(match[1] ?? match[2] ?? match[3] ?? "")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return values.filter(Boolean)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function importCursorSessionFromCurl(curlCommand: string): CursorCurlImportResult | null {
|
|
139
|
+
const cookieHeader = extractCurlArguments(curlCommand, [
|
|
140
|
+
"-b",
|
|
141
|
+
"--cookie",
|
|
142
|
+
])[0] ?? extractCurlArguments(curlCommand, [
|
|
143
|
+
"-H",
|
|
144
|
+
"--header",
|
|
145
|
+
])
|
|
146
|
+
.map((header) => header.match(/^cookie:\s*(.+)$/i)?.[1] ?? null)
|
|
147
|
+
.find((header): header is string => Boolean(header))
|
|
148
|
+
?? null
|
|
149
|
+
|
|
150
|
+
if (!cookieHeader) return null
|
|
151
|
+
const cookies = parseCookieHeaderValue(cookieHeader)
|
|
152
|
+
if (!cookies.some((cookie) => cookie.name === CURSOR_SESSION_COOKIE_NAME)) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { cookies }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function responseSetCookies(response: Response): CursorSessionCookie[] {
|
|
160
|
+
const rawGetSetCookie = (response.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie
|
|
161
|
+
const cookieHeaders = typeof rawGetSetCookie === "function"
|
|
162
|
+
? rawGetSetCookie.call(response.headers)
|
|
163
|
+
: (() => {
|
|
164
|
+
const combined = response.headers.get("set-cookie")
|
|
165
|
+
return combined ? [combined] : []
|
|
166
|
+
})()
|
|
167
|
+
|
|
168
|
+
return cookieHeaders
|
|
169
|
+
.map((value) => parseSetCookie(value))
|
|
170
|
+
.filter((cookie): cookie is CursorSessionCookie => Boolean(cookie))
|
|
171
|
+
.filter((cookie) => normalizeCookieDomain(cookie.domain).endsWith("cursor.com"))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseCursorProfileCookiesDb(args: {
|
|
175
|
+
cookiesPath: string
|
|
176
|
+
browserName: string
|
|
177
|
+
platform: NodeJS.Platform
|
|
178
|
+
}): CursorSessionCookie[] {
|
|
179
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "kaizen-cursor-cookies-"))
|
|
180
|
+
const tempDbPath = path.join(tempDir, "Cookies")
|
|
181
|
+
try {
|
|
182
|
+
copyFileSync(args.cookiesPath, tempDbPath)
|
|
183
|
+
const database = new Database(tempDbPath, { readonly: true })
|
|
184
|
+
try {
|
|
185
|
+
const rows = database
|
|
186
|
+
.query(`
|
|
187
|
+
SELECT host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly
|
|
188
|
+
FROM cookies
|
|
189
|
+
WHERE host_key LIKE '%cursor.com'
|
|
190
|
+
`)
|
|
191
|
+
.all() as Array<{
|
|
192
|
+
host_key: string
|
|
193
|
+
name: string
|
|
194
|
+
value: string
|
|
195
|
+
encrypted_value: Uint8Array | null
|
|
196
|
+
path: string
|
|
197
|
+
expires_utc: number
|
|
198
|
+
is_secure: number
|
|
199
|
+
is_httponly: number
|
|
200
|
+
}>
|
|
201
|
+
|
|
202
|
+
const key = getChromiumCookieKey({
|
|
203
|
+
cookiesPath: args.cookiesPath,
|
|
204
|
+
browserName: args.browserName,
|
|
205
|
+
platform: args.platform,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
return rows
|
|
209
|
+
.map((row) => {
|
|
210
|
+
const value = row.value || decryptChromiumCookieValue(row.encrypted_value, key, args.platform)
|
|
211
|
+
if (!value) return null
|
|
212
|
+
return {
|
|
213
|
+
name: row.name,
|
|
214
|
+
value,
|
|
215
|
+
domain: normalizeCookieDomain(row.host_key),
|
|
216
|
+
path: row.path || "/",
|
|
217
|
+
expiresAt: chromeTimestampToUnixMs(row.expires_utc),
|
|
218
|
+
secure: row.is_secure === 1,
|
|
219
|
+
httpOnly: row.is_httponly === 1,
|
|
220
|
+
} satisfies CursorSessionCookie
|
|
221
|
+
})
|
|
222
|
+
.filter((cookie): cookie is CursorSessionCookie => Boolean(cookie))
|
|
223
|
+
} finally {
|
|
224
|
+
database.close()
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
return []
|
|
228
|
+
} finally {
|
|
229
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function chromeTimestampToUnixMs(value: number | null | undefined) {
|
|
234
|
+
if (!value || value <= 0) return null
|
|
235
|
+
return CHROME_EPOCH_OFFSET_MS + Math.floor(value / 1000)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function browserRootCandidates(platform: NodeJS.Platform) {
|
|
239
|
+
const home = homedir()
|
|
240
|
+
if (platform === "darwin") {
|
|
241
|
+
return [
|
|
242
|
+
{
|
|
243
|
+
name: "chrome",
|
|
244
|
+
rootPath: path.join(home, "Library", "Application Support", "Google", "Chrome"),
|
|
245
|
+
safeStorageName: "Chrome Safe Storage",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "chromium",
|
|
249
|
+
rootPath: path.join(home, "Library", "Application Support", "Chromium"),
|
|
250
|
+
safeStorageName: "Chromium Safe Storage",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "brave",
|
|
254
|
+
rootPath: path.join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
|
|
255
|
+
safeStorageName: "Brave Safe Storage",
|
|
256
|
+
},
|
|
257
|
+
]
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (platform === "linux") {
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
name: "chrome",
|
|
264
|
+
rootPath: path.join(home, ".config", "google-chrome"),
|
|
265
|
+
safeStorageName: "Chrome Safe Storage",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "chromium",
|
|
269
|
+
rootPath: path.join(home, ".config", "chromium"),
|
|
270
|
+
safeStorageName: "Chromium Safe Storage",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "brave",
|
|
274
|
+
rootPath: path.join(home, ".config", "BraveSoftware", "Brave-Browser"),
|
|
275
|
+
safeStorageName: "Brave Safe Storage",
|
|
276
|
+
},
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return []
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function discoverChromiumCookieSources(platform: NodeJS.Platform) {
|
|
284
|
+
const profiles: Array<{ browserName: string; cookiesPath: string }> = []
|
|
285
|
+
for (const browser of browserRootCandidates(platform)) {
|
|
286
|
+
if (!existsSync(browser.rootPath)) continue
|
|
287
|
+
for (const entry of readdirSync(browser.rootPath, { withFileTypes: true })) {
|
|
288
|
+
if (!entry.isDirectory()) continue
|
|
289
|
+
if (entry.name !== "Default" && !entry.name.startsWith("Profile ")) continue
|
|
290
|
+
const cookiesPath = path.join(browser.rootPath, entry.name, "Cookies")
|
|
291
|
+
if (!existsSync(cookiesPath)) continue
|
|
292
|
+
profiles.push({ browserName: browser.name, cookiesPath })
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return profiles
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function safeStoragePasswordForMac(safeStorageName: string) {
|
|
299
|
+
const result = Bun.spawnSync(["security", "find-generic-password", "-w", "-s", safeStorageName], {
|
|
300
|
+
stdin: "ignore",
|
|
301
|
+
stdout: "pipe",
|
|
302
|
+
stderr: "ignore",
|
|
303
|
+
})
|
|
304
|
+
if (result.exitCode !== 0) return null
|
|
305
|
+
const output = new TextDecoder().decode(result.stdout).trim()
|
|
306
|
+
return output || null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function safeStoragePasswordForLinux(browserName: string, safeStorageName: string) {
|
|
310
|
+
const candidateCommands = [
|
|
311
|
+
["secret-tool", "lookup", "application", browserName],
|
|
312
|
+
["secret-tool", "lookup", "service", safeStorageName],
|
|
313
|
+
["secret-tool", "lookup", "application", `${browserName} Safe Storage`],
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
for (const command of candidateCommands) {
|
|
317
|
+
const result = Bun.spawnSync(command, {
|
|
318
|
+
stdin: "ignore",
|
|
319
|
+
stdout: "pipe",
|
|
320
|
+
stderr: "ignore",
|
|
321
|
+
})
|
|
322
|
+
if (result.exitCode !== 0) continue
|
|
323
|
+
const output = new TextDecoder().decode(result.stdout).trim()
|
|
324
|
+
if (output) return output
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return "peanuts"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getChromiumCookieKey(args: {
|
|
331
|
+
cookiesPath: string
|
|
332
|
+
browserName: string
|
|
333
|
+
platform: NodeJS.Platform
|
|
334
|
+
}) {
|
|
335
|
+
const browser = browserRootCandidates(args.platform).find((candidate) => candidate.name === args.browserName)
|
|
336
|
+
if (!browser) return null
|
|
337
|
+
const password = args.platform === "darwin"
|
|
338
|
+
? safeStoragePasswordForMac(browser.safeStorageName)
|
|
339
|
+
: args.platform === "linux"
|
|
340
|
+
? safeStoragePasswordForLinux(args.browserName, browser.safeStorageName)
|
|
341
|
+
: null
|
|
342
|
+
|
|
343
|
+
if (!password) return null
|
|
344
|
+
|
|
345
|
+
const iterations = args.platform === "darwin" ? 1003 : 1
|
|
346
|
+
return pbkdf2Sync(password, "saltysalt", iterations, 16, "sha1")
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function decryptChromiumCookieValue(
|
|
350
|
+
encryptedValue: Uint8Array | null,
|
|
351
|
+
key: Buffer | null,
|
|
352
|
+
platform: NodeJS.Platform
|
|
353
|
+
) {
|
|
354
|
+
if (!encryptedValue || encryptedValue.length === 0 || !key) return null
|
|
355
|
+
const encrypted = Buffer.from(encryptedValue)
|
|
356
|
+
const versionPrefix = encrypted.subarray(0, 3).toString("utf8")
|
|
357
|
+
if (platform !== "darwin" && platform !== "linux") return null
|
|
358
|
+
if (versionPrefix !== "v10" && versionPrefix !== "v11") return null
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const decipher = createDecipheriv("aes-128-cbc", key, Buffer.alloc(16, 0x20))
|
|
362
|
+
const decrypted = Buffer.concat([
|
|
363
|
+
decipher.update(encrypted.subarray(3)),
|
|
364
|
+
decipher.final(),
|
|
365
|
+
])
|
|
366
|
+
return decrypted.toString("utf8")
|
|
367
|
+
} catch {
|
|
368
|
+
return null
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function bootstrapCursorSessionFromBrowser(platform = process.platform): CursorSessionCache | null {
|
|
373
|
+
if (platform !== "linux" && platform !== "darwin") return null
|
|
374
|
+
|
|
375
|
+
for (const source of discoverChromiumCookieSources(platform)) {
|
|
376
|
+
const cookies = parseCursorProfileCookiesDb({
|
|
377
|
+
cookiesPath: source.cookiesPath,
|
|
378
|
+
browserName: source.browserName,
|
|
379
|
+
platform,
|
|
380
|
+
})
|
|
381
|
+
if (!cookies.some((cookie) => cookie.name === CURSOR_SESSION_COOKIE_NAME)) continue
|
|
382
|
+
return {
|
|
383
|
+
cookies,
|
|
384
|
+
updatedAt: Date.now(),
|
|
385
|
+
lastSuccessAt: null,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return null
|
|
390
|
+
}
|