centaurus-cli 2.6.2 → 2.7.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 (92) hide show
  1. package/dist/cli-adapter.d.ts +13 -22
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +383 -240
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/defaultConfig.d.ts +1 -0
  6. package/dist/config/defaultConfig.d.ts.map +1 -1
  7. package/dist/config/defaultConfig.js +3 -1
  8. package/dist/config/defaultConfig.js.map +1 -1
  9. package/dist/config/types.d.ts +1 -0
  10. package/dist/config/types.d.ts.map +1 -1
  11. package/dist/config/types.js +1 -0
  12. package/dist/config/types.js.map +1 -1
  13. package/dist/index.js +4 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/services/ai-service-client.d.ts +1 -1
  16. package/dist/services/ai-service-client.d.ts.map +1 -1
  17. package/dist/services/ai-service-client.js +7 -2
  18. package/dist/services/ai-service-client.js.map +1 -1
  19. package/dist/services/api-client.d.ts +6 -0
  20. package/dist/services/api-client.d.ts.map +1 -1
  21. package/dist/services/api-client.js +16 -0
  22. package/dist/services/api-client.js.map +1 -1
  23. package/dist/tools/command.d.ts.map +1 -1
  24. package/dist/tools/command.js +77 -25
  25. package/dist/tools/command.js.map +1 -1
  26. package/dist/tools/file-ops.d.ts.map +1 -1
  27. package/dist/tools/file-ops.js +28 -4
  28. package/dist/tools/file-ops.js.map +1 -1
  29. package/dist/tools/registry.d.ts +1 -0
  30. package/dist/tools/registry.d.ts.map +1 -1
  31. package/dist/tools/registry.js +25 -1
  32. package/dist/tools/registry.js.map +1 -1
  33. package/dist/tools/task-complete.d.ts +3 -0
  34. package/dist/tools/task-complete.d.ts.map +1 -0
  35. package/dist/tools/task-complete.js +48 -0
  36. package/dist/tools/task-complete.js.map +1 -0
  37. package/dist/tools/types.d.ts +1 -0
  38. package/dist/tools/types.d.ts.map +1 -1
  39. package/dist/tools/web-search.d.ts.map +1 -1
  40. package/dist/tools/web-search.js +16 -2
  41. package/dist/tools/web-search.js.map +1 -1
  42. package/dist/ui/components/AgentTimer.d.ts +7 -0
  43. package/dist/ui/components/AgentTimer.d.ts.map +1 -0
  44. package/dist/ui/components/AgentTimer.js +27 -0
  45. package/dist/ui/components/AgentTimer.js.map +1 -0
  46. package/dist/ui/components/App.d.ts +2 -0
  47. package/dist/ui/components/App.d.ts.map +1 -1
  48. package/dist/ui/components/App.js +229 -46
  49. package/dist/ui/components/App.js.map +1 -1
  50. package/dist/ui/components/InputBox.d.ts.map +1 -1
  51. package/dist/ui/components/InputBox.js +586 -130
  52. package/dist/ui/components/InputBox.js.map +1 -1
  53. package/dist/ui/components/InteractiveShell.d.ts +16 -0
  54. package/dist/ui/components/InteractiveShell.d.ts.map +1 -0
  55. package/dist/ui/components/InteractiveShell.js +152 -0
  56. package/dist/ui/components/InteractiveShell.js.map +1 -0
  57. package/dist/ui/components/LoadingIndicator.js +1 -1
  58. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  59. package/dist/ui/components/MarkdownRenderer.js +1 -1
  60. package/dist/ui/components/StreamingMessageDisplay.js +1 -1
  61. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  62. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  63. package/dist/ui/components/ToolExecutionMessage.js +43 -47
  64. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  65. package/dist/utils/ansi-encoder.d.ts +2 -0
  66. package/dist/utils/ansi-encoder.d.ts.map +1 -0
  67. package/dist/utils/ansi-encoder.js +57 -0
  68. package/dist/utils/ansi-encoder.js.map +1 -0
  69. package/dist/utils/command-history.d.ts +14 -0
  70. package/dist/utils/command-history.d.ts.map +1 -0
  71. package/dist/utils/command-history.js +140 -0
  72. package/dist/utils/command-history.js.map +1 -0
  73. package/dist/utils/input-classifier.d.ts +26 -0
  74. package/dist/utils/input-classifier.d.ts.map +1 -0
  75. package/dist/utils/input-classifier.js +154 -0
  76. package/dist/utils/input-classifier.js.map +1 -0
  77. package/dist/utils/markdown-parser.d.ts.map +1 -1
  78. package/dist/utils/markdown-parser.js +6 -5
  79. package/dist/utils/markdown-parser.js.map +1 -1
  80. package/dist/utils/shell.d.ts +7 -0
  81. package/dist/utils/shell.d.ts.map +1 -1
  82. package/dist/utils/shell.js +97 -37
  83. package/dist/utils/shell.js.map +1 -1
  84. package/models-config.json +30 -0
  85. package/package.json +2 -1
  86. package/prompts/system-prompt-autonomous.md +428 -0
  87. package/dist/tools/ToolRegistry.d.ts +0 -55
  88. package/dist/tools/ToolRegistry.d.ts.map +0 -1
  89. package/dist/tools/ToolRegistry.js +0 -600
  90. package/dist/tools/ToolRegistry.js.map +0 -1
  91. package/prompts/system-prompt-enhanced.md +0 -659
  92. package/prompts/system-prompt.md +0 -206
@@ -2,6 +2,7 @@ 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';
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
7
8
  import { ConfigManager } from './config/manager.js';
@@ -9,6 +10,7 @@ import { ToolRegistry } from './tools/registry.js';
9
10
  import { readFileTool, writeFileTool, editFileTool, listDirectoryTool } from './tools/file-ops.js';
