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.
- package/LICENSE +22 -0
- package/README.md +246 -0
- package/bin/kaizen +15 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/index-D-ORCGrq.js +603 -0
- package/dist/client/assets/index-r28mcHqz.css +32 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +22 -0
- package/dist/client/manifest-dark.webmanifest +24 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/client/pwa-192.png +0 -0
- package/dist/client/pwa-512.png +0 -0
- package/dist/client/pwa-icon.svg +15 -0
- package/dist/client/pwa-splash.png +0 -0
- package/dist/client/pwa-splash.svg +15 -0
- package/package.json +103 -0
- package/src/server/acp-shared.ts +315 -0
- package/src/server/agent.ts +1120 -0
- package/src/server/attachments.ts +133 -0
- package/src/server/backgrounds.ts +74 -0
- package/src/server/cli-runtime.ts +333 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +68 -0
- package/src/server/codex-app-server-protocol.ts +453 -0
- package/src/server/codex-app-server.ts +1350 -0
- package/src/server/cursor-acp.ts +819 -0
- package/src/server/discovery.ts +322 -0
- package/src/server/event-store.ts +1369 -0
- package/src/server/events.ts +244 -0
- package/src/server/external-open.ts +272 -0
- package/src/server/gemini-acp.ts +844 -0
- package/src/server/gemini-cli.ts +525 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-manager.ts +79 -0
- package/src/server/git-repository.ts +101 -0
- package/src/server/harness-types.ts +20 -0
- package/src/server/keybindings.ts +177 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths.ts +112 -0
- package/src/server/process-utils.ts +22 -0
- package/src/server/project-icon.ts +344 -0
- package/src/server/project-metadata.ts +10 -0
- package/src/server/provider-catalog.ts +85 -0
- package/src/server/provider-settings.ts +155 -0
- package/src/server/quick-response.ts +153 -0
- package/src/server/read-models.ts +275 -0
- package/src/server/recovery.ts +507 -0
- package/src/server/restart.ts +30 -0
- package/src/server/server.ts +244 -0
- package/src/server/terminal-manager.ts +350 -0
- package/src/server/theme-settings.ts +179 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/usage/base-provider-usage.ts +57 -0
- package/src/server/usage/claude-usage.ts +558 -0
- package/src/server/usage/codex-usage.ts +144 -0
- package/src/server/usage/cursor-browser.ts +120 -0
- package/src/server/usage/cursor-cookies.ts +390 -0
- package/src/server/usage/cursor-usage.ts +490 -0
- package/src/server/usage/gemini-usage.ts +24 -0
- package/src/server/usage/provider-usage.ts +61 -0
- package/src/server/usage/test-helpers.ts +9 -0
- package/src/server/usage/types.ts +54 -0
- package/src/server/usage/utils.ts +325 -0
- package/src/server/ws-router.ts +717 -0
- package/src/shared/branding.ts +83 -0
- package/src/shared/dev-ports.ts +43 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +152 -0
- package/src/shared/tools.ts +251 -0
- 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
|
+
}
|