kaizenai 0.3.0 → 0.4.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 (73) hide show
  1. package/bin/kaizen +16 -5
  2. package/dist/client/app-icon-180.png +0 -0
  3. package/dist/client/app-icon-192.png +0 -0
  4. package/dist/client/app-icon-512.png +0 -0
  5. package/dist/client/app-icon-96.png +0 -0
  6. package/dist/client/apple-touch-icon.png +0 -0
  7. package/dist/client/assets/index-BR0jGwm_.css +32 -0
  8. package/dist/client/assets/index-CYqy6l6r.js +628 -0
  9. package/dist/client/favicon.png +0 -0
  10. package/dist/client/index.html +17 -10
  11. package/dist/client/manifest-dark.webmanifest +3 -3
  12. package/dist/client/manifest.webmanifest +3 -3
  13. package/dist/client/pwa-192.png +0 -0
  14. package/dist/client/pwa-512.png +0 -0
  15. package/dist/server/cli-supervisor.js +307 -0
  16. package/dist/server/cli.js +12371 -0
  17. package/package.json +7 -9
  18. package/dist/client/assets/index-BBs80KD-.js +0 -623
  19. package/dist/client/assets/index-CkCgyLNq.css +0 -32
  20. package/src/server/acp-shared.ts +0 -315
  21. package/src/server/agent.ts +0 -1159
  22. package/src/server/attachments.ts +0 -133
  23. package/src/server/backgrounds.ts +0 -74
  24. package/src/server/cli-runtime.ts +0 -375
  25. package/src/server/cli-supervisor.ts +0 -97
  26. package/src/server/cli.ts +0 -68
  27. package/src/server/codex-app-server-protocol.ts +0 -453
  28. package/src/server/codex-app-server.ts +0 -1350
  29. package/src/server/cursor-acp.ts +0 -819
  30. package/src/server/discovery.ts +0 -322
  31. package/src/server/event-store.ts +0 -1470
  32. package/src/server/events.ts +0 -252
  33. package/src/server/external-open.ts +0 -272
  34. package/src/server/gemini-acp.ts +0 -844
  35. package/src/server/gemini-cli.ts +0 -525
  36. package/src/server/generate-title.ts +0 -36
  37. package/src/server/git-manager.ts +0 -79
  38. package/src/server/git-repository.ts +0 -101
  39. package/src/server/harness-types.ts +0 -20
  40. package/src/server/keybindings.ts +0 -177
  41. package/src/server/machine-name.ts +0 -22
  42. package/src/server/paths.ts +0 -112
  43. package/src/server/process-utils.ts +0 -22
  44. package/src/server/project-icon.ts +0 -352
  45. package/src/server/project-metadata.ts +0 -10
  46. package/src/server/provider-catalog.ts +0 -85
  47. package/src/server/provider-settings.ts +0 -155
  48. package/src/server/quick-response.ts +0 -153
  49. package/src/server/read-models.ts +0 -275
  50. package/src/server/recovery.ts +0 -507
  51. package/src/server/restart.ts +0 -56
  52. package/src/server/server.ts +0 -244
  53. package/src/server/terminal-manager.ts +0 -350
  54. package/src/server/theme-settings.ts +0 -179
  55. package/src/server/update-manager.ts +0 -230
  56. package/src/server/usage/base-provider-usage.ts +0 -57
  57. package/src/server/usage/claude-usage.ts +0 -558
  58. package/src/server/usage/codex-usage.ts +0 -144
  59. package/src/server/usage/cursor-browser.ts +0 -120
  60. package/src/server/usage/cursor-cookies.ts +0 -390
  61. package/src/server/usage/cursor-usage.ts +0 -490
  62. package/src/server/usage/gemini-usage.ts +0 -24
  63. package/src/server/usage/provider-usage.ts +0 -61
  64. package/src/server/usage/test-helpers.ts +0 -9
  65. package/src/server/usage/types.ts +0 -54
  66. package/src/server/usage/utils.ts +0 -325
  67. package/src/server/ws-router.ts +0 -742
  68. package/src/shared/branding.ts +0 -83
  69. package/src/shared/dev-ports.ts +0 -43
  70. package/src/shared/ports.ts +0 -2
  71. package/src/shared/protocol.ts +0 -156
  72. package/src/shared/tools.ts +0 -251
  73. 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
- }