10
11
  import { executeCommandTool, grepSearchTool } from './tools/command.js';
11
12
  import { webSearchTool, fetchUrlTool } from './tools/web-search.js';
13
+ import { taskCompleteTool } from './tools/task-complete.js';
12
14
  import { apiClient } from './services/api-client.js';
13
15
  import { conversationManager } from './services/conversation-manager.js';
14
16
  import { aiServiceClient } from './services/ai-service-client.js';
@@ -43,11 +45,13 @@ export class CentaurusCLI {
43
45
  onPlanModeChange;
44
46
  onPlanApprovalRequest;
45
47
  onPasswordRequest;
48
+ currentInteractiveProcess;
46
49
  conversationStarted = false;
47
50
  contextManager;
48
51
  commandDetector;
49
52
  aiContextInjector;
50
53
  onSubshellContextChange;
54
+ currentAbortController;
51
55
  constructor() {
52
56
  this.configManager = new ConfigManager();
53
57
  this.toolRegistry = new ToolRegistry();
@@ -116,6 +120,16 @@ export class CentaurusCLI {
116
120
  this.sshHandler.setPasswordRequestCallback(callback);
117
121
  }
118
122
  }
123
+ writeToShellStdin(input) {
124
+ if (this.currentInteractiveProcess) {
125
+ try {
126
+ this.currentInteractiveProcess.write(input);
127
+ }
128
+ catch (error) {
129
+ // Silently ignore - process might have already exited
130
+ }
131
+ }
132
+ }
119
133
  getPlanMode() {
120
134
  return this.planMode;
121
135
  }
@@ -169,6 +183,7 @@ export class CentaurusCLI {
169
183
  this.toolRegistry.register(grepSearchTool);
170
184
  this.toolRegistry.register(webSearchTool);
171
185
  this.toolRegistry.register(fetchUrlTool);
186
+ this.toolRegistry.register(taskCompleteTool);
172
187
  // Load configuration
173
188
  const config = this.configManager.load();
174
189
  // Enable backend sync if authenticated
@@ -243,8 +258,7 @@ Press Enter to continue...
243
258
  this.conversationStarted = true;
244
259
  }
245
260
  catch (error) {
246
- console.error('Failed to start conversation in backend:', error);
247
- // Continue without backend persistence
261
+ // Silently continue without backend persistence
248
262
  }
249
263
  }
