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,507 @@
|
|
|
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, TranscriptEntry } from "../shared/types"
|
|
5
|
+
import { normalizeToolCall } from "../shared/tools"
|
|
6
|
+
import { normalizeClaudeStreamMessage } from "./agent"
|
|
7
|
+
|
|
8
|
+
interface RecoveryStore {
|
|
9
|
+
listChatsByProject(projectId: string): Array<{
|
|
10
|
+
id: string
|
|
11
|
+
provider: AgentProvider | null
|
|
12
|
+
sessionToken: string | null
|
|
13
|
+
lastMessageAt?: number
|
|
14
|
+
updatedAt: number
|
|
15
|
+
}>
|
|
16
|
+
isProjectHidden(repoKey: string): boolean
|
|
17
|
+
createChat(projectId: string): Promise<{ id: string }>
|
|
18
|
+
deleteChat(chatId: string): Promise<void>
|
|
19
|
+
renameChat(chatId: string, title: string): Promise<void>
|
|
20
|
+
setChatProvider(chatId: string, provider: AgentProvider): Promise<void>
|
|
21
|
+
setSessionToken(chatId: string, sessionToken: string | null): Promise<void>
|
|
22
|
+
appendMessage(chatId: string, entry: TranscriptEntry): Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface RecoveryChat {
|
|
26
|
+
provider: AgentProvider
|
|
27
|
+
sessionToken: string
|
|
28
|
+
localPath: string
|
|
29
|
+
title: string
|
|
30
|
+
modifiedAt: number
|
|
31
|
+
entries: TranscriptEntry[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProjectImportResult {
|
|
35
|
+
importedChatIds: string[]
|
|
36
|
+
importedChats: number
|
|
37
|
+
importedMessages: number
|
|
38
|
+
newestChatId: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseJsonRecord(line: string): Record<string, unknown> | null {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(line)
|
|
44
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
return parsed as Record<string, unknown>
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectFiles(directory: string, extension: string): string[] {
|
|
54
|
+
if (!existsSync(directory)) {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const files: string[] = []
|
|
59
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
60
|
+
const fullPath = path.join(directory, entry.name)
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
files.push(...collectFiles(fullPath, extension))
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (entry.isFile() && entry.name.endsWith(extension)) {
|
|
66
|
+
files.push(fullPath)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return files
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function makeEntryId(prefix: string, sessionToken: string, index: number) {
|
|
74
|
+
return `${prefix}:${sessionToken}:${index}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function textFromClaudeContentArray(content: unknown[]): string {
|
|
78
|
+
return content
|
|
79
|
+
.map((item) => {
|
|
80
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return ""
|
|
81
|
+
const record = item as Record<string, unknown>
|
|
82
|
+
return record.type === "text" && typeof record.text === "string" ? record.text : ""
|
|
83
|
+
})
|
|
84
|
+
.filter((part) => part.trim())
|
|
85
|
+
.join("\n")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function claudeUserEntriesFromRecord(record: Record<string, unknown>, timestamp: number, messageId: string): TranscriptEntry[] {
|
|
89
|
+
const message = record.message
|
|
90
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
91
|
+
return []
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const messageRecord = message as Record<string, unknown>
|
|
95
|
+
let content = ""
|
|
96
|
+
if (typeof messageRecord.content === "string") {
|
|
97
|
+
content = messageRecord.content
|
|
98
|
+
} else if (Array.isArray(messageRecord.content)) {
|
|
99
|
+
content = textFromClaudeContentArray(messageRecord.content)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const trimmed = content.trim()
|
|
103
|
+
if (!trimmed) {
|
|
104
|
+
return []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (trimmed.startsWith("This session is being continued")) {
|
|
108
|
+
return [{
|
|
109
|
+
_id: messageId,
|
|
110
|
+
messageId,
|
|
111
|
+
createdAt: timestamp,
|
|
112
|
+
kind: "compact_summary",
|
|
113
|
+
summary: trimmed,
|
|
114
|
+
}]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return [{
|
|
118
|
+
_id: messageId,
|
|
119
|
+
messageId,
|
|
120
|
+
createdAt: timestamp,
|
|
121
|
+
kind: "user_prompt",
|
|
122
|
+
content: trimmed,
|
|
123
|
+
}]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function claudeEntriesFromRecord(record: Record<string, unknown>): TranscriptEntry[] {
|
|
127
|
+
const timestamp = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Number.NaN
|
|
128
|
+
if (Number.isNaN(timestamp)) {
|
|
129
|
+
return []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const messageId = typeof record.uuid === "string"
|
|
133
|
+
? record.uuid
|
|
134
|
+
: makeEntryId("claude-message", String(record.sessionId ?? "session"), 0)
|
|
135
|
+
|
|
136
|
+
if (record.type === "user") {
|
|
137
|
+
return claudeUserEntriesFromRecord(record, timestamp, messageId)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const entries = normalizeClaudeStreamMessage(record).filter((entry) => {
|
|
141
|
+
if (entry.kind === "assistant_text" && !entry.text.trim()) return false
|
|
142
|
+
if (entry.kind === "compact_summary" && !entry.summary.trim()) return false
|
|
143
|
+
return entry.kind !== "tool_call" && entry.kind !== "tool_result" && entry.kind !== "system_init"
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return entries.map((entry, index) => ({
|
|
147
|
+
...entry,
|
|
148
|
+
_id: entry._id || makeEntryId("claude", String(record.sessionId ?? "session"), index),
|
|
149
|
+
createdAt: timestamp + index,
|
|
150
|
+
}))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function firstUserPrompt(entries: TranscriptEntry[]): string | null {
|
|
154
|
+
const entry = entries.find((candidate) => candidate.kind === "user_prompt" && candidate.content.trim())
|
|
155
|
+
if (!entry || entry.kind !== "user_prompt") {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
return entry.content.trim()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function firstLine(value: string, fallback: string) {
|
|
162
|
+
const line = value.split("\n").map((part) => part.trim()).find(Boolean)
|
|
163
|
+
if (!line) return fallback
|
|
164
|
+
return line.length > 80 ? `${line.slice(0, 77)}...` : line
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isInternalTitleGenerationPrompt(value: string | null) {
|
|
168
|
+
return Boolean(
|
|
169
|
+
value?.startsWith("Generate a short, descriptive title (under 30 chars) for a conversation that starts with this message.")
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function markSkippedSession(
|
|
174
|
+
skippedSessionKeys: Set<string>,
|
|
175
|
+
provider: AgentProvider,
|
|
176
|
+
sessionToken: string | null
|
|
177
|
+
) {
|
|
178
|
+
if (sessionToken) {
|
|
179
|
+
skippedSessionKeys.add(`${provider}:${sessionToken}`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readClaudeProjectChats(homeDir: string, trackedWorktreePaths: Set<string>, skippedSessionKeys: Set<string>): RecoveryChat[] {
|
|
184
|
+
const projectsDir = path.join(homeDir, ".claude", "projects")
|
|
185
|
+
const chats: RecoveryChat[] = []
|
|
186
|
+
|
|
187
|
+
for (const sessionFile of collectFiles(projectsDir, ".jsonl")) {
|
|
188
|
+
const lines = readFileSync(sessionFile, "utf8").split("\n")
|
|
189
|
+
const entries: TranscriptEntry[] = []
|
|
190
|
+
let sessionToken: string | null = null
|
|
191
|
+
let sessionLocalPath: string | null = null
|
|
192
|
+
let modifiedAt = statSync(sessionFile).mtimeMs
|
|
193
|
+
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
if (!line.trim()) continue
|
|
196
|
+
const record = parseJsonRecord(line)
|
|
197
|
+
if (!record) continue
|
|
198
|
+
|
|
199
|
+
if (!sessionToken && typeof record.sessionId === "string") {
|
|
200
|
+
sessionToken = record.sessionId
|
|
201
|
+
}
|
|
202
|
+
if (!sessionLocalPath && typeof record.cwd === "string" && path.isAbsolute(record.cwd)) {
|
|
203
|
+
sessionLocalPath = path.normalize(record.cwd)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const timestamp = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Number.NaN
|
|
207
|
+
if (!Number.isNaN(timestamp)) {
|
|
208
|
+
modifiedAt = Math.max(modifiedAt, timestamp)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
entries.push(...claudeEntriesFromRecord(record))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!sessionToken || !sessionLocalPath || !trackedWorktreePaths.has(sessionLocalPath) || entries.length === 0) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const prompt = firstUserPrompt(entries)
|
|
219
|
+
if (!prompt || isInternalTitleGenerationPrompt(prompt)) {
|
|
220
|
+
markSkippedSession(skippedSessionKeys, "claude", sessionToken)
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
chats.push({
|
|
225
|
+
provider: "claude",
|
|
226
|
+
sessionToken,
|
|
227
|
+
localPath: sessionLocalPath,
|
|
228
|
+
title: prompt,
|
|
229
|
+
modifiedAt,
|
|
230
|
+
entries,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return chats
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function codexAssistantTextFromResponseItem(record: Record<string, unknown>, index: number): TranscriptEntry | null {
|
|
238
|
+
const payload = record.payload
|
|
239
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const payloadRecord = payload as Record<string, unknown>
|
|
244
|
+
if (payloadRecord.type !== "message") {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const timestamp = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Date.now()
|
|
249
|
+
const content = Array.isArray(payloadRecord.content) ? payloadRecord.content : []
|
|
250
|
+
const text = content
|
|
251
|
+
.map((item) => {
|
|
252
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return ""
|
|
253
|
+
const contentItem = item as Record<string, unknown>
|
|
254
|
+
return contentItem.type === "output_text" && typeof contentItem.text === "string"
|
|
255
|
+
? contentItem.text
|
|
256
|
+
: ""
|
|
257
|
+
})
|
|
258
|
+
.filter(Boolean)
|
|
259
|
+
.join("\n")
|
|
260
|
+
|
|
261
|
+
if (!text.trim()) {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
_id: makeEntryId("codex", String(payloadRecord.id ?? "assistant"), index),
|
|
267
|
+
createdAt: timestamp + index,
|
|
268
|
+
kind: "assistant_text",
|
|
269
|
+
text,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function codexToolCallFromResponseItem(record: Record<string, unknown>, index: number): TranscriptEntry | null {
|
|
274
|
+
const payload = record.payload
|
|
275
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const payloadRecord = payload as Record<string, unknown>
|
|
280
|
+
if (payloadRecord.type !== "function_call" || typeof payloadRecord.name !== "string") {
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const toolId = typeof payloadRecord.call_id === "string"
|
|
285
|
+
? payloadRecord.call_id
|
|
286
|
+
: makeEntryId("codex-tool", payloadRecord.name, index)
|
|
287
|
+
const timestamp = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Date.now()
|
|
288
|
+
let input: Record<string, unknown> = {}
|
|
289
|
+
|
|
290
|
+
if (typeof payloadRecord.arguments === "string") {
|
|
291
|
+
input = parseJsonRecord(payloadRecord.arguments) ?? {}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
_id: makeEntryId("codex", toolId, index),
|
|
296
|
+
createdAt: timestamp + index,
|
|
297
|
+
kind: "tool_call",
|
|
298
|
+
tool: normalizeToolCall({
|
|
299
|
+
toolName: payloadRecord.name,
|
|
300
|
+
toolId,
|
|
301
|
+
input,
|
|
302
|
+
}),
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function codexEntriesFromRecord(record: Record<string, unknown>, index: number): TranscriptEntry[] {
|
|
307
|
+
const timestamp = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Date.now()
|
|
308
|
+
|
|
309
|
+
if (record.type === "event_msg") {
|
|
310
|
+
const payload = record.payload
|
|
311
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
312
|
+
return []
|
|
313
|
+
}
|
|
314
|
+
const payloadRecord = payload as Record<string, unknown>
|
|
315
|
+
|
|
316
|
+
if (payloadRecord.type === "user_message" && typeof payloadRecord.message === "string" && payloadRecord.message.trim()) {
|
|
317
|
+
return [{
|
|
318
|
+
_id: makeEntryId("codex-user", String(index), index),
|
|
319
|
+
createdAt: timestamp + index,
|
|
320
|
+
kind: "user_prompt",
|
|
321
|
+
content: payloadRecord.message.trim(),
|
|
322
|
+
}]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (payloadRecord.type === "agent_message" && typeof payloadRecord.message === "string" && payloadRecord.message.trim()) {
|
|
326
|
+
return [{
|
|
327
|
+
_id: makeEntryId("codex-assistant", String(index), index),
|
|
328
|
+
createdAt: timestamp + index,
|
|
329
|
+
kind: "assistant_text",
|
|
330
|
+
text: payloadRecord.message,
|
|
331
|
+
}]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return []
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (record.type === "response_item") {
|
|
338
|
+
const toolCall = codexToolCallFromResponseItem(record, index)
|
|
339
|
+
if (toolCall) {
|
|
340
|
+
return [toolCall]
|
|
341
|
+
}
|
|
342
|
+
const assistantText = codexAssistantTextFromResponseItem(record, index)
|
|
343
|
+
return assistantText ? [assistantText] : []
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return []
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isCodexSubagentSession(payload: Record<string, unknown>) {
|
|
350
|
+
if (typeof payload.forked_from_id === "string" && payload.forked_from_id.trim()) {
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const source = payload.source
|
|
355
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
356
|
+
return false
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const sourceRecord = source as Record<string, unknown>
|
|
360
|
+
return Boolean(sourceRecord.subagent)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function readCodexProjectChats(homeDir: string, trackedWorktreePaths: Set<string>, skippedSessionKeys: Set<string>): RecoveryChat[] {
|
|
364
|
+
const sessionsDir = path.join(homeDir, ".codex", "sessions")
|
|
365
|
+
const chats: RecoveryChat[] = []
|
|
366
|
+
|
|
367
|
+
for (const sessionFile of collectFiles(sessionsDir, ".jsonl")) {
|
|
368
|
+
const lines = readFileSync(sessionFile, "utf8").split("\n")
|
|
369
|
+
const entries: TranscriptEntry[] = []
|
|
370
|
+
let sessionToken: string | null = null
|
|
371
|
+
let sessionLocalPath: string | null = null
|
|
372
|
+
let isSubagentSession = false
|
|
373
|
+
let modifiedAt = statSync(sessionFile).mtimeMs
|
|
374
|
+
|
|
375
|
+
lines.forEach((line, index) => {
|
|
376
|
+
if (!line.trim()) return
|
|
377
|
+
const record = parseJsonRecord(line)
|
|
378
|
+
if (!record) return
|
|
379
|
+
|
|
380
|
+
if (record.type === "session_meta") {
|
|
381
|
+
const payload = record.payload
|
|
382
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
383
|
+
const payloadRecord = payload as Record<string, unknown>
|
|
384
|
+
isSubagentSession = isSubagentSession || isCodexSubagentSession(payloadRecord)
|
|
385
|
+
if (!sessionToken && typeof payloadRecord.id === "string") {
|
|
386
|
+
sessionToken = payloadRecord.id
|
|
387
|
+
}
|
|
388
|
+
if (!sessionLocalPath && typeof payloadRecord.cwd === "string" && path.isAbsolute(payloadRecord.cwd)) {
|
|
389
|
+
sessionLocalPath = path.normalize(payloadRecord.cwd)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const timestamp = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Number.NaN
|
|
395
|
+
if (!Number.isNaN(timestamp)) {
|
|
396
|
+
modifiedAt = Math.max(modifiedAt, timestamp)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
entries.push(...codexEntriesFromRecord(record, index))
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
if (isSubagentSession) {
|
|
403
|
+
markSkippedSession(skippedSessionKeys, "codex", sessionToken)
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!sessionToken || !sessionLocalPath || !trackedWorktreePaths.has(sessionLocalPath) || entries.length === 0) {
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const prompt = firstUserPrompt(entries)
|
|
412
|
+
if (!prompt || isInternalTitleGenerationPrompt(prompt)) {
|
|
413
|
+
markSkippedSession(skippedSessionKeys, "codex", sessionToken)
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
chats.push({
|
|
418
|
+
provider: "codex",
|
|
419
|
+
sessionToken,
|
|
420
|
+
localPath: sessionLocalPath,
|
|
421
|
+
title: prompt,
|
|
422
|
+
modifiedAt,
|
|
423
|
+
entries,
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return chats
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function collectProjectChats(homeDir: string, trackedWorktreePaths: Set<string>) {
|
|
431
|
+
const skippedSessionKeys = new Set<string>()
|
|
432
|
+
return {
|
|
433
|
+
skippedSessionKeys,
|
|
434
|
+
chats: [
|
|
435
|
+
...readClaudeProjectChats(homeDir, trackedWorktreePaths, skippedSessionKeys),
|
|
436
|
+
...readCodexProjectChats(homeDir, trackedWorktreePaths, skippedSessionKeys),
|
|
437
|
+
],
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function importProjectHistory(args: {
|
|
442
|
+
store: RecoveryStore
|
|
443
|
+
projectId: string
|
|
444
|
+
repoKey: string
|
|
445
|
+
localPath: string
|
|
446
|
+
worktreePaths: string[]
|
|
447
|
+
homeDir?: string
|
|
448
|
+
log?: (message: string) => void
|
|
449
|
+
}): Promise<ProjectImportResult> {
|
|
450
|
+
const normalizedPaths = new Set(args.worktreePaths.map((worktreePath) => path.normalize(worktreePath)))
|
|
451
|
+
normalizedPaths.add(path.normalize(args.localPath))
|
|
452
|
+
if (args.store.isProjectHidden(args.repoKey)) {
|
|
453
|
+
return {
|
|
454
|
+
importedChatIds: [],
|
|
455
|
+
importedChats: 0,
|
|
456
|
+
importedMessages: 0,
|
|
457
|
+
newestChatId: null,
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const { chats, skippedSessionKeys } = collectProjectChats(args.homeDir ?? homedir(), normalizedPaths)
|
|
462
|
+
const existingChats = args.store.listChatsByProject(args.projectId)
|
|
463
|
+
|
|
464
|
+
for (const chat of existingChats.filter((candidate) => {
|
|
465
|
+
if (!candidate.provider || !candidate.sessionToken) return false
|
|
466
|
+
return skippedSessionKeys.has(`${candidate.provider}:${candidate.sessionToken}`)
|
|
467
|
+
})) {
|
|
468
|
+
await args.store.deleteChat(chat.id)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const refreshedExistingChats = args.store.listChatsByProject(args.projectId)
|
|
472
|
+
const existingSessionKeys = new Set(
|
|
473
|
+
refreshedExistingChats
|
|
474
|
+
.filter((chat) => chat.provider && chat.sessionToken)
|
|
475
|
+
.map((chat) => `${chat.provider}:${chat.sessionToken}`)
|
|
476
|
+
)
|
|
477
|
+
const importedChatIds: string[] = []
|
|
478
|
+
let importedMessages = 0
|
|
479
|
+
|
|
480
|
+
for (const chat of chats
|
|
481
|
+
.filter((candidate) => !existingSessionKeys.has(`${candidate.provider}:${candidate.sessionToken}`))
|
|
482
|
+
.sort((a, b) => a.modifiedAt - b.modifiedAt)) {
|
|
483
|
+
const createdChat = await args.store.createChat(args.projectId)
|
|
484
|
+
await args.store.renameChat(createdChat.id, firstLine(chat.title, "Recovered Chat"))
|
|
485
|
+
await args.store.setChatProvider(createdChat.id, chat.provider)
|
|
486
|
+
await args.store.setSessionToken(createdChat.id, chat.sessionToken)
|
|
487
|
+
|
|
488
|
+
for (const entry of chat.entries) {
|
|
489
|
+
await args.store.appendMessage(createdChat.id, entry)
|
|
490
|
+
importedMessages += 1
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
importedChatIds.push(createdChat.id)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const newestChatId = args.store.listChatsByProject(args.projectId)[0]?.id ?? null
|
|
497
|
+
args.log?.(
|
|
498
|
+
`[kaizen] project import repo=${args.repoKey} paths=${[...normalizedPaths].join(",")} discovered=${chats.length} imported=${importedChatIds.length} messages=${importedMessages}`
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
importedChatIds,
|
|
503
|
+
importedChats: importedChatIds.length,
|
|
504
|
+
importedMessages,
|
|
505
|
+
newestChatId,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const CLI_CHILD_MODE_ENV_VAR = "KAIZEN_CLI_MODE"
|
|
2
|
+
export const CLI_CHILD_MODE = "child"
|
|
3
|
+
export const CLI_STARTUP_UPDATE_RESTART_EXIT_CODE = 75
|
|
4
|
+
export const CLI_UI_UPDATE_RESTART_EXIT_CODE = 76
|
|
5
|
+
export const CLI_CHILD_COMMAND_ENV_VAR = "KAIZEN_CLI_CHILD_COMMAND"
|
|
6
|
+
export const CLI_CHILD_ARGS_ENV_VAR = "KAIZEN_CLI_CHILD_ARGS"
|
|
7
|
+
export const CLI_SUPPRESS_OPEN_ONCE_ENV_VAR = "KAIZEN_SUPPRESS_OPEN_ONCE"
|
|
8
|
+
|
|
9
|
+
export function shouldRestartCliProcess(code: number | null, signal: NodeJS.Signals | null) {
|
|
10
|
+
return signal === null && (code === CLI_STARTUP_UPDATE_RESTART_EXIT_CODE || code === CLI_UI_UPDATE_RESTART_EXIT_CODE)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isUiUpdateRestart(code: number | null, signal: NodeJS.Signals | null) {
|
|
14
|
+
return signal === null && code === CLI_UI_UPDATE_RESTART_EXIT_CODE
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseChildArgsEnv(value: string | undefined) {
|
|
18
|
+
if (!value) return []
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(value) as unknown
|
|
22
|
+
if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) {
|
|
23
|
+
throw new Error("child args must be an array of strings")
|
|
24
|
+
}
|
|
25
|
+
return parsed
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
28
|
+
throw new Error(`Invalid ${CLI_CHILD_ARGS_ENV_VAR}: ${message}`)
|
|
29
|
+
}
|
|
30
|
+
}
|