centaurus-cli 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +151 -1
  2. package/dist/cli-adapter.d.ts +41 -2
  3. package/dist/cli-adapter.d.ts.map +1 -1
  4. package/dist/cli-adapter.js +407 -79
  5. package/dist/cli-adapter.js.map +1 -1
  6. package/dist/config/types.d.ts +23 -0
  7. package/dist/config/types.d.ts.map +1 -1
  8. package/dist/config/types.js +20 -0
  9. package/dist/config/types.js.map +1 -1
  10. package/dist/context/__tests__/command-detector.test.d.ts +14 -0
  11. package/dist/context/__tests__/command-detector.test.d.ts.map +1 -0
  12. package/dist/context/__tests__/command-detector.test.js +318 -0
  13. package/dist/context/__tests__/command-detector.test.js.map +1 -0
  14. package/dist/context/__tests__/context-manager.test.d.ts +16 -0
  15. package/dist/context/__tests__/context-manager.test.d.ts.map +1 -0
  16. package/dist/context/__tests__/context-manager.test.js +375 -0
  17. package/dist/context/__tests__/context-manager.test.js.map +1 -0
  18. package/dist/context/__tests__/error-handling.test.d.ts +15 -0
  19. package/dist/context/__tests__/error-handling.test.d.ts.map +1 -0
  20. package/dist/context/__tests__/error-handling.test.js +447 -0
  21. package/dist/context/__tests__/error-handling.test.js.map +1 -0
  22. package/dist/context/command-detector.d.ts +50 -0
  23. package/dist/context/command-detector.d.ts.map +1 -0
  24. package/dist/context/command-detector.js +72 -0
  25. package/dist/context/command-detector.js.map +1 -0
  26. package/dist/context/context-manager.d.ts +144 -0
  27. package/dist/context/context-manager.d.ts.map +1 -0
  28. package/dist/context/context-manager.js +487 -0
  29. package/dist/context/context-manager.js.map +1 -0
  30. package/dist/context/handlers/__tests__/docker-handler.test.d.ts +13 -0
  31. package/dist/context/handlers/__tests__/docker-handler.test.d.ts.map +1 -0
  32. package/dist/context/handlers/__tests__/docker-handler.test.js +285 -0
  33. package/dist/context/handlers/__tests__/docker-handler.test.js.map +1 -0
  34. package/dist/context/handlers/__tests__/ssh-handler.test.d.ts +13 -0
  35. package/dist/context/handlers/__tests__/ssh-handler.test.d.ts.map +1 -0
  36. package/dist/context/handlers/__tests__/ssh-handler.test.js +251 -0
  37. package/dist/context/handlers/__tests__/ssh-handler.test.js.map +1 -0
  38. package/dist/context/handlers/__tests__/wsl-handler.test.d.ts +7 -0
  39. package/dist/context/handlers/__tests__/wsl-handler.test.d.ts.map +1 -0
  40. package/dist/context/handlers/__tests__/wsl-handler.test.js +331 -0
  41. package/dist/context/handlers/__tests__/wsl-handler.test.js.map +1 -0
  42. package/dist/context/handlers/docker-handler.d.ts +111 -0
  43. package/dist/context/handlers/docker-handler.d.ts.map +1 -0
  44. package/dist/context/handlers/docker-handler.js +439 -0
  45. package/dist/context/handlers/docker-handler.js.map +1 -0
  46. package/dist/context/handlers/ssh-handler.d.ts +120 -0
  47. package/dist/context/handlers/ssh-handler.d.ts.map +1 -0
  48. package/dist/context/handlers/ssh-handler.js +523 -0
  49. package/dist/context/handlers/ssh-handler.js.map +1 -0
  50. package/dist/context/handlers/wsl-handler.d.ts +128 -0
  51. package/dist/context/handlers/wsl-handler.d.ts.map +1 -0
  52. package/dist/context/handlers/wsl-handler.js +590 -0
  53. package/dist/context/handlers/wsl-handler.js.map +1 -0
  54. package/dist/context/index.d.ts +8 -0
  55. package/dist/context/index.d.ts.map +1 -0
  56. package/dist/context/index.js +7 -0
  57. package/dist/context/index.js.map +1 -0
  58. package/dist/context/subshell-handler.d.ts +130 -0
  59. package/dist/context/subshell-handler.d.ts.map +1 -0
  60. package/dist/context/subshell-handler.js +5 -0
  61. package/dist/context/subshell-handler.js.map +1 -0
  62. package/dist/context/types.d.ts +70 -0
  63. package/dist/context/types.d.ts.map +1 -0
  64. package/dist/context/types.js +34 -0
  65. package/dist/context/types.js.map +1 -0
  66. package/dist/index.js +6 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/services/__tests__/ai-context-injector.test.d.ts +15 -0
  69. package/dist/services/__tests__/ai-context-injector.test.d.ts.map +1 -0
  70. package/dist/services/__tests__/ai-context-injector.test.js +326 -0
  71. package/dist/services/__tests__/ai-context-injector.test.js.map +1 -0
  72. package/dist/services/ai-context-injector.d.ts +41 -0
  73. package/dist/services/ai-context-injector.d.ts.map +1 -0
  74. package/dist/services/ai-context-injector.js +97 -0
  75. package/dist/services/ai-context-injector.js.map +1 -0
  76. package/dist/services/ai-service-client.d.ts +4 -1
  77. package/dist/services/ai-service-client.d.ts.map +1 -1
  78. package/dist/services/ai-service-client.js +6 -2
  79. package/dist/services/ai-service-client.js.map +1 -1
  80. package/dist/services/api-client.js +1 -1
  81. package/dist/services/api-client.js.map +1 -1
  82. package/dist/src/context/types.js +27 -0
  83. package/dist/src/services/ai-context-injector.js +96 -0
  84. package/dist/src/services/ai-service-client.js +270 -0
  85. package/dist/src/services/api-client.js +349 -0
  86. package/dist/src/tools/types.js +1 -0
  87. package/dist/src/types/index.js +1 -0
  88. package/dist/test/context/types.js +27 -0
  89. package/dist/test/services/__tests__/ai-context-injector.test.js +325 -0
  90. package/dist/test/services/ai-context-injector.js +96 -0
  91. package/dist/test/services/ai-service-client.js +270 -0
  92. package/dist/test/services/api-client.js +349 -0
  93. package/dist/test/tools/types.js +1 -0
  94. package/dist/test/types/index.js +1 -0
  95. package/dist/test-ai-context-injector.js +97 -0
  96. package/dist/test-ssh-handler.d.ts +8 -0
  97. package/dist/test-ssh-handler.d.ts.map +1 -0
  98. package/dist/test-ssh-handler.js +198 -0
  99. package/dist/test-ssh-handler.js.map +1 -0
  100. package/dist/tools/command.d.ts.map +1 -1
  101. package/dist/tools/command.js +123 -46
  102. package/dist/tools/command.js.map +1 -1
  103. package/dist/tools/file-ops.d.ts.map +1 -1
  104. package/dist/tools/file-ops.js +115 -48
  105. package/dist/tools/file-ops.js.map +1 -1
  106. package/dist/tools/types.d.ts +1 -0
  107. package/dist/tools/types.d.ts.map +1 -1
  108. package/dist/tools/web-search.js +2 -2
  109. package/dist/tools/web-search.js.map +1 -1
  110. package/dist/types/index.d.ts +41 -0
  111. package/dist/types/index.d.ts.map +1 -1
  112. package/dist/ui/components/App.d.ts +3 -0
  113. package/dist/ui/components/App.d.ts.map +1 -1
  114. package/dist/ui/components/App.js +213 -46
  115. package/dist/ui/components/App.js.map +1 -1
  116. package/dist/ui/components/Breadcrumbs.d.ts +12 -0
  117. package/dist/ui/components/Breadcrumbs.d.ts.map +1 -0
  118. package/dist/ui/components/Breadcrumbs.js +62 -0
  119. package/dist/ui/components/Breadcrumbs.js.map +1 -0
  120. package/dist/ui/components/CodeBlock.js +1 -1
  121. package/dist/ui/components/CodeBlock.js.map +1 -1
  122. package/dist/ui/components/DiffViewer.js +1 -1
  123. package/dist/ui/components/DiffViewer.js.map +1 -1
  124. package/dist/ui/components/FileViewerScreen.d.ts +14 -0
  125. package/dist/ui/components/FileViewerScreen.d.ts.map +1 -0
  126. package/dist/ui/components/FileViewerScreen.js +74 -0
  127. package/dist/ui/components/FileViewerScreen.js.map +1 -0
  128. package/dist/ui/components/InputBox.d.ts +2 -0
  129. package/dist/ui/components/InputBox.d.ts.map +1 -1
  130. package/dist/ui/components/InputBox.js +85 -41
  131. package/dist/ui/components/InputBox.js.map +1 -1
  132. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  133. package/dist/ui/components/MessageDisplay.js +3 -28
  134. package/dist/ui/components/MessageDisplay.js.map +1 -1
  135. package/dist/ui/components/PasswordPrompt.d.ts +9 -0
  136. package/dist/ui/components/PasswordPrompt.d.ts.map +1 -0
  137. package/dist/ui/components/PasswordPrompt.js +20 -0
  138. package/dist/ui/components/PasswordPrompt.js.map +1 -0
  139. package/dist/ui/components/StatusBar.d.ts +2 -0
  140. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  141. package/dist/ui/components/StatusBar.js +36 -1
  142. package/dist/ui/components/StatusBar.js.map +1 -1
  143. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  144. package/dist/ui/components/ToolExecutionMessage.js +13 -24
  145. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  146. package/dist/ui/components/VersionUpdatePrompt.d.ts +10 -0
  147. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -0
  148. package/dist/ui/components/VersionUpdatePrompt.js +41 -0
  149. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -0
  150. package/dist/utils/shell.d.ts.map +1 -1
  151. package/dist/utils/shell.js +38 -10
  152. package/dist/utils/shell.js.map +1 -1
  153. package/dist/utils/version-checker.d.ts +14 -0
  154. package/dist/utils/version-checker.d.ts.map +1 -0
  155. package/dist/utils/version-checker.js +63 -0
  156. package/dist/utils/version-checker.js.map +1 -0
  157. package/package.json +71 -69
