kaizenai 0.3.0 → 0.5.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/README.md +0 -4
  2. package/bin/kaizen +16 -5
  3. package/dist/client/app-icon-180.png +0 -0
  4. package/dist/client/app-icon-192.png +0 -0
  5. package/dist/client/app-icon-512.png +0 -0
  6. package/dist/client/app-icon-96.png +0 -0
  7. package/dist/client/apple-touch-icon.png +0 -0
  8. package/dist/client/assets/index-C4Zm475L.js +638 -0
  9. package/dist/client/assets/index-CxKis6wK.css +32 -0
  10. package/dist/client/favicon.png +0 -0
  11. package/dist/client/index.html +17 -10
  12. package/dist/client/manifest-dark.webmanifest +3 -3
  13. package/dist/client/manifest.webmanifest +3 -3
  14. package/dist/client/pwa-192.png +0 -0
  15. package/dist/client/pwa-512.png +0 -0
  16. package/dist/server/cli-supervisor.js +306 -0
  17. package/dist/server/cli.js +12636 -0
  18. package/package.json +7 -10
  19. package/dist/client/assets/index-BBs80KD-.js +0 -623
  20. package/dist/client/assets/index-CkCgyLNq.css +0 -32
  21. package/src/server/acp-shared.ts +0 -315
  22. package/src/server/agent.ts +0 -1159
  23. package/src/server/attachments.ts +0 -133
  24. package/src/server/backgrounds.ts +0 -74
  25. package/src/server/cli-runtime.ts +0 -375
  26. package/src/server/cli-supervisor.ts +0 -97
  27. package/src/server/cli.ts +0 -68
  28. package/src/server/codex-app-server-protocol.ts +0 -453
  29. package/src/server/codex-app-server.ts +0 -1350
  30. package/src/server/cursor-acp.ts +0 -819
  31. package/src/server/discovery.ts +0 -322
  32. package/src/server/event-store.ts +0 -1470
  33. package/src/server/events.ts +0 -252
  34. package/src/server/external-open.ts +0 -272
  35. package/src/server/gemini-acp.ts +0 -844
  36. package/src/server/gemini-cli.ts +0 -525
  37. package/src/server/generate-title.ts +0 -36
  38. package/src/server/git-manager.ts +0 -79
  39. package/src/server/git-repository.ts +0 -101
  40. package/src/server/harness-types.ts +0 -20
  41. package/src/server/keybindings.ts +0 -177
  42. package/src/server/machine-name.ts +0 -22
  43. package/src/server/paths.ts +0 -112
  44. package/src/server/process-utils.ts +0 -22
  45. package/src/server/project-icon.ts +0 -352
  46. package/src/server/project-metadata.ts +0 -10
  47. package/src/server/provider-catalog.ts +0 -85
  48. package/src/server/provider-settings.ts +0 -155
  49. package/src/server/quick-response.ts +0 -153
  50. package/src/server/read-models.ts +0 -275
  51. package/src/server/recovery.ts +0 -507
  52. package/src/server/restart.ts +0 -56
  53. package/src/server/server.ts +0 -244
  54. package/src/server/terminal-manager.ts +0 -350
  55. package/src/server/theme-settings.ts +0 -179
  56. package/src/server/update-manager.ts +0 -230
  57. package/src/server/usage/base-provider-usage.ts +0 -57
  58. package/src/server/usage/claude-usage.ts +0 -558
  59. package/src/server/usage/codex-usage.ts +0 -144
  60. package/src/server/usage/cursor-browser.ts +0 -120
  61. package/src/server/usage/cursor-cookies.ts +0 -390
  62. package/src/server/usage/cursor-usage.ts +0 -490
  63. package/src/server/usage/gemini-usage.ts +0 -24
  64. package/src/server/usage/provider-usage.ts +0 -61
  65. package/src/server/usage/test-helpers.ts +0 -9
  66. package/src/server/usage/types.ts +0 -54
  67. package/src/server/usage/utils.ts +0 -325
  68. package/src/server/ws-router.ts +0 -742
  69. package/src/shared/branding.ts +0 -83
  70. package/src/shared/dev-ports.ts +0 -43
  71. package/src/shared/ports.ts +0 -2
  72. package/src/shared/protocol.ts +0 -156
  73. package/src/shared/tools.ts +0 -251
  74. package/src/shared/types.ts +0 -1040
