centaurus-cli 2.7.3 → 2.8.1

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 (97) hide show
  1. package/dist/cli-adapter.d.ts +10 -6
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +613 -154
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/slash-commands.d.ts.map +1 -1
  6. package/dist/config/slash-commands.js +1 -0
  7. package/dist/config/slash-commands.js.map +1 -1
  8. package/dist/context/context-manager.d.ts +4 -1
  9. package/dist/context/context-manager.d.ts.map +1 -1
  10. package/dist/context/context-manager.js +30 -7
  11. package/dist/context/context-manager.js.map +1 -1
  12. package/dist/context/handlers/wsl-handler.d.ts +10 -0
  13. package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
  14. package/dist/context/handlers/wsl-handler.js +31 -2
  15. package/dist/context/handlers/wsl-handler.js.map +1 -1
  16. package/dist/index.js +33 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/services/ai-service-client.d.ts +1 -0
  19. package/dist/services/ai-service-client.d.ts.map +1 -1
  20. package/dist/services/ai-service-client.js +20 -0
  21. package/dist/services/ai-service-client.js.map +1 -1
  22. package/dist/tools/command.d.ts.map +1 -1
  23. package/dist/tools/command.js +136 -21
  24. package/dist/tools/command.js.map +1 -1
  25. package/dist/tools/file-ops.d.ts +1 -0
  26. package/dist/tools/file-ops.d.ts.map +1 -1
  27. package/dist/tools/file-ops.js +144 -3
  28. package/dist/tools/file-ops.js.map +1 -1
  29. package/dist/tools/inspect-symbol.js +27 -27
  30. package/dist/tools/inspect-symbol.js.map +1 -1
  31. package/dist/tools/plan-mode.d.ts +55 -19
  32. package/dist/tools/plan-mode.d.ts.map +1 -1
  33. package/dist/tools/plan-mode.js +204 -123
  34. package/dist/tools/plan-mode.js.map +1 -1
  35. package/dist/tools/types.d.ts +1 -1
  36. package/dist/tools/types.d.ts.map +1 -1
  37. package/dist/types/index.d.ts +11 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/ui/components/App.d.ts +6 -5
  40. package/dist/ui/components/App.d.ts.map +1 -1
  41. package/dist/ui/components/App.js +277 -125
  42. package/dist/ui/components/App.js.map +1 -1
  43. package/dist/ui/components/InputBox.d.ts.map +1 -1
  44. package/dist/ui/components/InputBox.js +24 -5
  45. package/dist/ui/components/InputBox.js.map +1 -1
  46. package/dist/ui/components/InteractiveShell.d.ts +2 -1
  47. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  48. package/dist/ui/components/InteractiveShell.js +41 -106
  49. package/dist/ui/components/InteractiveShell.js.map +1 -1
  50. package/dist/ui/components/MarkdownRenderer.d.ts.map +1 -1
  51. package/dist/ui/components/MarkdownRenderer.js +12 -8
  52. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  53. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  54. package/dist/ui/components/MessageDisplay.js +11 -3
  55. package/dist/ui/components/MessageDisplay.js.map +1 -1
  56. package/dist/ui/components/PlanAcceptedMessage.d.ts +12 -0
  57. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -0
  58. package/dist/ui/components/PlanAcceptedMessage.js +22 -0
  59. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -0
  60. package/dist/ui/components/PlanReviewScreen.d.ts +14 -0
  61. package/dist/ui/components/PlanReviewScreen.d.ts.map +1 -0
  62. package/dist/ui/components/PlanReviewScreen.js +52 -0
  63. package/dist/ui/components/PlanReviewScreen.js.map +1 -0
  64. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  65. package/dist/ui/components/StreamingMessageDisplay.js +5 -5
  66. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  67. package/dist/ui/components/TaskCompletedMessage.d.ts +14 -0
  68. package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -0
  69. package/dist/ui/components/TaskCompletedMessage.js +25 -0
  70. package/dist/ui/components/TaskCompletedMessage.js.map +1 -0
  71. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  72. package/dist/ui/components/ToolExecutionMessage.js +174 -17
  73. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  74. package/dist/utils/conversation-logger.d.ts +127 -0
  75. package/dist/utils/conversation-logger.d.ts.map +1 -0
  76. package/dist/utils/conversation-logger.js +283 -0
  77. package/dist/utils/conversation-logger.js.map +1 -0
  78. package/dist/utils/editor-utils.d.ts +87 -0
  79. package/dist/utils/editor-utils.d.ts.map +1 -0
  80. package/dist/utils/editor-utils.js +712 -0
  81. package/dist/utils/editor-utils.js.map +1 -0
  82. package/dist/utils/input-classifier.d.ts.map +1 -1
  83. package/dist/utils/input-classifier.js +12 -4
  84. package/dist/utils/input-classifier.js.map +1 -1
  85. package/dist/utils/markdown-parser.d.ts.map +1 -1
  86. package/dist/utils/markdown-parser.js +4 -2
  87. package/dist/utils/markdown-parser.js.map +1 -1
  88. package/dist/utils/shell.d.ts +32 -1
  89. package/dist/utils/shell.d.ts.map +1 -1
  90. package/dist/utils/shell.js +97 -161
  91. package/dist/utils/shell.js.map +1 -1
  92. package/dist/utils/syntax-checker.d.ts +24 -0
  93. package/dist/utils/syntax-checker.d.ts.map +1 -0
  94. package/dist/utils/syntax-checker.js +320 -0
  95. package/dist/utils/syntax-checker.js.map +1 -0
  96. package/package.json +4 -3
  97. package/prompts/system-prompt-autonomous.md +0 -377
@@ -7,13 +7,13 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
8
  import { ConfigManager } from './config/manager.js';
9
9
  import { ToolRegistry } from './tools/registry.js';
10
- import { viewFileTool, writeToFileTool, editFileTool, listDirTool } from './tools/file-ops.js';
10
+ import { viewFileTool, writeToFileTool, editFileTool, listDirTool, multiEditFileTool } from './tools/file-ops.js';
11
11
  import { runCommandTool } from './tools/command.js';
12
12
  import { grepSearchTool } from './tools/grep-search.js';
13
13
  import { findFilesTool } from './tools/find-files.js';
14
14
  import { getDiffTool } from './tools/get-diff.js';
15
15
  import { inspectSymbolTool } from './tools/inspect-symbol.js';
16
- import { exitPlanModeTool, getPlanStatusTool, updatePlanStepTool } from './tools/plan-mode.js';
16
+ import { createPlanTool, markTaskCompleteTool, getCurrentPlan, clearPlan, approvePlan, getPlanContextForPrompt } from './tools/plan-mode.js';
17
17
  import { webSearchTool, fetchUrlTool } from './tools/web-search.js';
18
18
  import { taskCompleteTool } from './tools/task-complete.js';
19
19
  import { apiClient } from './services/api-client.js';
@@ -27,16 +27,18 @@ import { SSHHandler } from './context/handlers/ssh-handler.js';
27
27
  import { WSLHandler } from './context/handlers/wsl-handler.js';
28
28
  import { DockerHandler } from './context/handlers/docker-handler.js';
29
29
  import { AIContextInjector } from './services/ai-context-injector.js';
30
- import { environmentContextInjector } from './services/environment-context-injector.js';
31
30
  import { MCPConfigManager } from './config/mcp-config-manager.js';
32
31
  import { MCPServerManager } from './mcp/mcp-server-manager.js';
33
32
  import { MCPCommandHandler } from './mcp/mcp-command-handler.js';
