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,344 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { PROJECT_METADATA_DIR_NAME } from "../shared/branding"
|
|
4
|
+
import { resolveLocalPath } from "./paths"
|
|
5
|
+
import { getProjectMetadataCandidateRelativePath } from "./project-metadata"
|
|
6
|
+
|
|
7
|
+
const MAX_ICON_BYTES = 128 * 1024
|
|
8
|
+
const ICON_CANDIDATE_PATHS = [
|
|
9
|
+
"icon.svg",
|
|
10
|
+
"icon.png",
|
|
11
|
+
"icon.jpg",
|
|
12
|
+
"icon.jpeg",
|
|
13
|
+
"icon.webp",
|
|
14
|
+
"icon.gif",
|
|
15
|
+
"icon.avif",
|
|
16
|
+
"icon.ico",
|
|
17
|
+
getProjectMetadataCandidateRelativePath("icon.svg"),
|
|
18
|
+
getProjectMetadataCandidateRelativePath("icon.png"),
|
|
19
|
+
getProjectMetadataCandidateRelativePath("icon.jpg"),
|
|
20
|
+
getProjectMetadataCandidateRelativePath("icon.jpeg"),
|
|
21
|
+
getProjectMetadataCandidateRelativePath("icon.webp"),
|
|
22
|
+
getProjectMetadataCandidateRelativePath("icon.gif"),
|
|
23
|
+
getProjectMetadataCandidateRelativePath("icon.avif"),
|
|
24
|
+
getProjectMetadataCandidateRelativePath("icon.ico"),
|
|
25
|
+
"favicon.svg",
|
|
26
|
+
"favicon.png",
|
|
27
|
+
"favicon.jpg",
|
|
28
|
+
"favicon.jpeg",
|
|
29
|
+
"favicon.webp",
|
|
30
|
+
"favicon.gif",
|
|
31
|
+
"favicon.avif",
|
|
32
|
+
"favicon.ico",
|
|
33
|
+
"public/favicon.svg",
|
|
34
|
+
"public/favicon.png",
|
|
35
|
+
"public/favicon.jpg",
|
|
36
|
+
"public/favicon.jpeg",
|
|
37
|
+
"public/favicon.webp",
|
|
38
|
+
"public/favicon.gif",
|
|
39
|
+
"public/favicon.avif",
|
|
40
|
+
"public/favicon.ico",
|
|
41
|
+
"public/icon.svg",
|
|
42
|
+
"public/icon.png",
|
|
43
|
+
"public/icon.jpg",
|
|
44
|
+
"public/icon.jpeg",
|
|
45
|
+
"public/icon.webp",
|
|
46
|
+
"public/icon.gif",
|
|
47
|
+
"public/icon.avif",
|
|
48
|
+
"public/icon.ico",
|
|
49
|
+
"app/favicon.svg",
|
|
50
|
+
"app/favicon.png",
|
|
51
|
+
"app/favicon.jpg",
|
|
52
|
+
"app/favicon.jpeg",
|
|
53
|
+
"app/favicon.webp",
|
|
54
|
+
"app/favicon.gif",
|
|
55
|
+
"app/favicon.avif",
|
|
56
|
+
"app/favicon.ico",
|
|
57
|
+
"app/icon.svg",
|
|
58
|
+
"app/icon.png",
|
|
59
|
+
"app/icon.jpg",
|
|
60
|
+
"app/icon.jpeg",
|
|
61
|
+
"app/icon.webp",
|
|
62
|
+
"app/icon.gif",
|
|
63
|
+
"app/icon.avif",
|
|
64
|
+
"app/icon.ico",
|
|
65
|
+
"src/favicon.svg",
|
|
66
|
+
"src/favicon.png",
|
|
67
|
+
"src/favicon.jpg",
|
|
68
|
+
"src/favicon.jpeg",
|
|
69
|
+
"src/favicon.webp",
|
|
70
|
+
"src/favicon.gif",
|
|
71
|
+
"src/favicon.avif",
|
|
72
|
+
"src/favicon.ico",
|
|
73
|
+
"src/app/favicon.svg",
|
|
74
|
+
"src/app/favicon.png",
|
|
75
|
+
"src/app/favicon.jpg",
|
|
76
|
+
"src/app/favicon.jpeg",
|
|
77
|
+
"src/app/favicon.webp",
|
|
78
|
+
"src/app/favicon.gif",
|
|
79
|
+
"src/app/favicon.avif",
|
|
80
|
+
"src/app/favicon.ico",
|
|
81
|
+
"src/app/icon.svg",
|
|
82
|
+
"src/app/icon.png",
|
|
83
|
+
"src/app/icon.jpg",
|
|
84
|
+
"src/app/icon.jpeg",
|
|
85
|
+
"src/app/icon.webp",
|
|
86
|
+
"src/app/icon.gif",
|
|
87
|
+
"src/app/icon.avif",
|
|
88
|
+
"src/app/icon.ico",
|
|
89
|
+
]
|
|
90
|
+
const ICON_SOURCE_FILES = [
|
|
91
|
+
"index.html",
|
|
92
|
+
"public/index.html",
|
|
93
|
+
"app/root.tsx",
|
|
94
|
+
"src/root.tsx",
|
|
95
|
+
"app/routes/__root.tsx",
|
|
96
|
+
"src/routes/__root.tsx",
|
|
97
|
+
]
|
|
98
|
+
const LINK_ICON_HTML_RE = /<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"'?]+)/i
|
|
99
|
+
const LINK_ICON_OBJ_RE = /rel:\s*["'](?:icon|shortcut icon)["'][^}]*href:\s*["']([^"'?]+)/i
|
|
100
|
+
|
|
101
|
+
function getMimeType(extension: string) {
|
|
102
|
+
switch (extension) {
|
|
103
|
+
case ".svg":
|
|
104
|
+
return "image/svg+xml"
|
|
105
|
+
case ".png":
|
|
106
|
+
return "image/png"
|
|
107
|
+
case ".ico":
|
|
108
|
+
return "image/vnd.microsoft.icon"
|
|
109
|
+
case ".jpg":
|
|
110
|
+
case ".jpeg":
|
|
111
|
+
return "image/jpeg"
|
|
112
|
+
case ".webp":
|
|
113
|
+
return "image/webp"
|
|
114
|
+
case ".gif":
|
|
115
|
+
return "image/gif"
|
|
116
|
+
case ".avif":
|
|
117
|
+
return "image/avif"
|
|
118
|
+
default:
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function compareIconNames(a: string, b: string) {
|
|
124
|
+
const aExt = path.extname(a).toLowerCase()
|
|
125
|
+
const bExt = path.extname(b).toLowerCase()
|
|
126
|
+
const aIndex = getExtensionPriority(aExt)
|
|
127
|
+
const bIndex = getExtensionPriority(bExt)
|
|
128
|
+
if (aIndex !== bIndex) return aIndex - bIndex
|
|
129
|
+
return a.localeCompare(b)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getExtensionPriority(extension: string) {
|
|
133
|
+
switch (extension) {
|
|
134
|
+
case ".svg":
|
|
135
|
+
return 0
|
|
136
|
+
case ".png":
|
|
137
|
+
return 1
|
|
138
|
+
case ".jpg":
|
|
139
|
+
case ".jpeg":
|
|
140
|
+
return 2
|
|
141
|
+
case ".webp":
|
|
142
|
+
return 3
|
|
143
|
+
case ".gif":
|
|
144
|
+
return 4
|
|
145
|
+
case ".avif":
|
|
146
|
+
return 5
|
|
147
|
+
case ".ico":
|
|
148
|
+
return 6
|
|
149
|
+
default:
|
|
150
|
+
return Number.MAX_SAFE_INTEGER
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function findFixedIconCandidates(projectPath: string) {
|
|
155
|
+
try {
|
|
156
|
+
const existingPaths = new Map<string, string>()
|
|
157
|
+
|
|
158
|
+
for (const relativePath of ICON_CANDIDATE_PATHS) {
|
|
159
|
+
const extension = path.extname(relativePath).toLowerCase()
|
|
160
|
+
if (getMimeType(extension) === null) continue
|
|
161
|
+
|
|
162
|
+
const absolutePath = resolveCaseInsensitiveProjectPath(projectPath, relativePath)
|
|
163
|
+
if (!absolutePath) continue
|
|
164
|
+
existingPaths.set(relativePath, absolutePath)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [...existingPaths.entries()]
|
|
168
|
+
.sort((a, b) => {
|
|
169
|
+
const pathCompare = ICON_CANDIDATE_PATHS.indexOf(a[0]) - ICON_CANDIDATE_PATHS.indexOf(b[0])
|
|
170
|
+
if (pathCompare !== 0) return pathCompare
|
|
171
|
+
return compareIconNames(a[0], b[0])
|
|
172
|
+
})
|
|
173
|
+
.map(([, absolutePath]) => absolutePath)
|
|
174
|
+
} catch {
|
|
175
|
+
return []
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function findAppWorkspaceIconCandidates(projectPath: string) {
|
|
180
|
+
const appsPath = path.join(projectPath, "apps")
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const appDirectories = readdirSync(appsPath, { withFileTypes: true })
|
|
184
|
+
.filter((entry) => entry.isDirectory())
|
|
185
|
+
.map((entry) => entry.name)
|
|
186
|
+
.sort((a, b) => a.localeCompare(b))
|
|
187
|
+
|
|
188
|
+
const existingPaths = new Map<string, string>()
|
|
189
|
+
|
|
190
|
+
for (const appDirectory of appDirectories) {
|
|
191
|
+
for (const relativePath of ICON_CANDIDATE_PATHS) {
|
|
192
|
+
if (relativePath.startsWith(`${PROJECT_METADATA_DIR_NAME}/`)) continue
|
|
193
|
+
const appRelativePath = path.join("apps", appDirectory, relativePath)
|
|
194
|
+
const extension = path.extname(appRelativePath).toLowerCase()
|
|
195
|
+
if (getMimeType(extension) === null) continue
|
|
196
|
+
|
|
197
|
+
const absolutePath = resolveCaseInsensitiveProjectPath(projectPath, appRelativePath)
|
|
198
|
+
if (!absolutePath) continue
|
|
199
|
+
existingPaths.set(appRelativePath, absolutePath)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return [...existingPaths.entries()]
|
|
204
|
+
.sort((a, b) => {
|
|
205
|
+
const [aPath] = a
|
|
206
|
+
const [bPath] = b
|
|
207
|
+
const aRelative = aPath.replace(/^apps\/[^/]+\//, "")
|
|
208
|
+
const bRelative = bPath.replace(/^apps\/[^/]+\//, "")
|
|
209
|
+
const pathCompare = ICON_CANDIDATE_PATHS.indexOf(aRelative) - ICON_CANDIDATE_PATHS.indexOf(bRelative)
|
|
210
|
+
if (pathCompare !== 0) return pathCompare
|
|
211
|
+
return compareIconNames(aPath, bPath)
|
|
212
|
+
})
|
|
213
|
+
.map(([, absolutePath]) => absolutePath)
|
|
214
|
+
} catch {
|
|
215
|
+
return []
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveCaseInsensitiveProjectPath(projectPath: string, relativePath: string) {
|
|
220
|
+
const segments = relativePath.split("/").filter(Boolean)
|
|
221
|
+
let currentPath = projectPath
|
|
222
|
+
|
|
223
|
+
for (const [index, segment] of segments.entries()) {
|
|
224
|
+
let entries: string[]
|
|
225
|
+
try {
|
|
226
|
+
entries = readdirSync(currentPath)
|
|
227
|
+
} catch {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const matchedEntry = entries.find((entry) => entry.toLowerCase() === segment.toLowerCase())
|
|
232
|
+
if (!matchedEntry) {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
currentPath = path.join(currentPath, matchedEntry)
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const stat = statSync(currentPath)
|
|
240
|
+
const isLast = index === segments.length - 1
|
|
241
|
+
if (isLast) {
|
|
242
|
+
return stat.isFile() ? currentPath : null
|
|
243
|
+
}
|
|
244
|
+
if (!stat.isDirectory()) {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function extractDeclaredIconHref(source: string) {
|
|
256
|
+
const htmlMatch = source.match(LINK_ICON_HTML_RE)
|
|
257
|
+
if (htmlMatch?.[1]) return htmlMatch[1]
|
|
258
|
+
const objMatch = source.match(LINK_ICON_OBJ_RE)
|
|
259
|
+
if (objMatch?.[1]) return objMatch[1]
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function resolveDeclaredIconCandidates(projectPath: string, href: string) {
|
|
264
|
+
const cleanHref = href.replace(/^\//, "")
|
|
265
|
+
return [
|
|
266
|
+
path.join(projectPath, "public", cleanHref),
|
|
267
|
+
path.join(projectPath, cleanHref),
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function findDeclaredIconCandidates(projectPath: string) {
|
|
272
|
+
const candidates: string[] = []
|
|
273
|
+
|
|
274
|
+
for (const sourceFile of ICON_SOURCE_FILES) {
|
|
275
|
+
const sourcePath = path.join(projectPath, sourceFile)
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const source = readFileSync(sourcePath, "utf8")
|
|
279
|
+
const href = extractDeclaredIconHref(source)
|
|
280
|
+
if (!href) continue
|
|
281
|
+
|
|
282
|
+
for (const candidatePath of resolveDeclaredIconCandidates(projectPath, href)) {
|
|
283
|
+
const extension = path.extname(candidatePath).toLowerCase()
|
|
284
|
+
if (getMimeType(extension) === null) continue
|
|
285
|
+
if (!candidates.includes(candidatePath)) {
|
|
286
|
+
candidates.push(candidatePath)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return candidates
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function tryReadImageDataUrl(filePath: string) {
|
|
298
|
+
try {
|
|
299
|
+
const stat = statSync(filePath)
|
|
300
|
+
if (!stat.isFile() || stat.size > MAX_ICON_BYTES) {
|
|
301
|
+
return null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const extension = path.extname(filePath).toLowerCase()
|
|
305
|
+
const mimeType = getMimeType(extension)
|
|
306
|
+
if (!mimeType) {
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (extension === ".svg") {
|
|
311
|
+
const svg = readFileSync(filePath, "utf8").trim()
|
|
312
|
+
if (!svg.includes("<svg")) {
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return `data:${mimeType};utf8,${encodeURIComponent(svg)}`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const image = readFileSync(filePath)
|
|
320
|
+
return `data:${mimeType};base64,${image.toString("base64")}`
|
|
321
|
+
} catch {
|
|
322
|
+
return null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function resolveProjectIconDataUrl(localPath: string): string | null {
|
|
329
|
+
const projectPath = resolveLocalPath(localPath)
|
|
330
|
+
const candidatePaths = [
|
|
331
|
+
...findFixedIconCandidates(projectPath),
|
|
332
|
+
...findAppWorkspaceIconCandidates(projectPath),
|
|
333
|
+
...findDeclaredIconCandidates(projectPath),
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
for (const candidatePath of candidatePaths) {
|
|
337
|
+
const dataUrl = tryReadImageDataUrl(candidatePath)
|
|
338
|
+
if (dataUrl) {
|
|
339
|
+
return dataUrl
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { PROJECT_METADATA_DIR_NAME } from "../shared/branding"
|
|
3
|
+
|
|
4
|
+
export function getProjectMetadataDirPath(localPath: string) {
|
|
5
|
+
return path.join(localPath, PROJECT_METADATA_DIR_NAME)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getProjectMetadataCandidateRelativePath(fileName: string) {
|
|
9
|
+
return path.posix.join(PROJECT_METADATA_DIR_NAME, fileName)
|
|
10
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentProvider,
|
|
3
|
+
ClaudeModelOptions,
|
|
4
|
+
ClaudeContextWindow,
|
|
5
|
+
CodexModelOptions,
|
|
6
|
+
CursorModelOptions,
|
|
7
|
+
GeminiModelOptions,
|
|
8
|
+
ModelOptions,
|
|
9
|
+
ProviderCatalogEntry,
|
|
10
|
+
ServiceTier,
|
|
11
|
+
} from "../shared/types"
|
|
12
|
+
import {
|
|
13
|
+
getProviderCatalog,
|
|
14
|
+
getProviderDefaultModelOptions,
|
|
15
|
+
DEFAULT_CLAUDE_MODEL_OPTIONS,
|
|
16
|
+
normalizeClaudeContextWindow,
|
|
17
|
+
isClaudeReasoningEffort,
|
|
18
|
+
isCodexReasoningEffort,
|
|
19
|
+
isGeminiThinkingMode,
|
|
20
|
+
normalizeCursorModelId,
|
|
21
|
+
} from "../shared/types"
|
|
22
|
+
|
|
23
|
+
export function getServerProviderCatalog(provider: AgentProvider): ProviderCatalogEntry {
|
|
24
|
+
return getProviderCatalog(provider)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeServerModel(provider: AgentProvider, model?: string): string {
|
|
28
|
+
if (provider === "cursor") {
|
|
29
|
+
return normalizeCursorModelId(model)
|
|
30
|
+
}
|
|
31
|
+
const catalog = getServerProviderCatalog(provider)
|
|
32
|
+
if (model && catalog.models.some((candidate) => candidate.id === model)) {
|
|
33
|
+
return model
|
|
34
|
+
}
|
|
35
|
+
return catalog.defaultModel
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeClaudeModelOptions(
|
|
39
|
+
model: string,
|
|
40
|
+
modelOptions?: ModelOptions,
|
|
41
|
+
legacyEffort?: string
|
|
42
|
+
): ClaudeModelOptions {
|
|
43
|
+
const reasoningEffort = modelOptions?.claude?.reasoningEffort
|
|
44
|
+
return {
|
|
45
|
+
reasoningEffort: isClaudeReasoningEffort(reasoningEffort)
|
|
46
|
+
? reasoningEffort
|
|
47
|
+
: isClaudeReasoningEffort(legacyEffort)
|
|
48
|
+
? legacyEffort
|
|
49
|
+
: DEFAULT_CLAUDE_MODEL_OPTIONS.reasoningEffort,
|
|
50
|
+
contextWindow: normalizeClaudeContextWindow(model, modelOptions?.claude?.contextWindow as ClaudeContextWindow | undefined),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeCodexModelOptions(modelOptions?: ModelOptions, legacyEffort?: string): CodexModelOptions {
|
|
55
|
+
const defaultOptions = getProviderDefaultModelOptions("codex")
|
|
56
|
+
const reasoningEffort = modelOptions?.codex?.reasoningEffort
|
|
57
|
+
return {
|
|
58
|
+
reasoningEffort: isCodexReasoningEffort(reasoningEffort)
|
|
59
|
+
? reasoningEffort
|
|
60
|
+
: isCodexReasoningEffort(legacyEffort)
|
|
61
|
+
? legacyEffort
|
|
62
|
+
: defaultOptions.reasoningEffort,
|
|
63
|
+
fastMode: typeof modelOptions?.codex?.fastMode === "boolean"
|
|
64
|
+
? modelOptions.codex.fastMode
|
|
65
|
+
: defaultOptions.fastMode,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeGeminiModelOptions(modelOptions?: ModelOptions): GeminiModelOptions {
|
|
70
|
+
const defaultOptions = getProviderDefaultModelOptions("gemini")
|
|
71
|
+
return {
|
|
72
|
+
thinkingMode: isGeminiThinkingMode(modelOptions?.gemini?.thinkingMode)
|
|
73
|
+
? modelOptions.gemini.thinkingMode
|
|
74
|
+
: defaultOptions.thinkingMode,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function normalizeCursorModelOptions(modelOptions?: ModelOptions): CursorModelOptions {
|
|
79
|
+
void modelOptions
|
|
80
|
+
return { ...getProviderDefaultModelOptions("cursor") }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function codexServiceTierFromModelOptions(modelOptions: CodexModelOptions): ServiceTier | undefined {
|
|
84
|
+
return modelOptions.fastMode ? "fast" : undefined
|
|
85
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from "node:fs"
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { getProviderSettingsFilePath, LOG_PREFIX } from "../shared/branding"
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_PROVIDER_SETTINGS,
|
|
8
|
+
type AgentProvider,
|
|
9
|
+
type ProviderSettingsEntry,
|
|
10
|
+
type ProviderSettingsSnapshot,
|
|
11
|
+
} from "../shared/types"
|
|
12
|
+
|
|
13
|
+
const PROVIDER_IDS = Object.keys(DEFAULT_PROVIDER_SETTINGS) as AgentProvider[]
|
|
14
|
+
|
|
15
|
+
type ProviderSettingsFile = Partial<Record<AgentProvider, Partial<ProviderSettingsEntry>>>
|
|
16
|
+
|
|
17
|
+
export class ProviderSettingsManager {
|
|
18
|
+
readonly filePath: string
|
|
19
|
+
private watcher: FSWatcher | null = null
|
|
20
|
+
private snapshot: ProviderSettingsSnapshot
|
|
21
|
+
private readonly listeners = new Set<(snapshot: ProviderSettingsSnapshot) => void>()
|
|
22
|
+
|
|
23
|
+
constructor(filePath = getProviderSettingsFilePath(homedir())) {
|
|
24
|
+
this.filePath = filePath
|
|
25
|
+
this.snapshot = createDefaultSnapshot(this.filePath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async initialize() {
|
|
29
|
+
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
30
|
+
const file = Bun.file(this.filePath)
|
|
31
|
+
if (!(await file.exists())) {
|
|
32
|
+
await writeFile(this.filePath, `${JSON.stringify(DEFAULT_PROVIDER_SETTINGS, null, 2)}\n`, "utf8")
|
|
33
|
+
}
|
|
34
|
+
await this.reload()
|
|
35
|
+
this.startWatching()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dispose() {
|
|
39
|
+
this.watcher?.close()
|
|
40
|
+
this.watcher = null
|
|
41
|
+
this.listeners.clear()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getSnapshot() {
|
|
45
|
+
return this.snapshot
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onChange(listener: (snapshot: ProviderSettingsSnapshot) => void) {
|
|
49
|
+
this.listeners.add(listener)
|
|
50
|
+
return () => {
|
|
51
|
+
this.listeners.delete(listener)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async reload() {
|
|
56
|
+
const nextSnapshot = await readProviderSettingsSnapshot(this.filePath)
|
|
57
|
+
this.setSnapshot(nextSnapshot)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async write(settings: ProviderSettingsSnapshot["settings"]) {
|
|
61
|
+
const nextSnapshot = normalizeProviderSettings(settings, this.filePath)
|
|
62
|
+
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
63
|
+
await writeFile(this.filePath, `${JSON.stringify(nextSnapshot.settings, null, 2)}\n`, "utf8")
|
|
64
|
+
this.setSnapshot(nextSnapshot)
|
|
65
|
+
return nextSnapshot
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private setSnapshot(snapshot: ProviderSettingsSnapshot) {
|
|
69
|
+
this.snapshot = snapshot
|
|
70
|
+
for (const listener of this.listeners) {
|
|
71
|
+
listener(snapshot)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private startWatching() {
|
|
76
|
+
this.watcher?.close()
|
|
77
|
+
try {
|
|
78
|
+
this.watcher = watch(path.dirname(this.filePath), { persistent: false }, (_eventType, filename) => {
|
|
79
|
+
if (filename && filename !== path.basename(this.filePath)) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
void this.reload().catch((error: unknown) => {
|
|
83
|
+
console.warn(`${LOG_PREFIX} Failed to reload provider settings:`, error)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn(`${LOG_PREFIX} Failed to watch provider settings file:`, error)
|
|
88
|
+
this.watcher = null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function readProviderSettingsSnapshot(filePath: string): Promise<ProviderSettingsSnapshot> {
|
|
94
|
+
try {
|
|
95
|
+
const text = await readFile(filePath, "utf8")
|
|
96
|
+
if (!text.trim()) {
|
|
97
|
+
return createDefaultSnapshot(filePath, "Provider settings file was empty. Using defaults.")
|
|
98
|
+
}
|
|
99
|
+
const parsed = JSON.parse(text) as ProviderSettingsFile
|
|
100
|
+
return normalizeProviderSettings(parsed, filePath)
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
103
|
+
return createDefaultSnapshot(filePath)
|
|
104
|
+
}
|
|
105
|
+
if (error instanceof SyntaxError) {
|
|
106
|
+
return createDefaultSnapshot(filePath, "Provider settings file is invalid JSON. Using defaults.")
|
|
107
|
+
}
|
|
108
|
+
throw error
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeProviderSettings(
|
|
113
|
+
value: ProviderSettingsFile | null | undefined,
|
|
114
|
+
filePath = getProviderSettingsFilePath(homedir())
|
|
115
|
+
): ProviderSettingsSnapshot {
|
|
116
|
+
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}
|
|
117
|
+
|
|
118
|
+
const settings = {} as ProviderSettingsSnapshot["settings"]
|
|
119
|
+
for (const provider of PROVIDER_IDS) {
|
|
120
|
+
const rawValue = source[provider]
|
|
121
|
+
const defaultValue = DEFAULT_PROVIDER_SETTINGS[provider]
|
|
122
|
+
|
|
123
|
+
settings[provider] = {
|
|
124
|
+
active: typeof rawValue?.active === "boolean" ? rawValue.active : defaultValue.active,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
settings,
|
|
130
|
+
warning: null,
|
|
131
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createDefaultSnapshot(filePath: string, warning: string | null = null): ProviderSettingsSnapshot {
|
|
136
|
+
return {
|
|
137
|
+
settings: {
|
|
138
|
+
claude: { ...DEFAULT_PROVIDER_SETTINGS.claude },
|
|
139
|
+
codex: { ...DEFAULT_PROVIDER_SETTINGS.codex },
|
|
140
|
+
gemini: { ...DEFAULT_PROVIDER_SETTINGS.gemini },
|
|
141
|
+
cursor: { ...DEFAULT_PROVIDER_SETTINGS.cursor },
|
|
142
|
+
},
|
|
143
|
+
warning,
|
|
144
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatDisplayPath(filePath: string) {
|
|
149
|
+
const homePath = homedir()
|
|
150
|
+
if (filePath === homePath) return "~"
|
|
151
|
+
if (filePath.startsWith(`${homePath}${path.sep}`)) {
|
|
152
|
+
return `~${filePath.slice(homePath.length)}`
|
|
153
|
+
}
|
|
154
|
+
return filePath
|
|
155
|
+
}
|