@@ -1,352 +0,0 @@
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
- "assets/icon.svg",
10
- "assets/icon.png",
11
- "assets/icon.jpg",
12
- "assets/icon.jpeg",
13
- "assets/icon.webp",
14
- "assets/icon.gif",
15
- "assets/icon.avif",
16
- "assets/icon.ico",
17
- "icon.svg",
18
- "icon.png",
19
- "icon.jpg",
20
- "icon.jpeg",
21
- "icon.webp",
22
- "icon.gif",
23
- "icon.avif",
24
- "icon.ico",
25
- getProjectMetadataCandidateRelativePath("icon.svg"),
26
- getProjectMetadataCandidateRelativePath("icon.png"),
27
- getProjectMetadataCandidateRelativePath("icon.jpg"),
28
- getProjectMetadataCandidateRelativePath("icon.jpeg"),
29
- getProjectMetadataCandidateRelativePath("icon.webp"),
30
- getProjectMetadataCandidateRelativePath("icon.gif"),
31
- getProjectMetadataCandidateRelativePath("icon.avif"),
32
- getProjectMetadataCandidateRelativePath("icon.ico"),
33
- "favicon.svg",
34
- "favicon.png",
35
- "favicon.jpg",
36
- "favicon.jpeg",
37
- "favicon.webp",
38
- "favicon.gif",
39
- "favicon.avif",
40
- "favicon.ico",
41
- "public/favicon.svg",
42
- "public/favicon.png",
43
- "public/favicon.jpg",
44
- "public/favicon.jpeg",
45
- "public/favicon.webp",
46
- "public/favicon.gif",
47
- "public/favicon.avif",
48
- "public/favicon.ico",
49
- "public/icon.svg",
50
- "public/icon.png",
51
- "public/icon.jpg",
52
- "public/icon.jpeg",
53
- "public/icon.webp",
54
- "public/icon.gif",
55
- "public/icon.avif",
56
- "public/icon.ico",
57
- "app/favicon.svg",
58
- "app/favicon.png",
59
- "app/favicon.jpg",
60
- "app/favicon.jpeg",
61
- "app/favicon.webp",
62
- "app/favicon.gif",
63
- "app/favicon.avif",
64
- "app/favicon.ico",
65
- "app/icon.svg",
66
- "app/icon.png",
67
- "app/icon.jpg",
68
- "app/icon.jpeg",
69
- "app/icon.webp",
70
- "app/icon.gif",
71
- "app/icon.avif",
72
- "app/icon.ico",
73
- "src/favicon.svg",
74
- "src/favicon.png",
75
- "src/favicon.jpg",
76
- "src/favicon.jpeg",
77
- "src/favicon.webp",
78
- "src/favicon.gif",
79
- "src/favicon.avif",
80
- "src/favicon.ico",
81
- "src/app/favicon.svg",
82
- "src/app/favicon.png",
83
- "src/app/favicon.jpg",
84
- "src/app/favicon.jpeg",
85
- "src/app/favicon.webp",
86
- "src/app/favicon.gif",
87
- "src/app/favicon.avif",
88
- "src/app/favicon.ico",
89
- "src/app/icon.svg",
90
- "src/app/icon.png",
91
- "src/app/icon.jpg",
92
- "src/app/icon.jpeg",
93
- "src/app/icon.webp",
94
- "src/app/icon.gif",
95
- "src/app/icon.avif",
96
- "src/app/icon.ico",
97
- ]
98
- const ICON_SOURCE_FILES = [
99
- "index.html",
100
- "public/index.html",
101
- "app/root.tsx",
102
- "src/root.tsx",
103
- "app/routes/__root.tsx",
104
- "src/routes/__root.tsx",
105
- ]
106
- const LINK_ICON_HTML_RE = /<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"'?]+)/i
107
- const LINK_ICON_OBJ_RE = /rel:\s*["'](?:icon|shortcut icon)["'][^}]*href:\s*["']([^"'?]+)/i
108
-
109
- function getMimeType(extension: string) {
110
- switch (extension) {
111
- case ".svg":
112
- return "image/svg+xml"
113
- case ".png":
114
- return "image/png"
115
- case ".ico":
116
- return "image/vnd.microsoft.icon"
117
- case ".jpg":
118
- case ".jpeg":
119
- return "image/jpeg"
120
- case ".webp":
121
- return "image/webp"
122
- case ".gif":
123
- return "image/gif"
124
- case ".avif":
125
- return "image/avif"
126
- default:
127
- return null
128
- }
129
- }
130
-
131
- function compareIconNames(a: string, b: string) {
132
- const aExt = path.extname(a).toLowerCase()
133
- const bExt = path.extname(b).toLowerCase()
134
- const aIndex = getExtensionPriority(aExt)
135
- const bIndex = getExtensionPriority(bExt)
136
- if (aIndex !== bIndex) return aIndex - bIndex
137
- return a.localeCompare(b)
138
- }
139
-
140
- function getExtensionPriority(extension: string) {
141
- switch (extension) {
142
- case ".svg":
143
- return 0
144
- case ".png":
145
- return 1
146
- case ".jpg":
147
- case ".jpeg":
148
- return 2
149
- case ".webp":
150
- return 3
151
- case ".gif":
152
- return 4
153
- case ".avif":
154
- return 5
155
- case ".ico":
156
- return 6
157
- default:
158
- return Number.MAX_SAFE_INTEGER
159
- }
160
- }
161
-
162
- function findFixedIconCandidates(projectPath: string) {
163
- try {
164
- const existingPaths = new Map<string, string>()
165
-
166
- for (const relativePath of ICON_CANDIDATE_PATHS) {
167
- const extension = path.extname(relativePath).toLowerCase()
168
- if (getMimeType(extension) === null) continue
169
-
170
- const absolutePath = resolveCaseInsensitiveProjectPath(projectPath, relativePath)
171
- if (!absolutePath) continue
172
- existingPaths.set(relativePath, absolutePath)
173
- }
174
-
175
- return [...existingPaths.entries()]
176
- .sort((a, b) => {
177
- const pathCompare = ICON_CANDIDATE_PATHS.indexOf(a[0]) - ICON_CANDIDATE_PATHS.indexOf(b[0])
178
- if (pathCompare !== 0) return pathCompare
179
- return compareIconNames(a[0], b[0])
180
- })
181
- .map(([, absolutePath]) => absolutePath)
182
- } catch {
183
- return []
184
- }
185
- }
186
-
187
- function findAppWorkspaceIconCandidates(projectPath: string) {
188
- const appsPath = path.join(projectPath, "apps")
189
-
190
- try {
191
- const appDirectories = readdirSync(appsPath, { withFileTypes: true })
192
- .filter((entry) => entry.isDirectory())
193
- .map((entry) => entry.name)
194
- .sort((a, b) => a.localeCompare(b))
195
-
196
- const existingPaths = new Map<string, string>()
197
-
198
- for (const appDirectory of appDirectories) {
199
- for (const relativePath of ICON_CANDIDATE_PATHS) {
200
- if (relativePath.startsWith(`${PROJECT_METADATA_DIR_NAME}/`)) continue
201
- const appRelativePath = path.join("apps", appDirectory, relativePath)
202
- const extension = path.extname(appRelativePath).toLowerCase()
203
- if (getMimeType(extension) === null) continue
204
-
205
- const absolutePath = resolveCaseInsensitiveProjectPath(projectPath, appRelativePath)
206
- if (!absolutePath) continue
207
- existingPaths.set(appRelativePath, absolutePath)
208
- }
209
- }
210
-
211
- return [...existingPaths.entries()]
212
- .sort((a, b) => {
213
- const [aPath] = a
214
- const [bPath] = b
215
- const aRelative = aPath.replace(/^apps\/[^/]+\//, "")
216
- const bRelative = bPath.replace(/^apps\/[^/]+\//, "")
217
- const pathCompare = ICON_CANDIDATE_PATHS.indexOf(aRelative) - ICON_CANDIDATE_PATHS.indexOf(bRelative)
218
- if (pathCompare !== 0) return pathCompare
219
- return compareIconNames(aPath, bPath)
220
- })
221
- .map(([, absolutePath]) => absolutePath)
222
- } catch {
223
- return []
224
- }
225
- }
226
-
227
- function resolveCaseInsensitiveProjectPath(projectPath: string, relativePath: string) {
228
- const segments = relativePath.split("/").filter(Boolean)
229
- let currentPath = projectPath
230
-
231
- for (const [index, segment] of segments.entries()) {
232
- let entries: string[]
233
- try {
234
- entries = readdirSync(currentPath)
235
- } catch {
236
- return null
237
- }
238
-
239
- const matchedEntry = entries.find((entry) => entry.toLowerCase() === segment.toLowerCase())
240
- if (!matchedEntry) {
241
- return null
242
- }
243
-
244
- currentPath = path.join(currentPath, matchedEntry)
245
-
246
- try {
247
- const stat = statSync(currentPath)
248
- const isLast = index === segments.length - 1
249
- if (isLast) {
250
- return stat.isFile() ? currentPath : null
251
- }
252
- if (!stat.isDirectory()) {
253
- return null
254
- }
255
- } catch {
256
- return null
257
- }
258
- }
259
-
260
- return null
261
- }
262
-
263
- function extractDeclaredIconHref(source: string) {
264
- const htmlMatch = source.match(LINK_ICON_HTML_RE)
265
- if (htmlMatch?.[1]) return htmlMatch[1]
266
- const objMatch = source.match(LINK_ICON_OBJ_RE)
267
- if (objMatch?.[1]) return objMatch[1]
268
- return null
269
- }
270
-
271
- function resolveDeclaredIconCandidates(projectPath: string, href: string) {
272
- const cleanHref = href.replace(/^\//, "")
273
- return [
274
- path.join(projectPath, "public", cleanHref),
275
- path.join(projectPath, cleanHref),
276
- ]
277
- }
278
-
279
- function findDeclaredIconCandidates(projectPath: string) {
280
- const candidates: string[] = []
281
-
282
- for (const sourceFile of ICON_SOURCE_FILES) {
283
- const sourcePath = path.join(projectPath, sourceFile)
284
-
285
- try {
286
- const source = readFileSync(sourcePath, "utf8")
287
- const href = extractDeclaredIconHref(source)
288
- if (!href) continue
289
-
290
- for (const candidatePath of resolveDeclaredIconCandidates(projectPath, href)) {
291
- const extension = path.extname(candidatePath).toLowerCase()
292
- if (getMimeType(extension) === null) continue
293
- if (!candidates.includes(candidatePath)) {
294
- candidates.push(candidatePath)
295
- }
296
- }
297
- } catch {
298
- continue
299
- }
300
- }
301
-
302
- return candidates
303
- }
304
-
305
- function tryReadImageDataUrl(filePath: string) {
306
- try {
307
- const stat = statSync(filePath)
308
- if (!stat.isFile() || stat.size > MAX_ICON_BYTES) {
309
- return null
310
- }
311
-
312
- const extension = path.extname(filePath).toLowerCase()
313
- const mimeType = getMimeType(extension)
314
- if (!mimeType) {
315
- return null
316
- }
317
-
318
- if (extension === ".svg") {
319
- const svg = readFileSync(filePath, "utf8").trim()
320
- if (!svg.includes("<svg")) {
321
- return null
322
- }
323
-
324
- return `data:${mimeType};utf8,${encodeURIComponent(svg)}`
325
- }
326
-
327
- const image = readFileSync(filePath)
328
- return `data:${mimeType};base64,${image.toString("base64")}`
329
- } catch {
330
- return null
331
- }
332
-
333
- return null
334
- }
335
-
336
- export function resolveProjectIconDataUrl(localPath: string): string | null {
337
- const projectPath = resolveLocalPath(localPath)
338
- const candidatePaths = [
339
- ...findFixedIconCandidates(projectPath),
340
- ...findAppWorkspaceIconCandidates(projectPath),
341
- ...findDeclaredIconCandidates(projectPath),
342
- ]
343
-
344
- for (const candidatePath of candidatePaths) {
345
- const dataUrl = tryReadImageDataUrl(candidatePath)
346
- if (dataUrl) {
347
- return dataUrl
348
- }
349
- }
350
-
351
- return null
352
- }
@@ -1,10 +0,0 @@
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
- }
@@ -1,85 +0,0 @@
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
- }
@@ -1,155 +0,0 @@
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
- }