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,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
+ }