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,153 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk"
2
+ import { CodexAppServerManager } from "./codex-app-server"
3
+
4
+ const CLAUDE_STRUCTURED_TIMEOUT_MS = 5_000
5
+
6
+ type JsonSchema = {
7
+ type: "object"
8
+ properties: Record<string, unknown>
9
+ required?: readonly string[]
10
+ additionalProperties?: boolean
11
+ }
12
+
13
+ export interface StructuredQuickResponseArgs<T> {
14
+ cwd: string
15
+ task: string
16
+ prompt: string
17
+ schema: JsonSchema
18
+ parse: (value: unknown) => T | null
19
+ }
20
+
21
+ interface QuickResponseAdapterArgs {
22
+ codexManager?: CodexAppServerManager
23
+ runClaudeStructured?: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
24
+ runCodexStructured?: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
25
+ }
26
+
27
+ function parseJsonText(value: string): unknown | null {
28
+ const trimmed = value.trim()
29
+ if (!trimmed) return null
30
+
31
+ const candidates = [trimmed]
32
+ const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)
33
+ if (fencedMatch?.[1]) {
34
+ candidates.unshift(fencedMatch[1].trim())
35
+ }
36
+
37
+ for (const candidate of candidates) {
38
+ try {
39
+ return JSON.parse(candidate)
40
+ } catch {
41
+ continue
42
+ }
43
+ }
44
+
45
+ return null
46
+ }
47
+
48
+ async function runClaudeStructured(args: Omit<StructuredQuickResponseArgs<unknown>, "parse">): Promise<unknown | null> {
49
+ const q = query({
50
+ prompt: args.prompt,
51
+ options: {
52
+ model: "haiku",
53
+ tools: [],
54
+ systemPrompt: "",
55
+ effort: "low",
56
+ permissionMode: "bypassPermissions",
57
+ outputFormat: {
58
+ type: "json_schema",
59
+ schema: args.schema,
60
+ },
61
+ env: { ...process.env },
62
+ },
63
+ })
64
+
65
+ try {
66
+ const result = await Promise.race<unknown | null>([
67
+ (async () => {
68
+ for await (const message of q) {
69
+ if ("result" in message) {
70
+ return (message as Record<string, unknown>).structured_output ?? null
71
+ }
72
+ }
73
+ return null
74
+ })(),
75
+ new Promise<null>((_, reject) => {
76
+ setTimeout(() => {
77
+ reject(new Error(`Claude structured response timed out after ${CLAUDE_STRUCTURED_TIMEOUT_MS}ms`))
78
+ }, CLAUDE_STRUCTURED_TIMEOUT_MS)
79
+ }),
80
+ ])
81
+
82
+ return result
83
+ } catch (error) {
84
+ return null
85
+ } finally {
86
+ try {
87
+ q.close()
88
+ } catch {
89
+ // Ignore close failures on timed-out or failed quick responses.
90
+ }
91
+ }
92
+ }
93
+
94
+ async function runCodexStructured(
95
+ codexManager: CodexAppServerManager,
96
+ args: Omit<StructuredQuickResponseArgs<unknown>, "parse">
97
+ ): Promise<unknown | null> {
98
+ const response = await codexManager.generateStructured({
99
+ cwd: args.cwd,
100
+ prompt: `${args.prompt}\n\nReturn JSON only that matches this schema:\n${JSON.stringify(args.schema, null, 2)}`,
101
+ })
102
+ if (typeof response !== "string") return null
103
+ return parseJsonText(response)
104
+ }
105
+
106
+ export class QuickResponseAdapter {
107
+ private readonly codexManager: CodexAppServerManager
108
+ private readonly runClaudeStructured: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
109
+ private readonly runCodexStructured: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
110
+
111
+ constructor(args: QuickResponseAdapterArgs = {}) {
112
+ this.codexManager = args.codexManager ?? new CodexAppServerManager()
113
+ this.runClaudeStructured = args.runClaudeStructured ?? runClaudeStructured
114
+ this.runCodexStructured = args.runCodexStructured ?? ((structuredArgs) =>
115
+ runCodexStructured(this.codexManager, structuredArgs))
116
+ }
117
+ async generateStructured<T>(args: StructuredQuickResponseArgs<T>): Promise<T | null> {
118
+ const request = {
119
+ cwd: args.cwd,
120
+ task: args.task,
121
+ prompt: args.prompt,
122
+ schema: args.schema,
123
+ }
124
+
125
+ const claudeResult = await this.tryProvider("claude", args.task, args.parse, () => this.runClaudeStructured(request))
126
+ if (claudeResult !== null) return claudeResult
127
+
128
+ return await this.tryProvider("codex", args.task, args.parse, () => this.runCodexStructured(request))
129
+ }
130
+
131
+ private async tryProvider<T>(
132
+ provider: "claude" | "codex",
133
+ task: string,
134
+ parse: (value: unknown) => T | null,
135
+ run: () => Promise<unknown | null>
136
+ ): Promise<T | null> {
137
+ try {
138
+ const result = await run()
139
+ if (result === null) {
140
+ return null
141
+ }
142
+
143
+ const parsed = parse(result)
144
+ if (parsed === null) {
145
+ return null
146
+ }
147
+
148
+ return parsed
149
+ } catch (error) {
150
+ return null
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,275 @@
1
+ import path from "node:path"
2
+ import { existsSync, readdirSync, statSync } from "node:fs"
3
+ import { homedir } from "node:os"
4
+ import type {
5
+ ChatPendingToolSnapshot,
6
+ ChatRuntime,
7
+ ChatSnapshot,
8
+ ChatUsageSnapshot,
9
+ DirectoryBrowserSnapshot,
10
+ KaizenStatus,
11
+ LocalProjectsSnapshot,
12
+ ProviderSettingsSnapshot,
13
+ ProviderUsageMap,
14
+ SuggestedProjectFolder,
15
+ SidebarChatRow,
16
+ SidebarData,
17
+ SidebarFeatureRow,
18
+ SidebarProjectGroup,
19
+ } from "../shared/types"
20
+ import type { ChatRecord, StoreState } from "./events"
21
+ import { getDefaultDirectoryRoot, resolveLocalPath } from "./paths"
22
+ import { getProviderInactiveMessage, getSelectableProviders } from "../shared/types"
23
+ import { resolveProjectIconDataUrl } from "./project-icon"
24
+
25
+ function deriveLastChatModel(chatId: string, getMessages: (chatId: string) => ChatSnapshot["messages"]): string | null {
26
+ const entries = getMessages(chatId)
27
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
28
+ const entry = entries[index]
29
+ if (entry.kind === "system_init") {
30
+ return entry.model
31
+ }
32
+ }
33
+ return null
34
+ }
35
+
36
+ export function deriveStatus(chat: ChatRecord, activeStatus?: KaizenStatus): KaizenStatus {
37
+ if (activeStatus) return activeStatus
38
+ if (chat.lastTurnOutcome === "failed") return "failed"
39
+ return "idle"
40
+ }
41
+
42
+ export function deriveSidebarData(
43
+ state: StoreState,
44
+ activeStatuses: Map<string, KaizenStatus>,
45
+ providerUsage?: ProviderUsageMap
46
+ ): SidebarData {
47
+ const iconDataUrlByPath = new Map<string, string | null>()
48
+ const getProjectIconDataUrl = (localPath: string) => {
49
+ const normalizedPath = resolveLocalPath(localPath)
50
+ if (!iconDataUrlByPath.has(normalizedPath)) {
51
+ iconDataUrlByPath.set(normalizedPath, resolveProjectIconDataUrl(normalizedPath))
52
+ }
53
+ return iconDataUrlByPath.get(normalizedPath) ?? null
54
+ }
55
+
56
+ const projects = [...state.projectsById.values()]
57
+ .filter((project) => !project.deletedAt && !state.hiddenProjectKeys.has(project.repoKey))
58
+ .sort((a, b) => b.updatedAt - a.updatedAt)
59
+
60
+ const projectGroups: SidebarProjectGroup[] = projects.map((project) => {
61
+ const chats: SidebarChatRow[] = [...state.chatsById.values()]
62
+ .filter((chat) => chat.projectId === project.id && !chat.deletedAt)
63
+ .sort((a, b) => (b.lastMessageAt ?? b.updatedAt) - (a.lastMessageAt ?? a.updatedAt))
64
+ .map((chat) => ({
65
+ _id: chat.id,
66
+ _creationTime: chat.createdAt,
67
+ chatId: chat.id,
68
+ title: chat.title,
69
+ status: deriveStatus(chat, activeStatuses.get(chat.id)),
70
+ localPath: project.localPath,
71
+ provider: chat.provider,
72
+ lastMessageAt: chat.lastMessageAt,
73
+ hasAutomation: false,
74
+ featureId: chat.featureId ?? null,
75
+ }))
76
+
77
+ const features: SidebarFeatureRow[] = [...state.featuresById.values()]
78
+ .filter((feature) => feature.projectId === project.id && !feature.deletedAt)
79
+ .sort((a, b) => {
80
+ const aDone = a.stage === "done" ? 1 : 0
81
+ const bDone = b.stage === "done" ? 1 : 0
82
+ if (aDone !== bDone) return aDone - bDone
83
+ if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
84
+ return b.updatedAt - a.updatedAt
85
+ })
86
+ .map((feature) => ({
87
+ featureId: feature.id,
88
+ title: feature.title,
89
+ description: feature.description,
90
+ browserState: feature.browserState,
91
+ stage: feature.stage,
92
+ sortOrder: feature.sortOrder,
93
+ directoryRelativePath: feature.directoryRelativePath,
94
+ overviewRelativePath: feature.overviewRelativePath,
95
+ updatedAt: feature.updatedAt,
96
+ chats: chats.filter((chat) => chat.featureId === feature.id),
97
+ }))
98
+
99
+ return {
100
+ groupKey: project.id,
101
+ title: project.title,
102
+ localPath: project.localPath,
103
+ iconDataUrl: getProjectIconDataUrl(project.localPath),
104
+ browserState: project.browserState,
105
+ generalChatsBrowserState: project.generalChatsBrowserState,
106
+ features,
107
+ generalChats: chats.filter((chat) => !chat.featureId),
108
+ }
109
+ })
110
+
111
+ return { projectGroups, providerUsage }
112
+ }
113
+
114
+ export function deriveLocalProjectsSnapshot(
115
+ state: StoreState,
116
+ discoveredProjects: Array<{ repoKey: string; localPath: string; worktreePaths?: string[]; title: string; modifiedAt: number }>,
117
+ machineName: string
118
+ ): LocalProjectsSnapshot {
119
+ const projects = new Map<string, LocalProjectsSnapshot["projects"][number]>()
120
+ const iconDataUrlByPath = new Map<string, string | null>()
121
+ const getProjectIconDataUrl = (localPath: string) => {
122
+ const normalizedPath = resolveLocalPath(localPath)
123
+ if (!iconDataUrlByPath.has(normalizedPath)) {
124
+ iconDataUrlByPath.set(normalizedPath, resolveProjectIconDataUrl(normalizedPath))
125
+ }
126
+ return iconDataUrlByPath.get(normalizedPath) ?? null
127
+ }
128
+
129
+ for (const project of discoveredProjects) {
130
+ const normalizedPath = resolveLocalPath(project.localPath)
131
+ projects.set(project.repoKey, {
132
+ localPath: normalizedPath,
133
+ title: project.title,
134
+ iconDataUrl: getProjectIconDataUrl(normalizedPath),
135
+ source: "discovered",
136
+ lastOpenedAt: project.modifiedAt,
137
+ chatCount: 0,
138
+ })
139
+ }
140
+
141
+ for (const project of [...state.projectsById.values()].filter((entry) => !entry.deletedAt && !state.hiddenProjectKeys.has(entry.repoKey))) {
142
+ const chats = [...state.chatsById.values()].filter((chat) => chat.projectId === project.id && !chat.deletedAt)
143
+ const lastOpenedAt = chats.reduce(
144
+ (latest, chat) => Math.max(latest, chat.lastMessageAt ?? chat.updatedAt ?? 0),
145
+ project.updatedAt
146
+ )
147
+
148
+ projects.set(project.repoKey, {
149
+ localPath: project.localPath,
150
+ title: project.title,
151
+ iconDataUrl: getProjectIconDataUrl(project.localPath),
152
+ source: "saved",
153
+ lastOpenedAt,
154
+ chatCount: chats.length,
155
+ })
156
+ }
157
+
158
+ const suggestedFolders = deriveSuggestedProjectFolders(state, discoveredProjects)
159
+ const rootDirectory = deriveRootDirectorySnapshot()
160
+
161
+ return {
162
+ machine: {
163
+ id: "local",
164
+ displayName: machineName,
165
+ },
166
+ projects: [...projects.values()].sort((a, b) => (b.lastOpenedAt ?? 0) - (a.lastOpenedAt ?? 0)),
167
+ suggestedFolders,
168
+ rootDirectory,
169
+ }
170
+ }
171
+
172
+ function deriveRootDirectorySnapshot(): DirectoryBrowserSnapshot | null {
173
+ const currentPath = getDefaultDirectoryRoot()
174
+
175
+ try {
176
+ const entries = readdirSync(currentPath, { withFileTypes: true })
177
+ .filter((entry) => entry.isDirectory())
178
+ .map((entry) => ({
179
+ name: entry.name,
180
+ localPath: path.join(currentPath, entry.name),
181
+ }))
182
+ .sort((a, b) => a.name.localeCompare(b.name))
183
+
184
+ return {
185
+ currentPath,
186
+ parentPath: path.dirname(currentPath) === currentPath ? null : path.dirname(currentPath),
187
+ roots: [{ name: currentPath, localPath: currentPath }],
188
+ entries,
189
+ }
190
+ } catch {
191
+ return null
192
+ }
193
+ }
194
+
195
+ function deriveSuggestedProjectFolders(
196
+ state: StoreState,
197
+ discoveredProjects: Array<{ localPath: string }>
198
+ ): SuggestedProjectFolder[] {
199
+ const candidates = new Map<string, string>()
200
+ const home = homedir()
201
+
202
+ const addCandidate = (label: string, candidatePath: string | null | undefined) => {
203
+ if (!candidatePath) return
204
+ const resolvedPath = resolveLocalPath(candidatePath)
205
+ if (candidates.has(resolvedPath)) return
206
+ if (!existsSync(resolvedPath)) return
207
+ try {
208
+ if (!statSync(resolvedPath).isDirectory()) return
209
+ } catch {
210
+ return
211
+ }
212
+ candidates.set(resolvedPath, label)
213
+ }
214
+
215
+ addCandidate("Home", home)
216
+ addCandidate("Documents", path.join(home, "Documents"))
217
+ addCandidate("Projects", path.join(home, "projects"))
218
+ addCandidate("Projects", path.join(home, "Projects"))
219
+ addCandidate("Downloads", path.join(home, "Downloads"))
220
+ addCandidate("Desktop", path.join(home, "Desktop"))
221
+ addCandidate("Current Project", process.cwd())
222
+ addCandidate("Current Project Parent", path.dirname(process.cwd()))
223
+
224
+ for (const project of discoveredProjects) {
225
+ addCandidate(`Nearby: ${path.basename(project.localPath) || project.localPath}`, project.localPath)
226
+ addCandidate(`Parent: ${path.basename(path.dirname(project.localPath)) || path.dirname(project.localPath)}`, path.dirname(project.localPath))
227
+ }
228
+
229
+ for (const project of state.projectsById.values()) {
230
+ if (project.deletedAt) continue
231
+ addCandidate(`Saved: ${project.title}`, project.localPath)
232
+ addCandidate(`Parent: ${path.basename(path.dirname(project.localPath)) || path.dirname(project.localPath)}`, path.dirname(project.localPath))
233
+ }
234
+
235
+ return [...candidates.entries()]
236
+ .map(([localPath, label]) => ({ localPath, label }))
237
+ .slice(0, 12)
238
+ }
239
+
240
+ export function deriveChatSnapshot(
241
+ state: StoreState,
242
+ activeStatuses: Map<string, KaizenStatus>,
243
+ chatId: string,
244
+ getMessages: (chatId: string) => ChatSnapshot["messages"],
245
+ pendingTool: ChatPendingToolSnapshot | null = null,
246
+ usage: ChatUsageSnapshot | null = null,
247
+ providerSettings?: ProviderSettingsSnapshot["settings"]
248
+ ): ChatSnapshot | null {
249
+ const chat = state.chatsById.get(chatId)
250
+ if (!chat || chat.deletedAt) return null
251
+ const project = state.projectsById.get(chat.projectId)
252
+ if (!project || project.deletedAt) return null
253
+
254
+ const runtime: ChatRuntime = {
255
+ chatId: chat.id,
256
+ projectId: project.id,
257
+ localPath: project.localPath,
258
+ title: chat.title,
259
+ status: deriveStatus(chat, activeStatuses.get(chat.id)),
260
+ provider: chat.provider,
261
+ model: deriveLastChatModel(chat.id, getMessages),
262
+ planMode: chat.planMode,
263
+ sessionToken: chat.sessionToken,
264
+ pendingTool,
265
+ inactiveProviderMessage: chat.provider ? getProviderInactiveMessage(chat.provider, providerSettings) : null,
266
+ }
267
+
268
+ return {
269
+ runtime,
270
+ messages: getMessages(chat.id),
271
+ usage,
272
+ availableProviders: getSelectableProviders(providerSettings),
273
+ providerSettings,
274
+ }
275
+ }