250
264
  /**
@@ -264,8 +278,7 @@ Press Enter to continue...
264
278
  });
265
279
  }
266
280
  catch (error) {
267
- console.error('Failed to save message to backend:', error);
268
- // Continue without backend persistence
281
+ // Silently continue without backend persistence
269
282
  }
270
283
  }
271
284
  getModel() {
@@ -273,6 +286,15 @@ Press Enter to continue...
273
286
  // Return the stored model name, or fall back to model ID
274
287
  return config.modelName || config.model || 'gemini-2.5-flash';
275
288
  }
289
+ /**
290
+ * Cancel the current AI request
291
+ */
292
+ cancelCurrentRequest() {
293
+ if (this.currentAbortController) {
294
+ this.currentAbortController.abort();
295
+ this.currentAbortController = undefined;
296
+ }
297
+ }
276
298
  async handleMessage(message) {
277
299
  // Handle command mode - execute commands directly
278
300
  if (this.commandMode) {
@@ -300,6 +322,7 @@ Press Enter to continue...
300
322
  const context = {
301
323
  cwd: this.cwd,
302
324
  contextManager: this.contextManager,
325
+ cliAdapter: this, // Pass CLI adapter reference for interactive process management
303
326
  requireApproval: async (message, risky, preview, operationType, operationDetails) => {
304
327
  if (this.onToolApprovalRequest) {
305
328
  return await this.onToolApprovalRequest({ message, risky, preview, operationType, operationDetails });
@@ -322,16 +345,15 @@ Press Enter to continue...
322
345
  const selectedModelConfig = ALL_MODELS.find(m => m.id === selectedModelId && m.name === config.modelName);
323
346
  const selectedModel = selectedModelId;
324
347
  const selectedModelThinkingConfig = selectedModelConfig?.thinkingConfig;
325
- // Create messages array and inject enhanced system prompt with environment context
326
- // Always use the enhanced system prompt for all models
327
- const systemPromptPath = path.join(__dirname, '..', 'prompts', 'system-prompt-enhanced.md');
348
+ // Create messages array and inject system prompt with environment context
349
+ // Always use the autonomous prompt (optimized for agentic code generation)
350
+ const systemPromptPath = path.join(__dirname, '..', 'prompts', 'system-prompt-autonomous.md');
328
351
  let systemPrompt = '';
329
352
  try {
330
353
  systemPrompt = fs.readFileSync(systemPromptPath, 'utf-8');
331
354
  }
332
355
  catch (error) {
333
356
  // Fallback to basic prompt if file not found
334
- console.warn('Failed to load system-prompt-enhanced.md, using fallback prompt');
335
357
  systemPrompt = 'You are Centaurus, a Senior Site Reliability Engineer (SRE) and Full-Stack Architect embedded in a CLI.';
336
358
  }
337
359
  // Enhance system prompt with environment context
@@ -351,12 +373,15 @@ Press Enter to continue...
351
373
  const environmentContext = this.getEnvironmentContext();
352
374
  const mode = this.getMode();
353
375
  let finalAssistantMessage = '';
354
- const MAX_TURNS = 100; // Allow up to 100 turns for complex tasks
376
+ const MAX_TURNS = 500; // Allow up to 500 turns for complex tasks
355
377
  const MAX_TOOL_CALLS_PER_TURN = 5; // Limit tool calls per turn to prevent overthinking
378
+ const MAX_NARRATION_ATTEMPTS = 3; // Maximum times we'll prompt AI to stop narrating
356
379
  let turnCount = 0;
357
- let streamBuffer = ''; // Buffer to handle TASK_COMPLETE marker split across chunks
380
+ let narrationAttempts = 0; // Track how many times AI narrated without executing
358
381
  let thoughtStartTime = null; // Track when thinking started
359
382
  let thoughtContent = ''; // Accumulate thought content
383
+ // Create AbortController for this request
384
+ this.currentAbortController = new AbortController();
360
385
  // Multi-turn tool execution loop
361
386
  while (turnCount < MAX_TURNS) {
362
387
  turnCount++;
@@ -364,7 +389,7 @@ Press Enter to continue...
364
389
  let toolCalls = [];
365
390
  // Stream AI response from backend
366
391
  // Backend will inject system prompt automatically with environment context
367
- for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig)) {
392
+ for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
368
393
  // Handle error chunks
369
394
  if (chunk.type === 'error') {
370
395
  throw new Error(chunk.message);
@@ -398,28 +423,15 @@ Press Enter to continue...
398
423
  thoughtStartTime = null;
399
424
  thoughtContent = '';
400
425
  }
401
- assistantMessage += chunk.content;
402
- // Send chunk to UI in real-time (but filter out completion marker)
403
- if (this.onResponseStreamCallback) {
404
- // Add to buffer
405
- streamBuffer += chunk.content;
406
- // Check if buffer contains complete TASK_COMPLETE marker
407
- if (streamBuffer.includes('<TASK_COMPLETE>')) {
408
- // Remove the marker and flush buffer
409
- const cleanBuffer = streamBuffer.replace(/<TASK_COMPLETE>/g, '');
410
- if (cleanBuffer) {
411
- this.onResponseStreamCallback(cleanBuffer);
412
- }
413
- streamBuffer = '';
414
- }
415
- else if (streamBuffer.length > 20) {
416
- // If buffer is getting long without marker, flush it
417
- // Keep last 20 chars in case marker is split
418
- const toFlush = streamBuffer.slice(0, -20);
419
- if (toFlush) {
420
- this.onResponseStreamCallback(toFlush);
421
- }
422
- streamBuffer = streamBuffer.slice(-20);
426
+ // Filter out <TASK_COMPLETE> markers (AI shouldn't output these)
427
+ let filteredContent = chunk.content;
428
+ filteredContent = filteredContent.replace(/<\/?TASK_COMPLETE>/gi, '');
429
+ filteredContent = filteredContent.trim();
430
+ if (filteredContent) {
431
+ assistantMessage += filteredContent;
432
+ // Send chunk to UI in real-time
433
+ if (this.onResponseStreamCallback) {
434
+ this.onResponseStreamCallback(filteredContent);
423
435
  }
424
436
  }
425
437
  }
@@ -455,28 +467,20 @@ Press Enter to continue...
455
467
  else {
456
468
  fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Stream done but no thinking was in progress\n`);
457
469
  }
458
- // Flush any remaining buffer
459
- if (streamBuffer && this.onResponseStreamCallback) {
460
- const cleanBuffer = streamBuffer.replace(/<TASK_COMPLETE>/g, '');
461
- if (cleanBuffer) {
462
- this.onResponseStreamCallback(cleanBuffer);
463
- }
464
- streamBuffer = '';
465
- }
466
470
  break;
467
471
  }
468
472
  }
469
- // Three-tier completion detection
470
- // Tier 1: Check for explicit completion marker
471
- if (this.hasCompletionMarker(assistantMessage)) {
472
- finalAssistantMessage = this.removeCompletionMarker(assistantMessage);
473
- break;
474
- }
475
473
  // If there are tool calls, execute them and continue loop
476
474
  if (toolCalls.length > 0) {
475
+ // CRITICAL: AI should ONLY communicate via reason_text and task_complete summary
476
+ // Any text output alongside tool calls should be suppressed
477
+ if (assistantMessage && assistantMessage.trim()) {
478
+ // Suppress text output - AI should only use reason_text
479
+ assistantMessage = ''; // Clear ALL text output - AI should only use reason_text
480
+ }
477
481
  // Limit tool calls per turn to prevent overthinking
478
482
  if (toolCalls.length > MAX_TOOL_CALLS_PER_TURN) {
479
- console.warn(`⚠️ Model attempted ${toolCalls.length} tool calls, limiting to ${MAX_TOOL_CALLS_PER_TURN}`);
483
+ // Silently limit tool calls
480
484
  toolCalls = toolCalls.slice(0, MAX_TOOL_CALLS_PER_TURN);
481
485
  }
482
486
  // Helper function to truncate large results
@@ -491,20 +495,58 @@ Press Enter to continue...
491
495
  };
492
496
  const toolResults = [];
493
497
  let userCancelledOperation = false;
498
+ let taskCompleted = false;
499
+ let taskCompleteSummary = '';
494
500
  for (let i = 0; i < toolCalls.length; i++) {
495
501
  const toolCall = toolCalls[i];
496
502
  try {
503
+ // Check if this is task_complete FIRST (before displaying anything)
504
+ if (toolCall.name === 'task_complete') {
505
+ taskCompleted = true;
506
+ taskCompleteSummary = toolCall.arguments.summary || '';
507
+ // CRITICAL: Suppress any text that came before task_complete in this turn
508
+ // The AI should ONLY communicate through the task_complete summary
509
+ assistantMessage = '';
510
+ // Stream the summary to UI so it's visible
511
+ if (taskCompleteSummary && this.onResponseStreamCallback) {
512
+ this.onResponseStreamCallback(taskCompleteSummary);
513
+ }
514
+ // Execute the tool for proper result handling
515
+ await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
516
+ // Stop processing remaining tools
517
+ break;
518
+ }
519
+ // Extract and display reason_text if present (but skip for task_complete)
520
+ const reasonText = toolCall.arguments.reason_text;
521
+ if (reasonText && this.onResponseStreamCallback) {
522
+ this.onResponseStreamCallback(reasonText + '\n\n');
523
+ }
524
+ // Notify UI: tool is executing (send before execution starts)
525
+ if (this.onToolExecutionUpdate) {
526
+ // Add cwd to arguments for execute_command tool
527
+ const toolArgs = toolCall.name === 'execute_command'
528
+ ? { ...toolCall.arguments, cwd: this.cwd }
529
+ : toolCall.arguments;
530
+ this.onToolExecutionUpdate({
531
+ toolName: toolCall.name,
532
+ status: 'executing',
533
+ arguments: toolArgs
534
+ });
535
+ }
497
536
  // Execute the tool (it will request approval if needed)
498
- // Don't send 'executing' status here - it causes duplication with approval UI
499
537
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
500
538
  if (result.success) {
501
539
  // Notify UI: tool succeeded (send full result to UI)
502
540
  if (this.onToolExecutionUpdate) {
541
+ // Add cwd to arguments for execute_command tool
542
+ const toolArgs = toolCall.name === 'execute_command'
543
+ ? { ...toolCall.arguments, cwd: this.cwd }
544
+ : toolCall.arguments;
503
545
  this.onToolExecutionUpdate({
504
546
  toolName: toolCall.name,
505
547
  status: 'completed',
506
548
  result: result.result,
507
- arguments: toolCall.arguments
549
+ arguments: toolArgs
508
550
  });
509
551
  }
510
552
  // Parse result if it's a string (avoid double-stringification)
@@ -533,11 +575,15 @@ Press Enter to continue...
533
575
  }
534
576
  // Notify UI: tool failed
535
577
  if (this.onToolExecutionUpdate) {
578
+ // Add cwd to arguments for execute_command tool
579
+ const toolArgs = toolCall.name === 'execute_command'
580
+ ? { ...toolCall.arguments, cwd: this.cwd }
581
+ : toolCall.arguments;
536
582
  this.onToolExecutionUpdate({
537
583
  toolName: toolCall.name,
538
584
  status: 'error',
539
585
  error: result.error,
540
- arguments: toolCall.arguments
586
+ arguments: toolArgs
541
587
  });
542
588
  }
543
589
  toolResults.push({
@@ -558,11 +604,15 @@ Press Enter to continue...
558
604
  }
559
605
  // Notify UI: tool failed
560
606
  if (this.onToolExecutionUpdate) {
607
+ // Add cwd to arguments for execute_command tool
608
+ const toolArgs = toolCall.name === 'execute_command'
609
+ ? { ...toolCall.arguments, cwd: this.cwd }
610
+ : toolCall.arguments;
561
611
  this.onToolExecutionUpdate({
562
612
  toolName: toolCall.name,
563
613
  status: 'error',
564
614
  error: error.message,
565
- arguments: toolCall.arguments
615
+ arguments: toolArgs
566
616
  });
567
617
  }
568
618
  toolResults.push({
@@ -576,6 +626,12 @@ Press Enter to continue...
576
626
  }
577
627
  }
578
628
  }
629
+ // If task_complete was called, stop the agentic loop immediately
630
+ if (taskCompleted) {
631
+ // Set the final message to the summary from task_complete
632
+ finalAssistantMessage = taskCompleteSummary;
633
+ break;
634
+ }
579
635
  // If user cancelled an operation, stop the agentic loop immediately
580
636
  if (userCancelledOperation) {
581
637
  // Add assistant message to history
@@ -600,6 +656,11 @@ Press Enter to continue...
600
656
  finalAssistantMessage = 'Operation cancelled by user. The task was not completed.';
601
657
  break;
602
658
  }
659
+ // Send assistant message to UI first (if there's text)
660
+ if (assistantMessage && this.onResponseStreamCallback) {
661
+ // Send the message that came with the tool calls
662
+ this.onResponseStreamCallback(assistantMessage);
663
+ }
603
664
  // Add assistant message with tool calls to conversation history
604
665
  this.conversationHistory.push({
605
666
  role: 'assistant',
@@ -614,7 +675,7 @@ Press Enter to continue...
614
675
  tool_call_id: toolResult.tool_call_id,
615
676
  name: toolResult.name,
616
677
  result: toolResult.result,
617
- 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.'
678
+ 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.'
618
679
  };
619
680
  this.conversationHistory.push({
620
681
  role: 'tool',
@@ -622,8 +683,14 @@ Press Enter to continue...
622
683
  });
623
684
  }
624
685
  // Rebuild messages array with updated history
625
- // Backend will inject system prompt automatically
626
- messages = [...this.conversationHistory];
686
+ // IMPORTANT: Re-add system prompt for each turn
687
+ messages = [
688
+ {
689
+ role: 'system',
690
+ content: enhancedSystemPrompt,
691
+ },
692
+ ...this.conversationHistory
693
+ ];
627
694
  // Re-inject subshell context for the next turn
628
695
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
629
696
  // Add delay between turns to prevent rate limiting
@@ -631,63 +698,139 @@ Press Enter to continue...
631
698
  // Continue to next turn to let AI process results
632
699
  continue;
633
700
  }
634
- // No tool calls - apply three-tier completion detection
701
+ // No tool calls - AI must make tool calls or finish
635
702
  if (assistantMessage) {
636
- // Tier 2: Check for strong completion phrases
637
- if (this.hasStrongCompletionPhrase(assistantMessage)) {
638
- finalAssistantMessage = assistantMessage;
639
- break;
640
- }
641
- // Tier 3: Check if response seems incomplete (only within first 15 turns)
642
- if (this.seemsIncomplete(assistantMessage, turnCount)) {
643
- // Add assistant message to history
644
- this.conversationHistory.push({
645
- role: 'assistant',
646
- content: assistantMessage,
647
- });
648
- // Add continuation prompt to messages
649
- const continuationPrompt = 'Continue with the remaining steps. Execute all necessary tools to complete the entire task.';
650
- this.conversationHistory.push({
651
- role: 'user',
652
- content: continuationPrompt,
653
- });
654
- // Rebuild messages array with continuation prompt
655
- messages = [...this.conversationHistory];
656
- // Re-inject subshell context for continuation
657
- messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
658
- // Add delay before continuation
659
- await new Promise(resolve => setTimeout(resolve, 500));
660
- // Continue loop to process continuation
661
- continue;
703
+ // CRITICAL: AI should NOT output text without tool calls
704
+ // The ONLY valid text output is through task_complete summary
705
+ // Suppress this text and prompt AI to use tools
706
+ // Add assistant message to history (for AI context, but don't show to user)
707
+ this.conversationHistory.push({
708
+ role: 'assistant',
709
+ content: assistantMessage,
710
+ });
711
+ // Check if this looks like it should have been a task_complete
712
+ const looksLikeFinalAnswer = assistantMessage.length < 500 &&
713
+ !assistantMessage.includes('?') &&
714
+ (assistantMessage.includes('!') || assistantMessage.match(/\./g)?.length || 0 > 1);
715
+ 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);
716
+ if (isNarration) {
717
+ narrationAttempts++;
718
+ // If AI keeps narrating without executing, force completion immediately
719
+ if (narrationAttempts >= MAX_NARRATION_ATTEMPTS) {
720
+ // Force task completion with error message
721
+ finalAssistantMessage = '⚠️ **Task Incomplete**: The AI repeatedly described actions without executing them.\n\n' +
722
+ '**What happened**: The AI entered a narration loop, describing what it wanted to do instead of using tool calls.\n\n' +
723
+ '**Suggestions**:\n' +
724
+ '1. Try rephrasing your request more specifically\n' +
725
+ '2. Break the task into smaller, concrete steps\n' +
726
+ '3. Provide explicit file paths if known\n' +
727
+ '4. Check if the model supports tool calling properly\n\n' +
728
+ '**Last message**: ' + assistantMessage;
729
+ break;
730
+ }
731
+ // First narration attempt - give a strong warning with specific guidance
732
+ if (narrationAttempts === 1) {
733
+ const completionPrompt = '🛑 **CRITICAL ERROR**: You output text without using tools.\n\n' +
734
+ '**COMMUNICATION RULE VIOLATION**: You can ONLY communicate through:\n' +
735
+ '1. `reason_text` parameter in tool calls\n' +
736
+ '2. `summary` parameter in task_complete tool\n\n' +
737
+ '**Your text output was HIDDEN from the user.**\n\n' +
738
+ '**MANDATORY CORRECTION**:\n' +
739
+ '- If you need to DO something: Call the tool with `reason_text`\n' +
740
+ '- If you are DONE: Call `task_complete(summary="your message")`\n' +
741
+ '- NEVER output plain text - it will be hidden\n\n' +
742
+ '**Example for greeting**:\n' +
743
+ '```\n' +
744
+ '<thought>User said hello, I should greet back</thought>\n' +
745
+ '(Call task_complete with summary="Hello! How can I help you today?")\n' +
746
+ '```\n\n' +
747
+ '**Your NEXT response MUST use tools.**';
748
+ this.conversationHistory.push({
749
+ role: 'user',
750
+ content: completionPrompt,
751
+ });
752
+ }
753
+ else {
754
+ // Second narration attempt - final warning before forced completion
755
+ const completionPrompt = '🚨 **FINAL WARNING** (Attempt ' + narrationAttempts + '/' + MAX_NARRATION_ATTEMPTS + '): You are STILL narrating instead of executing.\n\n' +
756
+ '**This is your LAST chance**:\n' +
757
+ '1. Execute a tool call NOW, or\n' +
758
+ '2. Call task_complete() to end\n\n' +
759
+ 'If you output narration text again, the task will be forcibly terminated.';
760
+ this.conversationHistory.push({
761
+ role: 'user',
762
+ content: completionPrompt,
763
+ });
764
+ }
662
765
  }
663
- // Tier 3 fallback: Prompt AI for explicit completion (after turn 2)
664
- if (turnCount >= 2) {
665
- // Add assistant message to history
666
- this.conversationHistory.push({
667
- role: 'assistant',
668
- content: assistantMessage,
669
- });
670
- // Prompt AI to either continue or confirm completion
671
- 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.';
672
- this.conversationHistory.push({
673
- role: 'user',
674
- content: completionPrompt,
675
- });
676
- // Rebuild messages array
677
- messages = [...this.conversationHistory];
678
- // Re-inject subshell context for completion check
679
- messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
680
- // Add delay before prompting
681
- await new Promise(resolve => setTimeout(resolve, 500));
682
- // Continue loop to get AI's response
683
- continue;
766
+ else {
767
+ // AI output a response without narration - it should finish
768
+ // Reset narration counter since this is a valid response
769
+ narrationAttempts = 0;
770
+ // Check if the message looks like a final answer/summary
771
+ const isFinalAnswer = assistantMessage.length > 50 &&
772
+ (assistantMessage.includes('found') ||
773
+ assistantMessage.includes('identified') ||
774
+ assistantMessage.includes('completed') ||
775
+ assistantMessage.includes('summary') ||
776
+ assistantMessage.includes('result'));
777
+ if (isFinalAnswer) {
778
+ // This looks like a final answer - prompt to call task_complete
779
+ const completionPrompt = '✅ Your response looks complete. Call task_complete() now to finalize the conversation.\n\n' +
780
+ 'If there are more steps needed, execute the next tool immediately.';
781
+ this.conversationHistory.push({
782
+ role: 'user',
783
+ content: completionPrompt,
784
+ });
785
+ }
786
+ else {
787
+ // Short message without clear intent - ask for clarification or completion
788
+ const completionPrompt = 'Your response is unclear. Either:\n' +
789
+ '1. Execute the next tool call if more work is needed, or\n' +
790
+ '2. Call task_complete() if the task is done';
791
+ this.conversationHistory.push({
792
+ role: 'user',
793
+ content: completionPrompt,
794
+ });
795
+ }
684
796
  }
685
- // Response seems complete (turn 1 or 2 without clear signals)
686
- finalAssistantMessage = assistantMessage;
797
+ // Rebuild messages array
798
+ messages = [...this.conversationHistory];
799
+ // Re-inject subshell context
800
+ messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
801
+ // Add delay before prompting
802
+ await new Promise(resolve => setTimeout(resolve, 500));
803
+ // Continue loop to get AI's response
804
+ continue;
805
+ }
806
+ // No tool calls and no message - AI stopped silently
807
+ // This usually means the AI thinks it's done but didn't call task_complete
808
+ // Prompt it to either continue or complete
809
+ const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
810
+ '**This is not allowed.** You must either:\n' +
811
+ '1. Execute a tool call if more work is needed, OR\n' +
812
+ '2. Call task_complete() with a summary of what you accomplished\n\n' +
813
+ '**If you have completed the task**, call task_complete() NOW with a comprehensive summary.\n' +
814
+ '**If more work is needed**, execute the next tool call immediately.';
815
+ this.conversationHistory.push({
816
+ role: 'user',
817
+ content: silentStopPrompt,
818
+ });
819
+ // Rebuild messages array
820
+ messages = [...this.conversationHistory];
821
+ // Re-inject subshell context
822
+ messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
823
+ // Add delay before prompting
824
+ await new Promise(resolve => setTimeout(resolve, 500));
825
+ // Increment turn count and continue (give AI one more chance)
826
+ turnCount++;
827
+ if (turnCount >= MAX_TURNS) {
828
+ // If we've hit max turns, force completion
829
+ finalAssistantMessage = '⚠️ **Task Incomplete**: The AI stopped responding after multiple prompts.\n\n' +
830
+ 'The task may be partially complete. Please review the work done and provide more specific instructions if needed.';
687
831
  break;
688
832
  }
689
- // No tool calls and no message - something went wrong
690
- break;
833
+ continue;
691
834
  }
692
835
  // Check if max turns was reached
693
836
  if (turnCount >= MAX_TURNS) {
@@ -696,15 +839,18 @@ Press Enter to continue...
696
839
  finalAssistantMessage = (finalAssistantMessage || 'Task processing limit reached.') + warningMessage;
697
840
  }
698
841
  // Send final message to user (without tool execution logs)
699
- // Always remove the completion marker before displaying
700
- const finalMessage = this.removeCompletionMarker(finalAssistantMessage || 'Task completed.');
701
- // Save to conversation history
702
- this.conversationHistory.push({
703
- role: 'assistant',
704
- content: finalMessage,
705
- });
706
- // Save assistant message to backend
707
- await this.saveMessageToBackend('assistant', finalMessage);
842
+ // If finalAssistantMessage is empty (task_complete with no summary), don't show anything
843
+ const finalMessage = finalAssistantMessage;
844
+ // Only save and display if there's a message
845
+ if (finalMessage) {
846
+ // Save to conversation history
847
+ this.conversationHistory.push({
848
+ role: 'assistant',
849
+ content: finalMessage,
850
+ });
851
+ // Save assistant message to backend
852
+ await this.saveMessageToBackend('assistant', finalMessage);
853
+ }
708
854
  // Parse response for plan mode
709
855
  if (this.planMode) {
710
856
  const planData = this.parsePlanResponse(finalMessage);
@@ -723,14 +869,25 @@ Press Enter to continue...
723
869
  }
724
870
  }
725
871
  }
726
- // Send response back to UI
727
- if (this.onResponseCallback) {
872
+ // Send response back to UI (only if there's a message)
873
+ if (this.onResponseCallback && finalMessage) {
728
874
  this.onResponseCallback(finalMessage);
729
875
  }
730
876
  }
731
877
  catch (error) {
878
+ // Check if this was an abort/cancellation
879
+ if (error.name === 'AbortError' || error.message?.includes('aborted')) {
880
+ if (this.onResponseCallback) {
881
+ this.onResponseCallback('⚠️ Request cancelled by user.');
882
+ }
883
+ return;
884
+ }
732
885
  throw new Error(`AI Error: ${error.message}`);
733
886
  }
887
+ finally {
888
+ // Clean up abort controller
889
+ this.currentAbortController = undefined;
890
+ }
734
891
  }
735
892
  async handleSlashCommand(command) {
736
893
  const parts = command.slice(1).split(' ');
@@ -746,6 +903,7 @@ Press Enter to continue...
746
903
  `/model - Select from available Google models\n` +
747
904
  `/plan - Toggle plan mode for complex implementations\n` +
748
905
  `/quality - Toggle enhanced quality features (thinking protocol, validation)\n` +
906
+ `/autonomous - Toggle autonomous mode (Silent Operator with task_complete)\n` +
749
907
  `/sign-in - Sign in with Google (if not already signed in)\n` +
750
908
  `/logout - Sign out, clear session, and exit CLI\n` +
751
909
  `/exit - Exit the application\n\n` +
@@ -864,6 +1022,26 @@ Press Enter to continue...
864
1022
  '• Advanced validation\n' +
865
1023
  '• Enhanced tool descriptions';
866
1024
  break;
1025
+ case 'autonomous':
1026
+ // Toggle autonomous mode (Silent Operator)
1027
+ const currentAutonomous = this.configManager.get('autonomousMode');
1028
+ const newAutonomous = !currentAutonomous; // Toggle (default is false)
1029
+ this.configManager.set('autonomousMode', newAutonomous);
1030
+ responseMessage = newAutonomous
1031
+ ? '✅ Autonomous Mode enabled (Silent Operator)\n\n' +
1032
+ 'The AI will now:\n' +
1033
+ '• Work silently without narrating actions\n' +
1034
+ '• Use Touch-First safety (never guess file paths)\n' +
1035
+ '• Apply surgical precision to file edits\n' +
1036
+ '• Call task_complete when done with comprehensive summary\n' +
1037
+ '• Inject intelligent error recovery hints\n\n' +
1038
+ 'This is the industry-standard autonomous agent mode.'
1039
+ : '⚠️ Autonomous Mode disabled\n\n' +
1040
+ 'The AI will use the standard enhanced prompt with:\n' +
1041
+ '• Verbose communication after each action\n' +
1042
+ '• Standard tool descriptions\n' +
1043
+ '• Manual completion detection';
1044
+ break;
867
1045
  case 'clear':
868
1046
  this.conversationHistory = [];
869
1047
  this.conversationStarted = false;
@@ -891,11 +1069,11 @@ Press Enter to continue...
891
1069
  responseMessage = `❌ Failed to update configuration: ${error.message}`;
892
1070
  }
893
1071
  }
