centaurus-cli 3.1.3 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/cli-adapter.js +685 -153
  2. package/dist/cli-adapter.js.map +1 -1
  3. package/dist/config/defaultConfig.js +1 -4
  4. package/dist/config/defaultConfig.js.map +1 -1
  5. package/dist/config/models.js +4 -0
  6. package/dist/config/models.js.map +1 -1
  7. package/dist/config/slash-commands.js +66 -2
  8. package/dist/config/slash-commands.js.map +1 -1
  9. package/dist/config/types.js +4 -4
  10. package/dist/config/types.js.map +1 -1
  11. package/dist/index.js +36 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/services/ai-context-injector.js +109 -0
  14. package/dist/services/ai-context-injector.js.map +1 -1
  15. package/dist/services/api-client.js.map +1 -1
  16. package/dist/services/background-task-manager.js +59 -0
  17. package/dist/services/background-task-manager.js.map +1 -1
  18. package/dist/services/local-chat-storage.js +2 -0
  19. package/dist/services/local-chat-storage.js.map +1 -1
  20. package/dist/services/skill-storage.js +141 -0
  21. package/dist/services/skill-storage.js.map +1 -0
  22. package/dist/services/sub-agent-manager.js +49 -8
  23. package/dist/services/sub-agent-manager.js.map +1 -1
  24. package/dist/services/warpify-detector.js +17 -5
  25. package/dist/services/warpify-detector.js.map +1 -1
  26. package/dist/tools/background-command.js +5 -2
  27. package/dist/tools/background-command.js.map +1 -1
  28. package/dist/tools/command.js +367 -109
  29. package/dist/tools/command.js.map +1 -1
  30. package/dist/tools/file-ops.js +23 -6
  31. package/dist/tools/file-ops.js.map +1 -1
  32. package/dist/tools/plan-mode.js +184 -336
  33. package/dist/tools/plan-mode.js.map +1 -1
  34. package/dist/tools/sub-agent.js +24 -5
  35. package/dist/tools/sub-agent.js.map +1 -1
  36. package/dist/tools/todo-list.js +157 -0
  37. package/dist/tools/todo-list.js.map +1 -0
  38. package/dist/types/skill.js +30 -0
  39. package/dist/types/skill.js.map +1 -0
  40. package/dist/ui/components/App.js +956 -162
  41. package/dist/ui/components/App.js.map +1 -1
  42. package/dist/ui/components/AuthScreen.js +3 -1
  43. package/dist/ui/components/AuthScreen.js.map +1 -1
  44. package/dist/ui/components/AuthWelcomeScreen.js +3 -1
  45. package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
  46. package/dist/ui/components/CodeBlock.js +3 -1
  47. package/dist/ui/components/CodeBlock.js.map +1 -1
  48. package/dist/ui/components/CompactShellPreview.js +44 -0
  49. package/dist/ui/components/CompactShellPreview.js.map +1 -0
  50. package/dist/ui/components/ConfigViewer.js +3 -1
  51. package/dist/ui/components/ConfigViewer.js.map +1 -1
  52. package/dist/ui/components/ConfirmPrompt.js +3 -1
  53. package/dist/ui/components/ConfirmPrompt.js.map +1 -1
  54. package/dist/ui/components/ConnectionStatusMessage.js +3 -1
  55. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  56. package/dist/ui/components/DetailedPlanReviewScreen.js +84 -74
  57. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
  58. package/dist/ui/components/DiffViewer.js +6 -3
  59. package/dist/ui/components/DiffViewer.js.map +1 -1
  60. package/dist/ui/components/FileCreationPreview.js.map +1 -1
  61. package/dist/ui/components/FileTagAutocomplete.js +4 -2
  62. package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
  63. package/dist/ui/components/InputBox.js +243 -40
  64. package/dist/ui/components/InputBox.js.map +1 -1
  65. package/dist/ui/components/InteractiveShell.js +5 -3
  66. package/dist/ui/components/InteractiveShell.js.map +1 -1
  67. package/dist/ui/components/KeyboardHelp.js +4 -1
  68. package/dist/ui/components/KeyboardHelp.js.map +1 -1
  69. package/dist/ui/components/LoadingIndicator.js +3 -1
  70. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  71. package/dist/ui/components/MCPAddScreen.js +63 -13
  72. package/dist/ui/components/MCPAddScreen.js.map +1 -1
  73. package/dist/ui/components/MarkdownRenderer.js +3 -1
  74. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  75. package/dist/ui/components/MessageDisplay.js +9 -7
  76. package/dist/ui/components/MessageDisplay.js.map +1 -1
  77. package/dist/ui/components/ModelPicker.js +170 -0
  78. package/dist/ui/components/ModelPicker.js.map +1 -0
  79. package/dist/ui/components/MonitorModeAIPanel.js +3 -1
  80. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
  81. package/dist/ui/components/PlanAcceptedMessage.js +12 -6
  82. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  83. package/dist/ui/components/PlanQuestionMessage.js +37 -0
  84. package/dist/ui/components/PlanQuestionMessage.js.map +1 -0
  85. package/dist/ui/components/PlanQuestionScreen.js +138 -0
  86. package/dist/ui/components/PlanQuestionScreen.js.map +1 -0
  87. package/dist/ui/components/PlanReviewScreen.js +7 -9
  88. package/dist/ui/components/PlanReviewScreen.js.map +1 -1
  89. package/dist/ui/components/RulesEditorScreen.js +65 -28
  90. package/dist/ui/components/RulesEditorScreen.js.map +1 -1
  91. package/dist/ui/components/SelectPrompt.js +3 -1
  92. package/dist/ui/components/SelectPrompt.js.map +1 -1
  93. package/dist/ui/components/SkillCreatorScreen.js +217 -0
  94. package/dist/ui/components/SkillCreatorScreen.js.map +1 -0
  95. package/dist/ui/components/SlashCommandAutocomplete.js +4 -2
  96. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  97. package/dist/ui/components/StatusBar.js +4 -2
  98. package/dist/ui/components/StatusBar.js.map +1 -1
  99. package/dist/ui/components/StreamingMessageDisplay.js +5 -3
  100. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  101. package/dist/ui/components/SubAgentListScreen.js +65 -0
  102. package/dist/ui/components/SubAgentListScreen.js.map +1 -0
  103. package/dist/ui/components/SubAgentViewScreen.js +123 -0
  104. package/dist/ui/components/SubAgentViewScreen.js.map +1 -0
  105. package/dist/ui/components/TaskCompletedMessage.js +40 -8
  106. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  107. package/dist/ui/components/TaskProgressIndicator.js +6 -4
  108. package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
  109. package/dist/ui/components/TextEditor.js +297 -0
  110. package/dist/ui/components/TextEditor.js.map +1 -0
  111. package/dist/ui/components/TodoListMessage.js +59 -0
  112. package/dist/ui/components/TodoListMessage.js.map +1 -0
  113. package/dist/ui/components/ToolExecutionMessage.js +134 -84
  114. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  115. package/dist/ui/components/ToolExecutionStatus.js +3 -1
  116. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  117. package/dist/ui/components/WelcomeBanner.js +33 -33
  118. package/dist/ui/components/WelcomeBanner.js.map +1 -1
  119. package/dist/ui/components/WorkflowCreatorScreen.js +5 -3
  120. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
  121. package/dist/ui/theme.js +97 -0
  122. package/dist/ui/theme.js.map +1 -0
  123. package/dist/ui/utils/chat-history-limit.js +247 -0
  124. package/dist/ui/utils/chat-history-limit.js.map +1 -0
  125. package/dist/utils/chat-formatter.js +22 -9
  126. package/dist/utils/chat-formatter.js.map +1 -1
  127. package/dist/utils/input-classifier.js +11 -1
  128. package/dist/utils/input-classifier.js.map +1 -1
  129. package/dist/utils/output-truncation.js +175 -0
  130. package/dist/utils/output-truncation.js.map +1 -0
  131. package/dist/utils/rule-reference-resolver.js +3 -3
  132. package/dist/utils/rule-reference-resolver.js.map +1 -1
  133. package/dist/utils/tunnel-commands-manager.js +134 -0
  134. package/dist/utils/tunnel-commands-manager.js.map +1 -0
  135. package/package.json +91 -90
  136. package/postinstall.js +4 -11
  137. package/dist/ui/components/MultiLineInput.js +0 -255
  138. package/dist/ui/components/MultiLineInput.js.map +0 -1
