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,322 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import path from "node:path"
4
+ import type { AgentProvider } from "../shared/types"
5
+ import { resolveProjectRepositoryIdentity, resolveProjectWorktreePaths } from "./git-repository"
6
+ import { resolveLocalPath } from "./paths"
7
+
8
+ export interface DiscoveredProject {
9
+ repoKey: string
10
+ localPath: string
11
+ worktreePaths: string[]
12
+ title: string
13
+ modifiedAt: number
14
+ }
15
+
16
+ export interface ProviderDiscoveredProject extends DiscoveredProject {
17
+ provider: AgentProvider
18
+ }
19
+
20
+ export interface ProjectDiscoveryAdapter {
21
+ provider: AgentProvider
22
+ scan(homeDir?: string): ProviderDiscoveredProject[]
23
+ }
24
+
25
+ function resolveEncodedClaudePath(folderName: string) {
26
+ const segments = folderName.replace(/^-/, "").split("-").filter(Boolean)
27
+ let currentPath = ""
28
+ let remainingSegments = [...segments]
29
+
30
+ while (remainingSegments.length > 0) {
31
+ let found = false
32
+
33
+ for (let index = remainingSegments.length; index >= 1; index -= 1) {
34
+ const segment = remainingSegments.slice(0, index).join("-")
35
+ const candidate = `${currentPath}/${segment}`
36
+
37
+ if (existsSync(candidate)) {
38
+ currentPath = candidate
39
+ remainingSegments = remainingSegments.slice(index)
40
+ found = true
41
+ break
42
+ }
43
+ }
44
+
45
+ if (!found) {
46
+ const [head, ...tail] = remainingSegments
47
+ currentPath = `${currentPath}/${head}`
48
+ remainingSegments = tail
49
+ }
50
+ }
51
+
52
+ return currentPath || "/"
53
+ }
54
+
55
+ function normalizeExistingDirectory(localPath: string) {
56
+ try {
57
+ const normalized = resolveLocalPath(localPath)
58
+ if (!statSync(normalized).isDirectory()) {
59
+ return null
60
+ }
61
+ return normalized
62
+ } catch {
63
+ return null
64
+ }
65
+ }
66
+
67
+ function toDiscoveredProject(localPath: string, modifiedAt: number): DiscoveredProject | null {
68
+ const normalizedPath = normalizeExistingDirectory(localPath)
69
+ if (!normalizedPath) {
70
+ return null
71
+ }
72
+
73
+ const identity = resolveProjectRepositoryIdentity(normalizedPath)
74
+ return {
75
+ repoKey: identity.repoKey,
76
+ localPath: identity.localPath,
77
+ worktreePaths: identity.isGitRepo ? resolveProjectWorktreePaths(normalizedPath) : [identity.worktreePath],
78
+ title: identity.title,
79
+ modifiedAt,
80
+ }
81
+ }
82
+
83
+ function mergeDiscoveredProjects(projects: Iterable<DiscoveredProject>): DiscoveredProject[] {
84
+ const merged = new Map<string, DiscoveredProject>()
85
+
86
+ for (const project of projects) {
87
+ const existing = merged.get(project.repoKey)
88
+ if (!existing || project.modifiedAt > existing.modifiedAt) {
89
+ merged.set(project.repoKey, {
90
+ repoKey: project.repoKey,
91
+ localPath: project.localPath,
92
+ worktreePaths: [...project.worktreePaths],
93
+ title: project.title || path.basename(project.localPath) || project.localPath,
94
+ modifiedAt: project.modifiedAt,
95
+ })
96
+ continue
97
+ }
98
+
99
+ for (const worktreePath of project.worktreePaths) {
100
+ if (!existing.worktreePaths.includes(worktreePath)) {
101
+ existing.worktreePaths.push(worktreePath)
102
+ }
103
+ }
104
+
105
+ if (!existing.title && project.title) {
106
+ existing.title = project.title
107
+ }
108
+ }
109
+
110
+ return [...merged.values()].sort((a, b) => b.modifiedAt - a.modifiedAt)
111
+ }
112
+
113
+ export class ClaudeProjectDiscoveryAdapter implements ProjectDiscoveryAdapter {
114
+ readonly provider = "claude" as const
115
+
116
+ scan(homeDir: string = homedir()): ProviderDiscoveredProject[] {
117
+ const projectsDir = path.join(homeDir, ".claude", "projects")
118
+ if (!existsSync(projectsDir)) {
119
+ return []
120
+ }
121
+
122
+ const entries = readdirSync(projectsDir, { withFileTypes: true })
123
+ const projects: ProviderDiscoveredProject[] = []
124
+
125
+ for (const entry of entries) {
126
+ if (!entry.isDirectory()) continue
127
+
128
+ const resolvedPath = resolveEncodedClaudePath(entry.name)
129
+ const stat = statSync(path.join(projectsDir, entry.name))
130
+ const project = toDiscoveredProject(resolvedPath, stat.mtimeMs)
131
+ if (!project) {
132
+ continue
133
+ }
134
+
135
+ projects.push({
136
+ provider: this.provider,
137
+ ...project,
138
+ })
139
+ }
140
+
141
+ const mergedProjects = mergeDiscoveredProjects(projects).map((project) => ({
142
+ provider: this.provider,
143
+ ...project,
144
+ }))
145
+
146
+ return mergedProjects
147
+ }
148
+ }
149
+
150
+ function parseJsonRecord(line: string): Record<string, unknown> | null {
151
+ try {
152
+ const parsed = JSON.parse(line)
153
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
154
+ return null
155
+ }
156
+ return parsed as Record<string, unknown>
157
+ } catch {
158
+ return null
159
+ }
160
+ }
161
+
162
+ function readCodexSessionIndex(indexPath: string) {
163
+ const updatedAtById = new Map<string, number>()
164
+ if (!existsSync(indexPath)) {
165
+ return updatedAtById
166
+ }
167
+
168
+ for (const line of readFileSync(indexPath, "utf8").split("\n")) {
169
+ if (!line.trim()) continue
170
+ const record = parseJsonRecord(line)
171
+ if (!record) continue
172
+
173
+ const id = typeof record.id === "string" ? record.id : null
174
+ const updatedAt = typeof record.updated_at === "string" ? Date.parse(record.updated_at) : Number.NaN
175
+ if (!id || Number.isNaN(updatedAt)) continue
176
+
177
+ const existing = updatedAtById.get(id)
178
+ if (existing === undefined || updatedAt > existing) {
179
+ updatedAtById.set(id, updatedAt)
180
+ }
181
+ }
182
+
183
+ return updatedAtById
184
+ }
185
+
186
+ function collectCodexSessionFiles(directory: string): string[] {
187
+ if (!existsSync(directory)) {
188
+ return []
189
+ }
190
+
191
+ const files: string[] = []
192
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
193
+ const fullPath = path.join(directory, entry.name)
194
+ if (entry.isDirectory()) {
195
+ files.push(...collectCodexSessionFiles(fullPath))
196
+ continue
197
+ }
198
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
199
+ files.push(fullPath)
200
+ }
201
+ }
202
+ return files
203
+ }
204
+
205
+ function readCodexConfiguredProjects(configPath: string) {
206
+ const projects = new Map<string, number>()
207
+ if (!existsSync(configPath)) {
208
+ return projects
209
+ }
210
+
211
+ const configMtime = statSync(configPath).mtimeMs
212
+ for (const line of readFileSync(configPath, "utf8").split("\n")) {
213
+ const match = line.match(/^\[projects\."(.+)"\]$/)
214
+ if (!match?.[1]) continue
215
+ projects.set(match[1], configMtime)
216
+ }
217
+
218
+ return projects
219
+ }
220
+
221
+ function readCodexSessionMetadata(sessionsDir: string) {
222
+ const metadataById = new Map<string, { cwd: string; modifiedAt: number }>()
223
+
224
+ for (const sessionFile of collectCodexSessionFiles(sessionsDir)) {
225
+ const fileStat = statSync(sessionFile)
226
+ const firstLine = readFileSync(sessionFile, "utf8").split("\n", 1)[0]
227
+ if (!firstLine?.trim()) continue
228
+
229
+ const record = parseJsonRecord(firstLine)
230
+ if (!record || record.type !== "session_meta") continue
231
+
232
+ const payload = record.payload
233
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) continue
234
+
235
+ const payloadRecord = payload as Record<string, unknown>
236
+ const sessionId = typeof payloadRecord.id === "string" ? payloadRecord.id : null
237
+ const cwd = typeof payloadRecord.cwd === "string" ? payloadRecord.cwd : null
238
+ if (!sessionId || !cwd) continue
239
+
240
+ const recordTimestamp = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Number.NaN
241
+ const payloadTimestamp = typeof payloadRecord.timestamp === "string" ? Date.parse(payloadRecord.timestamp) : Number.NaN
242
+ const modifiedAt = [recordTimestamp, payloadTimestamp, fileStat.mtimeMs].find((value) => !Number.isNaN(value)) ?? fileStat.mtimeMs
243
+
244
+ metadataById.set(sessionId, { cwd, modifiedAt })
245
+ }
246
+
247
+ return metadataById
248
+ }
249
+
250
+ export class CodexProjectDiscoveryAdapter implements ProjectDiscoveryAdapter {
251
+ readonly provider = "codex" as const
252
+
253
+ scan(homeDir: string = homedir()): ProviderDiscoveredProject[] {
254
+ const indexPath = path.join(homeDir, ".codex", "session_index.jsonl")
255
+ const sessionsDir = path.join(homeDir, ".codex", "sessions")
256
+ const configPath = path.join(homeDir, ".codex", "config.toml")
257
+ const updatedAtById = readCodexSessionIndex(indexPath)
258
+ const metadataById = readCodexSessionMetadata(sessionsDir)
259
+ const configuredProjects = readCodexConfiguredProjects(configPath)
260
+ const projects: ProviderDiscoveredProject[] = []
261
+
262
+ for (const [sessionId, metadata] of metadataById.entries()) {
263
+ const modifiedAt = updatedAtById.get(sessionId) ?? metadata.modifiedAt
264
+ const cwd = metadata.cwd
265
+ if (!cwd) {
266
+ continue
267
+ }
268
+ if (!path.isAbsolute(cwd)) {
269
+ continue
270
+ }
271
+
272
+ const project = toDiscoveredProject(cwd, modifiedAt)
273
+ if (!project) {
274
+ continue
275
+ }
276
+
277
+ projects.push({
278
+ provider: this.provider,
279
+ ...project,
280
+ })
281
+ }
282
+
283
+ for (const [configuredPath, modifiedAt] of configuredProjects.entries()) {
284
+ if (!path.isAbsolute(configuredPath)) {
285
+ continue
286
+ }
287
+
288
+ const project = toDiscoveredProject(configuredPath, modifiedAt)
289
+ if (!project) {
290
+ continue
291
+ }
292
+
293
+ projects.push({
294
+ provider: this.provider,
295
+ ...project,
296
+ })
297
+ }
298
+
299
+ const mergedProjects = mergeDiscoveredProjects(projects).map((project) => ({
300
+ provider: this.provider,
301
+ ...project,
302
+ }))
303
+
304
+ return mergedProjects
305
+ }
306
+ }
307
+
308
+ export const DEFAULT_PROJECT_DISCOVERY_ADAPTERS: ProjectDiscoveryAdapter[] = [
309
+ new ClaudeProjectDiscoveryAdapter(),
310
+ new CodexProjectDiscoveryAdapter(),
311
+ ]
312
+
313
+ export function discoverProjects(
314
+ homeDir: string = homedir(),
315
+ adapters: ProjectDiscoveryAdapter[] = DEFAULT_PROJECT_DISCOVERY_ADAPTERS
316
+ ): DiscoveredProject[] {
317
+ const mergedProjects = mergeDiscoveredProjects(
318
+ adapters.flatMap((adapter) => adapter.scan(homeDir).map(({ provider: _provider, ...project }) => project))
319
+ )
320
+
321
+ return mergedProjects
322
+ }