centaurus-cli 3.1.2 → 3.1.4
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/dist/cli-adapter.js +689 -155
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/defaultConfig.js +1 -4
- package/dist/config/defaultConfig.js.map +1 -1
- package/dist/config/models.js +6 -0
- package/dist/config/models.js.map +1 -1
- package/dist/config/slash-commands.js +66 -2
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/config/types.js +4 -4
- package/dist/config/types.js.map +1 -1
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -1
- package/dist/services/ai-context-injector.js +109 -0
- package/dist/services/ai-context-injector.js.map +1 -1
- package/dist/services/ai-service-client.js +3 -2
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/background-task-manager.js +59 -0
- package/dist/services/background-task-manager.js.map +1 -1
- package/dist/services/local-chat-storage.js +2 -0
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/services/skill-storage.js +141 -0
- package/dist/services/skill-storage.js.map +1 -0
- package/dist/services/sub-agent-manager.js +49 -8
- package/dist/services/sub-agent-manager.js.map +1 -1
- package/dist/services/warpify-detector.js +17 -5
- package/dist/services/warpify-detector.js.map +1 -1
- package/dist/tools/background-command.js +5 -2
- package/dist/tools/background-command.js.map +1 -1
- package/dist/tools/command.js +367 -109
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.js +23 -6
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/plan-mode.js +184 -336
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/sub-agent.js +24 -5
- package/dist/tools/sub-agent.js.map +1 -1
- package/dist/tools/todo-list.js +157 -0
- package/dist/tools/todo-list.js.map +1 -0
- package/dist/types/skill.js +30 -0
- package/dist/types/skill.js.map +1 -0
- package/dist/ui/components/App.js +956 -162
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/AuthScreen.js +3 -1
- package/dist/ui/components/AuthScreen.js.map +1 -1
- package/dist/ui/components/AuthWelcomeScreen.js +3 -1
- package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
- package/dist/ui/components/CodeBlock.js +3 -1
- package/dist/ui/components/CodeBlock.js.map +1 -1
- package/dist/ui/components/CompactShellPreview.js +44 -0
- package/dist/ui/components/CompactShellPreview.js.map +1 -0
- package/dist/ui/components/ConfigViewer.js +3 -1
- package/dist/ui/components/ConfigViewer.js.map +1 -1
- package/dist/ui/components/ConfirmPrompt.js +3 -1
- package/dist/ui/components/ConfirmPrompt.js.map +1 -1
- package/dist/ui/components/ConnectionStatusMessage.js +3 -1
- package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
- package/dist/ui/components/DetailedPlanReviewScreen.js +84 -74
- package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
- package/dist/ui/components/DiffViewer.js +6 -3
- package/dist/ui/components/DiffViewer.js.map +1 -1
- package/dist/ui/components/FileCreationPreview.js.map +1 -1
- package/dist/ui/components/FileTagAutocomplete.js +4 -2
- package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
- package/dist/ui/components/InputBox.js +243 -40
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.js +5 -3
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/KeyboardHelp.js +4 -1
- package/dist/ui/components/KeyboardHelp.js.map +1 -1
- package/dist/ui/components/LoadingIndicator.js +3 -1
- package/dist/ui/components/LoadingIndicator.js.map +1 -1
- package/dist/ui/components/MCPAddScreen.js +63 -13
- package/dist/ui/components/MCPAddScreen.js.map +1 -1
- package/dist/ui/components/MarkdownRenderer.js +3 -1
- package/dist/ui/components/MarkdownRenderer.js.map +1 -1
- package/dist/ui/components/MessageDisplay.js +9 -7
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/ModelPicker.js +170 -0
- package/dist/ui/components/ModelPicker.js.map +1 -0
- package/dist/ui/components/MonitorModeAIPanel.js +3 -1
- package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.js +12 -6
- package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
- package/dist/ui/components/PlanQuestionMessage.js +37 -0
- package/dist/ui/components/PlanQuestionMessage.js.map +1 -0
- package/dist/ui/components/PlanQuestionScreen.js +138 -0
- package/dist/ui/components/PlanQuestionScreen.js.map +1 -0
- package/dist/ui/components/PlanReviewScreen.js +7 -9
- package/dist/ui/components/PlanReviewScreen.js.map +1 -1
- package/dist/ui/components/RulesEditorScreen.js +65 -28
- package/dist/ui/components/RulesEditorScreen.js.map +1 -1
- package/dist/ui/components/SelectPrompt.js +3 -1
- package/dist/ui/components/SelectPrompt.js.map +1 -1
- package/dist/ui/components/SkillCreatorScreen.js +217 -0
- package/dist/ui/components/SkillCreatorScreen.js.map +1 -0
- package/dist/ui/components/SlashCommandAutocomplete.js +4 -2
- package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
- package/dist/ui/components/StatusBar.js +4 -2
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.js +5 -3
- package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
- package/dist/ui/components/SubAgentListScreen.js +65 -0
- package/dist/ui/components/SubAgentListScreen.js.map +1 -0
- package/dist/ui/components/SubAgentViewScreen.js +123 -0
- package/dist/ui/components/SubAgentViewScreen.js.map +1 -0
- package/dist/ui/components/TaskCompletedMessage.js +40 -8
- package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
- package/dist/ui/components/TaskProgressIndicator.js +6 -4
- package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
- package/dist/ui/components/TextEditor.js +297 -0
- package/dist/ui/components/TextEditor.js.map +1 -0
- package/dist/ui/components/TodoListMessage.js +59 -0
- package/dist/ui/components/TodoListMessage.js.map +1 -0
- package/dist/ui/components/ToolExecutionMessage.js +134 -84
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.js +3 -1
- package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
- package/dist/ui/components/WelcomeBanner.js +33 -33
- package/dist/ui/components/WelcomeBanner.js.map +1 -1
- package/dist/ui/components/WorkflowCreatorScreen.js +5 -3
- package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
- package/dist/ui/theme.js +97 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui/utils/chat-history-limit.js +247 -0
- package/dist/ui/utils/chat-history-limit.js.map +1 -0
- package/dist/utils/chat-formatter.js +22 -9
- package/dist/utils/chat-formatter.js.map +1 -1
- package/dist/utils/git-stats.js +7 -5
- package/dist/utils/git-stats.js.map +1 -1
- package/dist/utils/input-classifier.js +11 -1
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/output-truncation.js +175 -0
- package/dist/utils/output-truncation.js.map +1 -0
- package/dist/utils/rule-reference-resolver.js +3 -3
- package/dist/utils/rule-reference-resolver.js.map +1 -1
- package/dist/utils/tunnel-commands-manager.js +134 -0
- package/dist/utils/tunnel-commands-manager.js.map +1 -0
- package/package.json +91 -90
- package/postinstall.js +4 -11
- package/dist/ui/components/MultiLineInput.js +0 -255
- package/dist/ui/components/MultiLineInput.js.map +0 -1
|
@@ -41,8 +41,12 @@ class BackgroundTaskManagerClass extends EventEmitter {
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
task.ptyProcess = ptyProcess;
|
|
44
|
+
const MAX_OUTPUT_SIZE = 100 * 1024;
|
|
44
45
|
ptyProcess.onData((data) => {
|
|
45
46
|
task.output += data;
|
|
47
|
+
if (task.output.length > MAX_OUTPUT_SIZE) {
|
|
48
|
+
task.output = task.output.slice(-MAX_OUTPUT_SIZE);
|
|
49
|
+
}
|
|
46
50
|
this.emit("taskOutput", id, data);
|
|
47
51
|
});
|
|
48
52
|
ptyProcess.onExit(({ exitCode }) => {
|
|
@@ -95,6 +99,9 @@ class BackgroundTaskManagerClass extends EventEmitter {
|
|
|
95
99
|
id,
|
|
96
100
|
onData: (data) => {
|
|
97
101
|
task.output += data;
|
|
102
|
+
if (task.output.length > 100 * 1024) {
|
|
103
|
+
task.output = task.output.slice(-100 * 1024);
|
|
104
|
+
}
|
|
98
105
|
this.emit("taskOutput", id, data);
|
|
99
106
|
},
|
|
100
107
|
onExit: (exitCode) => {
|
|
@@ -114,6 +121,58 @@ class BackgroundTaskManagerClass extends EventEmitter {
|
|
|
114
121
|
}
|
|
115
122
|
};
|
|
116
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Adopt a running foreground process into background task management.
|
|
126
|
+
*
|
|
127
|
+
* This is used for timeout-based transfer: a command started as foreground
|
|
128
|
+
* exceeds its timeout, so the STILL-RUNNING process is handed over to the
|
|
129
|
+
* background task manager without killing it. The process continues running
|
|
130
|
+
* and its output is captured by the background task.
|
|
131
|
+
*
|
|
132
|
+
* @param command The command string (for display)
|
|
133
|
+
* @param cwd The working directory (for display)
|
|
134
|
+
* @param existingOutput Output already captured while the process was foreground
|
|
135
|
+
* @param remoteContext Optional remote context label (e.g. "user@host")
|
|
136
|
+
* @returns Object with task ID and callbacks for wiring into the running process
|
|
137
|
+
*/
|
|
138
|
+
adoptRunningProcess(command, cwd, existingOutput, remoteContext) {
|
|
139
|
+
const id = `bkg-${++this.taskCounter}-${Date.now()}`;
|
|
140
|
+
const task = {
|
|
141
|
+
id,
|
|
142
|
+
command,
|
|
143
|
+
cwd,
|
|
144
|
+
startTime: /* @__PURE__ */ new Date(),
|
|
145
|
+
output: existingOutput,
|
|
146
|
+
isRunning: true,
|
|
147
|
+
remoteContext
|
|
148
|
+
};
|
|
149
|
+
this.tasks.set(id, task);
|
|
150
|
+
this.emit("taskStarted", id, command);
|
|
151
|
+
this.emit("countChanged", this.getRunningCount());
|
|
152
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [BackgroundTaskManager] Adopted running process as ${id}: ${command}
|
|
153
|
+
`);
|
|
154
|
+
return {
|
|
155
|
+
id,
|
|
156
|
+
onData: (data) => {
|
|
157
|
+
task.output += data;
|
|
158
|
+
if (task.output.length > 100 * 1024) {
|
|
159
|
+
task.output = task.output.slice(-100 * 1024);
|
|
160
|
+
}
|
|
161
|
+
this.emit("taskOutput", id, data);
|
|
162
|
+
},
|
|
163
|
+
onExit: (exitCode) => {
|
|
164
|
+
task.isRunning = false;
|
|
165
|
+
task.exitCode = exitCode;
|
|
166
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [BackgroundTaskManager] Adopted task completed: id=${id}, exitCode=${exitCode}
|
|
167
|
+
`);
|
|
168
|
+
this.emit("taskCompleted", id, exitCode);
|
|
169
|
+
this.emit("countChanged", this.getRunningCount());
|
|
170
|
+
},
|
|
171
|
+
setRemotePty: (remotePty) => {
|
|
172
|
+
task.remotePtyProcess = remotePty;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
117
176
|
/**
|
|
118
177
|
* Get all tasks (running and completed)
|
|
119
178
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/background-task-manager.ts"],"sourcesContent":["/**\r\n * Background Task Manager\r\n * \r\n * Manages background shell tasks, tracking their state and providing\r\n * notifications when tasks start, complete, or are cancelled.\r\n * Uses PTY for proper terminal emulation and signal handling.\r\n */\r\n\r\nimport { EventEmitter } from 'events';\r\nimport * as os from 'os';\r\nimport * as path from 'path';\r\nimport { createRequire } from 'module';\r\n\r\n// Use createRequire for ESM compatibility with native modules\r\nconst require = createRequire(import.meta.url);\r\nconst nodePty = require('@homebridge/node-pty-prebuilt-multiarch');\r\n\r\nimport { quickLog } from '../utils/conversation-logger.js';\r\n\r\n// PTY process interface\r\ninterface PtyProcess {\r\n write: (data: string) => void;\r\n resize: (cols: number, rows: number) => void;\r\n kill: () => void;\r\n onData: (callback: (data: string) => void) => void;\r\n onExit: (callback: (e: { exitCode: number }) => void) => void;\r\n pid: number;\r\n}\r\n\r\n// Remote PTY process interface (SSH/WSL/Docker)\r\n// These have a different callback signature (using onData string callback directly)\r\nexport interface RemotePtyProcess {\r\n write: (data: string) => void;\r\n kill: () => void;\r\n resize: (cols: number, rows: number) => void;\r\n isRunning: () => boolean;\r\n}\r\n\r\nexport interface BackgroundTask {\r\n id: string;\r\n command: string;\r\n cwd: string;\r\n startTime: Date;\r\n output: string;\r\n isRunning: boolean;\r\n exitCode?: number;\r\n error?: string;\r\n ptyProcess?: PtyProcess;\r\n remotePtyProcess?: RemotePtyProcess;\r\n remoteContext?: string; // Display string for remote context (e.g., \"user@host\", \"wsl:Ubuntu\")\r\n}\r\n\r\n\r\nexport interface BackgroundTaskInfo {\r\n id: string;\r\n command: string;\r\n cwd: string;\r\n startTime: Date;\r\n durationMs: number;\r\n isRunning: boolean;\r\n exitCode?: number;\r\n error?: string;\r\n outputPreview: string;\r\n}\r\n\r\nclass BackgroundTaskManagerClass extends EventEmitter {\r\n private tasks: Map<string, BackgroundTask> = new Map();\r\n private taskCounter: number = 0;\r\n\r\n /**\r\n * Start a new background task using PTY\r\n */\r\n startTask(command: string, cwd: string): string {\r\n const id = `bkg-${++this.taskCounter}-${Date.now()}`;\r\n\r\n const task: BackgroundTask = {\r\n id,\r\n command,\r\n cwd,\r\n startTime: new Date(),\r\n output: '',\r\n isRunning: true,\r\n };\r\n\r\n // Determine shell based on platform\r\n const isWindows = os.platform() === 'win32';\r\n const shell = isWindows ? 'powershell.exe' : (process.env.SHELL || '/bin/bash');\r\n const args = isWindows ? ['-Command', command] : ['-c', command];\r\n\r\n // Get initial terminal dimensions\r\n const cols = process.stdout.columns || 80;\r\n const rows = process.stdout.rows || 24;\r\n\r\n try {\r\n // Spawn PTY process for proper terminal emulation\r\n const ptyProcess = nodePty.spawn(shell, args, {\r\n name: 'xterm-256color',\r\n cols,\r\n rows,\r\n cwd,\r\n env: {\r\n ...process.env,\r\n TERM: 'xterm-256color',\r\n COLORTERM: 'truecolor',\r\n FORCE_COLOR: '1',\r\n CLICOLOR: '1',\r\n PYTHONUNBUFFERED: '1',\r\n } as { [key: string]: string },\r\n });\r\n\r\n task.ptyProcess = ptyProcess;\r\n\r\n // Capture output\r\n ptyProcess.onData((data: string) => {\r\n task.output += data;\r\n this.emit('taskOutput', id, data);\r\n });\r\n\r\n // Handle process completion\r\n ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {\r\n task.isRunning = false;\r\n task.exitCode = exitCode;\r\n\r\n // Debug logging\r\n const fs = require('fs');\r\n try {\r\n quickLog(`[${new Date().toISOString()}] [BackgroundTaskManager] taskCompleted event emitted: id=${id}, exitCode=${exitCode}\\n`);\r\n } catch (e) { }\r\n\r\n this.emit('taskCompleted', id, exitCode);\r\n this.emit('countChanged', this.getRunningCount());\r\n });\r\n\r\n this.tasks.set(id, task);\r\n this.emit('taskStarted', id, command);\r\n this.emit('countChanged', this.getRunningCount());\r\n\r\n return id;\r\n } catch (err: any) {\r\n task.isRunning = false;\r\n task.error = err.message;\r\n this.tasks.set(id, task);\r\n this.emit('taskError', id, err.message);\r\n return id;\r\n }\r\n }\r\n\r\n /**\r\n * Start a new remote background task (SSH/WSL/Docker)\r\n * This registers the task and returns callbacks that should be passed to the remote PTY creator\r\n * @param command The command being executed\r\n * @param cwd The working directory on the remote system\r\n * @param remoteContext Display string for the remote context (e.g., \"user@host\", \"wsl:Ubuntu\")\r\n * @returns Object with task ID and callbacks to pass to the remote PTY creator\r\n */\r\n startRemoteTask(\r\n command: string,\r\n cwd: string,\r\n remoteContext: string\r\n ): {\r\n id: string;\r\n onData: (data: string) => void;\r\n onExit: (exitCode: number) => void;\r\n setRemotePty: (remotePty: RemotePtyProcess) => void;\r\n } {\r\n const id = `bkg-${++this.taskCounter}-${Date.now()}`;\r\n\r\n const task: BackgroundTask = {\r\n id,\r\n command,\r\n cwd,\r\n startTime: new Date(),\r\n output: '',\r\n isRunning: true,\r\n remoteContext\r\n };\r\n\r\n this.tasks.set(id, task);\r\n this.emit('taskStarted', id, command);\r\n this.emit('countChanged', this.getRunningCount());\r\n\r\n // Return callbacks that the caller should pass to runSSHCommand/runWSLCommand/runDockerCommand\r\n return {\r\n id,\r\n onData: (data: string) => {\r\n task.output += data;\r\n this.emit('taskOutput', id, data);\r\n },\r\n onExit: (exitCode: number) => {\r\n task.isRunning = false;\r\n task.exitCode = exitCode;\r\n\r\n // Debug logging\r\n const fs = require('fs');\r\n try {\r\n quickLog(`[${new Date().toISOString()}] [BackgroundTaskManager] Remote taskCompleted event emitted: id=${id}, exitCode=${exitCode}\\n`);\r\n } catch (e) { }\r\n\r\n this.emit('taskCompleted', id, exitCode);\r\n this.emit('countChanged', this.getRunningCount());\r\n },\r\n setRemotePty: (remotePty: RemotePtyProcess) => {\r\n task.remotePtyProcess = remotePty;\r\n }\r\n };\r\n }\r\n\r\n\r\n /**\r\n * Get all tasks (running and completed)\r\n */\r\n\r\n getAllTasks(): BackgroundTaskInfo[] {\r\n const now = Date.now();\r\n return Array.from(this.tasks.values()).map(task => ({\r\n id: task.id,\r\n command: task.command,\r\n cwd: task.cwd,\r\n startTime: task.startTime,\r\n durationMs: now - task.startTime.getTime(),\r\n isRunning: task.isRunning,\r\n exitCode: task.exitCode,\r\n error: task.error,\r\n outputPreview: task.output.slice(-200), // Last 200 chars\r\n }));\r\n }\r\n\r\n /**\r\n * Get only running tasks\r\n */\r\n getRunningTasks(): BackgroundTaskInfo[] {\r\n return this.getAllTasks().filter(t => t.isRunning);\r\n }\r\n\r\n /**\r\n * Get the count of running tasks\r\n */\r\n getRunningCount(): number {\r\n let count = 0;\r\n for (const task of this.tasks.values()) {\r\n if (task.isRunning) count++;\r\n }\r\n return count;\r\n }\r\n\r\n /**\r\n * Get a specific task by ID\r\n */\r\n getTask(id: string): BackgroundTask | undefined {\r\n return this.tasks.get(id);\r\n }\r\n\r\n /**\r\n * Get task output (full output for streaming view)\r\n */\r\n getTaskOutput(id: string): string {\r\n return this.tasks.get(id)?.output || '';\r\n }\r\n\r\n /**\r\n * Send input to a running background task (via PTY or remote PTY)\r\n */\r\n sendInput(id: string, input: string): boolean {\r\n const task = this.tasks.get(id);\r\n\r\n // Debug logging\r\n const fs = require('fs');\r\n try {\r\n quickLog(`[${new Date().toISOString()}] [BackgroundTaskManager.sendInput] taskId: ${id}, input: ${JSON.stringify(input)}, taskFound: ${!!task}, isRunning: ${task?.isRunning}, hasPty: ${!!task?.ptyProcess}, hasRemotePty: ${!!task?.remotePtyProcess}\\n`);\r\n } catch (e) { }\r\n\r\n if (!task || !task.isRunning) {\r\n return false;\r\n }\r\n\r\n try {\r\n // Handle local PTY\r\n if (task.ptyProcess) {\r\n task.ptyProcess.write(input);\r\n return true;\r\n }\r\n // Handle remote PTY (SSH/WSL/Docker)\r\n if (task.remotePtyProcess) {\r\n task.remotePtyProcess.write(input);\r\n return true;\r\n }\r\n return false;\r\n } catch (err) {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Send a signal to a running background task (via PTY or remote PTY)\r\n * For PTY, we send control characters directly - same as normal shell focus mode\r\n */\r\n sendSignal(id: string, signal: NodeJS.Signals): boolean {\r\n const task = this.tasks.get(id);\r\n if (!task || !task.isRunning) {\r\n return false;\r\n }\r\n\r\n try {\r\n // Handle local PTY\r\n if (task.ptyProcess) {\r\n if (signal === 'SIGINT') {\r\n task.ptyProcess.write('\\x03'); // Ctrl+C\r\n setTimeout(() => {\r\n if (task.isRunning && task.ptyProcess) {\r\n task.ptyProcess.kill();\r\n }\r\n }, 500);\r\n } else {\r\n task.ptyProcess.kill();\r\n }\r\n return true;\r\n }\r\n // Handle remote PTY (SSH/WSL/Docker)\r\n if (task.remotePtyProcess) {\r\n if (signal === 'SIGINT') {\r\n task.remotePtyProcess.write('\\x03'); // Ctrl+C\r\n setTimeout(() => {\r\n if (task.isRunning && task.remotePtyProcess) {\r\n task.remotePtyProcess.kill();\r\n }\r\n }, 500);\r\n } else {\r\n task.remotePtyProcess.kill();\r\n }\r\n return true;\r\n }\r\n return false;\r\n } catch (err) {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Cancel a running task (via PTY or remote PTY)\r\n */\r\n cancelTask(id: string): boolean {\r\n const task = this.tasks.get(id);\r\n if (!task || !task.isRunning) {\r\n return false;\r\n }\r\n\r\n try {\r\n // Handle local PTY\r\n if (task.ptyProcess) {\r\n task.ptyProcess.kill();\r\n }\r\n // Handle remote PTY (SSH/WSL/Docker)\r\n if (task.remotePtyProcess) {\r\n task.remotePtyProcess.kill();\r\n }\r\n\r\n task.isRunning = false;\r\n task.error = 'Cancelled by user';\r\n this.emit('taskCancelled', id);\r\n this.emit('countChanged', this.getRunningCount());\r\n return true;\r\n } catch (err) {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Clear completed tasks from the list\r\n */\r\n clearCompleted(): void {\r\n for (const [id, task] of this.tasks.entries()) {\r\n if (!task.isRunning) {\r\n this.tasks.delete(id);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Check if a task is running\r\n */\r\n isTaskRunning(id: string): boolean {\r\n return this.tasks.get(id)?.isRunning ?? false;\r\n }\r\n}\r\n\r\n// Singleton instance\r\nexport const BackgroundTaskManager = new BackgroundTaskManagerClass();\r\n"],"mappings":"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","names":["require"]}
|
|
1
|
+
{"version":3,"sources":["../../src/services/background-task-manager.ts"],"sourcesContent":["/**\r\n * Background Task Manager\r\n * \r\n * Manages background shell tasks, tracking their state and providing\r\n * notifications when tasks start, complete, or are cancelled.\r\n * Uses PTY for proper terminal emulation and signal handling.\r\n */\r\n\r\nimport { EventEmitter } from 'events';\r\nimport * as os from 'os';\r\nimport * as path from 'path';\r\nimport { createRequire } from 'module';\r\n\r\n// Use createRequire for ESM compatibility with native modules\r\nconst require = createRequire(import.meta.url);\r\nconst nodePty = require('@homebridge/node-pty-prebuilt-multiarch');\r\n\r\nimport { quickLog } from '../utils/conversation-logger.js';\r\n\r\n// PTY process interface\r\ninterface PtyProcess {\r\n write: (data: string) => void;\r\n resize: (cols: number, rows: number) => void;\r\n kill: () => void;\r\n onData: (callback: (data: string) => void) => void;\r\n onExit: (callback: (e: { exitCode: number }) => void) => void;\r\n pid: number;\r\n}\r\n\r\n// Remote PTY process interface (SSH/WSL/Docker)\r\n// These have a different callback signature (using onData string callback directly)\r\nexport interface RemotePtyProcess {\r\n write: (data: string) => void;\r\n kill: () => void;\r\n resize: (cols: number, rows: number) => void;\r\n isRunning: () => boolean;\r\n}\r\n\r\nexport interface BackgroundTask {\r\n id: string;\r\n command: string;\r\n cwd: string;\r\n startTime: Date;\r\n output: string;\r\n isRunning: boolean;\r\n exitCode?: number;\r\n error?: string;\r\n ptyProcess?: PtyProcess;\r\n remotePtyProcess?: RemotePtyProcess;\r\n remoteContext?: string; // Display string for remote context (e.g., \"user@host\", \"wsl:Ubuntu\")\r\n}\r\n\r\n\r\nexport interface BackgroundTaskInfo {\r\n id: string;\r\n command: string;\r\n cwd: string;\r\n startTime: Date;\r\n durationMs: number;\r\n isRunning: boolean;\r\n exitCode?: number;\r\n error?: string;\r\n outputPreview: string;\r\n}\r\n\r\nclass BackgroundTaskManagerClass extends EventEmitter {\r\n private tasks: Map<string, BackgroundTask> = new Map();\r\n private taskCounter: number = 0;\r\n\r\n /**\r\n * Start a new background task using PTY\r\n */\r\n startTask(command: string, cwd: string): string {\r\n const id = `bkg-${++this.taskCounter}-${Date.now()}`;\r\n\r\n const task: BackgroundTask = {\r\n id,\r\n command,\r\n cwd,\r\n startTime: new Date(),\r\n output: '',\r\n isRunning: true,\r\n };\r\n\r\n // Determine shell based on platform\r\n const isWindows = os.platform() === 'win32';\r\n const shell = isWindows ? 'powershell.exe' : (process.env.SHELL || '/bin/bash');\r\n const args = isWindows ? ['-Command', command] : ['-c', command];\r\n\r\n // Get initial terminal dimensions\r\n const cols = process.stdout.columns || 80;\r\n const rows = process.stdout.rows || 24;\r\n\r\n try {\r\n // Spawn PTY process for proper terminal emulation\r\n const ptyProcess = nodePty.spawn(shell, args, {\r\n name: 'xterm-256color',\r\n cols,\r\n rows,\r\n cwd,\r\n env: {\r\n ...process.env,\r\n TERM: 'xterm-256color',\r\n COLORTERM: 'truecolor',\r\n FORCE_COLOR: '1',\r\n CLICOLOR: '1',\r\n PYTHONUNBUFFERED: '1',\r\n } as { [key: string]: string },\r\n });\r\n\r\n task.ptyProcess = ptyProcess;\r\n\r\n // Capture output (capped to prevent unbounded memory growth)\r\n const MAX_OUTPUT_SIZE = 100 * 1024; // 100KB max\r\n ptyProcess.onData((data: string) => {\r\n task.output += data;\r\n if (task.output.length > MAX_OUTPUT_SIZE) {\r\n task.output = task.output.slice(-MAX_OUTPUT_SIZE);\r\n }\r\n this.emit('taskOutput', id, data);\r\n });\r\n\r\n // Handle process completion\r\n ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {\r\n task.isRunning = false;\r\n task.exitCode = exitCode;\r\n\r\n // Debug logging\r\n const fs = require('fs');\r\n try {\r\n quickLog(`[${new Date().toISOString()}] [BackgroundTaskManager] taskCompleted event emitted: id=${id}, exitCode=${exitCode}\\n`);\r\n } catch (e) { }\r\n\r\n this.emit('taskCompleted', id, exitCode);\r\n this.emit('countChanged', this.getRunningCount());\r\n });\r\n\r\n this.tasks.set(id, task);\r\n this.emit('taskStarted', id, command);\r\n this.emit('countChanged', this.getRunningCount());\r\n\r\n return id;\r\n } catch (err: any) {\r\n task.isRunning = false;\r\n task.error = err.message;\r\n this.tasks.set(id, task);\r\n this.emit('taskError', id, err.message);\r\n return id;\r\n }\r\n }\r\n\r\n /**\r\n * Start a new remote background task (SSH/WSL/Docker)\r\n * This registers the task and returns callbacks that should be passed to the remote PTY creator\r\n * @param command The command being executed\r\n * @param cwd The working directory on the remote system\r\n * @param remoteContext Display string for the remote context (e.g., \"user@host\", \"wsl:Ubuntu\")\r\n * @returns Object with task ID and callbacks to pass to the remote PTY creator\r\n */\r\n startRemoteTask(\r\n command: string,\r\n cwd: string,\r\n remoteContext: string\r\n ): {\r\n id: string;\r\n onData: (data: string) => void;\r\n onExit: (exitCode: number) => void;\r\n setRemotePty: (remotePty: RemotePtyProcess) => void;\r\n } {\r\n const id = `bkg-${++this.taskCounter}-${Date.now()}`;\r\n\r\n const task: BackgroundTask = {\r\n id,\r\n command,\r\n cwd,\r\n startTime: new Date(),\r\n output: '',\r\n isRunning: true,\r\n remoteContext\r\n };\r\n\r\n this.tasks.set(id, task);\r\n this.emit('taskStarted', id, command);\r\n this.emit('countChanged', this.getRunningCount());\r\n\r\n // Return callbacks that the caller should pass to runSSHCommand/runWSLCommand/runDockerCommand\r\n return {\r\n id,\r\n onData: (data: string) => {\r\n task.output += data;\r\n if (task.output.length > 100 * 1024) {\r\n task.output = task.output.slice(-100 * 1024);\r\n }\r\n this.emit('taskOutput', id, data);\r\n },\r\n onExit: (exitCode: number) => {\r\n task.isRunning = false;\r\n task.exitCode = exitCode;\r\n\r\n // Debug logging\r\n const fs = require('fs');\r\n try {\r\n quickLog(`[${new Date().toISOString()}] [BackgroundTaskManager] Remote taskCompleted event emitted: id=${id}, exitCode=${exitCode}\\n`);\r\n } catch (e) { }\r\n\r\n this.emit('taskCompleted', id, exitCode);\r\n this.emit('countChanged', this.getRunningCount());\r\n },\r\n setRemotePty: (remotePty: RemotePtyProcess) => {\r\n task.remotePtyProcess = remotePty;\r\n }\r\n };\r\n }\r\n\r\n /**\r\n * Adopt a running foreground process into background task management.\r\n * \r\n * This is used for timeout-based transfer: a command started as foreground\r\n * exceeds its timeout, so the STILL-RUNNING process is handed over to the\r\n * background task manager without killing it. The process continues running\r\n * and its output is captured by the background task.\r\n * \r\n * @param command The command string (for display)\r\n * @param cwd The working directory (for display)\r\n * @param existingOutput Output already captured while the process was foreground\r\n * @param remoteContext Optional remote context label (e.g. \"user@host\")\r\n * @returns Object with task ID and callbacks for wiring into the running process\r\n */\r\n adoptRunningProcess(\r\n command: string,\r\n cwd: string,\r\n existingOutput: string,\r\n remoteContext?: string,\r\n ): {\r\n id: string;\r\n onData: (data: string) => void;\r\n onExit: (exitCode: number) => void;\r\n setRemotePty: (remotePty: RemotePtyProcess) => void;\r\n } {\r\n const id = `bkg-${++this.taskCounter}-${Date.now()}`;\r\n\r\n const task: BackgroundTask = {\r\n id,\r\n command,\r\n cwd,\r\n startTime: new Date(),\r\n output: existingOutput,\r\n isRunning: true,\r\n remoteContext,\r\n };\r\n\r\n this.tasks.set(id, task);\r\n this.emit('taskStarted', id, command);\r\n this.emit('countChanged', this.getRunningCount());\r\n\r\n quickLog(`[${new Date().toISOString()}] [BackgroundTaskManager] Adopted running process as ${id}: ${command}\\n`);\r\n\r\n return {\r\n id,\r\n onData: (data: string) => {\r\n task.output += data;\r\n if (task.output.length > 100 * 1024) {\r\n task.output = task.output.slice(-100 * 1024);\r\n }\r\n this.emit('taskOutput', id, data);\r\n },\r\n onExit: (exitCode: number) => {\r\n task.isRunning = false;\r\n task.exitCode = exitCode;\r\n\r\n quickLog(`[${new Date().toISOString()}] [BackgroundTaskManager] Adopted task completed: id=${id}, exitCode=${exitCode}\\n`);\r\n\r\n this.emit('taskCompleted', id, exitCode);\r\n this.emit('countChanged', this.getRunningCount());\r\n },\r\n setRemotePty: (remotePty: RemotePtyProcess) => {\r\n task.remotePtyProcess = remotePty;\r\n }\r\n };\r\n }\r\n\r\n\r\n /**\r\n * Get all tasks (running and completed)\r\n */\r\n\r\n getAllTasks(): BackgroundTaskInfo[] {\r\n const now = Date.now();\r\n return Array.from(this.tasks.values()).map(task => ({\r\n id: task.id,\r\n command: task.command,\r\n cwd: task.cwd,\r\n startTime: task.startTime,\r\n durationMs: now - task.startTime.getTime(),\r\n isRunning: task.isRunning,\r\n exitCode: task.exitCode,\r\n error: task.error,\r\n outputPreview: task.output.slice(-200), // Last 200 chars\r\n }));\r\n }\r\n\r\n /**\r\n * Get only running tasks\r\n */\r\n getRunningTasks(): BackgroundTaskInfo[] {\r\n return this.getAllTasks().filter(t => t.isRunning);\r\n }\r\n\r\n /**\r\n * Get the count of running tasks\r\n */\r\n getRunningCount(): number {\r\n let count = 0;\r\n for (const task of this.tasks.values()) {\r\n if (task.isRunning) count++;\r\n }\r\n return count;\r\n }\r\n\r\n /**\r\n * Get a specific task by ID\r\n */\r\n getTask(id: string): BackgroundTask | undefined {\r\n return this.tasks.get(id);\r\n }\r\n\r\n /**\r\n * Get task output (full output for streaming view)\r\n */\r\n getTaskOutput(id: string): string {\r\n return this.tasks.get(id)?.output || '';\r\n }\r\n\r\n /**\r\n * Send input to a running background task (via PTY or remote PTY)\r\n */\r\n sendInput(id: string, input: string): boolean {\r\n const task = this.tasks.get(id);\r\n\r\n // Debug logging\r\n const fs = require('fs');\r\n try {\r\n quickLog(`[${new Date().toISOString()}] [BackgroundTaskManager.sendInput] taskId: ${id}, input: ${JSON.stringify(input)}, taskFound: ${!!task}, isRunning: ${task?.isRunning}, hasPty: ${!!task?.ptyProcess}, hasRemotePty: ${!!task?.remotePtyProcess}\\n`);\r\n } catch (e) { }\r\n\r\n if (!task || !task.isRunning) {\r\n return false;\r\n }\r\n\r\n try {\r\n // Handle local PTY\r\n if (task.ptyProcess) {\r\n task.ptyProcess.write(input);\r\n return true;\r\n }\r\n // Handle remote PTY (SSH/WSL/Docker)\r\n if (task.remotePtyProcess) {\r\n task.remotePtyProcess.write(input);\r\n return true;\r\n }\r\n return false;\r\n } catch (err) {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Send a signal to a running background task (via PTY or remote PTY)\r\n * For PTY, we send control characters directly - same as normal shell focus mode\r\n */\r\n sendSignal(id: string, signal: NodeJS.Signals): boolean {\r\n const task = this.tasks.get(id);\r\n if (!task || !task.isRunning) {\r\n return false;\r\n }\r\n\r\n try {\r\n // Handle local PTY\r\n if (task.ptyProcess) {\r\n if (signal === 'SIGINT') {\r\n task.ptyProcess.write('\\x03'); // Ctrl+C\r\n setTimeout(() => {\r\n if (task.isRunning && task.ptyProcess) {\r\n task.ptyProcess.kill();\r\n }\r\n }, 500);\r\n } else {\r\n task.ptyProcess.kill();\r\n }\r\n return true;\r\n }\r\n // Handle remote PTY (SSH/WSL/Docker)\r\n if (task.remotePtyProcess) {\r\n if (signal === 'SIGINT') {\r\n task.remotePtyProcess.write('\\x03'); // Ctrl+C\r\n setTimeout(() => {\r\n if (task.isRunning && task.remotePtyProcess) {\r\n task.remotePtyProcess.kill();\r\n }\r\n }, 500);\r\n } else {\r\n task.remotePtyProcess.kill();\r\n }\r\n return true;\r\n }\r\n return false;\r\n } catch (err) {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Cancel a running task (via PTY or remote PTY)\r\n */\r\n cancelTask(id: string): boolean {\r\n const task = this.tasks.get(id);\r\n if (!task || !task.isRunning) {\r\n return false;\r\n }\r\n\r\n try {\r\n // Handle local PTY\r\n if (task.ptyProcess) {\r\n task.ptyProcess.kill();\r\n }\r\n // Handle remote PTY (SSH/WSL/Docker)\r\n if (task.remotePtyProcess) {\r\n task.remotePtyProcess.kill();\r\n }\r\n\r\n task.isRunning = false;\r\n task.error = 'Cancelled by user';\r\n this.emit('taskCancelled', id);\r\n this.emit('countChanged', this.getRunningCount());\r\n return true;\r\n } catch (err) {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Clear completed tasks from the list\r\n */\r\n clearCompleted(): void {\r\n for (const [id, task] of this.tasks.entries()) {\r\n if (!task.isRunning) {\r\n this.tasks.delete(id);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Check if a task is running\r\n */\r\n isTaskRunning(id: string): boolean {\r\n return this.tasks.get(id)?.isRunning ?? false;\r\n }\r\n}\r\n\r\n// Singleton instance\r\nexport const BackgroundTaskManager = new BackgroundTaskManagerClass();\r\n"],"mappings":"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","names":["require"]}
|
|
@@ -2,6 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as os from "os";
|
|
4
4
|
import { logError } from "../utils/logger.js";
|
|
5
|
+
import { cleanupToolOutputsForChat } from "../utils/output-truncation.js";
|
|
5
6
|
class LocalChatStorage {
|
|
6
7
|
chatsDir;
|
|
7
8
|
indexPath;
|
|
@@ -291,6 +292,7 @@ class LocalChatStorage {
|
|
|
291
292
|
if (fs.existsSync(checkpointsPath)) {
|
|
292
293
|
fs.rmSync(checkpointsPath, { recursive: true, force: true });
|
|
293
294
|
}
|
|
295
|
+
cleanupToolOutputsForChat(chatId);
|
|
294
296
|
this.chatIndex.delete(chatId);
|
|
295
297
|
this.saveIndex();
|
|
296
298
|
return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/local-chat-storage.ts"],"sourcesContent":["/**\r\n * Local Chat Storage Service\r\n * \r\n * Provides persistent local storage for CLI conversations.\r\n * Stores chats in ~/.centaurus/chats/ directory as JSON files.\r\n */\r\n\r\nimport * as fs from 'fs';\r\nimport * as path from 'path';\r\nimport * as os from 'os';\r\nimport { logError } from '../utils/logger.js';\r\n\r\n/**\r\n * Message interface matching the AIMessage type from ai-service-client\r\n */\r\nexport interface StoredMessage {\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n tool_calls?: Array<{\r\n id: string;\r\n name: string;\r\n arguments: Record<string, any>;\r\n }>;\r\n tool_call_id?: string;\r\n}\r\n\r\n/**\r\n * UI Message for restoring full chat history display\r\n * This is a serializable version of the Message type from types/index.ts\r\n */\r\nexport interface StoredUIMessage {\r\n id: string;\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n timestamp?: string; // ISO string (Date is not JSON serializable)\r\n toolExecution?: {\r\n toolName: string;\r\n status: 'pending' | 'executing' | 'completed' | 'error';\r\n result?: string;\r\n error?: string;\r\n arguments?: Record<string, any>;\r\n };\r\n shouldStream?: boolean;\r\n isCommandMode?: boolean;\r\n tool_call_id?: string;\r\n tool_calls?: Array<{ id: string; name: string; arguments: Record<string, any> }>;\r\n thinkingDuration?: number;\r\n taskCompletion?: {\r\n taskNumber: number;\r\n totalTasks: number;\r\n taskDescription: string;\r\n completionNote?: string;\r\n };\r\n planAccepted?: {\r\n planTitle?: string;\r\n totalTasks?: number;\r\n };\r\n connectionStatus?: { // For SSH/WSL/Docker connection status\r\n type: 'ssh' | 'wsl' | 'docker';\r\n status: 'connecting' | 'connected' | 'error' | 'disconnected';\r\n connectionString?: string;\r\n error?: string;\r\n };\r\n}\r\n\r\n/**\r\n * Remote environment context for SSH/WSL/Docker sessions\r\n * Used to restore remote session state when resuming a chat\r\n */\r\nexport interface StoredRemoteContext {\r\n type: 'ssh' | 'wsl' | 'docker';\r\n connectionCommand: string; // Original command to reconnect (e.g., \"ssh user@host\" or \"wsl\")\r\n remoteCwd: string; // CWD in the remote environment\r\n localCwdBeforeRemote: string; // Local CWD before entering remote session\r\n metadata?: {\r\n hostname?: string; // For SSH\r\n username?: string; // For SSH \r\n distroName?: string; // For WSL\r\n containerId?: string; // For Docker\r\n port?: number; // For SSH\r\n };\r\n}\r\n\r\n/**\r\n * Serialized checkpoint metadata stored with chat history.\r\n * This allows checkpoint state to be restored even if the checkpoint index file is missing.\r\n */\r\nexport interface StoredCheckpointMeta {\r\n id: string;\r\n prompt: string;\r\n createdAt: string;\r\n createdAtMs: number;\r\n cwd: string;\r\n contextType: 'local' | 'ssh' | 'wsl' | 'docker';\r\n remoteSessionInfo?: {\r\n hostname?: string;\r\n username?: string;\r\n sessionId: string;\r\n connectionString?: string;\r\n };\r\n conversationIndex: number;\r\n uiMessageIndex?: number;\r\n uiMessageId?: string;\r\n snapshotDir: string;\r\n manifestPath: string;\r\n changes?: {\r\n added: string[];\r\n modified: string[];\r\n deleted: string[];\r\n };\r\n commands: string[];\r\n toolCalls: Array<{\r\n id?: string;\r\n name: string;\r\n arguments?: Record<string, any>;\r\n timestamp: string;\r\n }>;\r\n status: 'active' | 'finalized' | 'discarded';\r\n}\r\n\r\n/**\r\n * Local chat metadata (without messages, for listing)\r\n */\r\nexport interface LocalChatMeta {\r\n id: string;\r\n title: string;\r\n createdAt: string;\r\n updatedAt: string;\r\n messageCount: number;\r\n environment?: string; // 'local' or 'ssh:user@host' or 'wsl:distro' or 'docker:container'\r\n}\r\n\r\n/**\r\n * Full local chat with messages\r\n */\r\nexport interface LocalChat extends LocalChatMeta {\r\n messages: StoredMessage[];\r\n uiMessages?: StoredUIMessage[]; // Full UI history for restoration\r\n cwd?: string; // Working directory at time of save (for restoration)\r\n remoteContext?: StoredRemoteContext; // Remote context if saved while in SSH/WSL/Docker (backward compat)\r\n remoteContextStack?: StoredRemoteContext[]; // Stack of nested contexts (for SSH>SSH, SSH>Docker, etc.)\r\n checkpointIndex?: StoredCheckpointMeta[]; // Serialized checkpoint metadata for resume/revert restoration\r\n backendConversationId?: string; // Backend conversation UUID (for file storage/deletion)\r\n}\r\n\r\n/**\r\n * LocalChatStorage class for managing local conversation persistence\r\n */\r\nexport class LocalChatStorage {\r\n private chatsDir: string;\r\n private indexPath: string;\r\n private chatIndex: Map<string, LocalChatMeta> = new Map();\r\n private indexLoaded: boolean = false;\r\n\r\n constructor() {\r\n const homeDir = os.homedir();\r\n this.chatsDir = path.join(homeDir, '.centaurus', 'chats');\r\n this.indexPath = path.join(this.chatsDir, 'index.json');\r\n\r\n // Ensure chats directory exists\r\n if (!fs.existsSync(this.chatsDir)) {\r\n fs.mkdirSync(this.chatsDir, { recursive: true });\r\n }\r\n\r\n // Load or build the index\r\n this.loadIndex();\r\n }\r\n\r\n /**\r\n * Load the metadata index from disk, or rebuild it from chat files if missing\r\n */\r\n private loadIndex(): void {\r\n if (this.indexLoaded) return;\r\n\r\n try {\r\n if (fs.existsSync(this.indexPath)) {\r\n const data = fs.readFileSync(this.indexPath, 'utf-8');\r\n const indexArray: LocalChatMeta[] = JSON.parse(data);\r\n this.chatIndex.clear();\r\n for (const meta of indexArray) {\r\n this.chatIndex.set(meta.id, meta);\r\n }\r\n this.indexLoaded = true;\r\n return;\r\n }\r\n } catch (error) {\r\n // Index file corrupted, rebuild it\r\n }\r\n\r\n // Rebuild index from chat files (one-time migration)\r\n this.rebuildIndex();\r\n }\r\n\r\n /**\r\n * Rebuild the index by reading metadata from all chat files\r\n * This is a one-time migration that reads full files, but only happens once\r\n */\r\n private rebuildIndex(): void {\r\n this.chatIndex.clear();\r\n\r\n if (!fs.existsSync(this.chatsDir)) {\r\n this.indexLoaded = true;\r\n return;\r\n }\r\n\r\n const files = fs.readdirSync(this.chatsDir)\r\n .filter(f => f.endsWith('.json') && f !== 'index.json');\r\n\r\n for (const file of files) {\r\n try {\r\n const filePath = path.join(this.chatsDir, file);\r\n const data = fs.readFileSync(filePath, 'utf-8');\r\n const chat = JSON.parse(data) as LocalChat;\r\n\r\n // Determine environment string from remoteContext\r\n let environment = 'local';\r\n if (chat.remoteContext) {\r\n const rc = chat.remoteContext;\r\n if (rc.type === 'ssh' && rc.metadata?.hostname) {\r\n environment = `ssh:${rc.metadata.username || 'user'}@${rc.metadata.hostname}`;\r\n } else if (rc.type === 'wsl') {\r\n environment = `wsl:${rc.metadata?.distroName || 'Ubuntu'}`;\r\n } else if (rc.type === 'docker' && rc.metadata?.containerId) {\r\n environment = `docker:${rc.metadata.containerId.substring(0, 12)}`;\r\n } else {\r\n environment = rc.type;\r\n }\r\n }\r\n\r\n // Store metadata only\r\n this.chatIndex.set(chat.id, {\r\n id: chat.id,\r\n title: chat.title,\r\n createdAt: chat.createdAt,\r\n updatedAt: chat.updatedAt,\r\n messageCount: chat.messageCount,\r\n environment,\r\n });\r\n } catch (error) {\r\n // Skip invalid files\r\n continue;\r\n }\r\n }\r\n\r\n this.indexLoaded = true;\r\n this.saveIndex();\r\n }\r\n\r\n /**\r\n * Save the metadata index to disk\r\n */\r\n private saveIndex(): void {\r\n try {\r\n const indexArray = Array.from(this.chatIndex.values());\r\n fs.writeFileSync(this.indexPath, JSON.stringify(indexArray, null, 2), 'utf-8');\r\n } catch (error) {\r\n logError('Failed to save chat index', error as Error);\r\n }\r\n }\r\n\r\n /**\r\n * Update the index entry for a chat\r\n */\r\n private updateIndexEntry(chat: LocalChat): void {\r\n let environment = 'local';\r\n if (chat.remoteContext) {\r\n const rc = chat.remoteContext;\r\n if (rc.type === 'ssh' && rc.metadata?.hostname) {\r\n environment = `ssh:${rc.metadata.username || 'user'}@${rc.metadata.hostname}`;\r\n } else if (rc.type === 'wsl') {\r\n environment = `wsl:${rc.metadata?.distroName || 'Ubuntu'}`;\r\n } else if (rc.type === 'docker' && rc.metadata?.containerId) {\r\n environment = `docker:${rc.metadata.containerId.substring(0, 12)}`;\r\n } else {\r\n environment = rc.type;\r\n }\r\n }\r\n\r\n this.chatIndex.set(chat.id, {\r\n id: chat.id,\r\n title: chat.title,\r\n createdAt: chat.createdAt,\r\n updatedAt: chat.updatedAt,\r\n messageCount: chat.messageCount,\r\n environment,\r\n });\r\n this.saveIndex();\r\n }\r\n\r\n /**\r\n * Generate a unique chat ID based on timestamp\r\n */\r\n generateChatId(): string {\r\n const now = new Date();\r\n const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);\r\n const random = Math.random().toString(36).substring(2, 6);\r\n return `chat-${timestamp}-${random}`;\r\n }\r\n\r\n /**\r\n * Generate a title from the first user message\r\n */\r\n generateTitle(messages: StoredMessage[]): string {\r\n const firstUserMessage = messages.find(m => m.role === 'user');\r\n if (!firstUserMessage || !firstUserMessage.content) {\r\n return 'New Chat';\r\n }\r\n\r\n // Take first 60 chars, trim to last word boundary\r\n let title = firstUserMessage.content.substring(0, 60);\r\n if (firstUserMessage.content.length > 60) {\r\n const lastSpace = title.lastIndexOf(' ');\r\n if (lastSpace > 30) {\r\n title = title.substring(0, lastSpace);\r\n }\r\n title += '...';\r\n }\r\n\r\n // Clean up whitespace and newlines\r\n title = title.replace(/\\s+/g, ' ').trim();\r\n\r\n return title || 'New Chat';\r\n }\r\n\r\n /**\r\n * Get the file path for a chat\r\n */\r\n private getChatPath(chatId: string): string {\r\n return path.join(this.chatsDir, `${chatId}.json`);\r\n }\r\n\r\n /**\r\n * Save a chat to disk\r\n * @param remoteContext - undefined = preserve existing, null = clear, StoredRemoteContext = set new value\r\n * @param remoteContextStack - Stack of nested contexts for SSH>SSH, SSH>Docker etc.\r\n * @param checkpointIndex - undefined = preserve existing, null = clear, array = set latest checkpoint metadata\r\n */\r\n saveChat(\r\n chatId: string,\r\n messages: StoredMessage[],\r\n uiMessages?: StoredUIMessage[],\r\n cwd?: string,\r\n remoteContext?: StoredRemoteContext | null,\r\n remoteContextStack?: StoredRemoteContext[] | null,\r\n checkpointIndex?: StoredCheckpointMeta[] | null\r\n ): LocalChat {\r\n const chatPath = this.getChatPath(chatId);\r\n const now = new Date().toISOString();\r\n\r\n let chat: LocalChat;\r\n\r\n if (fs.existsSync(chatPath)) {\r\n // Update existing chat\r\n const existing = this.loadChat(chatId);\r\n if (existing) {\r\n chat = {\r\n ...existing,\r\n messages,\r\n uiMessages: uiMessages || existing.uiMessages,\r\n cwd: cwd || existing.cwd, // Preserve CWD if not provided\r\n // remoteContext: null = clear it, undefined = preserve existing\r\n remoteContext: remoteContext === null ? undefined : (remoteContext !== undefined ? remoteContext : existing.remoteContext),\r\n // remoteContextStack: null = clear it, undefined = preserve existing\r\n remoteContextStack: remoteContextStack === null ? undefined : (remoteContextStack !== undefined ? remoteContextStack : existing.remoteContextStack),\r\n // checkpointIndex: null = clear it, undefined = preserve existing\r\n checkpointIndex: checkpointIndex === null ? undefined : (checkpointIndex !== undefined ? checkpointIndex : existing.checkpointIndex),\r\n // Preserve backendConversationId if it exists\r\n backendConversationId: existing.backendConversationId,\r\n messageCount: messages.length,\r\n updatedAt: now,\r\n };\r\n } else {\r\n // File exists but couldn't be parsed, create new\r\n chat = {\r\n id: chatId,\r\n title: this.generateTitle(messages),\r\n createdAt: now,\r\n updatedAt: now,\r\n messageCount: messages.length,\r\n messages,\r\n uiMessages,\r\n cwd,\r\n remoteContext: remoteContext ?? undefined,\r\n remoteContextStack: remoteContextStack ?? undefined,\r\n checkpointIndex: checkpointIndex ?? undefined,\r\n };\r\n }\r\n } else {\r\n // Create new chat\r\n chat = {\r\n id: chatId,\r\n title: this.generateTitle(messages),\r\n createdAt: now,\r\n updatedAt: now,\r\n messageCount: messages.length,\r\n messages,\r\n uiMessages,\r\n cwd,\r\n remoteContext: remoteContext ?? undefined,\r\n remoteContextStack: remoteContextStack ?? undefined,\r\n checkpointIndex: checkpointIndex ?? undefined,\r\n };\r\n }\r\n\r\n fs.writeFileSync(chatPath, JSON.stringify(chat, null, 2), 'utf-8');\r\n\r\n // Update the metadata index\r\n this.updateIndexEntry(chat);\r\n\r\n return chat;\r\n }\r\n\r\n /**\r\n * Set the backend conversation ID for a chat\r\n * This is used to associate local chats with backend file storage\r\n */\r\n setBackendConversationId(chatId: string, backendConversationId: string): boolean {\r\n const chat = this.loadChat(chatId);\r\n if (!chat) {\r\n return false;\r\n }\r\n\r\n try {\r\n chat.backendConversationId = backendConversationId;\r\n chat.updatedAt = new Date().toISOString();\r\n const chatPath = this.getChatPath(chatId);\r\n fs.writeFileSync(chatPath, JSON.stringify(chat, null, 2), 'utf-8');\r\n return true;\r\n } catch (error) {\r\n logError(`Failed to set backend conversation ID for chat ${chatId}`, error as Error);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Get the backend conversation ID for a chat (if any)\r\n */\r\n getBackendConversationId(chatId: string): string | undefined {\r\n const chat = this.loadChat(chatId);\r\n return chat?.backendConversationId;\r\n }\r\n\r\n /**\r\n * Load a chat by ID\r\n */\r\n loadChat(chatId: string): LocalChat | null {\r\n const chatPath = this.getChatPath(chatId);\r\n\r\n if (!fs.existsSync(chatPath)) {\r\n return null;\r\n }\r\n\r\n try {\r\n const data = fs.readFileSync(chatPath, 'utf-8');\r\n return JSON.parse(data) as LocalChat;\r\n } catch (error) {\r\n logError(`Failed to load chat ${chatId}`, error as Error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * List all chats (metadata only, sorted by updatedAt descending)\r\n * Uses the in-memory index to avoid loading full chat files\r\n */\r\n listChats(): LocalChatMeta[] {\r\n // Ensure index is loaded\r\n this.loadIndex();\r\n\r\n // Return sorted metadata from the cache\r\n const chats = Array.from(this.chatIndex.values());\r\n\r\n // Sort by updatedAt descending (most recent first)\r\n chats.sort((a, b) =>\r\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()\r\n );\r\n\r\n return chats;\r\n }\r\n\r\n /**\r\n * Delete a chat by ID\r\n */\r\n deleteChat(chatId: string): boolean {\r\n const chatPath = this.getChatPath(chatId);\r\n const checkpointsPath = path.join(os.homedir(), '.centaurus', 'checkpoints', chatId);\r\n\r\n if (!fs.existsSync(chatPath)) {\r\n // Best-effort cleanup for orphaned checkpoint data.\r\n if (fs.existsSync(checkpointsPath)) {\r\n try {\r\n fs.rmSync(checkpointsPath, { recursive: true, force: true });\r\n } catch {\r\n // Ignore cleanup errors for missing chats.\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n try {\r\n fs.unlinkSync(chatPath);\r\n if (fs.existsSync(checkpointsPath)) {\r\n fs.rmSync(checkpointsPath, { recursive: true, force: true });\r\n }\r\n\r\n // Remove from index\r\n this.chatIndex.delete(chatId);\r\n this.saveIndex();\r\n\r\n return true;\r\n } catch (error) {\r\n logError(`Failed to delete chat ${chatId}`, error as Error);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Import a full chat object (used for restoring from cloud sync)\r\n * Overwrites existing chat with same ID if present\r\n */\r\n importChat(chat: LocalChat): boolean {\r\n if (!chat || !chat.id) {\r\n return false;\r\n }\r\n\r\n try {\r\n const chatPath = this.getChatPath(chat.id);\r\n\r\n // Ensure the chat has required fields\r\n const now = new Date().toISOString();\r\n const importedChat: LocalChat = {\r\n id: chat.id,\r\n title: chat.title || 'Imported Chat',\r\n createdAt: chat.createdAt || now,\r\n updatedAt: now, // Always update the timestamp on import\r\n messageCount: chat.messages?.length || chat.messageCount || 0,\r\n messages: chat.messages || [],\r\n uiMessages: chat.uiMessages,\r\n cwd: chat.cwd,\r\n remoteContext: chat.remoteContext,\r\n remoteContextStack: chat.remoteContextStack,\r\n checkpointIndex: chat.checkpointIndex,\r\n backendConversationId: chat.backendConversationId,\r\n environment: chat.environment,\r\n };\r\n\r\n fs.writeFileSync(chatPath, JSON.stringify(importedChat, null, 2), 'utf-8');\r\n\r\n // Update the index\r\n this.updateIndexEntry(importedChat);\r\n\r\n return true;\r\n } catch (error) {\r\n logError(`Failed to import chat ${chat.id}`, error as Error);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Rename a chat by ID\r\n */\r\n renameChat(chatId: string, newTitle: string): boolean {\r\n const chat = this.loadChat(chatId);\r\n if (!chat) {\r\n return false;\r\n }\r\n\r\n try {\r\n chat.title = newTitle;\r\n chat.updatedAt = new Date().toISOString();\r\n const chatPath = this.getChatPath(chatId);\r\n fs.writeFileSync(chatPath, JSON.stringify(chat, null, 2), 'utf-8');\r\n\r\n // Update the index\r\n this.updateIndexEntry(chat);\r\n\r\n return true;\r\n } catch (error) {\r\n logError(`Failed to rename chat ${chatId}`, error as Error);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Get the chats directory path\r\n */\r\n getChatsDir(): string {\r\n return this.chatsDir;\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const localChatStorage = new LocalChatStorage();\r\n"],"mappings":"AAOA,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,YAAY,QAAQ;AACpB,SAAS,gBAAgB;AA0IlB,MAAM,iBAAiB;AAAA,EACpB;AAAA,EACA;AAAA,EACA,YAAwC,oBAAI,IAAI;AAAA,EAChD,cAAuB;AAAA,EAE/B,cAAc;AACZ,UAAM,UAAU,GAAG,QAAQ;AAC3B,SAAK,WAAW,KAAK,KAAK,SAAS,cAAc,OAAO;AACxD,SAAK,YAAY,KAAK,KAAK,KAAK,UAAU,YAAY;AAGtD,QAAI,CAAC,GAAG,WAAW,KAAK,QAAQ,GAAG;AACjC,SAAG,UAAU,KAAK,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,IACjD;AAGA,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAkB;AACxB,QAAI,KAAK,YAAa;AAEtB,QAAI;AACF,UAAI,GAAG,WAAW,KAAK,SAAS,GAAG;AACjC,cAAM,OAAO,GAAG,aAAa,KAAK,WAAW,OAAO;AACpD,cAAM,aAA8B,KAAK,MAAM,IAAI;AACnD,aAAK,UAAU,MAAM;AACrB,mBAAW,QAAQ,YAAY;AAC7B,eAAK,UAAU,IAAI,KAAK,IAAI,IAAI;AAAA,QAClC;AACA,aAAK,cAAc;AACnB;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AAAA,IAEhB;AAGA,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,eAAqB;AAC3B,SAAK,UAAU,MAAM;AAErB,QAAI,CAAC,GAAG,WAAW,KAAK,QAAQ,GAAG;AACjC,WAAK,cAAc;AACnB;AAAA,IACF;AAEA,UAAM,QAAQ,GAAG,YAAY,KAAK,QAAQ,EACvC,OAAO,OAAK,EAAE,SAAS,OAAO,KAAK,MAAM,YAAY;AAExD,eAAW,QAAQ,OAAO;AACxB,UAAI;AACF,cAAM,WAAW,KAAK,KAAK,KAAK,UAAU,IAAI;AAC9C,cAAM,OAAO,GAAG,aAAa,UAAU,OAAO;AAC9C,cAAM,OAAO,KAAK,MAAM,IAAI;AAG5B,YAAI,cAAc;AAClB,YAAI,KAAK,eAAe;AACtB,gBAAM,KAAK,KAAK;AAChB,cAAI,GAAG,SAAS,SAAS,GAAG,UAAU,UAAU;AAC9C,0BAAc,OAAO,GAAG,SAAS,YAAY,MAAM,IAAI,GAAG,SAAS,QAAQ;AAAA,UAC7E,WAAW,GAAG,SAAS,OAAO;AAC5B,0BAAc,OAAO,GAAG,UAAU,cAAc,QAAQ;AAAA,UAC1D,WAAW,GAAG,SAAS,YAAY,GAAG,UAAU,aAAa;AAC3D,0BAAc,UAAU,GAAG,SAAS,YAAY,UAAU,GAAG,EAAE,CAAC;AAAA,UAClE,OAAO;AACL,0BAAc,GAAG;AAAA,UACnB;AAAA,QACF;AAGA,aAAK,UAAU,IAAI,KAAK,IAAI;AAAA,UAC1B,IAAI,KAAK;AAAA,UACT,OAAO,KAAK;AAAA,UACZ,WAAW,KAAK;AAAA,UAChB,WAAW,KAAK;AAAA,UAChB,cAAc,KAAK;AAAA,UACnB;AAAA,QACF,CAAC;AAAA,MACH,SAAS,OAAO;AAEd;AAAA,MACF;AAAA,IACF;AAEA,SAAK,cAAc;AACnB,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAkB;AACxB,QAAI;AACF,YAAM,aAAa,MAAM,KAAK,KAAK,UAAU,OAAO,CAAC;AACrD,SAAG,cAAc,KAAK,WAAW,KAAK,UAAU,YAAY,MAAM,CAAC,GAAG,OAAO;AAAA,IAC/E,SAAS,OAAO;AACd,eAAS,6BAA6B,KAAc;AAAA,IACtD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAiB,MAAuB;AAC9C,QAAI,cAAc;AAClB,QAAI,KAAK,eAAe;AACtB,YAAM,KAAK,KAAK;AAChB,UAAI,GAAG,SAAS,SAAS,GAAG,UAAU,UAAU;AAC9C,sBAAc,OAAO,GAAG,SAAS,YAAY,MAAM,IAAI,GAAG,SAAS,QAAQ;AAAA,MAC7E,WAAW,GAAG,SAAS,OAAO;AAC5B,sBAAc,OAAO,GAAG,UAAU,cAAc,QAAQ;AAAA,MAC1D,WAAW,GAAG,SAAS,YAAY,GAAG,UAAU,aAAa;AAC3D,sBAAc,UAAU,GAAG,SAAS,YAAY,UAAU,GAAG,EAAE,CAAC;AAAA,MAClE,OAAO;AACL,sBAAc,GAAG;AAAA,MACnB;AAAA,IACF;AAEA,SAAK,UAAU,IAAI,KAAK,IAAI;AAAA,MAC1B,IAAI,KAAK;AAAA,MACT,OAAO,KAAK;AAAA,MACZ,WAAW,KAAK;AAAA,MAChB,WAAW,KAAK;AAAA,MAChB,cAAc,KAAK;AAAA,MACnB;AAAA,IACF,CAAC;AACD,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAyB;AACvB,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,YAAY,IAAI,YAAY,EAAE,QAAQ,SAAS,GAAG,EAAE,MAAM,GAAG,EAAE;AACrE,UAAM,SAAS,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,CAAC;AACxD,WAAO,QAAQ,SAAS,IAAI,MAAM;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,UAAmC;AAC/C,UAAM,mBAAmB,SAAS,KAAK,OAAK,EAAE,SAAS,MAAM;AAC7D,QAAI,CAAC,oBAAoB,CAAC,iBAAiB,SAAS;AAClD,aAAO;AAAA,IACT;AAGA,QAAI,QAAQ,iBAAiB,QAAQ,UAAU,GAAG,EAAE;AACpD,QAAI,iBAAiB,QAAQ,SAAS,IAAI;AACxC,YAAM,YAAY,MAAM,YAAY,GAAG;AACvC,UAAI,YAAY,IAAI;AAClB,gBAAQ,MAAM,UAAU,GAAG,SAAS;AAAA,MACtC;AACA,eAAS;AAAA,IACX;AAGA,YAAQ,MAAM,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAExC,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAY,QAAwB;AAC1C,WAAO,KAAK,KAAK,KAAK,UAAU,GAAG,MAAM,OAAO;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,SACE,QACA,UACA,YACA,KACA,eACA,oBACA,iBACW;AACX,UAAM,WAAW,KAAK,YAAY,MAAM;AACxC,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AAEnC,QAAI;AAEJ,QAAI,GAAG,WAAW,QAAQ,GAAG;AAE3B,YAAM,WAAW,KAAK,SAAS,MAAM;AACrC,UAAI,UAAU;AACZ,eAAO;AAAA,UACL,GAAG;AAAA,UACH;AAAA,UACA,YAAY,cAAc,SAAS;AAAA,UACnC,KAAK,OAAO,SAAS;AAAA;AAAA;AAAA,UAErB,eAAe,kBAAkB,OAAO,SAAa,kBAAkB,SAAY,gBAAgB,SAAS;AAAA;AAAA,UAE5G,oBAAoB,uBAAuB,OAAO,SAAa,uBAAuB,SAAY,qBAAqB,SAAS;AAAA;AAAA,UAEhI,iBAAiB,oBAAoB,OAAO,SAAa,oBAAoB,SAAY,kBAAkB,SAAS;AAAA;AAAA,UAEpH,uBAAuB,SAAS;AAAA,UAChC,cAAc,SAAS;AAAA,UACvB,WAAW;AAAA,QACb;AAAA,MACF,OAAO;AAEL,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,OAAO,KAAK,cAAc,QAAQ;AAAA,UAClC,WAAW;AAAA,UACX,WAAW;AAAA,UACX,cAAc,SAAS;AAAA,UACvB;AAAA,UACA;AAAA,UACA;AAAA,UACA,eAAe,iBAAiB;AAAA,UAChC,oBAAoB,sBAAsB;AAAA,UAC1C,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF;AAAA,IACF,OAAO;AAEL,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO,KAAK,cAAc,QAAQ;AAAA,QAClC,WAAW;AAAA,QACX,WAAW;AAAA,QACX,cAAc,SAAS;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,QACA,eAAe,iBAAiB;AAAA,QAChC,oBAAoB,sBAAsB;AAAA,QAC1C,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF;AAEA,OAAG,cAAc,UAAU,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,OAAO;AAGjE,SAAK,iBAAiB,IAAI;AAE1B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,yBAAyB,QAAgB,uBAAwC;AAC/E,UAAM,OAAO,KAAK,SAAS,MAAM;AACjC,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,IACT;AAEA,QAAI;AACF,WAAK,wBAAwB;AAC7B,WAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAM,WAAW,KAAK,YAAY,MAAM;AACxC,SAAG,cAAc,UAAU,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,OAAO;AACjE,aAAO;AAAA,IACT,SAAS,OAAO;AACd,eAAS,kDAAkD,MAAM,IAAI,KAAc;AACnF,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,yBAAyB,QAAoC;AAC3D,UAAM,OAAO,KAAK,SAAS,MAAM;AACjC,WAAO,MAAM;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,QAAkC;AACzC,UAAM,WAAW,KAAK,YAAY,MAAM;AAExC,QAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAC5B,aAAO;AAAA,IACT;AAEA,QAAI;AACF,YAAM,OAAO,GAAG,aAAa,UAAU,OAAO;AAC9C,aAAO,KAAK,MAAM,IAAI;AAAA,IACxB,SAAS,OAAO;AACd,eAAS,uBAAuB,MAAM,IAAI,KAAc;AACxD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAA6B;AAE3B,SAAK,UAAU;AAGf,UAAM,QAAQ,MAAM,KAAK,KAAK,UAAU,OAAO,CAAC;AAGhD,UAAM;AAAA,MAAK,CAAC,GAAG,MACb,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,QAAyB;AAClC,UAAM,WAAW,KAAK,YAAY,MAAM;AACxC,UAAM,kBAAkB,KAAK,KAAK,GAAG,QAAQ,GAAG,cAAc,eAAe,MAAM;AAEnF,QAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAE5B,UAAI,GAAG,WAAW,eAAe,GAAG;AAClC,YAAI;AACF,aAAG,OAAO,iBAAiB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,QAC7D,QAAQ;AAAA,QAER;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAEA,QAAI;AACF,SAAG,WAAW,QAAQ;AACtB,UAAI,GAAG,WAAW,eAAe,GAAG;AAClC,WAAG,OAAO,iBAAiB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MAC7D;AAGA,WAAK,UAAU,OAAO,MAAM;AAC5B,WAAK,UAAU;AAEf,aAAO;AAAA,IACT,SAAS,OAAO;AACd,eAAS,yBAAyB,MAAM,IAAI,KAAc;AAC1D,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAW,MAA0B;AACnC,QAAI,CAAC,QAAQ,CAAC,KAAK,IAAI;AACrB,aAAO;AAAA,IACT;AAEA,QAAI;AACF,YAAM,WAAW,KAAK,YAAY,KAAK,EAAE;AAGzC,YAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,YAAM,eAA0B;AAAA,QAC9B,IAAI,KAAK;AAAA,QACT,OAAO,KAAK,SAAS;AAAA,QACrB,WAAW,KAAK,aAAa;AAAA,QAC7B,WAAW;AAAA;AAAA,QACX,cAAc,KAAK,UAAU,UAAU,KAAK,gBAAgB;AAAA,QAC5D,UAAU,KAAK,YAAY,CAAC;AAAA,QAC5B,YAAY,KAAK;AAAA,QACjB,KAAK,KAAK;AAAA,QACV,eAAe,KAAK;AAAA,QACpB,oBAAoB,KAAK;AAAA,QACzB,iBAAiB,KAAK;AAAA,QACtB,uBAAuB,KAAK;AAAA,QAC5B,aAAa,KAAK;AAAA,MACpB;AAEA,SAAG,cAAc,UAAU,KAAK,UAAU,cAAc,MAAM,CAAC,GAAG,OAAO;AAGzE,WAAK,iBAAiB,YAAY;AAElC,aAAO;AAAA,IACT,SAAS,OAAO;AACd,eAAS,yBAAyB,KAAK,EAAE,IAAI,KAAc;AAC3D,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,QAAgB,UAA2B;AACpD,UAAM,OAAO,KAAK,SAAS,MAAM;AACjC,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,IACT;AAEA,QAAI;AACF,WAAK,QAAQ;AACb,WAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAM,WAAW,KAAK,YAAY,MAAM;AACxC,SAAG,cAAc,UAAU,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,OAAO;AAGjE,WAAK,iBAAiB,IAAI;AAE1B,aAAO;AAAA,IACT,SAAS,OAAO;AACd,eAAS,yBAAyB,MAAM,IAAI,KAAc;AAC1D,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,cAAsB;AACpB,WAAO,KAAK;AAAA,EACd;AACF;AAGO,MAAM,mBAAmB,IAAI,iBAAiB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/services/local-chat-storage.ts"],"sourcesContent":["/**\r\n * Local Chat Storage Service\r\n * \r\n * Provides persistent local storage for CLI conversations.\r\n * Stores chats in ~/.centaurus/chats/ directory as JSON files.\r\n */\r\n\r\nimport * as fs from 'fs';\r\nimport * as path from 'path';\r\nimport * as os from 'os';\r\nimport { logError } from '../utils/logger.js';\r\nimport { cleanupToolOutputsForChat } from '../utils/output-truncation.js';\r\n\r\n/**\r\n * Message interface matching the AIMessage type from ai-service-client\r\n */\r\nexport interface StoredMessage {\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n tool_calls?: Array<{\r\n id: string;\r\n name: string;\r\n arguments: Record<string, any>;\r\n }>;\r\n tool_call_id?: string;\r\n}\r\n\r\n/**\r\n * UI Message for restoring full chat history display\r\n * This is a serializable version of the Message type from types/index.ts\r\n */\r\nexport interface StoredUIMessage {\r\n id: string;\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n timestamp?: string; // ISO string (Date is not JSON serializable)\r\n toolExecution?: {\r\n toolName: string;\r\n status: 'pending' | 'executing' | 'completed' | 'error';\r\n result?: string;\r\n error?: string;\r\n arguments?: Record<string, any>;\r\n };\r\n shouldStream?: boolean;\r\n isCommandMode?: boolean;\r\n tool_call_id?: string;\r\n tool_calls?: Array<{ id: string; name: string; arguments: Record<string, any> }>;\r\n thinkingDuration?: number;\r\n taskCompletion?: {\r\n completedStepNumber: number;\r\n completedStepDescription: string;\r\n completedCount: number;\r\n totalCount: number;\r\n completionNote?: string;\r\n allSteps?: Array<{\r\n id: number;\r\n description: string;\r\n status: 'pending' | 'completed';\r\n }>;\r\n };\r\n planAccepted?: {\r\n planTitle?: string;\r\n totalSteps?: number;\r\n steps?: Array<{\r\n id: number;\r\n description: string;\r\n status: 'pending' | 'completed';\r\n }>;\r\n };\r\n planQuestion?: {\r\n question: string;\r\n options: string[];\r\n userAnswer: string;\r\n wasSkipped: boolean;\r\n };\r\n connectionStatus?: { // For SSH/WSL/Docker connection status\r\n type: 'ssh' | 'wsl' | 'docker';\r\n status: 'connecting' | 'connected' | 'error' | 'disconnected';\r\n connectionString?: string;\r\n error?: string;\r\n };\r\n}\r\n\r\n/**\r\n * Remote environment context for SSH/WSL/Docker sessions\r\n * Used to restore remote session state when resuming a chat\r\n */\r\nexport interface StoredRemoteContext {\r\n type: 'ssh' | 'wsl' | 'docker';\r\n connectionCommand: string; // Original command to reconnect (e.g., \"ssh user@host\" or \"wsl\")\r\n remoteCwd: string; // CWD in the remote environment\r\n localCwdBeforeRemote: string; // Local CWD before entering remote session\r\n metadata?: {\r\n hostname?: string; // For SSH\r\n username?: string; // For SSH \r\n distroName?: string; // For WSL\r\n containerId?: string; // For Docker\r\n port?: number; // For SSH\r\n };\r\n}\r\n\r\n/**\r\n * Serialized checkpoint metadata stored with chat history.\r\n * This allows checkpoint state to be restored even if the checkpoint index file is missing.\r\n */\r\nexport interface StoredCheckpointMeta {\r\n id: string;\r\n prompt: string;\r\n createdAt: string;\r\n createdAtMs: number;\r\n cwd: string;\r\n contextType: 'local' | 'ssh' | 'wsl' | 'docker';\r\n remoteSessionInfo?: {\r\n hostname?: string;\r\n username?: string;\r\n sessionId: string;\r\n connectionString?: string;\r\n };\r\n conversationIndex: number;\r\n uiMessageIndex?: number;\r\n uiMessageId?: string;\r\n snapshotDir: string;\r\n manifestPath: string;\r\n changes?: {\r\n added: string[];\r\n modified: string[];\r\n deleted: string[];\r\n };\r\n commands: string[];\r\n toolCalls: Array<{\r\n id?: string;\r\n name: string;\r\n arguments?: Record<string, any>;\r\n timestamp: string;\r\n }>;\r\n status: 'active' | 'finalized' | 'discarded';\r\n}\r\n\r\n/**\r\n * Local chat metadata (without messages, for listing)\r\n */\r\nexport interface LocalChatMeta {\r\n id: string;\r\n title: string;\r\n createdAt: string;\r\n updatedAt: string;\r\n messageCount: number;\r\n environment?: string; // 'local' or 'ssh:user@host' or 'wsl:distro' or 'docker:container'\r\n}\r\n\r\n/**\r\n * Full local chat with messages\r\n */\r\nexport interface LocalChat extends LocalChatMeta {\r\n messages: StoredMessage[];\r\n uiMessages?: StoredUIMessage[]; // Full UI history for restoration\r\n cwd?: string; // Working directory at time of save (for restoration)\r\n remoteContext?: StoredRemoteContext; // Remote context if saved while in SSH/WSL/Docker (backward compat)\r\n remoteContextStack?: StoredRemoteContext[]; // Stack of nested contexts (for SSH>SSH, SSH>Docker, etc.)\r\n checkpointIndex?: StoredCheckpointMeta[]; // Serialized checkpoint metadata for resume/revert restoration\r\n backendConversationId?: string; // Backend conversation UUID (for file storage/deletion)\r\n}\r\n\r\n/**\r\n * LocalChatStorage class for managing local conversation persistence\r\n */\r\nexport class LocalChatStorage {\r\n private chatsDir: string;\r\n private indexPath: string;\r\n private chatIndex: Map<string, LocalChatMeta> = new Map();\r\n private indexLoaded: boolean = false;\r\n\r\n constructor() {\r\n const homeDir = os.homedir();\r\n this.chatsDir = path.join(homeDir, '.centaurus', 'chats');\r\n this.indexPath = path.join(this.chatsDir, 'index.json');\r\n\r\n // Ensure chats directory exists\r\n if (!fs.existsSync(this.chatsDir)) {\r\n fs.mkdirSync(this.chatsDir, { recursive: true });\r\n }\r\n\r\n // Load or build the index\r\n this.loadIndex();\r\n }\r\n\r\n /**\r\n * Load the metadata index from disk, or rebuild it from chat files if missing\r\n */\r\n private loadIndex(): void {\r\n if (this.indexLoaded) return;\r\n\r\n try {\r\n if (fs.existsSync(this.indexPath)) {\r\n const data = fs.readFileSync(this.indexPath, 'utf-8');\r\n const indexArray: LocalChatMeta[] = JSON.parse(data);\r\n this.chatIndex.clear();\r\n for (const meta of indexArray) {\r\n this.chatIndex.set(meta.id, meta);\r\n }\r\n this.indexLoaded = true;\r\n return;\r\n }\r\n } catch (error) {\r\n // Index file corrupted, rebuild it\r\n }\r\n\r\n // Rebuild index from chat files (one-time migration)\r\n this.rebuildIndex();\r\n }\r\n\r\n /**\r\n * Rebuild the index by reading metadata from all chat files\r\n * This is a one-time migration that reads full files, but only happens once\r\n */\r\n private rebuildIndex(): void {\r\n this.chatIndex.clear();\r\n\r\n if (!fs.existsSync(this.chatsDir)) {\r\n this.indexLoaded = true;\r\n return;\r\n }\r\n\r\n const files = fs.readdirSync(this.chatsDir)\r\n .filter(f => f.endsWith('.json') && f !== 'index.json');\r\n\r\n for (const file of files) {\r\n try {\r\n const filePath = path.join(this.chatsDir, file);\r\n const data = fs.readFileSync(filePath, 'utf-8');\r\n const chat = JSON.parse(data) as LocalChat;\r\n\r\n // Determine environment string from remoteContext\r\n let environment = 'local';\r\n if (chat.remoteContext) {\r\n const rc = chat.remoteContext;\r\n if (rc.type === 'ssh' && rc.metadata?.hostname) {\r\n environment = `ssh:${rc.metadata.username || 'user'}@${rc.metadata.hostname}`;\r\n } else if (rc.type === 'wsl') {\r\n environment = `wsl:${rc.metadata?.distroName || 'Ubuntu'}`;\r\n } else if (rc.type === 'docker' && rc.metadata?.containerId) {\r\n environment = `docker:${rc.metadata.containerId.substring(0, 12)}`;\r\n } else {\r\n environment = rc.type;\r\n }\r\n }\r\n\r\n // Store metadata only\r\n this.chatIndex.set(chat.id, {\r\n id: chat.id,\r\n title: chat.title,\r\n createdAt: chat.createdAt,\r\n updatedAt: chat.updatedAt,\r\n messageCount: chat.messageCount,\r\n environment,\r\n });\r\n } catch (error) {\r\n // Skip invalid files\r\n continue;\r\n }\r\n }\r\n\r\n this.indexLoaded = true;\r\n this.saveIndex();\r\n }\r\n\r\n /**\r\n * Save the metadata index to disk\r\n */\r\n private saveIndex(): void {\r\n try {\r\n const indexArray = Array.from(this.chatIndex.values());\r\n fs.writeFileSync(this.indexPath, JSON.stringify(indexArray, null, 2), 'utf-8');\r\n } catch (error) {\r\n logError('Failed to save chat index', error as Error);\r\n }\r\n }\r\n\r\n /**\r\n * Update the index entry for a chat\r\n */\r\n private updateIndexEntry(chat: LocalChat): void {\r\n let environment = 'local';\r\n if (chat.remoteContext) {\r\n const rc = chat.remoteContext;\r\n if (rc.type === 'ssh' && rc.metadata?.hostname) {\r\n environment = `ssh:${rc.metadata.username || 'user'}@${rc.metadata.hostname}`;\r\n } else if (rc.type === 'wsl') {\r\n environment = `wsl:${rc.metadata?.distroName || 'Ubuntu'}`;\r\n } else if (rc.type === 'docker' && rc.metadata?.containerId) {\r\n environment = `docker:${rc.metadata.containerId.substring(0, 12)}`;\r\n } else {\r\n environment = rc.type;\r\n }\r\n }\r\n\r\n this.chatIndex.set(chat.id, {\r\n id: chat.id,\r\n title: chat.title,\r\n createdAt: chat.createdAt,\r\n updatedAt: chat.updatedAt,\r\n messageCount: chat.messageCount,\r\n environment,\r\n });\r\n this.saveIndex();\r\n }\r\n\r\n /**\r\n * Generate a unique chat ID based on timestamp\r\n */\r\n generateChatId(): string {\r\n const now = new Date();\r\n const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);\r\n const random = Math.random().toString(36).substring(2, 6);\r\n return `chat-${timestamp}-${random}`;\r\n }\r\n\r\n /**\r\n * Generate a title from the first user message\r\n */\r\n generateTitle(messages: StoredMessage[]): string {\r\n const firstUserMessage = messages.find(m => m.role === 'user');\r\n if (!firstUserMessage || !firstUserMessage.content) {\r\n return 'New Chat';\r\n }\r\n\r\n // Take first 60 chars, trim to last word boundary\r\n let title = firstUserMessage.content.substring(0, 60);\r\n if (firstUserMessage.content.length > 60) {\r\n const lastSpace = title.lastIndexOf(' ');\r\n if (lastSpace > 30) {\r\n title = title.substring(0, lastSpace);\r\n }\r\n title += '...';\r\n }\r\n\r\n // Clean up whitespace and newlines\r\n title = title.replace(/\\s+/g, ' ').trim();\r\n\r\n return title || 'New Chat';\r\n }\r\n\r\n /**\r\n * Get the file path for a chat\r\n */\r\n private getChatPath(chatId: string): string {\r\n return path.join(this.chatsDir, `${chatId}.json`);\r\n }\r\n\r\n /**\r\n * Save a chat to disk\r\n * @param remoteContext - undefined = preserve existing, null = clear, StoredRemoteContext = set new value\r\n * @param remoteContextStack - Stack of nested contexts for SSH>SSH, SSH>Docker etc.\r\n * @param checkpointIndex - undefined = preserve existing, null = clear, array = set latest checkpoint metadata\r\n */\r\n saveChat(\r\n chatId: string,\r\n messages: StoredMessage[],\r\n uiMessages?: StoredUIMessage[],\r\n cwd?: string,\r\n remoteContext?: StoredRemoteContext | null,\r\n remoteContextStack?: StoredRemoteContext[] | null,\r\n checkpointIndex?: StoredCheckpointMeta[] | null\r\n ): LocalChat {\r\n const chatPath = this.getChatPath(chatId);\r\n const now = new Date().toISOString();\r\n\r\n let chat: LocalChat;\r\n\r\n if (fs.existsSync(chatPath)) {\r\n // Update existing chat\r\n const existing = this.loadChat(chatId);\r\n if (existing) {\r\n chat = {\r\n ...existing,\r\n messages,\r\n uiMessages: uiMessages || existing.uiMessages,\r\n cwd: cwd || existing.cwd, // Preserve CWD if not provided\r\n // remoteContext: null = clear it, undefined = preserve existing\r\n remoteContext: remoteContext === null ? undefined : (remoteContext !== undefined ? remoteContext : existing.remoteContext),\r\n // remoteContextStack: null = clear it, undefined = preserve existing\r\n remoteContextStack: remoteContextStack === null ? undefined : (remoteContextStack !== undefined ? remoteContextStack : existing.remoteContextStack),\r\n // checkpointIndex: null = clear it, undefined = preserve existing\r\n checkpointIndex: checkpointIndex === null ? undefined : (checkpointIndex !== undefined ? checkpointIndex : existing.checkpointIndex),\r\n // Preserve backendConversationId if it exists\r\n backendConversationId: existing.backendConversationId,\r\n messageCount: messages.length,\r\n updatedAt: now,\r\n };\r\n } else {\r\n // File exists but couldn't be parsed, create new\r\n chat = {\r\n id: chatId,\r\n title: this.generateTitle(messages),\r\n createdAt: now,\r\n updatedAt: now,\r\n messageCount: messages.length,\r\n messages,\r\n uiMessages,\r\n cwd,\r\n remoteContext: remoteContext ?? undefined,\r\n remoteContextStack: remoteContextStack ?? undefined,\r\n checkpointIndex: checkpointIndex ?? undefined,\r\n };\r\n }\r\n } else {\r\n // Create new chat\r\n chat = {\r\n id: chatId,\r\n title: this.generateTitle(messages),\r\n createdAt: now,\r\n updatedAt: now,\r\n messageCount: messages.length,\r\n messages,\r\n uiMessages,\r\n cwd,\r\n remoteContext: remoteContext ?? undefined,\r\n remoteContextStack: remoteContextStack ?? undefined,\r\n checkpointIndex: checkpointIndex ?? undefined,\r\n };\r\n }\r\n\r\n fs.writeFileSync(chatPath, JSON.stringify(chat, null, 2), 'utf-8');\r\n\r\n // Update the metadata index\r\n this.updateIndexEntry(chat);\r\n\r\n return chat;\r\n }\r\n\r\n /**\r\n * Set the backend conversation ID for a chat\r\n * This is used to associate local chats with backend file storage\r\n */\r\n setBackendConversationId(chatId: string, backendConversationId: string): boolean {\r\n const chat = this.loadChat(chatId);\r\n if (!chat) {\r\n return false;\r\n }\r\n\r\n try {\r\n chat.backendConversationId = backendConversationId;\r\n chat.updatedAt = new Date().toISOString();\r\n const chatPath = this.getChatPath(chatId);\r\n fs.writeFileSync(chatPath, JSON.stringify(chat, null, 2), 'utf-8');\r\n return true;\r\n } catch (error) {\r\n logError(`Failed to set backend conversation ID for chat ${chatId}`, error as Error);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Get the backend conversation ID for a chat (if any)\r\n */\r\n getBackendConversationId(chatId: string): string | undefined {\r\n const chat = this.loadChat(chatId);\r\n return chat?.backendConversationId;\r\n }\r\n\r\n /**\r\n * Load a chat by ID\r\n */\r\n loadChat(chatId: string): LocalChat | null {\r\n const chatPath = this.getChatPath(chatId);\r\n\r\n if (!fs.existsSync(chatPath)) {\r\n return null;\r\n }\r\n\r\n try {\r\n const data = fs.readFileSync(chatPath, 'utf-8');\r\n return JSON.parse(data) as LocalChat;\r\n } catch (error) {\r\n logError(`Failed to load chat ${chatId}`, error as Error);\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * List all chats (metadata only, sorted by updatedAt descending)\r\n * Uses the in-memory index to avoid loading full chat files\r\n */\r\n listChats(): LocalChatMeta[] {\r\n // Ensure index is loaded\r\n this.loadIndex();\r\n\r\n // Return sorted metadata from the cache\r\n const chats = Array.from(this.chatIndex.values());\r\n\r\n // Sort by updatedAt descending (most recent first)\r\n chats.sort((a, b) =>\r\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()\r\n );\r\n\r\n return chats;\r\n }\r\n\r\n /**\r\n * Delete a chat by ID\r\n */\r\n deleteChat(chatId: string): boolean {\r\n const chatPath = this.getChatPath(chatId);\r\n const checkpointsPath = path.join(os.homedir(), '.centaurus', 'checkpoints', chatId);\r\n\r\n if (!fs.existsSync(chatPath)) {\r\n // Best-effort cleanup for orphaned checkpoint data.\r\n if (fs.existsSync(checkpointsPath)) {\r\n try {\r\n fs.rmSync(checkpointsPath, { recursive: true, force: true });\r\n } catch {\r\n // Ignore cleanup errors for missing chats.\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n try {\r\n fs.unlinkSync(chatPath);\r\n if (fs.existsSync(checkpointsPath)) {\r\n fs.rmSync(checkpointsPath, { recursive: true, force: true });\r\n }\r\n\r\n // Clean up tool output files for this chat\r\n cleanupToolOutputsForChat(chatId);\r\n\r\n // Remove from index\r\n this.chatIndex.delete(chatId);\r\n this.saveIndex();\r\n\r\n return true;\r\n } catch (error) {\r\n logError(`Failed to delete chat ${chatId}`, error as Error);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Import a full chat object (used for restoring from cloud sync)\r\n * Overwrites existing chat with same ID if present\r\n */\r\n importChat(chat: LocalChat): boolean {\r\n if (!chat || !chat.id) {\r\n return false;\r\n }\r\n\r\n try {\r\n const chatPath = this.getChatPath(chat.id);\r\n\r\n // Ensure the chat has required fields\r\n const now = new Date().toISOString();\r\n const importedChat: LocalChat = {\r\n id: chat.id,\r\n title: chat.title || 'Imported Chat',\r\n createdAt: chat.createdAt || now,\r\n updatedAt: now, // Always update the timestamp on import\r\n messageCount: chat.messages?.length || chat.messageCount || 0,\r\n messages: chat.messages || [],\r\n uiMessages: chat.uiMessages,\r\n cwd: chat.cwd,\r\n remoteContext: chat.remoteContext,\r\n remoteContextStack: chat.remoteContextStack,\r\n checkpointIndex: chat.checkpointIndex,\r\n backendConversationId: chat.backendConversationId,\r\n environment: chat.environment,\r\n };\r\n\r\n fs.writeFileSync(chatPath, JSON.stringify(importedChat, null, 2), 'utf-8');\r\n\r\n // Update the index\r\n this.updateIndexEntry(importedChat);\r\n\r\n return true;\r\n } catch (error) {\r\n logError(`Failed to import chat ${chat.id}`, error as Error);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Rename a chat by ID\r\n */\r\n renameChat(chatId: string, newTitle: string): boolean {\r\n const chat = this.loadChat(chatId);\r\n if (!chat) {\r\n return false;\r\n }\r\n\r\n try {\r\n chat.title = newTitle;\r\n chat.updatedAt = new Date().toISOString();\r\n const chatPath = this.getChatPath(chatId);\r\n fs.writeFileSync(chatPath, JSON.stringify(chat, null, 2), 'utf-8');\r\n\r\n // Update the index\r\n this.updateIndexEntry(chat);\r\n\r\n return true;\r\n } catch (error) {\r\n logError(`Failed to rename chat ${chatId}`, error as Error);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Get the chats directory path\r\n */\r\n getChatsDir(): string {\r\n return this.chatsDir;\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const localChatStorage = new LocalChatStorage();\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import {
|
|
5
|
+
SKILL_TOOL_PRESETS
|
|
6
|
+
} from "../types/skill.js";
|
|
7
|
+
const SKILLS_DIR = path.join(os.homedir(), ".centaurus", "skills");
|
|
8
|
+
function ensureSkillsDir() {
|
|
9
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
10
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function truncatePreview(content, maxLength = 80) {
|
|
14
|
+
const firstLine = content.split("\n").find((l) => l.trim()) || "";
|
|
15
|
+
return firstLine.length > maxLength ? firstLine.slice(0, maxLength - 1) + "\u2026" : firstLine;
|
|
16
|
+
}
|
|
17
|
+
class SkillStorageService {
|
|
18
|
+
static instance;
|
|
19
|
+
static getInstance() {
|
|
20
|
+
if (!SkillStorageService.instance) {
|
|
21
|
+
SkillStorageService.instance = new SkillStorageService();
|
|
22
|
+
}
|
|
23
|
+
return SkillStorageService.instance;
|
|
24
|
+
}
|
|
25
|
+
/** Sanitize a skill name for use as a filename and slash command */
|
|
26
|
+
sanitizeName(name) {
|
|
27
|
+
return name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
28
|
+
}
|
|
29
|
+
getSkillPath(name) {
|
|
30
|
+
return path.join(SKILLS_DIR, `${this.sanitizeName(name)}.json`);
|
|
31
|
+
}
|
|
32
|
+
/** Check if a skill exists */
|
|
33
|
+
exists(name) {
|
|
34
|
+
return fs.existsSync(this.getSkillPath(name));
|
|
35
|
+
}
|
|
36
|
+
/** Load a single skill by name */
|
|
37
|
+
load(name) {
|
|
38
|
+
try {
|
|
39
|
+
const filePath = this.getSkillPath(name);
|
|
40
|
+
if (!fs.existsSync(filePath)) return null;
|
|
41
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** List all saved skills */
|
|
48
|
+
list() {
|
|
49
|
+
ensureSkillsDir();
|
|
50
|
+
try {
|
|
51
|
+
const files = fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith(".json"));
|
|
52
|
+
const skills = [];
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
try {
|
|
55
|
+
const filePath = path.join(SKILLS_DIR, file);
|
|
56
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
57
|
+
const skill = JSON.parse(raw);
|
|
58
|
+
skills.push({
|
|
59
|
+
name: skill.name,
|
|
60
|
+
promptPreview: truncatePreview(skill.prompt),
|
|
61
|
+
accessLevel: skill.accessLevel,
|
|
62
|
+
model: skill.model,
|
|
63
|
+
createdAt: skill.createdAt,
|
|
64
|
+
updatedAt: skill.updatedAt
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return skills.sort(
|
|
70
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
71
|
+
);
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Get all skill names (for slash command detection) */
|
|
77
|
+
getSkillNames() {
|
|
78
|
+
return this.list().map((s) => s.name);
|
|
79
|
+
}
|
|
80
|
+
/** Save (create or update) a skill */
|
|
81
|
+
save(skill, previousName) {
|
|
82
|
+
ensureSkillsDir();
|
|
83
|
+
const normalizedName = this.sanitizeName(skill.name);
|
|
84
|
+
if (!normalizedName) {
|
|
85
|
+
return { success: false, error: "Skill name is required." };
|
|
86
|
+
}
|
|
87
|
+
if (!skill.prompt || !skill.prompt.trim()) {
|
|
88
|
+
return { success: false, error: "Skill prompt is required." };
|
|
89
|
+
}
|
|
90
|
+
const normalizedPreviousName = previousName ? this.sanitizeName(previousName) : void 0;
|
|
91
|
+
const isRename = !!normalizedPreviousName && normalizedPreviousName !== normalizedName;
|
|
92
|
+
if (isRename || !normalizedPreviousName) {
|
|
93
|
+
if (this.exists(normalizedName)) {
|
|
94
|
+
return { success: false, error: `A skill named "${normalizedName}" already exists.` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
98
|
+
const savedSkill = {
|
|
99
|
+
...skill,
|
|
100
|
+
name: normalizedName,
|
|
101
|
+
allowedTools: skill.allowedTools.length > 0 ? skill.allowedTools : SKILL_TOOL_PRESETS[skill.accessLevel],
|
|
102
|
+
updatedAt: now,
|
|
103
|
+
createdAt: skill.createdAt || now
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
const filePath = this.getSkillPath(normalizedName);
|
|
107
|
+
fs.writeFileSync(filePath, JSON.stringify(savedSkill, null, 2), "utf-8");
|
|
108
|
+
if (isRename && normalizedPreviousName) {
|
|
109
|
+
const oldPath = this.getSkillPath(normalizedPreviousName);
|
|
110
|
+
if (fs.existsSync(oldPath)) {
|
|
111
|
+
fs.unlinkSync(oldPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
savedName: normalizedName,
|
|
117
|
+
renamedFrom: isRename ? normalizedPreviousName : void 0
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return { success: false, error: error.message || "Failed to save skill." };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Delete a skill */
|
|
124
|
+
delete(name) {
|
|
125
|
+
const filePath = this.getSkillPath(name);
|
|
126
|
+
if (!fs.existsSync(filePath)) {
|
|
127
|
+
return { success: false, error: `Skill "${name}" not found.` };
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
fs.unlinkSync(filePath);
|
|
131
|
+
return { success: true };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return { success: false, error: error.message };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const skillStorage = SkillStorageService.getInstance();
|
|
138
|
+
export {
|
|
139
|
+
skillStorage
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=skill-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/services/skill-storage.ts"],"sourcesContent":["/**\r\n * Skill Storage Service\r\n *\r\n * Manages CRUD operations for user-defined skills stored as JSON files\r\n * in ~/.centaurus/skills/\r\n */\r\n\r\nimport * as fs from 'fs';\r\nimport * as path from 'path';\r\nimport * as os from 'os';\r\nimport {\r\n SavedSkill,\r\n SkillMeta,\r\n SaveSkillResult,\r\n SkillAccessLevel,\r\n SKILL_TOOL_PRESETS,\r\n} from '../types/skill.js';\r\n\r\nconst SKILLS_DIR = path.join(os.homedir(), '.centaurus', 'skills');\r\n\r\nfunction ensureSkillsDir(): void {\r\n if (!fs.existsSync(SKILLS_DIR)) {\r\n fs.mkdirSync(SKILLS_DIR, { recursive: true });\r\n }\r\n}\r\n\r\nfunction truncatePreview(content: string, maxLength = 80): string {\r\n const firstLine = content.split('\\n').find(l => l.trim()) || '';\r\n return firstLine.length > maxLength\r\n ? firstLine.slice(0, maxLength - 1) + '\\u2026'\r\n : firstLine;\r\n}\r\n\r\nclass SkillStorageService {\r\n private static instance: SkillStorageService;\r\n\r\n static getInstance(): SkillStorageService {\r\n if (!SkillStorageService.instance) {\r\n SkillStorageService.instance = new SkillStorageService();\r\n }\r\n return SkillStorageService.instance;\r\n }\r\n\r\n /** Sanitize a skill name for use as a filename and slash command */\r\n sanitizeName(name: string): string {\r\n return name\r\n .toLowerCase()\r\n .replace(/[^a-z0-9_-]/g, '-')\r\n .replace(/-+/g, '-')\r\n .replace(/^-|-$/g, '');\r\n }\r\n\r\n private getSkillPath(name: string): string {\r\n return path.join(SKILLS_DIR, `${this.sanitizeName(name)}.json`);\r\n }\r\n\r\n /** Check if a skill exists */\r\n exists(name: string): boolean {\r\n return fs.existsSync(this.getSkillPath(name));\r\n }\r\n\r\n /** Load a single skill by name */\r\n load(name: string): SavedSkill | null {\r\n try {\r\n const filePath = this.getSkillPath(name);\r\n if (!fs.existsSync(filePath)) return null;\r\n const raw = fs.readFileSync(filePath, 'utf-8');\r\n return JSON.parse(raw) as SavedSkill;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n /** List all saved skills */\r\n list(): SkillMeta[] {\r\n ensureSkillsDir();\r\n try {\r\n const files = fs.readdirSync(SKILLS_DIR).filter(f => f.endsWith('.json'));\r\n const skills: SkillMeta[] = [];\r\n\r\n for (const file of files) {\r\n try {\r\n const filePath = path.join(SKILLS_DIR, file);\r\n const raw = fs.readFileSync(filePath, 'utf-8');\r\n const skill = JSON.parse(raw) as SavedSkill;\r\n skills.push({\r\n name: skill.name,\r\n promptPreview: truncatePreview(skill.prompt),\r\n accessLevel: skill.accessLevel,\r\n model: skill.model,\r\n createdAt: skill.createdAt,\r\n updatedAt: skill.updatedAt,\r\n });\r\n } catch {\r\n // Skip malformed files\r\n }\r\n }\r\n\r\n return skills.sort((a, b) =>\r\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()\r\n );\r\n } catch {\r\n return [];\r\n }\r\n }\r\n\r\n /** Get all skill names (for slash command detection) */\r\n getSkillNames(): string[] {\r\n return this.list().map(s => s.name);\r\n }\r\n\r\n /** Save (create or update) a skill */\r\n save(skill: SavedSkill, previousName?: string): SaveSkillResult {\r\n ensureSkillsDir();\r\n\r\n const normalizedName = this.sanitizeName(skill.name);\r\n if (!normalizedName) {\r\n return { success: false, error: 'Skill name is required.' };\r\n }\r\n if (!skill.prompt || !skill.prompt.trim()) {\r\n return { success: false, error: 'Skill prompt is required.' };\r\n }\r\n\r\n const normalizedPreviousName = previousName ? this.sanitizeName(previousName) : undefined;\r\n const isRename = !!normalizedPreviousName && normalizedPreviousName !== normalizedName;\r\n\r\n // Check for conflicts\r\n if (isRename || !normalizedPreviousName) {\r\n if (this.exists(normalizedName)) {\r\n return { success: false, error: `A skill named \"${normalizedName}\" already exists.` };\r\n }\r\n }\r\n\r\n const now = new Date().toISOString();\r\n const savedSkill: SavedSkill = {\r\n ...skill,\r\n name: normalizedName,\r\n allowedTools: skill.allowedTools.length > 0\r\n ? skill.allowedTools\r\n : SKILL_TOOL_PRESETS[skill.accessLevel],\r\n updatedAt: now,\r\n createdAt: skill.createdAt || now,\r\n };\r\n\r\n try {\r\n const filePath = this.getSkillPath(normalizedName);\r\n fs.writeFileSync(filePath, JSON.stringify(savedSkill, null, 2), 'utf-8');\r\n\r\n // Handle rename\r\n if (isRename && normalizedPreviousName) {\r\n const oldPath = this.getSkillPath(normalizedPreviousName);\r\n if (fs.existsSync(oldPath)) {\r\n fs.unlinkSync(oldPath);\r\n }\r\n }\r\n\r\n return {\r\n success: true,\r\n savedName: normalizedName,\r\n renamedFrom: isRename ? normalizedPreviousName : undefined,\r\n };\r\n } catch (error: any) {\r\n return { success: false, error: error.message || 'Failed to save skill.' };\r\n }\r\n }\r\n\r\n /** Delete a skill */\r\n delete(name: string): { success: boolean; error?: string } {\r\n const filePath = this.getSkillPath(name);\r\n if (!fs.existsSync(filePath)) {\r\n return { success: false, error: `Skill \"${name}\" not found.` };\r\n }\r\n try {\r\n fs.unlinkSync(filePath);\r\n return { success: true };\r\n } catch (error: any) {\r\n return { success: false, error: error.message };\r\n }\r\n }\r\n}\r\n\r\nexport const skillStorage = SkillStorageService.getInstance();\r\n"],"mappings":"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","names":[]}
|
|
@@ -8,20 +8,21 @@ import { ContextManager } from "../context/context-manager.js";
|
|
|
8
8
|
const MAX_CONCURRENT_SUBAGENTS = 5;
|
|
9
9
|
const SUBAGENT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
10
10
|
const MAX_TURNS_PER_SUBAGENT = 50;
|
|
11
|
-
const SIMPLE_MODEL = "
|
|
12
|
-
const COMPLEX_MODEL = "
|
|
11
|
+
const SIMPLE_MODEL = "minimaxai/minimax-m2.5";
|
|
12
|
+
const COMPLEX_MODEL = "z-ai/glm5";
|
|
13
13
|
const COMPLEXITY_THRESHOLD = 5;
|
|
14
14
|
const SUBAGENT_SYSTEM_PROMPT = `You are a sub-agent spawned by a main AI agent to complete a specific task.
|
|
15
15
|
|
|
16
16
|
IMPORTANT RULES:
|
|
17
|
-
1. You have
|
|
17
|
+
1. You have access to file system tools (read, write, edit, list, grep, find)
|
|
18
18
|
2. You have access to command execution tools
|
|
19
19
|
3. Focus ONLY on the task assigned to you
|
|
20
20
|
4. Always provide reason_text for every tool call to explain what you're doing
|
|
21
21
|
5. Call task_complete when you've finished the assigned task
|
|
22
22
|
6. Be efficient - don't explore unnecessarily, stick to the task
|
|
23
23
|
7. If you encounter errors, try to resolve them or report clearly what went wrong
|
|
24
|
-
|
|
24
|
+
8. CRITICAL: Your working directory is {WORKING_DIRECTORY}. All file operations (create, edit, write) MUST be within this directory. Use relative paths from this directory. Do NOT create files in other directories.
|
|
25
|
+
{TOOL_RESTRICTIONS}
|
|
25
26
|
CONTEXT FROM MAIN AGENT:
|
|
26
27
|
Working Directory: {WORKING_DIRECTORY}
|
|
27
28
|
|
|
@@ -81,7 +82,16 @@ class SubAgentManagerClass {
|
|
|
81
82
|
* Generate system prompt for sub-agent
|
|
82
83
|
*/
|
|
83
84
|
generateSystemPrompt(config) {
|
|
84
|
-
|
|
85
|
+
let toolRestrictions = "";
|
|
86
|
+
if (config.allowedTools && config.allowedTools.length > 0) {
|
|
87
|
+
toolRestrictions = `
|
|
88
|
+
TOOL RESTRICTIONS (CRITICAL):
|
|
89
|
+
You are ONLY allowed to use the following tools: ${config.allowedTools.join(", ")}, task_complete
|
|
90
|
+
If you need a tool that is not in this list, DO NOT attempt to call it \u2014 it will be rejected.
|
|
91
|
+
You must complete your task using only the tools listed above.
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
return SUBAGENT_SYSTEM_PROMPT.replace("{WORKING_DIRECTORY}", config.workingDirectory).replace("{CONTEXT}", config.context || "No additional context provided.").replace("{PROMPT}", config.prompt).replace("{TOOL_RESTRICTIONS}", toolRestrictions);
|
|
85
95
|
}
|
|
86
96
|
/**
|
|
87
97
|
* Spawn a new sub-agent
|
|
@@ -112,7 +122,7 @@ class SubAgentManagerClass {
|
|
|
112
122
|
};
|
|
113
123
|
}
|
|
114
124
|
const agentId = `subagent-${randomUUID().substring(0, 8)}`;
|
|
115
|
-
const model = this.selectModel(config.complexity);
|
|
125
|
+
const model = config.modelOverride || this.selectModel(config.complexity);
|
|
116
126
|
const subAgent = {
|
|
117
127
|
id: agentId,
|
|
118
128
|
status: "pending",
|
|
@@ -127,7 +137,8 @@ class SubAgentManagerClass {
|
|
|
127
137
|
fileOperations: [],
|
|
128
138
|
toolHistory: [],
|
|
129
139
|
isRead: false,
|
|
130
|
-
abortController: new AbortController()
|
|
140
|
+
abortController: new AbortController(),
|
|
141
|
+
allowedTools: config.allowedTools
|
|
131
142
|
};
|
|
132
143
|
this.subAgents.set(agentId, subAgent);
|
|
133
144
|
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [SubAgentManager] Sub-agent ${agentId} added to map, calling notifyCountChange\\n`);
|
|
@@ -170,7 +181,11 @@ class SubAgentManagerClass {
|
|
|
170
181
|
role: "user",
|
|
171
182
|
content: systemPrompt
|
|
172
183
|
});
|
|
173
|
-
|
|
184
|
+
let tools = this.toolRegistry.getSchemas();
|
|
185
|
+
if (config.allowedTools && config.allowedTools.length > 0) {
|
|
186
|
+
const allowedSet = /* @__PURE__ */ new Set([...config.allowedTools, "task_complete"]);
|
|
187
|
+
tools = tools.filter((t) => allowedSet.has(t.name));
|
|
188
|
+
}
|
|
174
189
|
const effectiveContextManager = config.parentContextManager || new ContextManager(config.workingDirectory, process.platform);
|
|
175
190
|
const context = {
|
|
176
191
|
cwd: config.workingDirectory,
|
|
@@ -270,6 +285,28 @@ class SubAgentManagerClass {
|
|
|
270
285
|
subAgent.endTime = /* @__PURE__ */ new Date();
|
|
271
286
|
break;
|
|
272
287
|
}
|
|
288
|
+
if (subAgent.allowedTools && subAgent.allowedTools.length > 0) {
|
|
289
|
+
const allowedSet = /* @__PURE__ */ new Set([...subAgent.allowedTools, "task_complete"]);
|
|
290
|
+
if (!allowedSet.has(toolCall.name)) {
|
|
291
|
+
quickLog(`[SubAgent ${subAgent.id}] REJECTED tool "${toolCall.name}" \u2014 not in allowed list: ${subAgent.allowedTools.join(", ")}`);
|
|
292
|
+
const rejectionMsg = `Tool "${toolCall.name}" is not allowed for this sub-agent. Allowed tools: ${subAgent.allowedTools.join(", ")}, task_complete. Please use only allowed tools.`;
|
|
293
|
+
const rejectedExecution = {
|
|
294
|
+
toolName: toolCall.name,
|
|
295
|
+
arguments: toolCall.arguments,
|
|
296
|
+
reasonText: toolCall.arguments?.reason_text,
|
|
297
|
+
result: rejectionMsg,
|
|
298
|
+
success: false,
|
|
299
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
300
|
+
};
|
|
301
|
+
subAgent.toolHistory.push(rejectedExecution);
|
|
302
|
+
subAgent.conversationHistory.push({
|
|
303
|
+
role: "tool",
|
|
304
|
+
content: `ERROR: ${rejectionMsg}`,
|
|
305
|
+
tool_call_id: toolCall.id
|
|
306
|
+
});
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
273
310
|
const reasonText = toolCall.arguments?.reason_text;
|
|
274
311
|
const toolExecution = {
|
|
275
312
|
toolName: toolCall.name,
|
|
@@ -314,6 +351,10 @@ class SubAgentManagerClass {
|
|
|
314
351
|
} finally {
|
|
315
352
|
clearTimeout(timeoutId);
|
|
316
353
|
this.notifyCountChange();
|
|
354
|
+
if (subAgent.status !== "running") {
|
|
355
|
+
subAgent.conversationHistory = [];
|
|
356
|
+
}
|
|
357
|
+
this.cleanup();
|
|
317
358
|
}
|
|
318
359
|
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [SubAgent] ${agentId} finished with status: ${subAgent.status}
|
|
319
360
|
`);
|