@@ -174,6 +174,115 @@ class AIContextInjector {
174
174
  `;
175
175
  message += `- If the first server you try doesn't work, search for alternatives and try again
176
176
 
177
+ `;
178
+ return message;
179
+ }
180
+ /**
181
+ * Inject todo list context into message history.
182
+ *
183
+ * If there is an active todo list, this method inserts a system message
184
+ * with the current todo list state before the last user message.
185
+ * This ensures the AI remembers its todo list progress across turns.
186
+ *
187
+ * @param messages - The conversation history
188
+ * @param todoContext - The formatted todo list context string (empty if no active list)
189
+ * @returns Modified message array with todo context injected (if applicable)
190
+ */
191
+ injectTodoContext(messages, todoContext) {
192
+ if (!todoContext) {
193
+ return messages;
194
+ }
195
+ const contextMessage = {
196
+ role: "system",
197
+ content: todoContext
198
+ };
199
+ const result = [...messages];
200
+ let lastUserMessageIndex = -1;
201
+ for (let i = result.length - 1; i >= 0; i--) {
202
+ if (result[i].role === "user") {
203
+ lastUserMessageIndex = i;
204
+ break;
205
+ }
206
+ }
207
+ if (lastUserMessageIndex >= 0) {
208
+ result.splice(lastUserMessageIndex, 0, contextMessage);
209
+ } else {
210
+ result.push(contextMessage);
211
+ }
212
+ return result;
213
+ }
214
+ /**
215
+ * Inject custom tunnel context into message history.
216
+ *
217
+ * When a custom tunnel session is active (e.g. minicom, python venv, etc.),
218
+ * this method inserts a system message informing the AI about the tunnel
219
+ * environment so it can route commands appropriately.
220
+ *
221
+ * @param messages - The conversation history
222
+ * @param tunnelCommand - The command that started the tunnel (e.g. "minicom")
223
+ * @returns Modified message array with tunnel context injected
224
+ */
225
+ injectCustomTunnelContext(messages, tunnelCommand, parentContextType, parentContextInfo) {
226
+ const contextMessage = {
227
+ role: "system",
228
+ content: this.buildCustomTunnelContextMessage(tunnelCommand, parentContextType, parentContextInfo)
229
+ };
230
+ const result = [...messages];
231
+ let lastUserMessageIndex = -1;
232
+ for (let i = result.length - 1; i >= 0; i--) {
233
+ if (result[i].role === "user") {
234
+ lastUserMessageIndex = i;
235
+ break;
236
+ }
237
+ }
238
+ if (lastUserMessageIndex >= 0) {
239
+ result.splice(lastUserMessageIndex, 0, contextMessage);
240
+ } else {
241
+ result.push(contextMessage);
242
+ }
243
+ return result;
244
+ }
245
+ /**
246
+ * Build the custom tunnel context message.
247
+ */
248
+ buildCustomTunnelContextMessage(tunnelCommand, parentContextType, parentContextInfo) {
249
+ let message = `
250
+ ## CUSTOM TUNNEL SESSION ACTIVE
251
+
252
+ `;
253
+ message += `You are currently connected to a **custom tunnel** session started by the command: \`${tunnelCommand}\`
254
+
255
+ `;
256
+ if (parentContextType && parentContextType !== "local") {
257
+ message += `**Nesting:** This tunnel is running INSIDE a **${parentContextType.toUpperCase()}** session`;
258
+ if (parentContextInfo) {
259
+ message += ` (${parentContextInfo})`;
260
+ }
261
+ message += `. The tunnel process lives on the remote host, not locally.
262
+
263
+ `;
264
+ }
265
+ message += `**CRITICAL \u2014 READ CAREFULLY:**
266
+ `;
267
+ message += `This is NOT a standard OS shell. The PTY is running \`${tunnelCommand}\`. `;
268
+ message += `This could be a serial console (minicom), a Python REPL, a database CLI, a virtual environment, or any other interactive program.
269
+
270
+ `;
271
+ message += `**The ONLY tool you may use is \`execute_command\`.** Your command text will be automatically routed as text input to the running \`${tunnelCommand}\` process. `;
272
+ message += `All other tools have been removed from your available tool list because they cannot operate inside this tunnel.
273
+
274
+ `;
275
+ message += `**Rules:**
276
+ `;
277
+ message += `1. Use \`execute_command\` to send input to the tunnel process. Only send text that \`${tunnelCommand}\` would understand.
278
+ `;
279
+ message += `2. If the tunnel is a serial console (e.g. minicom), send device-specific commands, NOT Linux shell commands.
280
+ `;
281
+ message += `3. The tunnel output is displayed to the user in the chat as shell result boxes.
282
+ `;
283
+ message += `4. You CANNOT read files, write files, list directories, grep, or use any filesystem tool \u2014 they do not exist in your current tool set.
284
+ `;
285
+ message += `5. If the user asks you to do something that requires normal shell access, tell them to exit the tunnel first (${process.platform === "darwin" ? "Cmd+E" : "Alt+E"} to enter focus mode, then exit the program).
177
286
  `;
178
287
  return message;
179
288
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/ai-context-injector.ts"],"sourcesContent":["/**\r\n * AI Context Injector\r\n * \r\n * Injects subshell context into AI message history to make the AI aware\r\n * of the current execution environment (SSH, WSL, Docker, etc.)\r\n */\r\n\r\nimport type { Message } from './ai-service-client.js';\r\nimport type { SubshellContext } from '../context/types.js';\r\n\r\n/**\r\n * AI Context Injector class\r\n * Handles injection of subshell context into message history before AI calls\r\n */\r\nexport class AIContextInjector {\r\n /**\r\n * Inject subshell context into message history\r\n * \r\n * If the current context is a subshell (not local), this method inserts\r\n * a system message describing the environment before the last user message.\r\n * This ensures the AI is aware of where commands will execute.\r\n * \r\n * @param messages - The conversation history\r\n * @param context - The current subshell context\r\n * @returns Modified message array with context injected (if applicable)\r\n */\r\n injectSubshellContext(messages: Message[], context: SubshellContext): Message[] {\r\n // Don't inject context for local environment\r\n if (context.type === 'local') {\r\n return messages;\r\n }\r\n\r\n // Build context message\r\n const contextMessage: Message = {\r\n role: 'system',\r\n content: this.buildContextMessage(context),\r\n };\r\n\r\n // Insert context message before the last user message\r\n // This ensures the AI sees the context right before processing the request\r\n const result = [...messages];\r\n \r\n // Find the last user message index\r\n let lastUserMessageIndex = -1;\r\n for (let i = result.length - 1; i >= 0; i--) {\r\n if (result[i].role === 'user') {\r\n lastUserMessageIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n // If we found a user message, insert context before it\r\n // Otherwise, append to the end\r\n if (lastUserMessageIndex >= 0) {\r\n result.splice(lastUserMessageIndex, 0, contextMessage);\r\n } else {\r\n result.push(contextMessage);\r\n }\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Build context message describing the current subshell environment\r\n * \r\n * Creates a formatted message that informs the AI about:\r\n * - Environment type (SSH, WSL, Docker)\r\n * - Working directory\r\n * - Shell type\r\n * - Operating system\r\n * - Connection details (hostname, username, etc.)\r\n * \r\n * @param context - The current subshell context\r\n * @returns Formatted context message string\r\n */\r\n private buildContextMessage(context: SubshellContext): string {\r\n const { type, metadata } = context;\r\n\r\n let message = `\\n\\n## CURRENT EXECUTION ENVIRONMENT\\n\\n`;\r\n message += `You are currently operating in a ${type.toUpperCase()} environment.\\n\\n`;\r\n message += `**Environment Details:**\\n`;\r\n message += `- Type: ${type}\\n`;\r\n message += `- Working Directory: ${metadata.workingDirectory}\\n`;\r\n message += `- Shell: ${metadata.shell}\\n`;\r\n message += `- OS: ${metadata.os}\\n`;\r\n\r\n // Add type-specific details\r\n if (metadata.hostname) {\r\n message += `- Hostname: ${metadata.hostname}\\n`;\r\n }\r\n if (metadata.username) {\r\n message += `- Username: ${metadata.username}\\n`;\r\n }\r\n if (metadata.distroName) {\r\n message += `- Distribution: ${metadata.distroName}\\n`;\r\n }\r\n if (metadata.containerId) {\r\n message += `- Container: ${metadata.containerId}\\n`;\r\n }\r\n if (metadata.port) {\r\n message += `- Port: ${metadata.port}\\n`;\r\n }\r\n\r\n message += `\\n**IMPORTANT:** All commands and file operations you execute will run in this ${type} environment, not on the local machine.\\n`;\r\n message += `When reading files, writing files, executing commands, or performing any operations, they will all happen in the ${type} environment.\\n`;\r\n\r\n return message;\r\n }\r\n\r\n /**\r\n * Inject MCP auto-provisioning context into message history.\r\n *\r\n * Adds a system message informing the AI that it has the ability to\r\n * dynamically discover and add MCP servers at runtime, enabling it to\r\n * interact with external services (browsers, APIs, databases, etc.)\r\n * even when no MCP servers are currently configured.\r\n *\r\n * @param messages - The conversation history\r\n * @param connectedMcpServers - Names of currently connected MCP servers (for awareness)\r\n * @returns Modified message array with MCP context injected\r\n */\r\n injectMCPContext(messages: Message[], connectedMcpServers: string[]): Message[] {\r\n const contextMessage: Message = {\r\n role: 'system',\r\n content: this.buildMCPContextMessage(connectedMcpServers),\r\n };\r\n\r\n const result = [...messages];\r\n\r\n // Find the last user message index\r\n let lastUserMessageIndex = -1;\r\n for (let i = result.length - 1; i >= 0; i--) {\r\n if (result[i].role === 'user') {\r\n lastUserMessageIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n if (lastUserMessageIndex >= 0) {\r\n result.splice(lastUserMessageIndex, 0, contextMessage);\r\n } else {\r\n result.push(contextMessage);\r\n }\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Build the MCP auto-provisioning context message.\r\n */\r\n private buildMCPContextMessage(connectedServers: string[]): string {\r\n let message = `\\n## MCP AUTO-PROVISIONING\\n\\n`;\r\n message += `You have the ability to dynamically add MCP (Model Context Protocol) servers at runtime using the \\`add_mcp\\` tool. `;\r\n message += `This means you can interact with ANY external service, application, or environment — even if no MCP integration is currently configured.\\n\\n`;\r\n\r\n if (connectedServers.length > 0) {\r\n message += `**Currently connected MCP servers:** ${connectedServers.join(', ')}\\n`;\r\n message += `Tools from these servers are already available (prefixed with \\`mcp_<serverName>_\\`).\\n\\n`;\r\n } else {\r\n message += `**No MCP servers are currently connected.** You can add them on the fly when needed.\\n\\n`;\r\n }\r\n\r\n message += `**When the user asks you to interact with an external service or application (e.g., \"open Chrome\", \"send a Slack message\", \"query the database\", \"control my smart home\"), follow this workflow:**\\n\\n`;\r\n message += `1. **Check existing tools** — Look at your available tools to see if an MCP integration already provides what you need.\\n`;\r\n message += `2. **Search the web** — If no existing tool handles the request, use \\`web_search\\` to find the right MCP server package. Search for queries like \"MCP server for browser control\", \"MCP server for Slack\", etc. Good sources include the MCP server registry, npm, and GitHub.\\n`;\r\n message += `3. **Get the config** — From the search results or documentation, determine the correct command, args, and any required env vars (API keys, tokens).\\n`;\r\n message += `4. **Add the MCP server** — Use the \\`add_mcp\\` tool with the correct config. The tool will connect to the server and register its tools automatically.\\n`;\r\n message += `5. **Use the new tools** — Once connected, immediately use the newly available \\`mcp_<serverName>_<toolName>\\` tools to fulfill the user's request.\\n\\n`;\r\n\r\n message += `**Tips for finding MCP servers:**\\n`;\r\n message += `- Search npm for packages starting with \\`@modelcontextprotocol/\\` or \\`mcp-server-\\`\\n`;\r\n message += `- Many MCP servers are run via \\`npx -y <package-name>\\`\\n`;\r\n message += `- Check the package README for required environment variables\\n`;\r\n message += `- If a server needs an API key and the user hasn't provided one, ask them for it before adding the server\\n`;\r\n message += `- If the first server you try doesn't work, search for alternatives and try again\\n\\n`;\r\n\r\n return message;\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/services/ai-context-injector.ts"],"sourcesContent":["/**\r\n * AI Context Injector\r\n * \r\n * Injects subshell context into AI message history to make the AI aware\r\n * of the current execution environment (SSH, WSL, Docker, etc.)\r\n */\r\n\r\nimport type { Message } from './ai-service-client.js';\r\nimport type { SubshellContext } from '../context/types.js';\r\n\r\n/**\r\n * AI Context Injector class\r\n * Handles injection of subshell context into message history before AI calls\r\n */\r\nexport class AIContextInjector {\r\n /**\r\n * Inject subshell context into message history\r\n * \r\n * If the current context is a subshell (not local), this method inserts\r\n * a system message describing the environment before the last user message.\r\n * This ensures the AI is aware of where commands will execute.\r\n * \r\n * @param messages - The conversation history\r\n * @param context - The current subshell context\r\n * @returns Modified message array with context injected (if applicable)\r\n */\r\n injectSubshellContext(messages: Message[], context: SubshellContext): Message[] {\r\n // Don't inject context for local environment\r\n if (context.type === 'local') {\r\n return messages;\r\n }\r\n\r\n // Build context message\r\n const contextMessage: Message = {\r\n role: 'system',\r\n content: this.buildContextMessage(context),\r\n };\r\n\r\n // Insert context message before the last user message\r\n // This ensures the AI sees the context right before processing the request\r\n const result = [...messages];\r\n \r\n // Find the last user message index\r\n let lastUserMessageIndex = -1;\r\n for (let i = result.length - 1; i >= 0; i--) {\r\n if (result[i].role === 'user') {\r\n lastUserMessageIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n // If we found a user message, insert context before it\r\n // Otherwise, append to the end\r\n if (lastUserMessageIndex >= 0) {\r\n result.splice(lastUserMessageIndex, 0, contextMessage);\r\n } else {\r\n result.push(contextMessage);\r\n }\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Build context message describing the current subshell environment\r\n * \r\n * Creates a formatted message that informs the AI about:\r\n * - Environment type (SSH, WSL, Docker)\r\n * - Working directory\r\n * - Shell type\r\n * - Operating system\r\n * - Connection details (hostname, username, etc.)\r\n * \r\n * @param context - The current subshell context\r\n * @returns Formatted context message string\r\n */\r\n private buildContextMessage(context: SubshellContext): string {\r\n const { type, metadata } = context;\r\n\r\n let message = `\\n\\n## CURRENT EXECUTION ENVIRONMENT\\n\\n`;\r\n message += `You are currently operating in a ${type.toUpperCase()} environment.\\n\\n`;\r\n message += `**Environment Details:**\\n`;\r\n message += `- Type: ${type}\\n`;\r\n message += `- Working Directory: ${metadata.workingDirectory}\\n`;\r\n message += `- Shell: ${metadata.shell}\\n`;\r\n message += `- OS: ${metadata.os}\\n`;\r\n\r\n // Add type-specific details\r\n if (metadata.hostname) {\r\n message += `- Hostname: ${metadata.hostname}\\n`;\r\n }\r\n if (metadata.username) {\r\n message += `- Username: ${metadata.username}\\n`;\r\n }\r\n if (metadata.distroName) {\r\n message += `- Distribution: ${metadata.distroName}\\n`;\r\n }\r\n if (metadata.containerId) {\r\n message += `- Container: ${metadata.containerId}\\n`;\r\n }\r\n if (metadata.port) {\r\n message += `- Port: ${metadata.port}\\n`;\r\n }\r\n\r\n message += `\\n**IMPORTANT:** All commands and file operations you execute will run in this ${type} environment, not on the local machine.\\n`;\r\n message += `When reading files, writing files, executing commands, or performing any operations, they will all happen in the ${type} environment.\\n`;\r\n\r\n return message;\r\n }\r\n\r\n /**\r\n * Inject MCP auto-provisioning context into message history.\r\n *\r\n * Adds a system message informing the AI that it has the ability to\r\n * dynamically discover and add MCP servers at runtime, enabling it to\r\n * interact with external services (browsers, APIs, databases, etc.)\r\n * even when no MCP servers are currently configured.\r\n *\r\n * @param messages - The conversation history\r\n * @param connectedMcpServers - Names of currently connected MCP servers (for awareness)\r\n * @returns Modified message array with MCP context injected\r\n */\r\n injectMCPContext(messages: Message[], connectedMcpServers: string[]): Message[] {\r\n const contextMessage: Message = {\r\n role: 'system',\r\n content: this.buildMCPContextMessage(connectedMcpServers),\r\n };\r\n\r\n const result = [...messages];\r\n\r\n // Find the last user message index\r\n let lastUserMessageIndex = -1;\r\n for (let i = result.length - 1; i >= 0; i--) {\r\n if (result[i].role === 'user') {\r\n lastUserMessageIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n if (lastUserMessageIndex >= 0) {\r\n result.splice(lastUserMessageIndex, 0, contextMessage);\r\n } else {\r\n result.push(contextMessage);\r\n }\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Build the MCP auto-provisioning context message.\r\n */\r\n private buildMCPContextMessage(connectedServers: string[]): string {\r\n let message = `\\n## MCP AUTO-PROVISIONING\\n\\n`;\r\n message += `You have the ability to dynamically add MCP (Model Context Protocol) servers at runtime using the \\`add_mcp\\` tool. `;\r\n message += `This means you can interact with ANY external service, application, or environment — even if no MCP integration is currently configured.\\n\\n`;\r\n\r\n if (connectedServers.length > 0) {\r\n message += `**Currently connected MCP servers:** ${connectedServers.join(', ')}\\n`;\r\n message += `Tools from these servers are already available (prefixed with \\`mcp_<serverName>_\\`).\\n\\n`;\r\n } else {\r\n message += `**No MCP servers are currently connected.** You can add them on the fly when needed.\\n\\n`;\r\n }\r\n\r\n message += `**When the user asks you to interact with an external service or application (e.g., \"open Chrome\", \"send a Slack message\", \"query the database\", \"control my smart home\"), follow this workflow:**\\n\\n`;\r\n message += `1. **Check existing tools** — Look at your available tools to see if an MCP integration already provides what you need.\\n`;\r\n message += `2. **Search the web** — If no existing tool handles the request, use \\`web_search\\` to find the right MCP server package. Search for queries like \"MCP server for browser control\", \"MCP server for Slack\", etc. Good sources include the MCP server registry, npm, and GitHub.\\n`;\r\n message += `3. **Get the config** — From the search results or documentation, determine the correct command, args, and any required env vars (API keys, tokens).\\n`;\r\n message += `4. **Add the MCP server** — Use the \\`add_mcp\\` tool with the correct config. The tool will connect to the server and register its tools automatically.\\n`;\r\n message += `5. **Use the new tools** — Once connected, immediately use the newly available \\`mcp_<serverName>_<toolName>\\` tools to fulfill the user's request.\\n\\n`;\r\n\r\n message += `**Tips for finding MCP servers:**\\n`;\r\n message += `- Search npm for packages starting with \\`@modelcontextprotocol/\\` or \\`mcp-server-\\`\\n`;\r\n message += `- Many MCP servers are run via \\`npx -y <package-name>\\`\\n`;\r\n message += `- Check the package README for required environment variables\\n`;\r\n message += `- If a server needs an API key and the user hasn't provided one, ask them for it before adding the server\\n`;\r\n message += `- If the first server you try doesn't work, search for alternatives and try again\\n\\n`;\r\n\r\n return message;\r\n }\r\n\r\n /**\r\n * Inject todo list context into message history.\r\n *\r\n * If there is an active todo list, this method inserts a system message\r\n * with the current todo list state before the last user message.\r\n * This ensures the AI remembers its todo list progress across turns.\r\n *\r\n * @param messages - The conversation history\r\n * @param todoContext - The formatted todo list context string (empty if no active list)\r\n * @returns Modified message array with todo context injected (if applicable)\r\n */\r\n injectTodoContext(messages: Message[], todoContext: string): Message[] {\r\n // Don't inject if there's no active todo list\r\n if (!todoContext) {\r\n return messages;\r\n }\r\n\r\n const contextMessage: Message = {\r\n role: 'system',\r\n content: todoContext,\r\n };\r\n\r\n const result = [...messages];\r\n\r\n // Find the last user message index\r\n let lastUserMessageIndex = -1;\r\n for (let i = result.length - 1; i >= 0; i--) {\r\n if (result[i].role === 'user') {\r\n lastUserMessageIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n if (lastUserMessageIndex >= 0) {\r\n result.splice(lastUserMessageIndex, 0, contextMessage);\r\n } else {\r\n result.push(contextMessage);\r\n }\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Inject custom tunnel context into message history.\r\n *\r\n * When a custom tunnel session is active (e.g. minicom, python venv, etc.),\r\n * this method inserts a system message informing the AI about the tunnel\r\n * environment so it can route commands appropriately.\r\n *\r\n * @param messages - The conversation history\r\n * @param tunnelCommand - The command that started the tunnel (e.g. \"minicom\")\r\n * @returns Modified message array with tunnel context injected\r\n */\r\n injectCustomTunnelContext(messages: Message[], tunnelCommand: string, parentContextType?: string, parentContextInfo?: string): Message[] {\r\n const contextMessage: Message = {\r\n role: 'system',\r\n content: this.buildCustomTunnelContextMessage(tunnelCommand, parentContextType, parentContextInfo),\r\n };\r\n\r\n const result = [...messages];\r\n\r\n // Find the last user message index\r\n let lastUserMessageIndex = -1;\r\n for (let i = result.length - 1; i >= 0; i--) {\r\n if (result[i].role === 'user') {\r\n lastUserMessageIndex = i;\r\n break;\r\n }\r\n }\r\n\r\n if (lastUserMessageIndex >= 0) {\r\n result.splice(lastUserMessageIndex, 0, contextMessage);\r\n } else {\r\n result.push(contextMessage);\r\n }\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Build the custom tunnel context message.\r\n */\r\n private buildCustomTunnelContextMessage(tunnelCommand: string, parentContextType?: string, parentContextInfo?: string): string {\r\n let message = `\\n## CUSTOM TUNNEL SESSION ACTIVE\\n\\n`;\r\n message += `You are currently connected to a **custom tunnel** session started by the command: \\`${tunnelCommand}\\`\\n\\n`;\r\n\r\n if (parentContextType && parentContextType !== 'local') {\r\n message += `**Nesting:** This tunnel is running INSIDE a **${parentContextType.toUpperCase()}** session`;\r\n if (parentContextInfo) {\r\n message += ` (${parentContextInfo})`;\r\n }\r\n message += `. The tunnel process lives on the remote host, not locally.\\n\\n`;\r\n }\r\n\r\n message += `**CRITICAL — READ CAREFULLY:**\\n`;\r\n message += `This is NOT a standard OS shell. The PTY is running \\`${tunnelCommand}\\`. `;\r\n message += `This could be a serial console (minicom), a Python REPL, a database CLI, a virtual environment, or any other interactive program.\\n\\n`;\r\n message += `**The ONLY tool you may use is \\`execute_command\\`.** Your command text will be automatically routed as text input to the running \\`${tunnelCommand}\\` process. `;\r\n message += `All other tools have been removed from your available tool list because they cannot operate inside this tunnel.\\n\\n`;\r\n message += `**Rules:**\\n`;\r\n message += `1. Use \\`execute_command\\` to send input to the tunnel process. Only send text that \\`${tunnelCommand}\\` would understand.\\n`;\r\n message += `2. If the tunnel is a serial console (e.g. minicom), send device-specific commands, NOT Linux shell commands.\\n`;\r\n message += `3. The tunnel output is displayed to the user in the chat as shell result boxes.\\n`;\r\n message += `4. You CANNOT read files, write files, list directories, grep, or use any filesystem tool — they do not exist in your current tool set.\\n`;\r\n message += `5. If the user asks you to do something that requires normal shell access, tell them to exit the tunnel first (${process.platform === 'darwin' ? 'Cmd+E' : 'Alt+E'} to enter focus mode, then exit the program).\\n`;\r\n\r\n return message;\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/api-client.ts"],"sourcesContent":["/**\r\n * API Client Service for Centaurus CLI\r\n * \r\n * Handles all communication with the backend REST API including:\r\n * - Authentication and session management\r\n * - Conversation and message operations\r\n * - User settings management\r\n * - API key storage and retrieval\r\n */\r\n\r\nimport axios, { AxiosInstance, AxiosError } from 'axios';\r\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\nimport { homedir } from 'os';\r\nimport { IS_DEV_BUILD, DEV_BACKEND_URL, PRODUCTION_BACKEND_URL } from '../config/build-config.js';\r\nimport { logError } from '../utils/logger.js';\r\n\r\n/**\r\n * Type definitions for API requests and responses\r\n */\r\n\r\n// Authentication types\r\ninterface GoogleAuthInitResponse {\r\n authUrl: string;\r\n state: string;\r\n}\r\n\r\ninterface AuthResponse {\r\n sessionToken: string;\r\n expiresAt: string;\r\n user: {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n };\r\n}\r\n\r\ninterface UserProfile {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n createdAt: string;\r\n}\r\n\r\n// Conversation types\r\ninterface CreateConversationRequest {\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags?: string[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Conversation {\r\n id: string;\r\n userId: string;\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags: string[];\r\n isPinned: boolean;\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n updatedAt: string;\r\n archivedAt?: string;\r\n}\r\n\r\ninterface UpdateConversationRequest {\r\n title?: string;\r\n tags?: string[];\r\n isPinned?: boolean;\r\n metadata?: Record<string, any>;\r\n}\r\n\r\n// Message types\r\ninterface CreateMessageRequest {\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType?: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Message {\r\n id: string;\r\n conversationId: string;\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n editedAt?: string;\r\n}\r\n\r\n// Settings types\r\ninterface UserSettings {\r\n defaultModel?: string;\r\n defaultProvider?: string;\r\n theme?: string;\r\n autoSave?: boolean;\r\n planMode?: boolean;\r\n [key: string]: any;\r\n}\r\n\r\n// Models configuration types\r\nexport interface ModelConfig {\r\n uid: string; // Unique identifier per entry (e.g. \"claude-opus-4-6-thinking\")\r\n id: string; // Actual API model identifier (may be shared across entries)\r\n name: string;\r\n description: string;\r\n provider: string;\r\n contextWindow: number;\r\n region: string;\r\n supportsThinking: boolean;\r\n thinkingConfig?: Record<string, any>;\r\n generationConfig?: {\r\n temperature?: number;\r\n topP?: number;\r\n topK?: number;\r\n maxOutputTokens?: number;\r\n };\r\n allowFrontendDisplay?: boolean; // If false, model is hidden from frontend model picker but valid for API use\r\n}\r\n\r\nexport interface ModelsConfig {\r\n models: ModelConfig[];\r\n defaultModel: string;\r\n}\r\n\r\n// API Response wrapper\r\ninterface ApiResponse<T = any> {\r\n success: boolean;\r\n data?: T;\r\n error?: {\r\n code: string;\r\n message: string;\r\n details?: any;\r\n };\r\n meta?: {\r\n page?: number;\r\n limit?: number;\r\n total?: number;\r\n };\r\n}\r\n\r\n/**\r\n * API Client class for communicating with the backend service\r\n */\r\nclass ApiClient {\r\n private client: AxiosInstance | null = null;\r\n private sessionToken: string | null = null;\r\n private configPath: string;\r\n private configDir: string;\r\n private cachedUser: UserProfile | null = null;\r\n\r\n constructor() {\r\n // Set up session storage path: ~/.centaurus/session.json\r\n this.configDir = join(homedir(), '.centaurus');\r\n this.configPath = join(this.configDir, 'session.json');\r\n\r\n // Load existing session if available\r\n this.loadSession();\r\n\r\n // Don't create axios client yet - wait until first use\r\n // This allows environment variables to be loaded first\r\n }\r\n\r\n /**\r\n * Helper to extract data from API response safely\r\n */\r\n private extractData<T>(response: { data: ApiResponse<T> }): T {\r\n if (response.data.data === undefined || response.data.data === null) {\r\n throw new Error('API response missing expected data payload');\r\n }\r\n return response.data.data as T;\r\n }\r\n\r\n /**\r\n * Get or create the axios client instance\r\n * This is lazy-loaded to ensure environment variables are loaded first\r\n */\r\n private getClient(): AxiosInstance {\r\n if (!this.client) {\r\n // Import build config - values frozen at compile time for security\r\n // SECURITY: This prevents malicious .env files from overriding production URLs\r\n\r\n // Use production URL in production builds, localhost only in dev builds\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n\r\n this.client = axios.create({\r\n baseURL,\r\n timeout: 30000,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n });\r\n\r\n // Request interceptor: Add Authorization header if session token exists\r\n this.getClient().interceptors.request.use(\r\n (config) => {\r\n if (this.sessionToken) {\r\n config.headers.Authorization = `Bearer ${this.sessionToken}`;\r\n }\r\n return config;\r\n },\r\n (error) => {\r\n return Promise.reject(error);\r\n }\r\n );\r\n\r\n // Response interceptor: Handle 401 errors (expired/invalid session)\r\n this.getClient().interceptors.response.use(\r\n (response) => response,\r\n async (error: AxiosError) => {\r\n if (error.response?.status === 401) {\r\n // Clear invalid session\r\n this.clearSession();\r\n\r\n // Create a more user-friendly error\r\n const authError = new Error('Session expired. Please sign in again.');\r\n authError.name = 'AuthenticationError';\r\n throw authError;\r\n }\r\n\r\n // For other errors, extract message from API response if available\r\n if (error.response?.data) {\r\n const apiError = error.response.data as ApiResponse;\r\n if (apiError.error) {\r\n const customError = new Error(apiError.error.message);\r\n customError.name = apiError.error.code;\r\n throw customError;\r\n }\r\n }\r\n\r\n throw error;\r\n }\r\n );\r\n }\r\n\r\n return this.client;\r\n }\r\n\r\n /**\r\n * Load session token from local config file\r\n */\r\n private loadSession(): void {\r\n try {\r\n if (existsSync(this.configPath)) {\r\n const data = readFileSync(this.configPath, 'utf-8');\r\n const session = JSON.parse(data);\r\n this.sessionToken = session.sessionToken || null;\r\n }\r\n } catch (error) {\r\n // If there's any error reading the session, just start fresh\r\n this.sessionToken = null;\r\n }\r\n }\r\n\r\n /**\r\n * Save session token to local config file\r\n */\r\n private saveSession(token: string, expiresAt?: string): void {\r\n try {\r\n // Ensure config directory exists\r\n if (!existsSync(this.configDir)) {\r\n mkdirSync(this.configDir, { recursive: true, mode: 0o700 });\r\n }\r\n\r\n // Save session data\r\n const sessionData = {\r\n sessionToken: token,\r\n expiresAt: expiresAt || null,\r\n savedAt: new Date().toISOString(),\r\n };\r\n\r\n writeFileSync(this.configPath, JSON.stringify(sessionData, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n this.sessionToken = token;\r\n } catch (error) {\r\n logError('Failed to save session', error as Error);\r\n throw new Error('Failed to save session locally');\r\n }\r\n }\r\n\r\n /**\r\n * Clear session token from memory and local storage\r\n */\r\n private clearSession(): void {\r\n this.sessionToken = null;\r\n this.cachedUser = null;\r\n try {\r\n if (existsSync(this.configPath)) {\r\n unlinkSync(this.configPath);\r\n }\r\n } catch (error) {\r\n // Ignore errors when clearing session\r\n }\r\n }\r\n\r\n /**\r\n * Check if user is authenticated\r\n */\r\n public isAuthenticated(): boolean {\r\n return this.sessionToken !== null;\r\n }\r\n\r\n // ==================== Authentication Methods ====================\r\n\r\n /**\r\n * Initialize Google OAuth flow\r\n * @param redirectUri - The URI to redirect to after OAuth\r\n * @returns OAuth URL and state parameter\r\n */\r\n async initGoogleAuth(redirectUri: string): Promise<GoogleAuthInitResponse> {\r\n const response = await this.getClient().post<ApiResponse<GoogleAuthInitResponse>>(\r\n '/auth/google/init',\r\n { redirectUri }\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Complete Google OAuth authentication\r\n * @param code - Authorization code from Google\r\n * @param state - State parameter for CSRF protection\r\n * @returns Session token and user information\r\n */\r\n async authenticate(code: string, state: string): Promise<AuthResponse> {\r\n const response = await this.getClient().post<ApiResponse<AuthResponse>>(\r\n '/auth/google/callback',\r\n { code, state }\r\n );\r\n\r\n const authData = this.extractData(response);\r\n this.saveSession(authData.sessionToken, authData.expiresAt);\r\n\r\n return authData;\r\n }\r\n\r\n /**\r\n * Set session token directly (used when receiving token from web app)\r\n * @param sessionToken - The session token to save\r\n * @param user - User information\r\n */\r\n setSessionToken(sessionToken: string, user?: any): void {\r\n // Calculate expiration (30 days from now)\r\n const expiresAt = new Date();\r\n expiresAt.setDate(expiresAt.getDate() + 30);\r\n\r\n this.saveSession(sessionToken, expiresAt.toISOString());\r\n }\r\n\r\n /**\r\n * Refresh the current session token\r\n * @returns New session token and expiration\r\n */\r\n async refreshSession(): Promise<{ sessionToken: string; expiresAt: string }> {\r\n const response = await this.getClient().post<ApiResponse<{ sessionToken: string; expiresAt: string }>>(\r\n '/auth/refresh'\r\n );\r\n\r\n const refreshData = this.extractData(response);\r\n this.saveSession(refreshData.sessionToken, refreshData.expiresAt);\r\n\r\n return refreshData;\r\n }\r\n\r\n /**\r\n * Logout and invalidate current session\r\n */\r\n async logout(): Promise<void> {\r\n try {\r\n await this.getClient().post('/auth/logout');\r\n } finally {\r\n // Always clear local session, even if API call fails\r\n this.clearSession();\r\n }\r\n }\r\n\r\n /**\r\n * Get current authenticated user profile\r\n * @returns User profile information\r\n */\r\n async getCurrentUser(): Promise<UserProfile> {\r\n const response = await this.getClient().get<ApiResponse<UserProfile>>('/auth/me');\r\n const user = this.extractData(response);\r\n this.cachedUser = user;\r\n return user;\r\n }\r\n\r\n getCachedUser(): UserProfile | null {\r\n return this.cachedUser;\r\n }\r\n\r\n // ==================== Conversation Methods ====================\r\n\r\n /**\r\n * Create a new conversation\r\n * @param data - Conversation creation parameters\r\n * @returns Created conversation\r\n */\r\n async createConversation(data: CreateConversationRequest): Promise<Conversation> {\r\n const response = await this.getClient().post<ApiResponse<Conversation>>(\r\n '/threads',\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all conversations for the authenticated user\r\n * @param params - Pagination and filter parameters\r\n * @returns List of conversations with pagination metadata\r\n */\r\n async getConversations(params?: {\r\n page?: number;\r\n limit?: number;\r\n includeArchived?: boolean;\r\n tags?: string[];\r\n }): Promise<ApiResponse<Conversation[]>> {\r\n const queryParams: any = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 20,\r\n };\r\n\r\n if (params?.includeArchived !== undefined) {\r\n queryParams.includeArchived = params.includeArchived;\r\n }\r\n\r\n if (params?.tags && params.tags.length > 0) {\r\n queryParams.tags = params.tags.join(',');\r\n }\r\n\r\n const response = await this.getClient().get<ApiResponse<Conversation[]>>(\r\n '/threads',\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n /**\r\n * Get a specific conversation by ID\r\n * @param conversationId - The conversation ID\r\n * @returns Conversation details\r\n */\r\n async getConversation(conversationId: string): Promise<Conversation> {\r\n const response = await this.getClient().get<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update a conversation\r\n * @param conversationId - The conversation ID\r\n * @param data - Fields to update\r\n * @returns Updated conversation\r\n */\r\n async updateConversation(\r\n conversationId: string,\r\n data: UpdateConversationRequest\r\n ): Promise<Conversation> {\r\n const response = await this.getClient().put<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`,\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete (archive) a conversation\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversation(conversationId: string): Promise<void> {\r\n await this.getClient().delete(`/threads/${conversationId}`);\r\n }\r\n\r\n // ==================== Message Methods ====================\r\n\r\n /**\r\n * Add a message to a conversation\r\n * @param conversationId - The conversation ID\r\n * @param message - Message data\r\n * @returns Created message\r\n */\r\n async addMessage(\r\n conversationId: string,\r\n message: CreateMessageRequest\r\n ): Promise<Message> {\r\n const response = await this.getClient().post<ApiResponse<Message>>(\r\n `/threads/${conversationId}/messages`,\r\n message\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all messages for a conversation\r\n * @param conversationId - The conversation ID\r\n * @param params - Pagination parameters\r\n * @returns List of messages with pagination metadata\r\n */\r\n async getMessages(\r\n conversationId: string,\r\n params?: { page?: number; limit?: number }\r\n ): Promise<ApiResponse<Message[]>> {\r\n const queryParams = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 50,\r\n };\r\n\r\n const response = await this.getClient().get<ApiResponse<Message[]>>(\r\n `/threads/${conversationId}/messages`,\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n // ==================== Settings Methods ====================\r\n\r\n /**\r\n * Get user settings\r\n * @returns User settings object\r\n */\r\n async getSettings(): Promise<UserSettings> {\r\n const response = await this.getClient().get<ApiResponse<UserSettings>>('/settings');\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update user settings\r\n * @param settings - Settings to update (partial update supported)\r\n * @returns Updated settings\r\n */\r\n async updateSettings(settings: Partial<UserSettings>): Promise<UserSettings> {\r\n const response = await this.getClient().put<ApiResponse<UserSettings>>(\r\n '/settings',\r\n settings\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Models Configuration Methods ====================\r\n\r\n /**\r\n * Get available AI models configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Models configuration including all model variants and default model\r\n */\r\n async getModelsConfig(): Promise<ModelsConfig> {\r\n const response = await this.getClient().get<ApiResponse<ModelsConfig>>('/models');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Rate Limits Configuration Methods ====================\r\n\r\n /**\r\n * Get rate limits configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Rate limits configuration including session quotas\r\n */\r\n async getRateLimitsConfig(): Promise<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }> {\r\n const response = await this.getClient().get<ApiResponse<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }>>('/rate-limits/session');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Classification Methods ====================\r\n\r\n /**\r\n * Classify user input to determine if it's a terminal command or AI message\r\n * @param text - Input text to classify\r\n * @returns Mode prediction: 'terminal' or 'ai'\r\n */\r\n async classifyInput(text: string): Promise<'terminal' | 'ai'> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ mode: 'terminal' | 'ai' }>>(\r\n '/classify',\r\n { text }\r\n );\r\n return response.data.data?.mode || 'ai';\r\n } catch (error) {\r\n // On error, return default 'ai' mode - silent fallback\r\n return 'ai';\r\n }\r\n }\r\n\r\n // ==================== File Upload Methods ====================\r\n\r\n /**\r\n * Upload a file to the backend for AI processing\r\n * @param conversationId - The conversation ID\r\n * @param fileName - Original file name\r\n * @param fileType - MIME type\r\n * @param fileData - Base64 encoded file data\r\n * @returns Upload result with gcsUri for Vertex AI\r\n */\r\n async uploadFile(\r\n conversationId: string,\r\n fileName: string,\r\n fileType: string,\r\n fileData: string\r\n ): Promise<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }>>('/files', { conversationId, fileName, fileType, mimeType: fileType, fileData, clientType: 'cli' });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete all files for a conversation from storage (Supabase and GCS)\r\n * Call this when deleting a conversation to clean up associated images\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversationFiles(conversationId: string): Promise<void> {\r\n try {\r\n await this.getClient().delete(`/files/by-thread/${conversationId}`);\r\n } catch (error) {\r\n // Silently fail - files might not exist or user might not be authenticated\r\n // This is a cleanup operation, so we don't want to block chat deletion\r\n }\r\n }\r\n\r\n // ==================== Sync Methods ====================\r\n\r\n /**\r\n * Upload sync data (combined chat history and config)\r\n * @param syncData - The combined data to sync\r\n * @returns Upload result with version info\r\n */\r\n async uploadSyncData(syncData: any): Promise<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }>>('/sync', { syncData });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get sync data for the current user\r\n * @returns Sync data or null if not found\r\n */\r\n async getSyncData(): Promise<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n } | null> {\r\n try {\r\n const response = await this.getClient().get<ApiResponse<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n }>>('/sync');\r\n return this.extractData(response);\r\n } catch (error: any) {\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Delete sync data for the current user\r\n */\r\n async deleteSyncData(): Promise<void> {\r\n await this.getClient().delete('/sync');\r\n }\r\n\r\n // ==================== Token Counting Methods ====================\r\n\r\n /**\r\n * Count tokens for a given model and messages\r\n * Uses backend's accurate token counting (Vertex AI countTokens API)\r\n * @param model - Model name (e.g., gemini-2.5-flash)\r\n * @param messages - Array of conversation messages\r\n * @returns Total token count including system prompt\r\n */\r\n async countTokens(model: string, messages: any[]): Promise<number> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ tokenCount: number; model: string }>>(\r\n '/chat/token-count',\r\n { model, messages }\r\n );\r\n return response.data.data?.tokenCount || 0;\r\n } catch (error) {\r\n logError('Failed to count tokens via API', error as Error);\r\n\r\n // Fallback to character-based estimation\r\n const totalCharacters = messages.reduce((sum, msg) => {\r\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);\r\n return sum + content.length;\r\n }, 0);\r\n\r\n // Add system prompt estimate (roughly 14000 characters)\r\n // Use 1 token ≈ 4 characters for Gemini models\r\n return Math.ceil((totalCharacters + 14000) / 4);\r\n }\r\n }\r\n\r\n // ==================== Health Check ====================\r\n\r\n /**\r\n * Check backend service health\r\n * @returns Health status information\r\n */\r\n async healthCheck(): Promise<{\r\n status: string;\r\n timestamp: string;\r\n database: string;\r\n version: string;\r\n }> {\r\n // Health endpoint is at root level, not under /api\r\n // So we need to construct the full URL manually\r\n // Use build config for URL (frozen at compile time)\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n const healthURL = baseURL.replace('/v1', '/health');\r\n\r\n const response = await axios.get<ApiResponse<any>>(healthURL);\r\n return this.extractData(response);\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const apiClient = new ApiClient();\r\n\r\n// Export types for use in other modules\r\nexport type {\r\n GoogleAuthInitResponse,\r\n AuthResponse,\r\n UserProfile,\r\n CreateConversationRequest,\r\n Conversation,\r\n UpdateConversationRequest,\r\n CreateMessageRequest,\r\n Message,\r\n UserSettings,\r\n ApiResponse,\r\n};\r\n\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/services/api-client.ts"],"sourcesContent":["/**\r\n * API Client Service for Centaurus CLI\r\n * \r\n * Handles all communication with the backend REST API including:\r\n * - Authentication and session management\r\n * - Conversation and message operations\r\n * - User settings management\r\n * - API key storage and retrieval\r\n */\r\n\r\nimport axios, { AxiosInstance, AxiosError } from 'axios';\r\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\nimport { homedir } from 'os';\r\nimport { IS_DEV_BUILD, DEV_BACKEND_URL, PRODUCTION_BACKEND_URL } from '../config/build-config.js';\r\nimport { logError } from '../utils/logger.js';\r\n\r\n/**\r\n * Type definitions for API requests and responses\r\n */\r\n\r\n// Authentication types\r\ninterface GoogleAuthInitResponse {\r\n authUrl: string;\r\n state: string;\r\n}\r\n\r\ninterface AuthResponse {\r\n sessionToken: string;\r\n expiresAt: string;\r\n user: {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n };\r\n}\r\n\r\ninterface UserProfile {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n createdAt: string;\r\n}\r\n\r\n// Conversation types\r\ninterface CreateConversationRequest {\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags?: string[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Conversation {\r\n id: string;\r\n userId: string;\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags: string[];\r\n isPinned: boolean;\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n updatedAt: string;\r\n archivedAt?: string;\r\n}\r\n\r\ninterface UpdateConversationRequest {\r\n title?: string;\r\n tags?: string[];\r\n isPinned?: boolean;\r\n metadata?: Record<string, any>;\r\n}\r\n\r\n// Message types\r\ninterface CreateMessageRequest {\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType?: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Message {\r\n id: string;\r\n conversationId: string;\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n editedAt?: string;\r\n}\r\n\r\n// Settings types\r\ninterface UserSettings {\r\n defaultModel?: string;\r\n defaultProvider?: string;\r\n theme?: string;\r\n autoSave?: boolean;\r\n planMode?: boolean;\r\n [key: string]: any;\r\n}\r\n\r\n// Models configuration types\r\nexport interface ModelConfig {\r\n uid: string; // Unique identifier per entry (e.g. \"claude-opus-4-6-thinking\")\r\n id: string; // Actual API model identifier (may be shared across entries)\r\n name: string;\r\n description: string;\r\n provider: string;\r\n contextWindow: number;\r\n region: string;\r\n supportsThinking: boolean;\r\n thinkingConfig?: Record<string, any>;\r\n generationConfig?: {\r\n temperature?: number;\r\n topP?: number;\r\n topK?: number;\r\n maxOutputTokens?: number;\r\n };\r\n allowFrontendDisplay?: boolean; // If false, model is hidden from frontend model picker but valid for API use\r\n group?: string; // Group key for models with multiple configs (e.g. \"gemini-3.1-pro\")\r\n variantLabel?: string; // Display label for this variant within its group (e.g. \"High Thinking\")\r\n displayProvider?: string; // Override provider name for UI grouping (e.g. \"anthropic\" for Claude-branded models on other backends)\r\n}\r\n\r\nexport interface ModelsConfig {\r\n models: ModelConfig[];\r\n defaultModel: string;\r\n}\r\n\r\n// API Response wrapper\r\ninterface ApiResponse<T = any> {\r\n success: boolean;\r\n data?: T;\r\n error?: {\r\n code: string;\r\n message: string;\r\n details?: any;\r\n };\r\n meta?: {\r\n page?: number;\r\n limit?: number;\r\n total?: number;\r\n };\r\n}\r\n\r\n/**\r\n * API Client class for communicating with the backend service\r\n */\r\nclass ApiClient {\r\n private client: AxiosInstance | null = null;\r\n private sessionToken: string | null = null;\r\n private configPath: string;\r\n private configDir: string;\r\n private cachedUser: UserProfile | null = null;\r\n\r\n constructor() {\r\n // Set up session storage path: ~/.centaurus/session.json\r\n this.configDir = join(homedir(), '.centaurus');\r\n this.configPath = join(this.configDir, 'session.json');\r\n\r\n // Load existing session if available\r\n this.loadSession();\r\n\r\n // Don't create axios client yet - wait until first use\r\n // This allows environment variables to be loaded first\r\n }\r\n\r\n /**\r\n * Helper to extract data from API response safely\r\n */\r\n private extractData<T>(response: { data: ApiResponse<T> }): T {\r\n if (response.data.data === undefined || response.data.data === null) {\r\n throw new Error('API response missing expected data payload');\r\n }\r\n return response.data.data as T;\r\n }\r\n\r\n /**\r\n * Get or create the axios client instance\r\n * This is lazy-loaded to ensure environment variables are loaded first\r\n */\r\n private getClient(): AxiosInstance {\r\n if (!this.client) {\r\n // Import build config - values frozen at compile time for security\r\n // SECURITY: This prevents malicious .env files from overriding production URLs\r\n\r\n // Use production URL in production builds, localhost only in dev builds\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n\r\n this.client = axios.create({\r\n baseURL,\r\n timeout: 30000,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n });\r\n\r\n // Request interceptor: Add Authorization header if session token exists\r\n this.getClient().interceptors.request.use(\r\n (config) => {\r\n if (this.sessionToken) {\r\n config.headers.Authorization = `Bearer ${this.sessionToken}`;\r\n }\r\n return config;\r\n },\r\n (error) => {\r\n return Promise.reject(error);\r\n }\r\n );\r\n\r\n // Response interceptor: Handle 401 errors (expired/invalid session)\r\n this.getClient().interceptors.response.use(\r\n (response) => response,\r\n async (error: AxiosError) => {\r\n if (error.response?.status === 401) {\r\n // Clear invalid session\r\n this.clearSession();\r\n\r\n // Create a more user-friendly error\r\n const authError = new Error('Session expired. Please sign in again.');\r\n authError.name = 'AuthenticationError';\r\n throw authError;\r\n }\r\n\r\n // For other errors, extract message from API response if available\r\n if (error.response?.data) {\r\n const apiError = error.response.data as ApiResponse;\r\n if (apiError.error) {\r\n const customError = new Error(apiError.error.message);\r\n customError.name = apiError.error.code;\r\n throw customError;\r\n }\r\n }\r\n\r\n throw error;\r\n }\r\n );\r\n }\r\n\r\n return this.client;\r\n }\r\n\r\n /**\r\n * Load session token from local config file\r\n */\r\n private loadSession(): void {\r\n try {\r\n if (existsSync(this.configPath)) {\r\n const data = readFileSync(this.configPath, 'utf-8');\r\n const session = JSON.parse(data);\r\n this.sessionToken = session.sessionToken || null;\r\n }\r\n } catch (error) {\r\n // If there's any error reading the session, just start fresh\r\n this.sessionToken = null;\r\n }\r\n }\r\n\r\n /**\r\n * Save session token to local config file\r\n */\r\n private saveSession(token: string, expiresAt?: string): void {\r\n try {\r\n // Ensure config directory exists\r\n if (!existsSync(this.configDir)) {\r\n mkdirSync(this.configDir, { recursive: true, mode: 0o700 });\r\n }\r\n\r\n // Save session data\r\n const sessionData = {\r\n sessionToken: token,\r\n expiresAt: expiresAt || null,\r\n savedAt: new Date().toISOString(),\r\n };\r\n\r\n writeFileSync(this.configPath, JSON.stringify(sessionData, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n this.sessionToken = token;\r\n } catch (error) {\r\n logError('Failed to save session', error as Error);\r\n throw new Error('Failed to save session locally');\r\n }\r\n }\r\n\r\n /**\r\n * Clear session token from memory and local storage\r\n */\r\n private clearSession(): void {\r\n this.sessionToken = null;\r\n this.cachedUser = null;\r\n try {\r\n if (existsSync(this.configPath)) {\r\n unlinkSync(this.configPath);\r\n }\r\n } catch (error) {\r\n // Ignore errors when clearing session\r\n }\r\n }\r\n\r\n /**\r\n * Check if user is authenticated\r\n */\r\n public isAuthenticated(): boolean {\r\n return this.sessionToken !== null;\r\n }\r\n\r\n // ==================== Authentication Methods ====================\r\n\r\n /**\r\n * Initialize Google OAuth flow\r\n * @param redirectUri - The URI to redirect to after OAuth\r\n * @returns OAuth URL and state parameter\r\n */\r\n async initGoogleAuth(redirectUri: string): Promise<GoogleAuthInitResponse> {\r\n const response = await this.getClient().post<ApiResponse<GoogleAuthInitResponse>>(\r\n '/auth/google/init',\r\n { redirectUri }\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Complete Google OAuth authentication\r\n * @param code - Authorization code from Google\r\n * @param state - State parameter for CSRF protection\r\n * @returns Session token and user information\r\n */\r\n async authenticate(code: string, state: string): Promise<AuthResponse> {\r\n const response = await this.getClient().post<ApiResponse<AuthResponse>>(\r\n '/auth/google/callback',\r\n { code, state }\r\n );\r\n\r\n const authData = this.extractData(response);\r\n this.saveSession(authData.sessionToken, authData.expiresAt);\r\n\r\n return authData;\r\n }\r\n\r\n /**\r\n * Set session token directly (used when receiving token from web app)\r\n * @param sessionToken - The session token to save\r\n * @param user - User information\r\n */\r\n setSessionToken(sessionToken: string, user?: any): void {\r\n // Calculate expiration (30 days from now)\r\n const expiresAt = new Date();\r\n expiresAt.setDate(expiresAt.getDate() + 30);\r\n\r\n this.saveSession(sessionToken, expiresAt.toISOString());\r\n }\r\n\r\n /**\r\n * Refresh the current session token\r\n * @returns New session token and expiration\r\n */\r\n async refreshSession(): Promise<{ sessionToken: string; expiresAt: string }> {\r\n const response = await this.getClient().post<ApiResponse<{ sessionToken: string; expiresAt: string }>>(\r\n '/auth/refresh'\r\n );\r\n\r\n const refreshData = this.extractData(response);\r\n this.saveSession(refreshData.sessionToken, refreshData.expiresAt);\r\n\r\n return refreshData;\r\n }\r\n\r\n /**\r\n * Logout and invalidate current session\r\n */\r\n async logout(): Promise<void> {\r\n try {\r\n await this.getClient().post('/auth/logout');\r\n } finally {\r\n // Always clear local session, even if API call fails\r\n this.clearSession();\r\n }\r\n }\r\n\r\n /**\r\n * Get current authenticated user profile\r\n * @returns User profile information\r\n */\r\n async getCurrentUser(): Promise<UserProfile> {\r\n const response = await this.getClient().get<ApiResponse<UserProfile>>('/auth/me');\r\n const user = this.extractData(response);\r\n this.cachedUser = user;\r\n return user;\r\n }\r\n\r\n getCachedUser(): UserProfile | null {\r\n return this.cachedUser;\r\n }\r\n\r\n // ==================== Conversation Methods ====================\r\n\r\n /**\r\n * Create a new conversation\r\n * @param data - Conversation creation parameters\r\n * @returns Created conversation\r\n */\r\n async createConversation(data: CreateConversationRequest): Promise<Conversation> {\r\n const response = await this.getClient().post<ApiResponse<Conversation>>(\r\n '/threads',\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all conversations for the authenticated user\r\n * @param params - Pagination and filter parameters\r\n * @returns List of conversations with pagination metadata\r\n */\r\n async getConversations(params?: {\r\n page?: number;\r\n limit?: number;\r\n includeArchived?: boolean;\r\n tags?: string[];\r\n }): Promise<ApiResponse<Conversation[]>> {\r\n const queryParams: any = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 20,\r\n };\r\n\r\n if (params?.includeArchived !== undefined) {\r\n queryParams.includeArchived = params.includeArchived;\r\n }\r\n\r\n if (params?.tags && params.tags.length > 0) {\r\n queryParams.tags = params.tags.join(',');\r\n }\r\n\r\n const response = await this.getClient().get<ApiResponse<Conversation[]>>(\r\n '/threads',\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n /**\r\n * Get a specific conversation by ID\r\n * @param conversationId - The conversation ID\r\n * @returns Conversation details\r\n */\r\n async getConversation(conversationId: string): Promise<Conversation> {\r\n const response = await this.getClient().get<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update a conversation\r\n * @param conversationId - The conversation ID\r\n * @param data - Fields to update\r\n * @returns Updated conversation\r\n */\r\n async updateConversation(\r\n conversationId: string,\r\n data: UpdateConversationRequest\r\n ): Promise<Conversation> {\r\n const response = await this.getClient().put<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`,\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete (archive) a conversation\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversation(conversationId: string): Promise<void> {\r\n await this.getClient().delete(`/threads/${conversationId}`);\r\n }\r\n\r\n // ==================== Message Methods ====================\r\n\r\n /**\r\n * Add a message to a conversation\r\n * @param conversationId - The conversation ID\r\n * @param message - Message data\r\n * @returns Created message\r\n */\r\n async addMessage(\r\n conversationId: string,\r\n message: CreateMessageRequest\r\n ): Promise<Message> {\r\n const response = await this.getClient().post<ApiResponse<Message>>(\r\n `/threads/${conversationId}/messages`,\r\n message\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all messages for a conversation\r\n * @param conversationId - The conversation ID\r\n * @param params - Pagination parameters\r\n * @returns List of messages with pagination metadata\r\n */\r\n async getMessages(\r\n conversationId: string,\r\n params?: { page?: number; limit?: number }\r\n ): Promise<ApiResponse<Message[]>> {\r\n const queryParams = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 50,\r\n };\r\n\r\n const response = await this.getClient().get<ApiResponse<Message[]>>(\r\n `/threads/${conversationId}/messages`,\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n // ==================== Settings Methods ====================\r\n\r\n /**\r\n * Get user settings\r\n * @returns User settings object\r\n */\r\n async getSettings(): Promise<UserSettings> {\r\n const response = await this.getClient().get<ApiResponse<UserSettings>>('/settings');\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update user settings\r\n * @param settings - Settings to update (partial update supported)\r\n * @returns Updated settings\r\n */\r\n async updateSettings(settings: Partial<UserSettings>): Promise<UserSettings> {\r\n const response = await this.getClient().put<ApiResponse<UserSettings>>(\r\n '/settings',\r\n settings\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Models Configuration Methods ====================\r\n\r\n /**\r\n * Get available AI models configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Models configuration including all model variants and default model\r\n */\r\n async getModelsConfig(): Promise<ModelsConfig> {\r\n const response = await this.getClient().get<ApiResponse<ModelsConfig>>('/models');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Rate Limits Configuration Methods ====================\r\n\r\n /**\r\n * Get rate limits configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Rate limits configuration including session quotas\r\n */\r\n async getRateLimitsConfig(): Promise<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }> {\r\n const response = await this.getClient().get<ApiResponse<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }>>('/rate-limits/session');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Classification Methods ====================\r\n\r\n /**\r\n * Classify user input to determine if it's a terminal command or AI message\r\n * @param text - Input text to classify\r\n * @returns Mode prediction: 'terminal' or 'ai'\r\n */\r\n async classifyInput(text: string): Promise<'terminal' | 'ai'> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ mode: 'terminal' | 'ai' }>>(\r\n '/classify',\r\n { text }\r\n );\r\n return response.data.data?.mode || 'ai';\r\n } catch (error) {\r\n // On error, return default 'ai' mode - silent fallback\r\n return 'ai';\r\n }\r\n }\r\n\r\n // ==================== File Upload Methods ====================\r\n\r\n /**\r\n * Upload a file to the backend for AI processing\r\n * @param conversationId - The conversation ID\r\n * @param fileName - Original file name\r\n * @param fileType - MIME type\r\n * @param fileData - Base64 encoded file data\r\n * @returns Upload result with gcsUri for Vertex AI\r\n */\r\n async uploadFile(\r\n conversationId: string,\r\n fileName: string,\r\n fileType: string,\r\n fileData: string\r\n ): Promise<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }>>('/files', { conversationId, fileName, fileType, mimeType: fileType, fileData, clientType: 'cli' });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete all files for a conversation from storage (Supabase and GCS)\r\n * Call this when deleting a conversation to clean up associated images\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversationFiles(conversationId: string): Promise<void> {\r\n try {\r\n await this.getClient().delete(`/files/by-thread/${conversationId}`);\r\n } catch (error) {\r\n // Silently fail - files might not exist or user might not be authenticated\r\n // This is a cleanup operation, so we don't want to block chat deletion\r\n }\r\n }\r\n\r\n // ==================== Sync Methods ====================\r\n\r\n /**\r\n * Upload sync data (combined chat history and config)\r\n * @param syncData - The combined data to sync\r\n * @returns Upload result with version info\r\n */\r\n async uploadSyncData(syncData: any): Promise<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }>>('/sync', { syncData });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get sync data for the current user\r\n * @returns Sync data or null if not found\r\n */\r\n async getSyncData(): Promise<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n } | null> {\r\n try {\r\n const response = await this.getClient().get<ApiResponse<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n }>>('/sync');\r\n return this.extractData(response);\r\n } catch (error: any) {\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Delete sync data for the current user\r\n */\r\n async deleteSyncData(): Promise<void> {\r\n await this.getClient().delete('/sync');\r\n }\r\n\r\n // ==================== Token Counting Methods ====================\r\n\r\n /**\r\n * Count tokens for a given model and messages\r\n * Uses backend's accurate token counting (Vertex AI countTokens API)\r\n * @param model - Model name (e.g., gemini-2.5-flash)\r\n * @param messages - Array of conversation messages\r\n * @returns Total token count including system prompt\r\n */\r\n async countTokens(model: string, messages: any[]): Promise<number> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ tokenCount: number; model: string }>>(\r\n '/chat/token-count',\r\n { model, messages }\r\n );\r\n return response.data.data?.tokenCount || 0;\r\n } catch (error) {\r\n logError('Failed to count tokens via API', error as Error);\r\n\r\n // Fallback to character-based estimation\r\n const totalCharacters = messages.reduce((sum, msg) => {\r\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);\r\n return sum + content.length;\r\n }, 0);\r\n\r\n // Add system prompt estimate (roughly 14000 characters)\r\n // Use 1 token ≈ 4 characters for Gemini models\r\n return Math.ceil((totalCharacters + 14000) / 4);\r\n }\r\n }\r\n\r\n // ==================== Health Check ====================\r\n\r\n /**\r\n * Check backend service health\r\n * @returns Health status information\r\n */\r\n async healthCheck(): Promise<{\r\n status: string;\r\n timestamp: string;\r\n database: string;\r\n version: string;\r\n }> {\r\n // Health endpoint is at root level, not under /api\r\n // So we need to construct the full URL manually\r\n // Use build config for URL (frozen at compile time)\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n const healthURL = baseURL.replace('/v1', '/health');\r\n\r\n const response = await axios.get<ApiResponse<any>>(healthURL);\r\n return this.extractData(response);\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const apiClient = new ApiClient();\r\n\r\n// Export types for use in other modules\r\nexport type {\r\n GoogleAuthInitResponse,\r\n AuthResponse,\r\n UserProfile,\r\n CreateConversationRequest,\r\n Conversation,\r\n UpdateConversationRequest,\r\n CreateMessageRequest,\r\n Message,\r\n UserSettings,\r\n ApiResponse,\r\n};\r\n\r\n"],"mappings":"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","names":[]}
@@ -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;