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.
Files changed (74) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +246 -0
  3. package/bin/kaizen +15 -0
  4. package/dist/client/apple-touch-icon.png +0 -0
  5. package/dist/client/assets/index-D-ORCGrq.js +603 -0
  6. package/dist/client/assets/index-r28mcHqz.css +32 -0
  7. package/dist/client/favicon.png +0 -0
  8. package/dist/client/fonts/body-medium.woff2 +0 -0
  9. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  10. package/dist/client/fonts/body-regular.woff2 +0 -0
  11. package/dist/client/fonts/body-semibold.woff2 +0 -0
  12. package/dist/client/index.html +22 -0
  13. package/dist/client/manifest-dark.webmanifest +24 -0
  14. package/dist/client/manifest.webmanifest +24 -0
  15. package/dist/client/pwa-192.png +0 -0
  16. package/dist/client/pwa-512.png +0 -0
  17. package/dist/client/pwa-icon.svg +15 -0
  18. package/dist/client/pwa-splash.png +0 -0
  19. package/dist/client/pwa-splash.svg +15 -0
  20. package/package.json +103 -0
  21. package/src/server/acp-shared.ts +315 -0
  22. package/src/server/agent.ts +1120 -0
  23. package/src/server/attachments.ts +133 -0
  24. package/src/server/backgrounds.ts +74 -0
  25. package/src/server/cli-runtime.ts +333 -0
  26. package/src/server/cli-supervisor.ts +81 -0
  27. package/src/server/cli.ts +68 -0
  28. package/src/server/codex-app-server-protocol.ts +453 -0
  29. package/src/server/codex-app-server.ts +1350 -0
  30. package/src/server/cursor-acp.ts +819 -0
  31. package/src/server/discovery.ts +322 -0
  32. package/src/server/event-store.ts +1369 -0
  33. package/src/server/events.ts +244 -0
  34. package/src/server/external-open.ts +272 -0
  35. package/src/server/gemini-acp.ts +844 -0
  36. package/src/server/gemini-cli.ts +525 -0
  37. package/src/server/generate-title.ts +36 -0
  38. package/src/server/git-manager.ts +79 -0
  39. package/src/server/git-repository.ts +101 -0
  40. package/src/server/harness-types.ts +20 -0
  41. package/src/server/keybindings.ts +177 -0
  42. package/src/server/machine-name.ts +22 -0
  43. package/src/server/paths.ts +112 -0
  44. package/src/server/process-utils.ts +22 -0
  45. package/src/server/project-icon.ts +344 -0
  46. package/src/server/project-metadata.ts +10 -0
  47. package/src/server/provider-catalog.ts +85 -0
  48. package/src/server/provider-settings.ts +155 -0
  49. package/src/server/quick-response.ts +153 -0
  50. package/src/server/read-models.ts +275 -0
  51. package/src/server/recovery.ts +507 -0
  52. package/src/server/restart.ts +30 -0
  53. package/src/server/server.ts +244 -0
  54. package/src/server/terminal-manager.ts +350 -0
  55. package/src/server/theme-settings.ts +179 -0
  56. package/src/server/update-manager.ts +230 -0
  57. package/src/server/usage/base-provider-usage.ts +57 -0
  58. package/src/server/usage/claude-usage.ts +558 -0
  59. package/src/server/usage/codex-usage.ts +144 -0
  60. package/src/server/usage/cursor-browser.ts +120 -0
  61. package/src/server/usage/cursor-cookies.ts +390 -0
  62. package/src/server/usage/cursor-usage.ts +490 -0
  63. package/src/server/usage/gemini-usage.ts +24 -0
  64. package/src/server/usage/provider-usage.ts +61 -0
  65. package/src/server/usage/test-helpers.ts +9 -0
  66. package/src/server/usage/types.ts +54 -0
  67. package/src/server/usage/utils.ts +325 -0
  68. package/src/server/ws-router.ts +717 -0
  69. package/src/shared/branding.ts +83 -0
  70. package/src/shared/dev-ports.ts +43 -0
  71. package/src/shared/ports.ts +2 -0
  72. package/src/shared/protocol.ts +152 -0
  73. package/src/shared/tools.ts +251 -0
  74. package/src/shared/types.ts +1028 -0