894
- else if (configKey === 'enhancedQuality') {
1072
+ else if (configKey === 'enhancedQuality' || configKey === 'autonomousMode') {
895
1073
  // Parse boolean value
896
1074
  const boolValue = configValue.toLowerCase() === 'true' || configValue === '1';
897
1075
  try {
898
- this.configManager.set('enhancedQuality', boolValue);
1076
+ this.configManager.set(configKey, boolValue);
899
1077
  responseMessage = `✅ Configuration updated: ${configKey} = ${boolValue}`;
900
1078
  }
901
1079
  catch (error) {
@@ -903,7 +1081,7 @@ Press Enter to continue...
903
1081
  }
904
1082
  }
905
1083
  else {
906
- responseMessage = `❌ Error: Unknown config key: ${configKey}\nValid keys: model, enhancedQuality`;
1084
+ responseMessage = `❌ Error: Unknown config key: ${configKey}\nValid keys: model, enhancedQuality, autonomousMode`;
907
1085
  }
908
1086
  }
909
1087
  else {
@@ -916,6 +1094,7 @@ Press Enter to continue...
916
1094
  `Version: ${currentVersion}\n` +
917
1095
  `Model: ${config.model || 'gemini-2.5-flash (default)'}\n` +
918
1096
  `Enhanced Quality: ${config.enhancedQuality !== false ? '✅ Enabled' : '❌ Disabled'}\n` +
1097
+ `Autonomous Mode: ${config.autonomousMode === true ? '✅ Enabled' : '❌ Disabled'}\n` +
919
1098
  `Authentication: ${apiClient.isAuthenticated() ? '✅ Signed in' : '❌ Not signed in'}`;
920
1099
  }