33
+ import { isInteractiveEditorCommand, runWSLCommand, runDockerCommand, runSSHCommand } from './utils/editor-utils.js';
34
+ import { conversationLogger } from './utils/conversation-logger.js';
34
35
  export class CentaurusCLI {
35
36
  configManager;
36
37
  toolRegistry;
37
38
  conversationHistory = [];
38
39
  cwd;
39
40
  planMode = false;
41
+ pendingPlanRequest = null; // Stores original user request during planning phase
40
42
  commandMode = false;
41
43
  previousMode = 'execution';
42
44
  onResponseCallback;
@@ -53,6 +55,8 @@ export class CentaurusCLI {
53
55
  onToolStreamingOutput;
54
56
  onPlanModeChange;
55
57
  onPlanApprovalRequest;
58
+ onPlanCreated;
59
+ onTaskCompleted;
56
60
  onPasswordRequest;
57
61
  currentInteractiveProcess;
58
62
  conversationStarted = false;
@@ -62,6 +66,7 @@ export class CentaurusCLI {
62
66
  onSubshellContextChange;
63
67
  currentAbortController;
64
68
  mcpCommandHandler;
69
+ onInteractiveEditorMode;
65
70
  constructor() {
66
71
  this.configManager = new ConfigManager();
67
72
  this.toolRegistry = new ToolRegistry();
@@ -116,6 +121,12 @@ export class CentaurusCLI {
116
121
  setOnPlanApprovalRequest(callback) {
117
122
  this.onPlanApprovalRequest = callback;
118
123
  }
124
+ setOnPlanCreated(callback) {
125
+ this.onPlanCreated = callback;
126
+ }
127
+ setOnTaskCompleted(callback) {
128
+ this.onTaskCompleted = callback;
129
+ }
119
130
  setOnCommandModeChange(callback) {
120
131
  this.onCommandModeChange = callback;
121
132
  }
@@ -135,6 +146,9 @@ export class CentaurusCLI {
135
146
  this.sshHandler.setPasswordRequestCallback(callback);
136
147
  }
137
148
  }
149
+ setOnInteractiveEditorMode(callback) {
150
+ this.onInteractiveEditorMode = callback;
151
+ }
138
152
  async initializeMCP() {
139
153
  try {
140
154
  const mcpConfigManager = new MCPConfigManager();
@@ -256,15 +270,15 @@ export class CentaurusCLI {
256
270
  this.toolRegistry.register(viewFileTool);
257
271
  this.toolRegistry.register(writeToFileTool);
258
272
  this.toolRegistry.register(editFileTool);
273
+ this.toolRegistry.register(multiEditFileTool);
259
274
  this.toolRegistry.register(listDirTool);
260
275
  this.toolRegistry.register(runCommandTool);
261
276
  this.toolRegistry.register(grepSearchTool);
262
277
  this.toolRegistry.register(findFilesTool);
263
278
  this.toolRegistry.register(getDiffTool);
264
279
  this.toolRegistry.register(inspectSymbolTool);
265
- this.toolRegistry.register(exitPlanModeTool);
266
- this.toolRegistry.register(getPlanStatusTool);
267
- this.toolRegistry.register(updatePlanStepTool);
280
+ this.toolRegistry.register(createPlanTool);
281
+ this.toolRegistry.register(markTaskCompleteTool);
268
282
  this.toolRegistry.register(webSearchTool);
269
283
  this.toolRegistry.register(fetchUrlTool);
270
284
  this.toolRegistry.register(taskCompleteTool);
@@ -394,6 +408,10 @@ Press Enter to continue...
394
408
  if (!apiClient.isAuthenticated()) {
395
409
  throw new Error('Authentication required. Please sign in to use AI features.');
396
410
  }
411
+ // Store original request if in planning mode (for execution phase after approval)
412
+ if (this.planMode && !this.pendingPlanRequest) {
413
+ this.pendingPlanRequest = message;
414
+ }
397
415
  // Add user message to history
398
416
  this.conversationHistory.push({
399
417
  role: 'user',
@@ -401,6 +419,9 @@ Press Enter to continue...
401
419
  });
402
420
  // Save user message to backend
403
421
  await this.saveMessageToBackend('user', message);
422
+ // Start logging session and log user message
423
+ conversationLogger.startSession();
424
+ conversationLogger.logUserMessage(message);
404
425
  try {
405
426
  const tools = this.toolRegistry.getSchemas();
406
427
  const context = {
@@ -429,27 +450,10 @@ Press Enter to continue...
429
450
  const selectedModelConfig = ALL_MODELS.find(m => m.id === selectedModelId && m.name === config.modelName);
430
451
  const selectedModel = selectedModelId;
431
452
  const selectedModelThinkingConfig = selectedModelConfig?.thinkingConfig;
432
- // Create messages array and inject system prompt with environment context
433
- // Always use the autonomous prompt (optimized for agentic code generation)
434
- const systemPromptPath = path.join(__dirname, '..', 'prompts', 'system-prompt-autonomous.md');
435
- let systemPrompt = '';
436
- try {
437
- systemPrompt = fs.readFileSync(systemPromptPath, 'utf-8');
438
- }
439
- catch (error) {
440
- // Fallback to basic prompt if file not found
441
- systemPrompt = 'You are Centaurus, a Senior Site Reliability Engineer (SRE) and Full-Stack Architect embedded in a CLI.';
442
- }
443
- // Enhance system prompt with environment context
444
- const enhancedSystemPrompt = environmentContextInjector.getEnhancedSystemPrompt(systemPrompt);
445
- // Build messages array with enhanced system prompt at the beginning
446
- let messages = [
447
- {
448
- role: 'system',
449
- content: enhancedSystemPrompt,
450
- },
451
- ...this.conversationHistory
452
- ];
453
+ // Build messages array WITHOUT system prompt - backend will inject it
454
+ // The backend uses cli-system-prompt.md for CLI clients
455
+ // We pass environmentContext and mode separately so backend can inject them
456
+ let messages = [...this.conversationHistory];
453
457
  // Inject subshell context if in a subshell environment
454
458
  const currentContext = this.contextManager.getCurrentContext();
455
459
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
@@ -466,44 +470,61 @@ Press Enter to continue...
466
470
  let completionAttempts = 0; // Track how many times AI provided text summary without task_complete
467
471
  let thoughtStartTime = null; // Track when thinking started
468
472
  let thoughtContent = ''; // Accumulate thought content
473
+ // ANTI-LOOP: Track duplicate tool calls to detect infinite loops
474
+ const MAX_DUPLICATE_CALLS = 2; // Max times same operation allowed on same target
475
+ const fileWriteTracker = new Map(); // Track writes per file
476
+ const recentToolCalls = [];
477
+ // ANTI-LOOP: Track ALL duplicate tool calls (not just file ops)
478
+ const toolCallTracker = new Map(); // Hash -> count
479
+ const MAX_IDENTICAL_TOOL_CALLS = 3; // Max times exact same tool call allowed
469
480
  // Create AbortController for this request
470
481
  this.currentAbortController = new AbortController();
471
482
  // Multi-turn tool execution loop
472
483
  while (turnCount < MAX_TURNS) {
473
484
  turnCount++;
474
485
  // Refresh environment context to capture any CWD changes from previous turns
486
+ // This is sent to backend which will inject it into the system prompt
475
487
  environmentContext = this.getEnvironmentContext();
476
- // Refresh system prompt with new CWD
477
- const refreshedSystemPrompt = environmentContextInjector.getEnhancedSystemPrompt(systemPrompt, this.cwd);
478
- // Update the system message in the messages array
479
- if (messages.length > 0 && messages[0].role === 'system') {
480
- messages[0].content = refreshedSystemPrompt;
481
- }
482
488
  let assistantMessage = '';
483
489
  let toolCalls = [];
490
+ // DEBUG: Log message history state before AI call
491
+ const messageStats = {
492
+ totalMessages: messages.length,
493
+ totalCharacters: messages.reduce((sum, m) => sum + (typeof m.content === 'string' ? m.content.length : 0), 0),
494
+ byRole: {
495
+ system: messages.filter(m => m.role === 'system').length,
496
+ user: messages.filter(m => m.role === 'user').length,
497
+ assistant: messages.filter(m => m.role === 'assistant').length,
498
+ tool: messages.filter(m => m.role === 'tool').length
499
+ },
500
+ assistantWithToolCalls: messages.filter(m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0).length
501
+ };
502
+ try {
503
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] === TURN ${turnCount} AI CALL ===\n`);
504
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Message history: ${messageStats.totalMessages} messages, ${messageStats.totalCharacters} chars\n`);
505
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] By role: system=${messageStats.byRole.system}, user=${messageStats.byRole.user}, assistant=${messageStats.byRole.assistant}, tool=${messageStats.byRole.tool}\n`);
506
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
507
+ }
508
+ catch (e) { }
484
509
  // Stream AI response from backend
485
510
  // Backend will inject system prompt automatically with environment context
486
511
  for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
487
512
  // Handle error chunks
488
513
  if (chunk.type === 'error') {
514
+ conversationLogger.logError('AI Stream', new Error(chunk.message));
489
515
  throw new Error(chunk.message);
490
516
  }
491
517
  // Handle thought chunks (internal reasoning)
492
518
  if (chunk.type === 'thought') {
493
- // Debug logging to file
494
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Received thought chunk: ${chunk.content.substring(0, 100)}\n`);
519
+ conversationLogger.logThoughtChunk(chunk.content);
495
520
  if (!thoughtStartTime) {
496
521
  thoughtStartTime = Date.now();
497
522
  }
498
523
  thoughtContent += chunk.content;
499
524
  // Send thought to UI callback if available
500
525
  if (this.onThoughtStreamCallback) {
501
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Calling onThoughtStreamCallback\n`);
502
526
  this.onThoughtStreamCallback(chunk.content);
503
527
  }
504
- else {
505
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] WARNING: onThoughtStreamCallback is not set!\n`);
506
- }
507
528
  continue;
508
529
  }
509
530
  // Handle text chunks
@@ -511,6 +532,7 @@ Press Enter to continue...
511
532
  // If we were thinking and now got text, finalize the thought
512
533
  if (thoughtStartTime) {
513
534
  const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
535
+ conversationLogger.logThoughtComplete(thinkingDuration);
514
536
  if (this.onThoughtCompleteCallback) {
515
537
  this.onThoughtCompleteCallback(thinkingDuration);
516
538
  }
@@ -523,6 +545,7 @@ Press Enter to continue...
523
545
  filteredContent = filteredContent.trim();
524
546
  if (filteredContent) {
525
547
  assistantMessage += filteredContent;
548
+ conversationLogger.logAITextChunk(filteredContent);
526
549
  // Send chunk to UI in real-time
527
550
  if (this.onResponseStreamCallback) {
528
551
  this.onResponseStreamCallback(filteredContent);
@@ -531,9 +554,17 @@ Press Enter to continue...
531
554
  }
532
555
  // Handle tool call chunks
533
556
  if (chunk.type === 'tool_call') {
557
+ const toolCall = chunk.toolCall;
558
+ // Debug: Log every tool_call chunk received
559
+ try {
560
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED: ${toolCall?.name || 'unknown'}, toolCalls.length before push: ${toolCalls.length}\n`);
561
+ }
562
+ catch (e) { }
563
+ conversationLogger.logToolCall(toolCall?.name || 'unknown', toolCall?.id || 'unknown', toolCall?.arguments || {});
534
564
  // If we were thinking and now got a tool call, finalize the thought
535
565
  if (thoughtStartTime) {
536
566
  const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
567
+ conversationLogger.logThoughtComplete(thinkingDuration);
537
568
  if (this.onThoughtCompleteCallback) {
538
569
  this.onThoughtCompleteCallback(thinkingDuration);
539
570
  }
@@ -541,29 +572,48 @@ Press Enter to continue...
541
572
  thoughtContent = '';
542
573
  }
543
574
  toolCalls.push(chunk.toolCall);
575
+ // IMMEDIATELY notify UI that a tool call was received (pending status)
576
+ // This shows tool calls in the UI as they stream in, not after the stream ends
577
+ if (this.onToolExecutionUpdate && toolCall) {
578
+ this.onToolExecutionUpdate({
579
+ toolName: toolCall.name,
580
+ status: 'pending', // 'pending' means received but not yet executing
581
+ arguments: toolCall.arguments
582
+ });
583
+ }
584
+ // Debug: Log after push
585
+ try {
586
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** TOOL_CALL PUSHED: ${toolCall?.name || 'unknown'}, toolCalls.length after push: ${toolCalls.length}\n`);
587
+ }
588
+ catch (e) { }
544
589
  }
545
590
  // Handle done chunk
546
591
  if (chunk.type === 'done') {
547
592
  // If we were thinking and stream ended, finalize the thought
548
593
  if (thoughtStartTime) {
549
594
  const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
550
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Stream done, finalizing thinking: ${thinkingDuration}s\n`);
595
+ conversationLogger.logThoughtComplete(thinkingDuration);
551
596
  if (this.onThoughtCompleteCallback) {
552
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Calling onThoughtCompleteCallback from done handler\n`);
553
597
  this.onThoughtCompleteCallback(thinkingDuration);
554
598
  }
555
- else {
556
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] WARNING: onThoughtCompleteCallback not set in done handler!\n`);
557
- }
558
599
  thoughtStartTime = null;
559
600
  thoughtContent = '';
560
601
  }
561
- else {
562
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Stream done but no thinking was in progress\n`);
563
- }
602
+ // Log AI text completion
603
+ conversationLogger.logAITextComplete();
604
+ conversationLogger.logStreamEnd('done');
564
605
  break;
565
606
  }
566
607
  } // End of stream loop
608
+ // Log loop state after stream ends
609
+ conversationLogger.logLoopState(turnCount, {
610
+ toolCallCount: toolCalls.length,
611
+ assistantMessageLength: assistantMessage.length,
612
+ hasToolCalls: toolCalls.length > 0,
613
+ willContinue: toolCalls.length > 0,
614
+ narrationAttempts,
615
+ completionAttempts,
616
+ });
567
617
  // If there are tool calls, execute them
568
618
  if (toolCalls.length > 0) {
569
619
  // CRITICAL: AI should ONLY communicate via reason_text and task_complete summary
@@ -578,16 +628,23 @@ Press Enter to continue...
578
628
  toolCalls = toolCalls.slice(0, MAX_TOOL_CALLS_PER_TURN);
579
629
  }
580
630
  const toolResults = [];
631
+ const handledToolCallIds = new Set(); // Track tool calls that were handled directly (e.g., create_plan, mark_task_complete)
581
632
  let userCancelledOperation = false;
582
633
  let taskCompleted = false;
583
634
  let taskCompleteSummary = '';
584
635
  for (let i = 0; i < toolCalls.length; i++) {
636
+ // Debug: Log which tool we're about to execute
637
+ try {
638
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** ABOUT TO EXECUTE TOOL [${i + 1}/${toolCalls.length}]: ${toolCalls[i].name}\n`);
639
+ }
640
+ catch (e) { }
585
641
  const toolCall = toolCalls[i];
586
642
  try {
587
643
  // Check if this is task_complete FIRST (before displaying anything)
588
644
  if (toolCall.name === 'task_complete') {
589
645
  taskCompleted = true;
590
646
  taskCompleteSummary = toolCall.arguments.summary || '';
647
+ conversationLogger.logTaskComplete(taskCompleteSummary);
591
648
  // CRITICAL: Suppress any text that came before task_complete in this turn
592
649
  // The AI should ONLY communicate through the task_complete summary
593
650
  assistantMessage = '';
@@ -597,19 +654,218 @@ Press Enter to continue...
597
654
  }
598
655
  // Execute the tool for proper result handling
599
656
  await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
657
+ // Clear the plan when task is complete
658
+ clearPlan();
600
659
  // Stop processing remaining tools
601
660
  break;
602
661
  }
662
+ if (toolCall.name === 'create_plan') {
663
+ // Execute the tool to create the plan
664
+ const execResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
665
+ // Extract the actual result string (toolRegistry.execute returns { success, result })
666
+ const result = execResult.success ? String(execResult.result) : `Error: ${execResult.error}`;
667
+ // Parse the PLAN_CREATED response to get the plan
668
+ if (typeof result === 'string' && result.startsWith('PLAN_CREATED:')) {
669
+ const planJson = result.substring('PLAN_CREATED:'.length);
670
+ try {
671
+ const plan = JSON.parse(planJson);
672
+ // Notify UI that a plan was created
673
+ if (this.onPlanCreated) {
674
+ this.onPlanCreated(plan);
675
+ }
676
+ // If we have approval callback, ask for approval
677
+ if (this.onPlanApprovalRequest) {
678
+ const approved = await this.onPlanApprovalRequest(plan);
679
+ if (approved) {
680
+ // Approve and activate the plan
681
+ approvePlan();
682
+ // Suppress any text output
683
+ assistantMessage = '';
684
+ // Switch out of plan mode to execution mode
685
+ this.planMode = false;
686
+ if (this.onPlanModeChange) {
687
+ this.onPlanModeChange(false);
688
+ }
689
+ // Add assistant message with plan tool call to history
690
+ this.conversationHistory.push({
691
+ role: 'assistant',
692
+ content: '',
693
+ tool_calls: [toolCall],
694
+ });
695
+ // Add plan approval response
696
+ this.conversationHistory.push({
697
+ role: 'tool',
698
+ tool_call_id: toolCall.id,
699
+ content: 'Plan approved by user. Now switching to execution mode.',
700
+ });
701
+ // Mark this tool call as handled so it's not duplicated
702
+ handledToolCallIds.add(toolCall.id);
703
+ // Add user message that includes plan context for execution phase
704
+ const planContext = getPlanContextForPrompt();
705
+ const originalRequest = this.pendingPlanRequest || message;
706
+ const executionPrompt = `${planContext}\n\nOriginal Request: ${originalRequest}\n\nProceed with task 1. After completing each task, call mark_task_complete with the task number.`;
707
+ this.conversationHistory.push({
708
+ role: 'user',
709
+ content: executionPrompt,
710
+ });
711
+ // Clear pending plan request
712
+ this.pendingPlanRequest = null;
713
+ // Update messages array for this turn
714
+ messages = [...this.conversationHistory];
715
+ // Continue the loop - AI will now execute with plan context
716
+ continue;
717
+ }
718
+ else {
719
+ // User wants to edit - stop the loop, they'll provide feedback
720
+ clearPlan();
721
+ finalAssistantMessage = 'Plan editing requested. Please provide your feedback or modifications.';
722
+ taskCompleted = true;
723
+ break;
724
+ }
725
+ }
726
+ else {
727
+ // No approval callback - add the tool result to history and wait for user response
728
+ // This ensures the AI doesn't get stuck in a silent loop
729
+ this.conversationHistory.push({
730
+ role: 'assistant',
731
+ content: '',
732
+ tool_calls: [toolCall],
733
+ });
734
+ this.conversationHistory.push({
735
+ role: 'tool',
736
+ tool_call_id: toolCall.id,
737
+ content: `Plan created: "${plan.title}" with ${plan.steps.length} tasks. Waiting for user approval.`,
738
+ });
739
+ // Mark this tool call as handled so it's not duplicated
740
+ handledToolCallIds.add(toolCall.id);
741
+ // Update messages for next iteration
742
+ messages = [...this.conversationHistory];
743
+ // Stop and wait for user to approve
744
+ finalAssistantMessage = `Plan created: ${plan.title}. Please approve to continue.`;
745
+ taskCompleted = true;
746
+ break;
747
+ }
748
+ }
749
+ catch (parseError) {
750
+ // Log error and add error result to history so AI knows
751
+ console.error('Failed to parse plan:', parseError);
752
+ // CRITICAL: Add tool result even on parse error to prevent silent loop
753
+ this.conversationHistory.push({
754
+ role: 'assistant',
755
+ content: '',
756
+ tool_calls: [toolCall],
757
+ });
758
+ this.conversationHistory.push({
759
+ role: 'tool',
760
+ tool_call_id: toolCall.id,
761
+ content: `Error parsing plan: ${parseError}. Please try again with valid plan format.`,
762
+ });
763
+ // Mark this tool call as handled so it's not duplicated
764
+ handledToolCallIds.add(toolCall.id);
765
+ messages = [...this.conversationHistory];
766
+ }
767
+ }
768
+ else {
769
+ // Tool returned non-PLAN_CREATED result - add it to history
770
+ this.conversationHistory.push({
771
+ role: 'assistant',
772
+ content: '',
773
+ tool_calls: [toolCall],
774
+ });
775
+ this.conversationHistory.push({
776
+ role: 'tool',
777
+ tool_call_id: toolCall.id,
778
+ content: result || 'create_plan executed but returned empty result.',
779
+ });
780
+ // Mark this tool call as handled so it's not duplicated
781
+ handledToolCallIds.add(toolCall.id);
782
+ messages = [...this.conversationHistory];
783
+ }
784
+ continue;
785
+ }
786
+ if (toolCall.name === 'mark_task_complete') {
787
+ // Execute the tool
788
+ const execResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
789
+ // Extract the actual result string (toolRegistry.execute returns { success, result })
790
+ const result = execResult.success ? String(execResult.result) : `Error: ${execResult.error}`;
791
+ // Parse the TASK_COMPLETED response
792
+ if (typeof result === 'string' && result.startsWith('TASK_COMPLETED:')) {
793
+ const completionJson = result.substring('TASK_COMPLETED:'.length);
794
+ try {
795
+ const completion = JSON.parse(completionJson);
796
+ const currentPlan = getCurrentPlan();
797
+ if (currentPlan && this.onTaskCompleted) {
798
+ const task = currentPlan.steps[completion.taskNumber - 1];
799
+ this.onTaskCompleted(task, completion.taskNumber, completion.totalCount, completion.completionNote);
800
+ }
801
+ // Notify UI about completed task
802
+ this.notifyToolStatus(toolCall.name, 'completed', toolCall.arguments, `Task ${completion.taskNumber} of ${completion.totalCount} completed: ${completion.taskDescription}`);
803
+ // Add to tool results
804
+ toolResults.push({
805
+ tool_call_id: toolCall.id,
806
+ name: toolCall.name,
807
+ result: completion.allComplete
808
+ ? 'All tasks completed! Call task_complete with a summary.'
809
+ : `Task ${completion.taskNumber} completed. Next: ${completion.nextTask}`,
810
+ });
811
+ // If all tasks are complete, prompt AI to call task_complete
812
+ if (completion.allComplete) {
813
+ toolResults[toolResults.length - 1].result =
814
+ 'All tasks in the plan are now completed! Call task_complete with a summary of what was accomplished.';
815
+ }
816
+ }
817
+ catch (parseError) {
818
+ console.error('Failed to parse task completion:', parseError);
819
+ }
820
+ }
821
+ continue;
822
+ }
823
+ // ANTI-LOOP: Only detect EXACT identical tool calls (same name + same args)
824
+ // This is a safety net - the backend fix (functionCall in messages) should prevent loops
825
+ // We use a HIGH threshold to avoid blocking legitimate multi-step operations
826
+ const toolArgsToTrack = { ...toolCall.arguments };
827
+ delete toolArgsToTrack.reason_text; // Ignore reason_text - only matters for params
828
+ const toolCallHash = `${toolCall.name}:${JSON.stringify(toolArgsToTrack)}`;
829
+ const toolCallCount = (toolCallTracker.get(toolCallHash) || 0) + 1;
830
+ toolCallTracker.set(toolCallHash, toolCallCount);
831
+ // Only stop after 5 IDENTICAL calls (same tool + same exact args)
832
+ if (toolCallCount > 5) {
833
+ // Log the loop detection
834
+ conversationLogger.logNarrationDetection('duplicate_tool_loop', {
835
+ toolName: toolCall.name,
836
+ callCount: toolCallCount,
837
+ maxAllowed: 5
838
+ });
839
+ // Force task completion with a helpful message
840
+ const loopMessage = `⚠️ **Loop Detected**: The AI called \`${toolCall.name}\` with identical parameters ${toolCallCount} times.\n\n` +
841
+ `The system has stopped to prevent an infinite loop.\n\n` +
842
+ `**Tip**: Try rephrasing your request or ask about a specific aspect of the task.`;
843
+ if (this.onResponseStreamCallback) {
844
+ this.onResponseStreamCallback(loopMessage);
845
+ }
846
+ // Set as completed and break
847
+ finalAssistantMessage = loopMessage;
848
+ taskCompleted = true;
849
+ break;
850
+ }
851
+ // NOTE: File-specific loop detection REMOVED
852
+ // The backend now includes functionCall parts in assistant messages,
853
+ // so the AI should properly remember its previous actions and not repeat them.
603
854
  // Extract and display reason_text if present (but skip for task_complete)
604
855
  const reasonText = toolCall.arguments.reason_text;
605
856
  if (reasonText && this.onResponseStreamCallback) {
606
857
  this.onResponseStreamCallback(reasonText + '\n\n');
607
858
  }
859
+ // Determine the effective CWD for this command (use remote context CWD if applicable)
860
+ const currentCtx = this.contextManager.getCurrentContext();
861
+ const effectiveCwd = currentCtx.type !== 'local'
862
+ ? currentCtx.metadata?.workingDirectory || '~'
863
+ : this.cwd;
608
864
  // Notify UI: tool is executing (send before execution starts)
609
865
  if (this.onToolExecutionUpdate) {
610
866
  // Add cwd to arguments for execute_command tool
611
867
  const toolArgs = toolCall.name === 'execute_command'
612
- ? { ...toolCall.arguments, cwd: this.cwd }
868
+ ? { ...toolCall.arguments, cwd: effectiveCwd }
613
869
  : toolCall.arguments;
614
870
  this.onToolExecutionUpdate({
615
871
  toolName: toolCall.name,
@@ -617,14 +873,18 @@ Press Enter to continue...
617
873
  arguments: toolArgs
618
874
  });
619
875
  }
876
+ // Log tool execution start
877
+ conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
620
878
  // Execute the tool (it will request approval if needed)
621
879
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
622
880
  if (result.success) {
881
+ // Log successful tool result
882
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
623
883
  // Notify UI: tool succeeded (send full result to UI)
624
884
  if (this.onToolExecutionUpdate) {
625
885
  // Add cwd to arguments for execute_command tool
626
886
  const toolArgs = toolCall.name === 'execute_command'
627
- ? { ...toolCall.arguments, cwd: this.cwd }
887
+ ? { ...toolCall.arguments, cwd: effectiveCwd }
628
888
  : toolCall.arguments;
629
889
  this.onToolExecutionUpdate({
630
890
  toolName: toolCall.name,
@@ -653,6 +913,8 @@ Press Enter to continue...
653
913
  });
654
914
  }
655
915
  else {
916
+ // Log failed tool result
917
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, result.error);
656
918
  // Check if operation was cancelled by user
657
919
  if (result.error && result.error.includes('Operation cancelled by user')) {
658
920
  userCancelledOperation = true;
@@ -661,7 +923,7 @@ Press Enter to continue...
661
923
  if (this.onToolExecutionUpdate) {
662
924
  // Add cwd to arguments for execute_command tool
663
925
  const toolArgs = toolCall.name === 'execute_command'
664
- ? { ...toolCall.arguments, cwd: this.cwd }
926
+ ? { ...toolCall.arguments, cwd: effectiveCwd }
665
927
  : toolCall.arguments;
666
928
  this.onToolExecutionUpdate({
667
929
  toolName: toolCall.name,
@@ -682,6 +944,8 @@ Press Enter to continue...
682
944
  }
683
945
  }
684
946
  catch (error) {
947
+ // Log tool execution error
948
+ conversationLogger.logError(`Tool execution: ${toolCall.name}`, error);
685
949
  // Check if operation was cancelled by user
686
950
  if (error.message && error.message.includes('Operation cancelled by user')) {
687
951
  userCancelledOperation = true;
@@ -726,14 +990,10 @@ Press Enter to continue...
726
990
  });
727
991
  // Add tool results to history
728
992
  for (const toolResult of toolResults) {
729
- const toolMessage = {
730
- tool_call_id: toolResult.tool_call_id,
731
- name: toolResult.name,
732
- result: toolResult.result,
733
- };
734
993
  this.conversationHistory.push({
735
994
  role: 'tool',
736
- content: JSON.stringify(toolMessage),
995
+ tool_call_id: toolResult.tool_call_id,
996
+ content: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
737
997
  });
738
998
  }
739
999
  // Set final message indicating cancellation
@@ -746,35 +1006,42 @@ Press Enter to continue...
746
1006
  this.onResponseStreamCallback(assistantMessage);
747
1007
  }
748
1008
  // Add assistant message with tool calls to conversation history
749
- this.conversationHistory.push({
750
- role: 'assistant',
751
- content: assistantMessage || '',
752
- tool_calls: toolCalls, // Store tool calls for MaaS models
753
- });
1009
+ if (toolCalls && toolCalls.length > 0) {
1010
+ try {
1011
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Adapting assistant message: has toolCalls=${toolCalls.length}, first=${JSON.stringify(toolCalls[0])}\n`);
1012
+ }
1013
+ catch (e) { }
1014
+ }
1015
+ else {
1016
+ try {
1017
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Adapting assistant message: NO toolCalls\n`);
1018
+ }
1019
+ catch (e) { }
1020
+ }
1021
+ // Filter out tool calls that were already handled directly (e.g., create_plan, mark_task_complete)
1022
+ const unhandledToolCalls = toolCalls.filter(tc => !handledToolCallIds.has(tc.id));
1023
+ // Only add assistant message if there are unhandled tool calls
1024
+ if (unhandledToolCalls.length > 0) {
1025
+ this.conversationHistory.push({
1026
+ role: 'assistant',
1027
+ content: assistantMessage || '',
1028
+ tool_calls: unhandledToolCalls, // Only include unhandled tool calls
1029
+ });
1030
+ }
754
1031
  // Add tool results to conversation history as tool messages
755
1032
  // Format: { tool_call_id, name, result: <object or string> }
756
- // Include system instruction to remind AI to continue if needed
757
1033
  for (const toolResult of toolResults) {
758
- const toolMessage = {
759
- tool_call_id: toolResult.tool_call_id,
760
- name: toolResult.name,
761
- result: toolResult.result,
762
- 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, call the task_complete tool with a comprehensive summary.'
763
- };
1034
+ // Add tool result to conversation history as tool message
1035
+ // IMPORTANT: tool_call_id must be a top-level property
764
1036
  this.conversationHistory.push({
765
1037
  role: 'tool',
766
- content: JSON.stringify(toolMessage),
1038
+ tool_call_id: toolResult.tool_call_id,
1039
+ content: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
767
1040
  });
768
1041
  }
769
1042
  // Rebuild messages array with updated history
770
- // IMPORTANT: Re-add system prompt for each turn
771
- messages = [
772
- {
773
- role: 'system',
774
- content: enhancedSystemPrompt,
775
- },
776
- ...this.conversationHistory
777
- ];
1043
+ // Backend will inject system prompt
1044
+ messages = [...this.conversationHistory];
778
1045
  // Re-inject subshell context
779
1046
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
780
1047
  continue; // Loop back to AI service
@@ -786,12 +1053,17 @@ Press Enter to continue...
786
1053
  // No tool calls and no message - AI stopped silently
787
1054
  // This usually means the AI thinks it's done but didn't call task_complete
788
1055
  // Prompt it to either continue or complete
1056
+ conversationLogger.logNarrationDetection('silent_stop', {
1057
+ turn: turnCount,
1058
+ assistantMessageLength: 0,
1059
+ });
789
1060
  const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
790
1061
  '**This is not allowed.** You must either:\n' +
791
1062
  '1. Execute a tool call if more work is needed, OR\n' +
792
1063
  '2. Call task_complete() with a summary of what you accomplished\n\n' +
793
1064
  '**If you have completed the task**, call task_complete() NOW with a comprehensive summary.\n' +
794
1065
  '**If more work is needed**, execute the next tool call immediately.';
1066
+ conversationLogger.logSystemPrompt('silent_stop_prompt', silentStopPrompt);
795
1067
  this.conversationHistory.push({
796
1068
  role: 'user',
797
1069
  content: silentStopPrompt,
@@ -802,9 +1074,19 @@ Press Enter to continue...
802
1074
  const isNarration = /\b(I will|I'll|Let me|Let's|I need to|I'm going to|I should|I can)\b/i.test(assistantMessage);
803
1075
  if (isNarration) {
804
1076
  narrationAttempts++;
1077
+ conversationLogger.logNarrationDetection('narration', {
1078
+ turn: turnCount,
1079
+ narrationAttempts,
1080
+ maxAttempts: MAX_NARRATION_ATTEMPTS,
1081
+ messagePreview: assistantMessage.substring(0, 200),
1082
+ });
805
1083
  // If AI keeps narrating without executing, force completion immediately
806
1084
  if (narrationAttempts >= MAX_NARRATION_ATTEMPTS) {
807
1085
  // Force task completion with error message
1086
+ conversationLogger.logNarrationDetection('narration', {
1087
+ action: 'forced_completion',
1088
+ reason: 'max_narration_attempts_reached',
1089
+ });
808
1090
  finalAssistantMessage = '⚠️ **Task Incomplete**: The AI repeatedly described actions without executing them.\n\n' +
809
1091
  '**What happened**: The AI entered a narration loop, describing what it wanted to do instead of using tool calls.\n\n' +
810
1092
  '**Suggestions**:\n' +
@@ -859,9 +1141,18 @@ Press Enter to continue...
859
1141
  const isFinalAnswer = assistantMessage.length > 20;
860
1142
  if (isFinalAnswer) {
861
1143
  completionAttempts++;
1144
+ conversationLogger.logNarrationDetection('final_answer', {
1145
+ turn: turnCount,
1146
+ completionAttempts,
1147
+ messagePreview: assistantMessage.substring(0, 200),
1148
+ });
862
1149
  // If AI keeps providing text summaries without calling task_complete, accept the text and finish
863
1150
  // This prevents the infinite loop where the AI keeps summarizing in response to our prompt
864
1151
  if (completionAttempts > 1) {
1152
+ conversationLogger.logNarrationDetection('final_answer', {
1153
+ action: 'accepting_text_as_final',
1154
+ reason: 'multiple_completion_attempts',
1155
+ });
865
1156
  finalAssistantMessage = assistantMessage;
866
1157
  break;
867
1158
  }
@@ -886,13 +1177,12 @@ Press Enter to continue...
886
1177
  }
887
1178
  }
888
1179
  }
889
- // Rebuild messages array
1180
+ // Rebuild messages array with updated history
1181
+ // Backend will inject system prompt
890
1182
  messages = [...this.conversationHistory];
891
1183
  // Re-inject subshell context
892
1184
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
893
- // Add delay before prompting
894
- await new Promise(resolve => setTimeout(resolve, 500));
895
- // Continue loop to get AI's response
1185
+ // Continue loop to get AI's response (removed 500ms delay for faster response)
896
1186
  continue;
897
1187
  }
898
1188
  // No tool calls and no message - AI stopped silently
@@ -908,7 +1198,8 @@ Press Enter to continue...
908
1198
  role: 'user',
909
1199
  content: silentStopPrompt,
910
1200
  });
911
- // Rebuild messages array
1201
+ // Rebuild messages array with updated history
1202
+ // Backend will inject system prompt
912
1203
  messages = [...this.conversationHistory];
913
1204
  // Re-inject subshell context
914
1205
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
@@ -943,32 +1234,19 @@ Press Enter to continue...
943
1234
  // Save assistant message to backend
944
1235
  await this.saveMessageToBackend('assistant', finalMessage);
945
1236
  } // End of while loop
946
- // Parse response for plan mode
947
- if (this.planMode) {
948
- const planData = this.parsePlanResponse(finalAssistantMessage);
949
- if (planData && this.onPlanApprovalRequest) {
950
- // Ask user for approval
951
- const approved = await this.onPlanApprovalRequest(planData);
952
- if (approved) {
953
- // Switch to execution mode
954
- this.planMode = false;
955
- if (this.onPlanModeChange) {
956
- this.onPlanModeChange(false);
957
- }
958
- // Send message to start execution
959
- await this.handleMessage('Let\'s implement the plan we just discussed. Start with the first task.');
960
- return;
961
- }
962
- }
963
- }
1237
+ // Log session end
1238
+ conversationLogger.endSession(finalAssistantMessage, turnCount);
964
1239
  // Send response back to UI (only if there's a message)
965
1240
  if (this.onResponseCallback && finalAssistantMessage) {
966
1241
  this.onResponseCallback(finalAssistantMessage);
967
1242
  }
968
1243
  }
969
1244
  catch (error) {
1245
+ // Log the error
1246
+ conversationLogger.logError('handleMessage', error);
970
1247
  // Check if this was an abort/cancellation
971
1248
  if (error.name === 'AbortError' || error.message?.includes('aborted')) {
1249
+ conversationLogger.logError('handleMessage', new Error('Request cancelled by user'));
972
1250
  if (this.onResponseCallback) {
973
1251
  this.onResponseCallback('⚠️ Request cancelled by user.');
974
1252
  }
@@ -995,16 +1273,20 @@ Press Enter to continue...
995
1273
  `/config - View current configuration\n` +
996
1274
  `/model - Select from available Google models\n` +
997
1275
  `/plan - Toggle plan mode for complex implementations\n` +
1276
+ `/mcp - Manage configured MCP servers and tools\n` +
1277
+ `/docs - Open Centaurus documentation in browser\n` +
998
1278
  `/quality - Toggle enhanced quality features (thinking protocol, validation)\n` +
999
1279
  `/autonomous - Toggle autonomous mode (Silent Operator with task_complete)\n` +
1000
1280
  `/sign-in - Sign in with Google (if not already signed in)\n` +
1001
1281
  `/logout - Sign out, clear session, and exit CLI\n` +
1002
1282
  `/exit - Exit the application\n\n` +
1003
1283
  `Keyboard Shortcuts:\n\n` +
1004
- `Ctrl+D - Toggle command mode (execute terminal commands directly)\n` +
1005
- `Tab - Autocomplete files/directories (in command mode)\n` +
1284
+ `Ctrl+D - Cycle modes (Agent Terminal → Auto)\n` +
1006
1285
  `Ctrl+T - Toggle auto-accept mode\n` +
1007
- `? - Show keyboard shortcuts help`;
1286
+ `Ctrl+C - Cancel operation / Exit (press twice)\n` +
1287
+ `Tab - Autocomplete files/directories (in command mode)\n` +
1288
+ `Ctrl+Z - Undo last input change\n` +
1289
+ `Ctrl+A - Select all text`;
1008
1290
  break;
1009
1291
  case 'init':
1010
1292
  try {
@@ -1392,6 +1674,22 @@ Start by listing the directory structure to understand what you're working with.
1392
1674
  responseMessage = '❌ MCP is not initialized. Please restart the CLI.';
1393
1675
  }
1394
1676
  break;
1677
+ case 'docs':
1678
+ // Open documentation URL in default browser
1679
+ const docsUrl = 'https://centaurus-web.vercel.app/docs';
1680
+ const { exec } = await import('child_process');
1681
+ const platform = process.platform;
1682
+ if (platform === 'win32') {
1683
+ exec(`start "" "${docsUrl}"`);
1684
+ }
1685
+ else if (platform === 'darwin') {
1686
+ exec(`open "${docsUrl}"`);
1687
+ }
1688
+ else {
1689
+ exec(`xdg-open "${docsUrl}"`);
1690
+ }
1691
+ responseMessage = `📖 Opening documentation in your browser...\n\n${docsUrl}`;
1692
+ break;
1395
1693
  case 'exit':
1396
1694
  process.exit(0);
1397
1695
  break;
@@ -1432,25 +1730,41 @@ Start by listing the directory structure to understand what you're working with.
1432
1730
  return 'default';
1433
1731
  }
1434
1732
  getPlanModeInstructions() {
1435
- 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`;
1436
- }
1437
- parsePlanResponse(response) {
1438
- // Extract tasks
1439
- const tasksMatch = response.match(/<tasks>([\s\S]*?)<\/tasks>/);
1440
- if (!tasksMatch)
1441
- return null;
1442
- const tasksText = tasksMatch[1].trim();
1443
- const taskLines = tasksText.split('\n').filter(line => line.trim());
1444
- const tasks = taskLines.map(line => {
1445
- // Remove numbering (e.g., "1. ", "2. ")
1446
- return line.replace(/^\d+\.\s*/, '').trim();
1447
- }).filter(task => task);
1448
- // Extract question
1449
- const questionMatch = response.match(/<question>([\s\S]*?)<\/question>/);
1450
- if (!questionMatch)
1451
- return null;
1452
- const question = questionMatch[1].trim();
1453
- return { tasks, question };
1733
+ return `\n\n## PLAN MODE ACTIVE (MODE=plan)
1734
+
1735
+ You are currently in PLAN MODE. In this mode, you MUST:
1736
+
1737
+ 1. **DO NOT execute any implementation tools** (no write_to_file, edit_file, execute_command, etc.)
1738
+ 2. **Call the \`create_plan\` tool** to present a structured plan to the user FIRST
1739
+
1740
+ ### How to Create a Plan:
1741
+
1742
+ Analyze the user's request, then call \`create_plan\` with:
1743
+ - A clear title describing what will be accomplished
1744
+ - A brief summary of the approach
1745
+ - An ordered list of specific, actionable tasks
1746
+
1747
+ Example:
1748
+ \`\`\`
1749
+ create_plan(
1750
+ title: "Create Python CSV Filter Script",
1751
+ summary: "Build a Python script that reads a CSV file, filters rows based on criteria, and writes output",
1752
+ tasks: [
1753
+ { description: "Create sample input.csv with test data", complexity: "low" },
1754
+ { description: "Write csv_filter.py with read/filter/write logic", complexity: "medium" },
1755
+ { description: "Execute and verify the script works correctly", complexity: "low" }
1756
+ ]
1757
+ )
1758
+ \`\`\`
1759
+
1760
+ ### After Plan Approval:
1761
+
1762
+ Once the user approves the plan:
1763
+ 1. Execute each task in order
1764
+ 2. After completing each task, call \`mark_task_complete(task_number: N)\`
1765
+ 3. After all tasks are done, call \`task_complete\` with a summary
1766
+
1767
+ **CRITICAL: In plan mode, ALWAYS call create_plan FIRST before any other tools.**`;
1454
1768
  }
1455
1769
  /**
1456
1770
  * Toggle command mode on/off
@@ -1496,6 +1810,24 @@ Start by listing the directory structure to understand what you're working with.
1496
1810
  if (!command.trim()) {
1497
1811
  return;
1498
1812
  }
1813
+ // Check if this is an interactive editor command (vim, nano, etc.)
1814
+ // These need full terminal control
1815
+ const currentContextForEditor = this.contextManager.getCurrentContext();
1816
+ if (isInteractiveEditorCommand(command)) {
1817
+ if (this.onInteractiveEditorMode) {
1818
+ if (currentContextForEditor.type === 'local') {
1819
+ // Local context: delegate to App for full terminal takeover
1820
+ this.onInteractiveEditorMode(true, command, this.cwd);
1821
+ }
1822
+ else {
1823
+ // Remote context (SSH, WSL, Docker) - pass remote context to App
1824
+ // Use the remote context's working directory, not the local Windows CWD
1825
+ const remoteCwd = currentContextForEditor.metadata?.workingDirectory || '~';
1826
+ this.onInteractiveEditorMode(true, command, remoteCwd, currentContextForEditor);
1827
+ }
1828
+ return;
1829
+ }
1830
+ }
1499
1831
  try {
1500
1832
  // Check for exit command in subshell
1501
1833
  if (command.trim() === 'exit') {
@@ -1585,15 +1917,19 @@ Start by listing the directory structure to understand what you're working with.
1585
1917
  return;
1586
1918
  }
1587
1919
  }
1920
+ // Get current context to determine correct CWD for notification
1921
+ const currentContext = this.contextManager.getCurrentContext();
1922
+ const effectiveCwd = currentContext.type !== 'local'
1923
+ ? currentContext.metadata?.workingDirectory || '~'
1924
+ : this.cwd;
1588
1925
  // Notify UI that command is executing
1589
1926
  if (this.onToolExecutionUpdate) {
1590
1927
  this.onToolExecutionUpdate({
1591
1928
  toolName: 'execute_command',
1592
1929
  status: 'executing',
1593
- arguments: { command, cwd: this.cwd }
1930
+ arguments: { command, cwd: effectiveCwd, isPty: shellUtils.isPtyAvailable() }
1594
1931
  });
1595
1932
  }
1596
- const currentContext = this.contextManager.getCurrentContext();
1597
1933
  // Execute with streaming support for local commands
1598
1934
  if (currentContext.type === 'local') {
1599
1935
  // Use interactive execution to support stdin
@@ -1631,38 +1967,161 @@ Start by listing the directory structure to understand what you're working with.
1631
1967
  });
1632
1968
  });
1633
1969
  }
1634
- else {
1635
- // Subshell execution (no streaming yet)
1636
- const result = await this.contextManager.executeCommand(command);
1970
+ else if (currentContext.type === 'wsl') {
1971
+ // WSL execution with PTY for proper TTY handling (sudo, etc.)
1972
+ const remoteCwd = currentContext.metadata?.workingDirectory || '~';
1973
+ const distribution = currentContext.metadata?.distroName || 'Ubuntu';
1637
1974
  let output = '';
1638
- if (result.stdout && result.stdout.trim()) {
1639
- output += result.stdout;
1640
- }
1641
- if (result.stderr && result.stderr.trim()) {
1642
- if (output)
1643
- output += '\n';
1644
- output += result.stderr;
1645
- }
1646
- // Notify UI of completion
1647
- if (this.onToolExecutionUpdate) {
1648
- if (result.exitCode !== 0) {
1649
- this.onToolExecutionUpdate({
1650
- toolName: 'execute_command',
1651
- status: 'error',
1652
- result: output,
1653
- error: `Exit Code: ${result.exitCode}`,
1654
- arguments: { command, cwd: this.cwd }
1655
- });
1656
- }
1657
- else {
1658
- this.onToolExecutionUpdate({
1659
- toolName: 'execute_command',
1660
- status: 'completed',
1661
- result: output || 'Command executed successfully',
1662
- arguments: { command, cwd: this.cwd }
1663
- });
1664
- }
1975
+ await new Promise((resolve) => {
1976
+ const wslPty = runWSLCommand(distribution, command, remoteCwd, (data) => {
1977
+ // Stream output to UI
1978
+ output += data;
1979
+ if (this.onToolStreamingOutput) {
1980
+ this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
1981
+ }
1982
+ }, (exitCode) => {
1983
+ // Notify UI of completion
1984
+ if (this.onToolExecutionUpdate) {
1985
+ if (exitCode !== 0) {
1986
+ this.onToolExecutionUpdate({
1987
+ toolName: 'execute_command',
1988
+ status: 'error',
1989
+ result: output,
1990
+ error: `Exit Code: ${exitCode}`,
1991
+ arguments: { command, cwd: remoteCwd }
1992
+ });
1993
+ }
1994
+ else {
1995
+ this.onToolExecutionUpdate({
1996
+ toolName: 'execute_command',
1997
+ status: 'completed',
1998
+ result: output || 'Command executed successfully',
1999
+ arguments: { command, cwd: remoteCwd }
2000
+ });
2001
+ }
2002
+ }
2003
+ this.currentInteractiveProcess = undefined;
2004
+ resolve();
2005
+ });
2006
+ // Set up interactive process for stdin
2007
+ this.currentInteractiveProcess = {
2008
+ process: null,
2009
+ write: (data) => wslPty.write(data),
2010
+ kill: () => wslPty.kill(),
2011
+ signal: (sig) => {
2012
+ if (sig === 'SIGINT') {
2013
+ wslPty.write('\x03'); // Ctrl+C
2014
+ }
2015
+ },
2016
+ resize: (cols, rows) => wslPty.resize(cols, rows),
2017
+ isPty: true
2018
+ };
2019
+ });
2020
+ }
2021
+ else if (currentContext.type === 'docker') {
2022
+ // Docker execution with PTY for proper TTY handling
2023
+ const remoteCwd = currentContext.metadata?.workingDirectory || '~';
2024
+ const containerId = currentContext.metadata?.containerId || '';
2025
+ let output = '';
2026
+ await new Promise((resolve) => {
2027
+ const dockerPty = runDockerCommand(containerId, command, remoteCwd, (data) => {
2028
+ // Stream output to UI
2029
+ output += data;
2030
+ if (this.onToolStreamingOutput) {
2031
+ this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
2032
+ }
2033
+ }, (exitCode) => {
2034
+ // Notify UI of completion
2035
+ if (this.onToolExecutionUpdate) {
2036
+ if (exitCode !== 0) {
2037
+ this.onToolExecutionUpdate({
2038
+ toolName: 'execute_command',
2039
+ status: 'error',
2040
+ result: output,
2041
+ error: `Exit Code: ${exitCode}`,
2042
+ arguments: { command, cwd: remoteCwd }
2043
+ });
2044
+ }
2045
+ else {
2046
+ this.onToolExecutionUpdate({
2047
+ toolName: 'execute_command',
2048
+ status: 'completed',
2049
+ result: output || 'Command executed successfully',
2050
+ arguments: { command, cwd: remoteCwd }
2051
+ });
2052
+ }
2053
+ }
2054
+ this.currentInteractiveProcess = undefined;
2055
+ resolve();
2056
+ });
2057
+ // Set up interactive process for stdin
2058
+ this.currentInteractiveProcess = {
2059
+ process: null,
2060
+ write: (data) => dockerPty.write(data),
2061
+ kill: () => dockerPty.kill(),
2062
+ signal: (sig) => {
2063
+ if (sig === 'SIGINT') {
2064
+ dockerPty.write('\x03'); // Ctrl+C
2065
+ }
2066
+ },
2067
+ resize: (cols, rows) => dockerPty.resize(cols, rows),
2068
+ isPty: true
2069
+ };
2070
+ });
2071
+ }
2072
+ else if (currentContext.type === 'ssh') {
2073
+ // SSH execution with PTY for proper TTY handling
2074
+ const remoteCwd = currentContext.metadata?.workingDirectory || '~';
2075
+ const sshClient = currentContext.handler?.client;
2076
+ if (!sshClient) {
2077
+ throw new Error('SSH client not available');
1665
2078
  }
2079
+ let output = '';
2080
+ await new Promise((resolve) => {
2081
+ const sshPty = runSSHCommand(sshClient, command, remoteCwd, (data) => {
2082
+ // Stream output to UI
2083
+ output += data;
2084
+ if (this.onToolStreamingOutput) {
2085
+ this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
2086
+ }
2087
+ }, (exitCode) => {
2088
+ // Notify UI of completion
2089
+ if (this.onToolExecutionUpdate) {
2090
+ if (exitCode !== 0) {
2091
+ this.onToolExecutionUpdate({
2092
+ toolName: 'execute_command',
2093
+ status: 'error',
2094
+ result: output,
2095
+ error: `Exit Code: ${exitCode}`,
2096
+ arguments: { command, cwd: remoteCwd }
2097
+ });
2098
+ }
2099
+ else {
2100
+ this.onToolExecutionUpdate({
2101
+ toolName: 'execute_command',
2102
+ status: 'completed',
2103
+ result: output || 'Command executed successfully',
2104
+ arguments: { command, cwd: remoteCwd }
2105
+ });
2106
+ }
2107
+ }
2108
+ this.currentInteractiveProcess = undefined;
2109
+ resolve();
2110
+ });
2111
+ // Set up interactive process for stdin
2112
+ this.currentInteractiveProcess = {
2113
+ process: null,
2114
+ write: (data) => sshPty.write(data),
2115
+ kill: () => sshPty.kill(),
2116
+ signal: (sig) => {
2117
+ if (sig === 'SIGINT') {
2118
+ sshPty.write('\x03'); // Ctrl+C
2119
+ }
2120
+ },
2121
+ resize: (cols, rows) => sshPty.resize(cols, rows),
2122
+ isPty: true
2123
+ };
2124
+ });
1666
2125
  }
1667
2126
  }
1668
2127
  catch (error) {