@@ -0,0 +1,179 @@
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 { getThemeSettingsFilePath, LOG_PREFIX } from "../shared/branding"
6
+ import {
7
+ DEFAULT_THEME_SETTINGS,
8
+ type ColorTheme,
9
+ type CustomAppearance,
10
+ type ThemePreference,
11
+ type ThemeSettingsSnapshot,
12
+ } from "../shared/types"
13
+
14
+ const VALID_THEME_PREFERENCES: ThemePreference[] = ["light", "dark", "system", "custom"]
15
+ const VALID_COLOR_THEMES: ColorTheme[] = ["default", "tokyo-night", "catppuccin", "dracula", "nord", "everforest", "rose-pine"]
16
+ const VALID_CUSTOM_APPEARANCES: CustomAppearance[] = ["light", "dark", "system"]
17
+
18
+ export class ThemeSettingsManager {
19
+ readonly filePath: string
20
+ private watcher: FSWatcher | null = null
21
+ private snapshot: ThemeSettingsSnapshot
22
+ private readonly listeners = new Set<(snapshot: ThemeSettingsSnapshot) => void>()
23
+
24
+ constructor(filePath = getThemeSettingsFilePath(homedir())) {
25
+ this.filePath = filePath
26
+ this.snapshot = createDefaultSnapshot(this.filePath)
27
+ }
28
+
29
+ async initialize() {
30
+ await mkdir(path.dirname(this.filePath), { recursive: true })
31
+ const file = Bun.file(this.filePath)
32
+ if (!(await file.exists())) {
33
+ await writeFile(this.filePath, `${JSON.stringify(DEFAULT_THEME_SETTINGS, null, 2)}\n`, "utf8")
34
+ }
35
+ await this.reload()
36
+ this.startWatching()
37
+ }
38
+
39
+ dispose() {
40
+ this.watcher?.close()
41
+ this.watcher = null
42
+ this.listeners.clear()
43
+ }
44
+
45
+ getSnapshot() {
46
+ return this.snapshot
47
+ }
48
+
49
+ onChange(listener: (snapshot: ThemeSettingsSnapshot) => void) {
50
+ this.listeners.add(listener)
51
+ return () => {
52
+ this.listeners.delete(listener)
53
+ }
54
+ }
55
+
56
+ async reload() {
57
+ const nextSnapshot = await readThemeSettingsSnapshot(this.filePath)
58
+ this.setSnapshot(nextSnapshot)
59
+ }
60
+
61
+ async write(settings: ThemeSettingsSnapshot["settings"]) {
62
+ const nextSnapshot = normalizeThemeSettings(settings, this.filePath)
63
+ await mkdir(path.dirname(this.filePath), { recursive: true })
64
+ await writeFile(this.filePath, `${JSON.stringify(nextSnapshot.settings, null, 2)}\n`, "utf8")
65
+ this.setSnapshot(nextSnapshot)
66
+ return nextSnapshot
67
+ }
68
+
69
+ private setSnapshot(snapshot: ThemeSettingsSnapshot) {
70
+ this.snapshot = snapshot
71
+ for (const listener of this.listeners) {
72
+ listener(snapshot)
73
+ }
74
+ }
75
+
76
+ private startWatching() {
77
+ this.watcher?.close()
78
+ try {
79
+ this.watcher = watch(path.dirname(this.filePath), { persistent: false }, (_eventType, filename) => {
80
+ if (filename && filename !== path.basename(this.filePath)) {
81
+ return
82
+ }
83
+ void this.reload().catch((error: unknown) => {
84
+ console.warn(`${LOG_PREFIX} Failed to reload theme settings:`, error)
85
+ })
86
+ })
87
+ } catch (error) {
88
+ console.warn(`${LOG_PREFIX} Failed to watch theme settings file:`, error)
89
+ this.watcher = null
90
+ }
91
+ }
92
+ }
93
+
94
+ export async function readThemeSettingsSnapshot(filePath: string): Promise<ThemeSettingsSnapshot> {
95
+ try {
96
+ const text = await readFile(filePath, "utf8")
97
+ if (!text.trim()) {
98
+ return createDefaultSnapshot(filePath, "Theme settings file was empty. Using defaults.")
99
+ }
100
+ const parsed = JSON.parse(text) as Partial<ThemeSettingsSnapshot["settings"]>
101
+ return normalizeThemeSettings(parsed, filePath)
102
+ } catch (error) {
103
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
104
+ return createDefaultSnapshot(filePath)
105
+ }
106
+ if (error instanceof SyntaxError) {
107
+ return createDefaultSnapshot(filePath, "Theme settings file is invalid JSON. Using defaults.")
108
+ }
109
+ throw error
110
+ }
111
+ }
112
+
113
+ export function normalizeThemeSettings(
114
+ value: Partial<ThemeSettingsSnapshot["settings"]> | null | undefined,
115
+ filePath = getThemeSettingsFilePath(homedir())
116
+ ): ThemeSettingsSnapshot {
117
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}
118
+
119
+ const themePreference: ThemePreference = VALID_THEME_PREFERENCES.includes(source.themePreference as ThemePreference)
120
+ ? source.themePreference as ThemePreference
121
+ : DEFAULT_THEME_SETTINGS.themePreference
122
+
123
+ const colorTheme: ColorTheme = VALID_COLOR_THEMES.includes(source.colorTheme as ColorTheme)
124
+ ? source.colorTheme as ColorTheme
125
+ : DEFAULT_THEME_SETTINGS.colorTheme
126
+
127
+ const customAppearance: CustomAppearance = VALID_CUSTOM_APPEARANCES.includes(source.customAppearance as CustomAppearance)
128
+ ? source.customAppearance as CustomAppearance
129
+ : DEFAULT_THEME_SETTINGS.customAppearance
130
+
131
+ const backgroundImage: string | null =
132
+ typeof source.backgroundImage === "string" ? source.backgroundImage : DEFAULT_THEME_SETTINGS.backgroundImage
133
+
134
+ const backgroundOpacity: number =
135
+ typeof source.backgroundOpacity === "number" && source.backgroundOpacity >= 0 && source.backgroundOpacity <= 1
136
+ ? source.backgroundOpacity
137
+ : DEFAULT_THEME_SETTINGS.backgroundOpacity
138
+
139
+ const backgroundBlur: number =
140
+ typeof source.backgroundBlur === "number" && source.backgroundBlur >= 0
141
+ ? source.backgroundBlur
142
+ : DEFAULT_THEME_SETTINGS.backgroundBlur
143
+
144
+ const showProjectIconsInSidebar: boolean =
145
+ typeof source.showProjectIconsInSidebar === "boolean"
146
+ ? source.showProjectIconsInSidebar
147
+ : DEFAULT_THEME_SETTINGS.showProjectIconsInSidebar
148
+
149
+ return {
150
+ settings: {
151
+ themePreference,
152
+ colorTheme,
153
+ customAppearance,
154
+ backgroundImage,
155
+ backgroundOpacity,
156
+ backgroundBlur,
157
+ showProjectIconsInSidebar,
158
+ },
159
+ warning: null,
160
+ filePathDisplay: formatDisplayPath(filePath),
161
+ }
162
+ }
163
+
164
+ function createDefaultSnapshot(filePath: string, warning: string | null = null): ThemeSettingsSnapshot {
165
+ return {
166
+ settings: { ...DEFAULT_THEME_SETTINGS },
167
+ warning,
168
+ filePathDisplay: formatDisplayPath(filePath),
169
+ }
170
+ }
171
+
172
+ function formatDisplayPath(filePath: string) {
173
+ const homePath = homedir()
174
+ if (filePath === homePath) return "~"
175
+ if (filePath.startsWith(`${homePath}${path.sep}`)) {
176
+ return `~${filePath.slice(homePath.length)}`
177
+ }
178
+ return filePath
179
+ }
@@ -0,0 +1,230 @@
1
+ import type { UpdateInstallResult, UpdateSnapshot } from "../shared/types"
2
+ import { PACKAGE_NAME } from "../shared/branding"
3
+ import { compareVersions, type UpdateInstallAttemptResult } from "./cli-runtime"
4
+
5
+ const UPDATE_CACHE_TTL_MS = 5 * 60 * 1000
6
+
7
+ export interface UpdateManagerDeps {
8
+ currentVersion: string
9
+ fetchLatestVersion: (packageName: string) => Promise<string>
10
+ installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
11
+ devMode?: boolean
12
+ }
13
+
14
+ export class UpdateManager {
15
+ private readonly deps: UpdateManagerDeps
16
+ private readonly listeners = new Set<(snapshot: UpdateSnapshot) => void>()
17
+ private snapshot: UpdateSnapshot
18
+ private checkPromise: Promise<UpdateSnapshot> | null = null
19
+ private installPromise: Promise<UpdateInstallResult> | null = null
20
+
21
+ constructor(deps: UpdateManagerDeps) {
22
+ this.deps = deps
23
+ this.snapshot = {
24
+ currentVersion: deps.currentVersion,
25
+ latestVersion: deps.devMode ? `${deps.currentVersion}-dev` : null,
26
+ status: deps.devMode ? "available" : "idle",
27
+ updateAvailable: Boolean(deps.devMode),
28
+ lastCheckedAt: deps.devMode ? Date.now() : null,
29
+ error: null,
30
+ installAction: "restart",
31
+ }
32
+ }
33
+
34
+ getSnapshot() {
35
+ return this.snapshot
36
+ }
37
+
38
+ onChange(listener: (snapshot: UpdateSnapshot) => void) {
39
+ this.listeners.add(listener)
40
+ return () => {
41
+ this.listeners.delete(listener)
42
+ }
43
+ }
44
+
45
+ async checkForUpdates(options: { force?: boolean } = {}) {
46
+ if (this.deps.devMode) {
47
+ return this.snapshot
48
+ }
49
+
50
+ if (this.snapshot.status === "updating" || this.snapshot.status === "restart_pending") {
51
+ return this.snapshot
52
+ }
53
+
54
+ if (this.checkPromise) {
55
+ return this.checkPromise
56
+ }
57
+
58
+ if (!options.force && this.snapshot.lastCheckedAt && Date.now() - this.snapshot.lastCheckedAt < UPDATE_CACHE_TTL_MS) {
59
+ return this.snapshot
60
+ }
61
+
62
+ this.setSnapshot({
63
+ ...this.snapshot,
64
+ status: "checking",
65
+ error: null,
66
+ })
67
+
68
+ const checkPromise = this.runCheck()
69
+ this.checkPromise = checkPromise
70
+
71
+ try {
72
+ return await checkPromise
73
+ } finally {
74
+ if (this.checkPromise === checkPromise) {
75
+ this.checkPromise = null
76
+ }
77
+ }
78
+ }
79
+
80
+ async installUpdate(): Promise<UpdateInstallResult> {
81
+ if (this.deps.devMode) {
82
+ this.setSnapshot({
83
+ ...this.snapshot,
84
+ status: "updating",
85
+ error: null,
86
+ })
87
+
88
+ this.setSnapshot({
89
+ ...this.snapshot,
90
+ status: "restart_pending",
91
+ updateAvailable: false,
92
+ error: null,
93
+ })
94
+
95
+ return {
96
+ ok: true,
97
+ action: "restart",
98
+ errorCode: null,
99
+ userTitle: null,
100
+ userMessage: null,
101
+ }
102
+ }
103
+
104
+ if (this.snapshot.status === "updating" || this.snapshot.status === "restart_pending") {
105
+ return {
106
+ ok: this.snapshot.updateAvailable,
107
+ action: "restart",
108
+ errorCode: null,
109
+ userTitle: null,
110
+ userMessage: null,
111
+ }
112
+ }
113
+
114
+ if (this.installPromise) {
115
+ return this.installPromise
116
+ }
117
+
118
+ const installPromise = this.runInstall()
119
+ this.installPromise = installPromise
120
+
121
+ try {
122
+ return await installPromise
123
+ } finally {
124
+ if (this.installPromise === installPromise) {
125
+ this.installPromise = null
126
+ }
127
+ }
128
+ }
129
+
130
+ private async runCheck() {
131
+ try {
132
+ const latestVersion = await this.deps.fetchLatestVersion(PACKAGE_NAME)
133
+ const updateAvailable = compareVersions(this.snapshot.currentVersion, latestVersion) < 0
134
+ const nextSnapshot: UpdateSnapshot = {
135
+ ...this.snapshot,
136
+ latestVersion,
137
+ updateAvailable,
138
+ status: updateAvailable ? "available" : "up_to_date",
139
+ lastCheckedAt: Date.now(),
140
+ error: null,
141
+ }
142
+ this.setSnapshot(nextSnapshot)
143
+ return nextSnapshot
144
+ } catch (error) {
145
+ const nextSnapshot: UpdateSnapshot = {
146
+ ...this.snapshot,
147
+ status: "error",
148
+ lastCheckedAt: Date.now(),
149
+ error: error instanceof Error ? error.message : String(error),
150
+ }
151
+ this.setSnapshot(nextSnapshot)
152
+ return nextSnapshot
153
+ }
154
+ }
155
+
156
+ private async runInstall(): Promise<UpdateInstallResult> {
157
+ if (!this.snapshot.updateAvailable) {
158
+ const snapshot = await this.checkForUpdates({ force: true })
159
+ if (!snapshot.updateAvailable) {
160
+ return {
161
+ ok: false,
162
+ action: "restart",
163
+ errorCode: null,
164
+ userTitle: null,
165
+ userMessage: null,
166
+ }
167
+ }
168
+ }
169
+
170
+ this.setSnapshot({
171
+ ...this.snapshot,
172
+ status: "updating",
173
+ error: null,
174
+ })
175
+
176
+ const targetVersion = this.snapshot.latestVersion
177
+ if (!targetVersion) {
178
+ this.setSnapshot({
179
+ ...this.snapshot,
180
+ status: "error",
181
+ error: "Unable to determine which version to install.",
182
+ })
183
+ return {
184
+ ok: false,
185
+ action: "restart",
186
+ errorCode: "install_failed",
187
+ userTitle: "Update failed",
188
+ userMessage: "Kaizen could not determine which version to install.",
189
+ }
190
+ }
191
+
192
+ const installed = this.deps.installVersion(PACKAGE_NAME, targetVersion)
193
+ if (!installed.ok) {
194
+ this.setSnapshot({
195
+ ...this.snapshot,
196
+ status: "error",
197
+ error: installed.userMessage ?? "Unable to install the latest version.",
198
+ })
199
+ return {
200
+ ok: false,
201
+ action: "restart",
202
+ errorCode: installed.errorCode,
203
+ userTitle: installed.userTitle,
204
+ userMessage: installed.userMessage,
205
+ }
206
+ }
207
+
208
+ this.setSnapshot({
209
+ ...this.snapshot,
210
+ currentVersion: this.snapshot.latestVersion ?? this.snapshot.currentVersion,
211
+ status: "restart_pending",
212
+ updateAvailable: false,
213
+ error: null,
214
+ })
215
+ return {
216
+ ok: true,
217
+ action: "restart",
218
+ errorCode: null,
219
+ userTitle: null,
220
+ userMessage: null,
221
+ }
222
+ }
223
+
224
+ private setSnapshot(snapshot: UpdateSnapshot) {
225
+ this.snapshot = snapshot
226
+ for (const listener of this.listeners) {
227
+ listener(snapshot)
228
+ }
229
+ }
230
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
2
+ import path from "node:path"
3
+ import type { AgentProvider, ProviderUsageEntry } from "../../shared/types"
4
+ import { PROVIDERS } from "../../shared/types"
5
+
6
+ function providerUsageRequestTimesPath(dataDir: string) {
7
+ return path.join(dataDir, "provider-usage-request-times.json")
8
+ }
9
+
10
+ function loadProviderUsageRequestTimes(dataDir: string): Partial<Record<AgentProvider, number>> {
11
+ try {
12
+ const filePath = providerUsageRequestTimesPath(dataDir)
13
+ if (!existsSync(filePath)) return {}
14
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"))
15
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}
16
+ const result: Partial<Record<AgentProvider, number>> = {}
17
+ for (const provider of PROVIDERS.map((entry) => entry.id)) {
18
+ const value = (parsed as Record<string, unknown>)[provider]
19
+ if (typeof value === "number" && Number.isFinite(value)) {
20
+ result[provider] = value
21
+ }
22
+ }
23
+ return result
24
+ } catch {
25
+ return {}
26
+ }
27
+ }
28
+
29
+ export abstract class BaseProviderUsage {
30
+ protected readonly dataDir: string
31
+
32
+ constructor(dataDir: string) {
33
+ this.dataDir = dataDir
34
+ }
35
+
36
+ protected recordRequestTime(provider: AgentProvider, at = Date.now()): void {
37
+ try {
38
+ const next = loadProviderUsageRequestTimes(this.dataDir)
39
+ next[provider] = at
40
+ writeFileSync(providerUsageRequestTimesPath(this.dataDir), JSON.stringify(next))
41
+ } catch {
42
+ // best-effort
43
+ }
44
+ }
45
+
46
+ protected readLastRequestedAt(provider: AgentProvider): number {
47
+ return loadProviderUsageRequestTimes(this.dataDir)[provider] ?? 0
48
+ }
49
+
50
+ protected shouldSkipRefresh(provider: AgentProvider, minInterval: number, force = false): boolean {
51
+ if (force) return false
52
+ return Date.now() - this.readLastRequestedAt(provider) < minInterval
53
+ }
54
+
55
+ abstract readonly provider: AgentProvider
56
+ abstract loadPersistedEntry(): ProviderUsageEntry | null
57
+ }