921
1100
  break;
@@ -994,107 +1173,6 @@ Press Enter to continue...
994
1173
  return 'plan';
995
1174
  return 'default';
996
1175
  }
997
- /**
998
- * Check if response contains completion marker
999
- * Returns true if the <TASK_COMPLETE> marker is found in the text
1000
- */
1001
- hasCompletionMarker(text) {
1002
- return text.includes('<TASK_COMPLETE>');
1003
- }
1004
- /**
1005
- * Remove completion marker from text
1006
- * Strips the <TASK_COMPLETE> marker from display text
1007
- */
1008
- removeCompletionMarker(text) {
1009
- return text.replace(/<TASK_COMPLETE>/g, '').trim();
1010
- }
1011
- /**
1012
- * Check if response contains strong completion phrases
1013
- * Returns true if the text contains phrases that strongly indicate task completion
1014
- * Used as fallback when completion marker is not present
1015
- */
1016
- hasStrongCompletionPhrase(text) {
1017
- const strongCompletionPhrases = [
1018
- 'task complete',
1019
- 'task is complete',
1020
- 'tasks complete',
1021
- 'tasks are complete',
1022
- 'all done',
1023
- 'everything is done',
1024
- 'everything is ready',
1025
- 'everything is set up',
1026
- 'everything is complete',
1027
- 'all set',
1028
- 'all finished',
1029
- 'finished everything',
1030
- 'completed everything',
1031
- 'completed all',
1032
- 'done with everything',
1033
- 'implementation complete',
1034
- 'implementation is complete',
1035
- 'setup complete',
1036
- 'setup is complete',
1037
- 'all tasks completed',
1038
- 'all steps completed',
1039
- 'work is complete',
1040
- 'work complete',
1041
- 'fully implemented',
1042
- 'fully complete',
1043
- 'ready to use',
1044
- 'ready to go',
1045
- 'successfully completed',
1046
- 'completed successfully'
1047
- ];
1048
- const lowerText = text.toLowerCase();
1049
- return strongCompletionPhrases.some(phrase => lowerText.includes(phrase));
1050
- }
1051
- /**
1052
- * Check if response seems incomplete
1053
- * Returns true if the text contains phrases that suggest the AI stopped prematurely
1054
- * Only triggers within first 15 turns to prevent infinite loops
1055
- */
1056
- seemsIncomplete(text, turnCount) {
1057
- // Only check for incomplete responses within first 15 turns
1058
- if (turnCount > 15) {
1059
- return false;
1060
- }
1061
- const incompletePhrases = [
1062
- 'i created',
1063
- 'i\'ve created',
1064
- 'created the',
1065
- 'i made',
1066
- 'i\'ve made',
1067
- 'i wrote',
1068
- 'i\'ve written',
1069
- 'next i',
1070
- 'now i',
1071
- 'i will',
1072
- 'i\'ll',
1073
- 'let me',
1074
- 'i should',
1075
- 'first',
1076
- 'step 1',
1077
- 'step 2',
1078
- 'step 3',
1079
- 'i\'ll create',
1080
- 'i\'ll make',
1081
- 'i\'ll write',
1082
- 'i\'ll add',
1083
- 'i\'ll implement',
1084
- 'i need to',
1085
- 'i\'ll need to',
1086
- 'next step',
1087
- 'following step',
1088
- 'then i',
1089
- 'after that',
1090
- 'i\'m creating',
1091
- 'i\'m making',
1092
- 'i\'m writing',
1093
- 'i\'m adding'
1094
- ];
1095
- const lowerText = text.toLowerCase();
1096
- return incompletePhrases.some(phrase => lowerText.includes(phrase));
1097
- }
1098
1176
  getPlanModeInstructions() {
1099
1177
  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`;
1100
1178
  }
@@ -1146,6 +1224,13 @@ Press Enter to continue...
1146
1224
  }
1147
1225
  }
1148
1226
  }
1227
+ /**
1228
+ * Set the current interactive process
1229
+ * This is called by the execute_command tool when starting an interactive command
1230
+ */
1231
+ setCurrentInteractiveProcess(process) {
1232
+ this.currentInteractiveProcess = process;
1233
+ }
1149
1234
  /**
1150
1235
  * Handle command execution in command mode
1151
1236
  */
@@ -1242,26 +1327,84 @@ Press Enter to continue...
1242
1327
  return;
1243
1328
  }
1244
1329
  }
1245
- // Execute the command through Context Manager
1246
- const result = await this.contextManager.executeCommand(command);
1247
- // Format and send the result
1248
- let output = '';
1249
- if (result.stdout && result.stdout.trim()) {
1250
- output += result.stdout;
1251
- }
1252
- if (result.stderr && result.stderr.trim()) {
1253
- if (output)
1254
- output += '\n';
1255
- output += result.stderr;
1330
+ // Notify UI that command is executing
1331
+ if (this.onToolExecutionUpdate) {
1332
+ this.onToolExecutionUpdate({
1333
+ toolName: 'execute_command',
1334
+ status: 'executing',
1335
+ arguments: { command, cwd: this.cwd }
1336
+ });
1256
1337
  }
1257
- if (result.exitCode !== 0) {
1258
- output += `\n\n❌ Exit Code: ${result.exitCode}`;
1259
- }
1260
- else if (!output.trim()) {
1261
- output = '✅ Command executed successfully (no output)';
1338
+ const currentContext = this.contextManager.getCurrentContext();
1339
+ // Execute with streaming support for local commands
1340
+ if (currentContext.type === 'local') {
1341
+ // Use interactive execution to support stdin
1342
+ let output = '';
1343
+ await new Promise((resolve) => {
1344
+ this.currentInteractiveProcess = shellUtils.executeCommandInteractive(command, this.cwd, (data) => {
1345
+ // PTY-style: single merged stream
1346
+ output += data;
1347
+ if (this.onToolStreamingOutput) {
1348
+ this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
1349
+ }
1350
+ }, (exitCode) => {
1351
+ // Notify UI of completion
1352
+ if (this.onToolExecutionUpdate) {
1353
+ if (exitCode !== 0) {
1354
+ this.onToolExecutionUpdate({
1355
+ toolName: 'execute_command',
1356
+ status: 'error',
1357
+ result: output,
1358
+ error: `Exit Code: ${exitCode}`,
1359
+ arguments: { command, cwd: this.cwd }
1360
+ });
1361
+ }
1362
+ else {
1363
+ this.onToolExecutionUpdate({
1364
+ toolName: 'execute_command',
1365
+ status: 'completed',
1366
+ result: output || 'Command executed successfully',
1367
+ arguments: { command, cwd: this.cwd }
1368
+ });
1369
+ }
1370
+ }
1371
+ this.currentInteractiveProcess = undefined;
1372
+ resolve();
1373
+ });
1374
+ });
1262
1375
  }
1263
- if (this.onResponseCallback) {
1264
- this.onResponseCallback(output);
1376
+ else {
1377
+ // Subshell execution (no streaming yet)
1378
+ const result = await this.contextManager.executeCommand(command);
1379
+ let output = '';
1380
+ if (result.stdout && result.stdout.trim()) {
1381
+ output += result.stdout;
1382
+ }
1383
+ if (result.stderr && result.stderr.trim()) {
1384
+ if (output)
1385
+ output += '\n';
1386
+ output += result.stderr;
1387
+ }
1388
+ // Notify UI of completion
1389
+ if (this.onToolExecutionUpdate) {
1390
+ if (result.exitCode !== 0) {
1391
+ this.onToolExecutionUpdate({
1392
+ toolName: 'execute_command',
1393
+ status: 'error',
1394
+ result: output,
1395
+ error: `Exit Code: ${result.exitCode}`,
1396
+ arguments: { command, cwd: this.cwd }
1397
+ });
1398
+ }
1399
+ else {
1400
+ this.onToolExecutionUpdate({
1401
+ toolName: 'execute_command',
1402
+ status: 'completed',
1403
+ result: output || 'Command executed successfully',
1404
+ arguments: { command, cwd: this.cwd }
1405
+ });
1406
+ }
1407
+ }
1265
1408
  }
1266
1409
  }
1267
1410
  catch (error) {