@@ -2,7 +2,6 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname } from 'path';
5
- import * as shellUtils from './utils/shell.js';
6
5
  const __filename = fileURLToPath(import.meta.url);
7
6
  const __dirname = dirname(__filename);
8
7
  import { ConfigManager } from './config/manager.js';
@@ -15,11 +14,16 @@ import { conversationManager } from './services/conversation-manager.js';
15
14
  import { aiServiceClient } from './services/ai-service-client.js';
16
15
  import { SUPPORTED_MODELS, getModelDisplayName, isValidModel, getInvalidModelError } from './config/models.js';
17
16
  import { authenticateWithGoogle } from './services/auth-handler.js';
17
+ import { ContextManager } from './context/context-manager.js';
18
+ import { CommandDetector } from './context/command-detector.js';
19
+ import { SSHHandler } from './context/handlers/ssh-handler.js';
20
+ import { WSLHandler } from './context/handlers/wsl-handler.js';
21
+ import { DockerHandler } from './context/handlers/docker-handler.js';
22
+ import { AIContextInjector } from './services/ai-context-injector.js';
18
23
  export class CentaurusCLI {
19
24
  configManager;
20
25
  toolRegistry;
21
26
  conversationHistory = [];
22
- systemPrompt;
23
27
  cwd;
24
28
  planMode = false;
25
29
  commandMode = false;
@@ -34,12 +38,30 @@ export class CentaurusCLI {
34
38
  onToolStreamingOutput;
35
39
  onPlanModeChange;
36
40
  onPlanApprovalRequest;
41
+ onPasswordRequest;
37
42
  conversationStarted = false;
43
+ contextManager;
44
+ commandDetector;
45
+ aiContextInjector;
46
+ onSubshellContextChange;
38
47
  constructor() {
39
48
  this.configManager = new ConfigManager();
40
49
  this.toolRegistry = new ToolRegistry();
41
50
  this.cwd = process.cwd();
42
- this.systemPrompt = this.loadSystemPrompt();
51
+ // Initialize Context Manager and Command Detector
52
+ this.contextManager = new ContextManager(this.cwd, process.platform);
53
+ this.commandDetector = new CommandDetector();
54
+ this.aiContextInjector = new AIContextInjector();
55
+ // Register context change callback to update cwd
56
+ this.contextManager.onContextChange((context) => {
57
+ this.cwd = context.metadata.workingDirectory;
58
+ if (this.onCwdChange) {
59
+ this.onCwdChange(this.cwd);
60
+ }
61
+ if (this.onSubshellContextChange) {
62
+ this.onSubshellContextChange(context);
63
+ }
64
+ });
43
65
  }
44
66
  setOnResponseCallback(callback) {
45
67
  this.onResponseCallback = callback;
@@ -71,6 +93,16 @@ export class CentaurusCLI {
71
93
  setOnCwdChange(callback) {
72
94
  this.onCwdChange = callback;
73
95
  }
96
+ setOnSubshellContextChange(callback) {
97
+ this.onSubshellContextChange = callback;
98
+ }
99
+ setOnPasswordRequest(callback) {
100
+ this.onPasswordRequest = callback;
101
+ // Update SSH handler if already initialized
102
+ if (this.sshHandler) {
103
+ this.sshHandler.setPasswordRequestCallback(callback);
104
+ }
105
+ }
74
106
  getPlanMode() {
75
107
  return this.planMode;
76
108
  }
@@ -80,6 +112,9 @@ export class CentaurusCLI {
80
112
  getCurrentWorkingDirectory() {
81
113
  return this.cwd;
82
114
  }
115
+ getCurrentSubshellContext() {
116
+ return this.contextManager.getCurrentContext();
117
+ }
83
118
  async handlePickerSelection(selection, pickerType) {
84
119
  try {
85
120
  // Validate and set model (ConfigManager will validate)
@@ -97,6 +132,7 @@ export class CentaurusCLI {
97
132
  }
98
133
  }
99
134
  }
135
+ sshHandler;
100
136
  async initialize() {
101
137
  // Register tools
102
138
  this.toolRegistry.register(readFileTool);
@@ -113,6 +149,16 @@ export class CentaurusCLI {
113
149
  if (apiClient.isAuthenticated()) {
114
150
  this.configManager.enableBackendSync();
115
151
  }
152
+ // Initialize subshell handlers
153
+ this.sshHandler = new SSHHandler();
154
+ this.contextManager.registerHandler('ssh', this.sshHandler);
155
+ this.commandDetector.registerHandler(this.sshHandler);
156
+ const wslHandler = new WSLHandler();
157
+ this.contextManager.registerHandler('wsl', wslHandler);
158
+ this.commandDetector.registerHandler(wslHandler);
159
+ const dockerHandler = new DockerHandler();
160
+ this.contextManager.registerHandler('docker', dockerHandler);
161
+ this.commandDetector.registerHandler(dockerHandler);
116
162
  // Note: No need to initialize AI provider - using backend proxy via aiServiceClient
117
163
  }
118
164
  /**
@@ -222,16 +268,17 @@ Press Enter to continue...
222
268
  });
223
269
  // Save user message to backend
224
270
  await this.saveMessageToBackend('user', message);
225
- // Create messages array with system prompt
226
- const systemContent = this.systemPrompt + this.getEnvironmentContext() + (this.planMode ? this.getPlanModeInstructions() : '');
227
- let messages = [
228
- { role: 'system', content: systemContent },
229
- ...this.conversationHistory,
230
- ];
271
+ // Create messages array WITHOUT system prompt
272
+ // Backend will inject the system prompt automatically
273
+ let messages = [...this.conversationHistory];
274
+ // Inject subshell context if in a subshell environment
275
+ const currentContext = this.contextManager.getCurrentContext();
276
+ messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
231
277
  try {
232
278
  const tools = this.toolRegistry.getSchemas();
233
279
  const context = {
234
280
  cwd: this.cwd,
281
+ contextManager: this.contextManager,
235
282
  requireApproval: async (message, risky, preview, operationType, operationDetails) => {
236
283
  if (this.onToolApprovalRequest) {
237
284
  return await this.onToolApprovalRequest({ message, risky, preview, operationType, operationDetails });
@@ -248,8 +295,11 @@ Press Enter to continue...
248
295
  // Get selected model from config
249
296
  const config = this.configManager.load();
250
297
  const selectedModel = config.model || 'gemini-2.0-flash-exp';
298
+ // Build environment context to send to backend
299
+ const environmentContext = this.getEnvironmentContext();
300
+ const mode = this.getMode();
251
301
  let finalAssistantMessage = '';
252
- const MAX_TURNS = 10; // Prevent infinite loops
302
+ const MAX_TURNS = 100; // Allow up to 100 turns for complex tasks
253
303
  let turnCount = 0;
254
304
  // Multi-turn tool execution loop
255
305
  while (turnCount < MAX_TURNS) {
@@ -257,7 +307,8 @@ Press Enter to continue...
257
307
  let assistantMessage = '';
258
308
  let toolCalls = [];
259
309
  // Stream AI response from backend
260
- for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools)) {
310
+ // Backend will inject system prompt automatically with environment context
311
+ for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode)) {
261
312
  // Handle error chunks
262
313
  if (chunk.type === 'error') {
263
314
  throw new Error(chunk.message);
@@ -265,9 +316,13 @@ Press Enter to continue...
265
316
  // Handle text chunks
266
317
  if (chunk.type === 'text') {
267
318
  assistantMessage += chunk.content;
268
- // Send chunk to UI in real-time
319
+ // Send chunk to UI in real-time (but filter out completion marker)
269
320
  if (this.onResponseStreamCallback) {
270
- this.onResponseStreamCallback(chunk.content);
321
+ // Don't stream the completion marker to the UI
322
+ const cleanChunk = chunk.content.replace(/<TASK_COMPLETE>/g, '');
323
+ if (cleanChunk) {
324
+ this.onResponseStreamCallback(cleanChunk);
325
+ }
271
326
  }
272
327
  }
273
328
  // Handle tool call chunks
@@ -279,19 +334,21 @@ Press Enter to continue...
279
334
  break;
280
335
  }
281
336
  }
337
+ // Three-tier completion detection
338
+ // Tier 1: Check for explicit completion marker
339
+ if (this.hasCompletionMarker(assistantMessage)) {
340
+ finalAssistantMessage = this.removeCompletionMarker(assistantMessage);
341
+ break;
342
+ }
282
343
  // If there are tool calls, execute them and continue loop
283
344
  if (toolCalls.length > 0) {
284
345
  const toolResults = [];
285
- for (const toolCall of toolCalls) {
346
+ let userCancelledOperation = false;
347
+ for (let i = 0; i < toolCalls.length; i++) {
348
+ const toolCall = toolCalls[i];
286
349
  try {
287
- // Notify UI: tool is executing
288
- if (this.onToolExecutionUpdate) {
289
- this.onToolExecutionUpdate({
290
- toolName: toolCall.name,
291
- status: 'executing',
292
- arguments: toolCall.arguments
293
- });
294
- }
350
+ // Execute the tool (it will request approval if needed)
351
+ // Don't send 'executing' status here - it causes duplication with approval UI
295
352
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
296
353
  if (result.success) {
297
354
  // Notify UI: tool succeeded
@@ -303,13 +360,28 @@ Press Enter to continue...
303
360
  arguments: toolCall.arguments
304
361
  });
305
362
  }
363
+ // Parse result if it's a string (avoid double-stringification)
364
+ let parsedResult = result.result;
365
+ if (typeof result.result === 'string') {
366
+ try {
367
+ parsedResult = JSON.parse(result.result);
368
+ }
369
+ catch {
370
+ // Keep as string if not valid JSON
371
+ parsedResult = result.result;
372
+ }
373
+ }
306
374
  toolResults.push({
307
375
  tool_call_id: toolCall.id,
308
376
  name: toolCall.name,
309
- content: result.result,
377
+ result: parsedResult,
310
378
  });
311
379
  }
312
380
  else {
381
+ // Check if operation was cancelled by user
382
+ if (result.error && result.error.includes('Operation cancelled by user')) {
383
+ userCancelledOperation = true;
384
+ }
313
385
  // Notify UI: tool failed
314
386
  if (this.onToolExecutionUpdate) {
315
387
  this.onToolExecutionUpdate({
@@ -322,11 +394,19 @@ Press Enter to continue...
322
394
  toolResults.push({
323
395
  tool_call_id: toolCall.id,
324
396
  name: toolCall.name,
325
- content: `Error: ${result.error}`,
397
+ result: `Error: ${result.error}`,
326
398
  });
399
+ // If user cancelled, stop processing remaining tools
400
+ if (userCancelledOperation) {
401
+ break;
402
+ }
327
403
  }
328
404
  }
329
405
  catch (error) {
406
+ // Check if operation was cancelled by user
407
+ if (error.message && error.message.includes('Operation cancelled by user')) {
408
+ userCancelledOperation = true;
409
+ }
330
410
  // Notify UI: tool failed
331
411
  if (this.onToolExecutionUpdate) {
332
412
  this.onToolExecutionUpdate({
@@ -339,42 +419,131 @@ Press Enter to continue...
339
419
  toolResults.push({
340
420
  tool_call_id: toolCall.id,
341
421
  name: toolCall.name,
342
- content: `Error: ${error.message}`,
422
+ result: `Error: ${error.message}`,
343
423
  });
424
+ // If user cancelled, stop processing remaining tools
425
+ if (userCancelledOperation) {
426
+ break;
427
+ }
344
428
  }
345
429
  }
430
+ // If user cancelled an operation, stop the agentic loop immediately
431
+ if (userCancelledOperation) {
432
+ // Add assistant message to history
433
+ this.conversationHistory.push({
434
+ role: 'assistant',
435
+ content: assistantMessage || '',
436
+ });
437
+ // Add tool results to history
438
+ for (const toolResult of toolResults) {
439
+ const toolMessage = {
440
+ tool_call_id: toolResult.tool_call_id,
441
+ name: toolResult.name,
442
+ result: toolResult.result,
443
+ };
444
+ this.conversationHistory.push({
445
+ role: 'tool',
446
+ content: JSON.stringify(toolMessage),
447
+ });
448
+ }
449
+ // Set final message indicating cancellation
450
+ finalAssistantMessage = 'Operation cancelled by user. The task was not completed.';
451
+ break;
452
+ }
346
453
  // Add assistant message with tool calls to conversation history
347
454
  this.conversationHistory.push({
348
455
  role: 'assistant',
349
456
  content: assistantMessage || '',
350
457
  });
351
458
  // Add tool results to conversation history as tool messages
459
+ // Format: { tool_call_id, name, result: <object or string> }
460
+ // Include system instruction to remind AI to continue if needed
352
461
  for (const toolResult of toolResults) {
462
+ const toolMessage = {
463
+ tool_call_id: toolResult.tool_call_id,
464
+ name: toolResult.name,
465
+ result: toolResult.result,
466
+ system_instruction: 'IMPORTANT: This tool executed successfully. If there are MORE steps needed to complete the task, continue executing the necessary tools. If the task is fully complete, respond with "<TASK_COMPLETE>" followed by a summary.'
467
+ };
353
468
  this.conversationHistory.push({
354
469
  role: 'tool',
355
- content: JSON.stringify({
356
- tool_call_id: toolResult.tool_call_id,
357
- name: toolResult.name,
358
- result: toolResult.content,
359
- }),
470
+ content: JSON.stringify(toolMessage),
360
471
  });
361
472
  }
362
473
  // Rebuild messages array with updated history
363
- messages = [
364
- { role: 'system', content: systemContent },
365
- ...this.conversationHistory,
366
- ];
474
+ // Backend will inject system prompt automatically
475
+ messages = [...this.conversationHistory];
476
+ // Re-inject subshell context for the next turn
477
+ messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
478
+ // Add delay between turns to prevent rate limiting
479
+ await new Promise(resolve => setTimeout(resolve, 500));
367
480
  // Continue to next turn to let AI process results
368
481
  continue;
369
482
  }
370
- // No more tool calls - AI provided final response
483
+ // No tool calls - apply three-tier completion detection
371
484
  if (assistantMessage) {
485
+ // Tier 2: Check for strong completion phrases
486
+ if (this.hasStrongCompletionPhrase(assistantMessage)) {
487
+ finalAssistantMessage = assistantMessage;
488
+ break;
489
+ }
490
+ // Tier 3: Check if response seems incomplete (only within first 15 turns)
491
+ if (this.seemsIncomplete(assistantMessage, turnCount)) {
492
+ // Add assistant message to history
493
+ this.conversationHistory.push({
494
+ role: 'assistant',
495
+ content: assistantMessage,
496
+ });
497
+ // Add continuation prompt to messages
498
+ const continuationPrompt = 'Continue with the remaining steps. Execute all necessary tools to complete the entire task.';
499
+ this.conversationHistory.push({
500
+ role: 'user',
501
+ content: continuationPrompt,
502
+ });
503
+ // Rebuild messages array with continuation prompt
504
+ messages = [...this.conversationHistory];
505
+ // Re-inject subshell context for continuation
506
+ messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
507
+ // Add delay before continuation
508
+ await new Promise(resolve => setTimeout(resolve, 500));
509
+ // Continue loop to process continuation
510
+ continue;
511
+ }
512
+ // Tier 3 fallback: Prompt AI for explicit completion (after turn 2)
513
+ if (turnCount >= 2) {
514
+ // Add assistant message to history
515
+ this.conversationHistory.push({
516
+ role: 'assistant',
517
+ content: assistantMessage,
518
+ });
519
+ // Prompt AI to either continue or confirm completion
520
+ const completionPrompt = 'Are there any remaining steps to complete this task? If yes, continue with them now. If the task is fully complete, respond with "<TASK_COMPLETE>" followed by a brief summary.';
521
+ this.conversationHistory.push({
522
+ role: 'user',
523
+ content: completionPrompt,
524
+ });
525
+ // Rebuild messages array
526
+ messages = [...this.conversationHistory];
527
+ // Re-inject subshell context for completion check
528
+ messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
529
+ // Add delay before prompting
530
+ await new Promise(resolve => setTimeout(resolve, 500));
531
+ // Continue loop to get AI's response
532
+ continue;
533
+ }
534
+ // Response seems complete (turn 1 or 2 without clear signals)
372
535
  finalAssistantMessage = assistantMessage;
373
536
  break;
374
537
  }
375
538
  // No tool calls and no message - something went wrong
376
539
  break;
377
540
  }
541
+ // Check if max turns was reached
542
+ if (turnCount >= MAX_TURNS) {
543
+ // Add a warning message to the final response
544
+ const warningMessage = '\n\n⚠️ Note: The task reached the maximum number of processing turns. The work may be incomplete. Please review the results and let me know if you need me to continue.';
545
+ finalAssistantMessage = (finalAssistantMessage || 'Task processing limit reached.') + warningMessage;
546
+ }
378
547
  // Send final message to user (without tool execution logs)
379
548
  const finalMessage = finalAssistantMessage || 'Task completed.';
380
549
  // Save to conversation history
@@ -558,7 +727,11 @@ Press Enter to continue...
558
727
  else {
559
728
  // View config
560
729
  const config = this.configManager.load();
730
+ // Import version checker to get current version
731
+ const { getCurrentVersion } = await import('./utils/version-checker.js');
732
+ const currentVersion = getCurrentVersion();
561
733
  responseMessage = `Current Configuration:\n\n` +
734
+ `Version: ${currentVersion}\n` +
562
735
  `Model: ${config.model || 'gemini-2.0-flash-exp (default)'}\n` +
563
736
  `Authentication: ${apiClient.isAuthenticated() ? '✅ Signed in' : '❌ Not signed in'}`;
564
737
  }
@@ -607,21 +780,134 @@ Press Enter to continue...
607
780
  this.onResponseCallback(responseMessage);
608
781
  }
609
782
  }
610
- loadSystemPrompt() {
611
- const promptPath = path.join(__dirname, '../prompts/centaurus-system-prompt.txt');
612
- try {
613
- return fs.readFileSync(promptPath, 'utf-8');
614
- }
615
- catch (error) {
616
- return 'You are Centaurus, an AI coding assistant.';
617
- }
618
- }
783
+ /**
784
+ * Get environment context for backend
785
+ * Returns structured environment information to be sent to backend
786
+ */
619
787
  getEnvironmentContext() {
620
788
  const platform = process.platform;
621
789
  const isWindows = platform === 'win32';
622
790
  const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
623
791
  const shell = process.env.SHELL || process.env.ComSpec || (isWindows ? 'cmd' : 'bash');
624
- return `\n## Environment Context\n\nYou are running in the following environment:\n- **OS**: ${isWindows ? 'Windows' : platform === 'darwin' ? 'macOS' : 'Linux'} (${platform})\n- **Current Directory**: ${this.cwd}\n- **Home Directory**: ${homeDir}\n- **Shell**: ${shell}\n\n${isWindows ? 'Windows Environment: Use commands like dir, type, cd. Downloads folder is at ' + homeDir + '\\Downloads' : 'Unix Environment: Use ls, pwd, cat. Downloads folder is at ' + homeDir + '/Downloads'}`;
792
+ return {
793
+ os: isWindows ? 'windows' : platform === 'darwin' ? 'macos' : 'linux',
794
+ platform,
795
+ shell,
796
+ cwd: this.cwd,
797
+ homeDir,
798
+ };
799
+ }
800
+ /**
801
+ * Get current mode
802
+ * Returns the current mode (default, plan, or command)
803
+ */
804
+ getMode() {
805
+ if (this.commandMode)
806
+ return 'command';
807
+ if (this.planMode)
808
+ return 'plan';
809
+ return 'default';
810
+ }
811
+ /**
812
+ * Check if response contains completion marker
813
+ * Returns true if the <TASK_COMPLETE> marker is found in the text
814
+ */
815
+ hasCompletionMarker(text) {
816
+ return text.includes('<TASK_COMPLETE>');
817
+ }
818
+ /**
819
+ * Remove completion marker from text
820
+ * Strips the <TASK_COMPLETE> marker from display text
821
+ */
822
+ removeCompletionMarker(text) {
823
+ return text.replace(/<TASK_COMPLETE>/g, '').trim();
824
+ }
825
+ /**
826
+ * Check if response contains strong completion phrases
827
+ * Returns true if the text contains phrases that strongly indicate task completion
828
+ * Used as fallback when completion marker is not present
829
+ */
830
+ hasStrongCompletionPhrase(text) {
831
+ const strongCompletionPhrases = [
832
+ 'task complete',
833
+ 'task is complete',
834
+ 'tasks complete',
835
+ 'tasks are complete',
836
+ 'all done',
837
+ 'everything is done',
838
+ 'everything is ready',
839
+ 'everything is set up',
840
+ 'everything is complete',
841
+ 'all set',
842
+ 'all finished',
843
+ 'finished everything',
844
+ 'completed everything',
845
+ 'completed all',
846
+ 'done with everything',
847
+ 'implementation complete',
848
+ 'implementation is complete',
849
+ 'setup complete',
850
+ 'setup is complete',
851
+ 'all tasks completed',
852
+ 'all steps completed',
853
+ 'work is complete',
854
+ 'work complete',
855
+ 'fully implemented',
856
+ 'fully complete',
857
+ 'ready to use',
858
+ 'ready to go',
859
+ 'successfully completed',
860
+ 'completed successfully'
861
+ ];
862
+ const lowerText = text.toLowerCase();
863
+ return strongCompletionPhrases.some(phrase => lowerText.includes(phrase));
864
+ }
865
+ /**
866
+ * Check if response seems incomplete
867
+ * Returns true if the text contains phrases that suggest the AI stopped prematurely
868
+ * Only triggers within first 15 turns to prevent infinite loops
869
+ */
870
+ seemsIncomplete(text, turnCount) {
871
+ // Only check for incomplete responses within first 15 turns
872
+ if (turnCount > 15) {
873
+ return false;
874
+ }
875
+ const incompletePhrases = [
876
+ 'i created',
877
+ 'i\'ve created',
878
+ 'created the',
879
+ 'i made',
880
+ 'i\'ve made',
881
+ 'i wrote',
882
+ 'i\'ve written',
883
+ 'next i',
884
+ 'now i',
885
+ 'i will',
886
+ 'i\'ll',
887
+ 'let me',
888
+ 'i should',
889
+ 'first',
890
+ 'step 1',
891
+ 'step 2',
892
+ 'step 3',
893
+ 'i\'ll create',
894
+ 'i\'ll make',
895
+ 'i\'ll write',
896
+ 'i\'ll add',
897
+ 'i\'ll implement',
898
+ 'i need to',
899
+ 'i\'ll need to',
900
+ 'next step',
901
+ 'following step',
902
+ 'then i',
903
+ 'after that',
904
+ 'i\'m creating',
905
+ 'i\'m making',
906
+ 'i\'m writing',
907
+ 'i\'m adding'
908
+ ];
909
+ const lowerText = text.toLowerCase();
910
+ return incompletePhrases.some(phrase => lowerText.includes(phrase));
625
911
  }
626
912
  getPlanModeInstructions() {
627
913
  return `\n\n## PLAN MODE ACTIVE\n\nYou are currently in PLAN MODE. In this mode, you should:\n\n1. **Explore the current directory** using list_directory and read_file tools to understand the codebase structure\n2. **Research the topic** using the web_search tool to gather best practices and implementation approaches\n3. **Create a detailed implementation plan** with ordered tasks\n\nWhen you've completed your planning, you MUST format your response in this EXACT format:\n\n<tasks>\n1. First task description\n2. Second task description\n3. Third task description\n...\n</tasks>\n\n<question>\nShall I proceed with implementing this plan?\n</question>\n\nIMPORTANT:\n- The <tasks> section must contain a numbered list of tasks in order\n- The <question> section must contain a yes/no question asking if you should proceed\n- Do NOT execute any implementation tasks in plan mode - only create the plan\n- Use tools to explore and research, but do not modify any files`;
@@ -682,54 +968,96 @@ Press Enter to continue...
682
968
  return;
683
969
  }
684
970
  try {
685
- // Special handling for cd command - change the actual working directory
686
- const cdMatch = command.match(/^cd\s+(.+)$/);
687
- if (cdMatch) {
688
- const targetDir = cdMatch[1].trim();
689
- const newCwd = path.resolve(this.cwd, targetDir);
690
- if (!fs.existsSync(newCwd)) {
971
+ // Check for exit command in subshell
972
+ if (command.trim() === 'exit') {
973
+ const currentContext = this.contextManager.getCurrentContext();
974
+ if (currentContext.type !== 'local') {
975
+ // Disconnect from subshell
976
+ if (currentContext.handler) {
977
+ await currentContext.handler.disconnect();
978
+ }
979
+ // Pop context
980
+ this.contextManager.popContext();
691
981
  if (this.onResponseCallback) {
692
- this.onResponseCallback(`❌ Directory not found: ${targetDir}`);
982
+ this.onResponseCallback('✅ Exited subshell');
693
983
  }
694
984
  return;
695
985
  }
696
- if (!fs.statSync(newCwd).isDirectory()) {
986
+ }
987
+ // Detect subshell commands
988
+ const detection = this.commandDetector.detect(command);
989
+ if (detection) {
990
+ // Show connecting message
991
+ if (this.onResponseCallback) {
992
+ this.onResponseCallback(`🔄 Connecting to ${detection.handler.type} environment...`);
993
+ }
994
+ // Update connection state
995
+ this.contextManager.updateConnectionState('connecting');
996
+ try {
997
+ // Connect to subshell
998
+ const context = await detection.handler.connect(command, this.cwd);
999
+ this.contextManager.pushContext(context);
1000
+ // Show success message
697
1001
  if (this.onResponseCallback) {
698
- this.onResponseCallback(`❌ Not a directory: ${targetDir}`);
1002
+ const breadcrumbs = detection.handler.getBreadcrumbs();
1003
+ const breadcrumbText = breadcrumbs.map(b => `${b.label}${b.value ? `: ${b.value}` : ''}`).join(' ');
1004
+ this.onResponseCallback(`✅ Connected to ${detection.handler.type} environment [${breadcrumbText}]`);
699
1005
  }
700
1006
  return;
701
1007
  }
702
- this.cwd = newCwd;
703
- // Notify UI of CWD change
704
- if (this.onCwdChange) {
705
- this.onCwdChange(newCwd);
706
- }
707
- if (this.onResponseCallback) {
708
- this.onResponseCallback(`Changed directory to: ${newCwd}`);
1008
+ catch (error) {
1009
+ // Connection failed
1010
+ this.contextManager.updateConnectionState('error');
1011
+ if (this.onResponseCallback) {
1012
+ this.onResponseCallback(`❌ Failed to connect: ${error.message}`);
1013
+ }
1014
+ return;
709
1015
  }
710
- return;
711
1016
  }
712
- // Execute the command and stream output
713
- const result = await shellUtils.executeCommand(command, this.cwd, (chunk, type) => {
714
- // Stream output to UI
715
- if (this.onToolStreamingOutput) {
716
- this.onToolStreamingOutput({ toolName: 'command_mode', chunk, type });
1017
+ // Special handling for cd command - change the actual working directory
1018
+ const cdMatch = command.match(/^cd\s+(.+)$/);
1019
+ if (cdMatch) {
1020
+ const currentContext = this.contextManager.getCurrentContext();
1021
+ if (currentContext.type === 'local') {
1022
+ // Local cd handling
1023
+ const targetDir = cdMatch[1].trim();
1024
+ const newCwd = path.resolve(this.cwd, targetDir);
1025
+ if (!fs.existsSync(newCwd)) {
1026
+ if (this.onResponseCallback) {
1027
+ this.onResponseCallback(`❌ Directory not found: ${targetDir}`);
1028
+ }
1029
+ return;
1030
+ }
1031
+ if (!fs.statSync(newCwd).isDirectory()) {
1032
+ if (this.onResponseCallback) {
1033
+ this.onResponseCallback(`❌ Not a directory: ${targetDir}`);
1034
+ }
1035
+ return;
1036
+ }
1037
+ this.contextManager.updateWorkingDirectory(newCwd);
1038
+ if (this.onResponseCallback) {
1039
+ this.onResponseCallback(`Changed directory to: ${newCwd}`);
1040
+ }
1041
+ return;
717
1042
  }
718
- });
719
- // After command execution, check if directory changed (for commands like pushd, popd)
720
- // Get current working directory using 'cd' command which prints current directory on Windows
721
- try {
722
- const pwdResult = await shellUtils.executeCommand('cd', this.cwd);
723
- if (pwdResult.stdout && pwdResult.stdout.trim()) {
724
- const newCwd = pwdResult.stdout.trim();
725
- if (newCwd !== this.cwd && fs.existsSync(newCwd)) {
726
- this.cwd = newCwd;
1043
+ else {
1044
+ // Subshell cd handling - execute via handler
1045
+ const result = await this.contextManager.executeCommand(command);
1046
+ if (result.exitCode === 0) {
1047
+ if (this.onResponseCallback) {
1048
+ this.onResponseCallback(`Changed directory to: ${currentContext.metadata.workingDirectory}`);
1049
+ }
727
1050
  }
1051
+ else {
1052
+ if (this.onResponseCallback) {
1053
+ this.onResponseCallback(`❌ ${result.stderr || 'Failed to change directory'}`);
1054
+ }
1055
+ }
1056
+ return;
728
1057
  }
729
1058
  }
730
- catch (error) {
731
- // Ignore errors when checking pwd
732
- }
1059
+ // Execute the command through Context Manager
1060
+ const result = await this.contextManager.executeCommand(command);
733
1061
  // Format and send the result
734
1062
  let output = '';
735
1063
  if (result.stdout && result.stdout.trim()) {