centaurus-cli 2.8.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-adapter.d.ts +8 -6
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +599 -182
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +1 -0
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/context/context-manager.d.ts +4 -1
- package/dist/context/context-manager.d.ts.map +1 -1
- package/dist/context/context-manager.js +30 -7
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/handlers/wsl-handler.d.ts +10 -0
- package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
- package/dist/context/handlers/wsl-handler.js +29 -1
- package/dist/context/handlers/wsl-handler.js.map +1 -1
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -1
- package/dist/services/ai-service-client.d.ts +1 -0
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +136 -21
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.d.ts +1 -0
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +131 -0
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/inspect-symbol.js +27 -27
- package/dist/tools/inspect-symbol.js.map +1 -1
- package/dist/tools/plan-mode.d.ts +55 -19
- package/dist/tools/plan-mode.d.ts.map +1 -1
- package/dist/tools/plan-mode.js +204 -123
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/reproduce_issue.d.ts +2 -0
- package/dist/tools/reproduce_issue.d.ts.map +1 -0
- package/dist/tools/reproduce_issue.js +166 -0
- package/dist/tools/reproduce_issue.js.map +1 -0
- package/dist/tools/types.d.ts +1 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/validation.d.ts.map +1 -1
- package/dist/tools/validation.js +4 -3
- package/dist/tools/validation.js.map +1 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui/components/App.d.ts +6 -6
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +228 -62
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/DiffViewer.d.ts.map +1 -1
- package/dist/ui/components/DiffViewer.js +69 -58
- package/dist/ui/components/DiffViewer.js.map +1 -1
- package/dist/ui/components/FileTagAutocomplete.d.ts +11 -0
- package/dist/ui/components/FileTagAutocomplete.d.ts.map +1 -0
- package/dist/ui/components/FileTagAutocomplete.js +27 -0
- package/dist/ui/components/FileTagAutocomplete.js.map +1 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +209 -7
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +177 -13
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +3 -3
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.d.ts +12 -0
- package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -0
- package/dist/ui/components/PlanAcceptedMessage.js +22 -0
- package/dist/ui/components/PlanAcceptedMessage.js.map +1 -0
- package/dist/ui/components/PlanReviewScreen.d.ts +14 -0
- package/dist/ui/components/PlanReviewScreen.d.ts.map +1 -0
- package/dist/ui/components/PlanReviewScreen.js +52 -0
- package/dist/ui/components/PlanReviewScreen.js.map +1 -0
- package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.js +3 -3
- package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.d.ts +14 -0
- package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -0
- package/dist/ui/components/TaskCompletedMessage.js +25 -0
- package/dist/ui/components/TaskCompletedMessage.js.map +1 -0
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +207 -20
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/utils/conversation-logger.d.ts +127 -0
- package/dist/utils/conversation-logger.d.ts.map +1 -0
- package/dist/utils/conversation-logger.js +283 -0
- package/dist/utils/conversation-logger.js.map +1 -0
- package/dist/utils/editor-utils.d.ts +37 -0
- package/dist/utils/editor-utils.d.ts.map +1 -1
- package/dist/utils/editor-utils.js +212 -1
- package/dist/utils/editor-utils.js.map +1 -1
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +10 -3
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/markdown-parser.d.ts.map +1 -1
- package/dist/utils/markdown-parser.js +4 -2
- package/dist/utils/markdown-parser.js.map +1 -1
- package/package.json +1 -1
- package/prompts/system-prompt-autonomous.md +0 -377
package/dist/cli-adapter.js
CHANGED
|
@@ -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 {
|
|
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,17 +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';
|
|
34
|
-
import { isInteractiveEditorCommand,
|
|
33
|
+
import { isInteractiveEditorCommand, runWSLCommand, runDockerCommand, runSSHCommand } from './utils/editor-utils.js';
|
|
34
|
+
import { conversationLogger } from './utils/conversation-logger.js';
|
|
35
35
|
export class CentaurusCLI {
|
|
36
36
|
configManager;
|
|
37
37
|
toolRegistry;
|
|
38
38
|
conversationHistory = [];
|
|
39
39
|
cwd;
|
|
40
40
|
planMode = false;
|
|
41
|
+
pendingPlanRequest = null; // Stores original user request during planning phase
|
|
41
42
|
commandMode = false;
|
|
42
43
|
previousMode = 'execution';
|
|
43
44
|
onResponseCallback;
|
|
@@ -54,6 +55,8 @@ export class CentaurusCLI {
|
|
|
54
55
|
onToolStreamingOutput;
|
|
55
56
|
onPlanModeChange;
|
|
56
57
|
onPlanApprovalRequest;
|
|
58
|
+
onPlanCreated;
|
|
59
|
+
onTaskCompleted;
|
|
57
60
|
onPasswordRequest;
|
|
58
61
|
currentInteractiveProcess;
|
|
59
62
|
conversationStarted = false;
|
|
@@ -118,6 +121,12 @@ export class CentaurusCLI {
|
|
|
118
121
|
setOnPlanApprovalRequest(callback) {
|
|
119
122
|
this.onPlanApprovalRequest = callback;
|
|
120
123
|
}
|
|
124
|
+
setOnPlanCreated(callback) {
|
|
125
|
+
this.onPlanCreated = callback;
|
|
126
|
+
}
|
|
127
|
+
setOnTaskCompleted(callback) {
|
|
128
|
+
this.onTaskCompleted = callback;
|
|
129
|
+
}
|
|
121
130
|
setOnCommandModeChange(callback) {
|
|
122
131
|
this.onCommandModeChange = callback;
|
|
123
132
|
}
|
|
@@ -261,15 +270,15 @@ export class CentaurusCLI {
|
|
|
261
270
|
this.toolRegistry.register(viewFileTool);
|
|
262
271
|
this.toolRegistry.register(writeToFileTool);
|
|
263
272
|
this.toolRegistry.register(editFileTool);
|
|
273
|
+
this.toolRegistry.register(multiEditFileTool);
|
|
264
274
|
this.toolRegistry.register(listDirTool);
|
|
265
275
|
this.toolRegistry.register(runCommandTool);
|
|
266
276
|
this.toolRegistry.register(grepSearchTool);
|
|
267
277
|
this.toolRegistry.register(findFilesTool);
|
|
268
278
|
this.toolRegistry.register(getDiffTool);
|
|
269
279
|
this.toolRegistry.register(inspectSymbolTool);
|
|
270
|
-
this.toolRegistry.register(
|
|
271
|
-
this.toolRegistry.register(
|
|
272
|
-
this.toolRegistry.register(updatePlanStepTool);
|
|
280
|
+
this.toolRegistry.register(createPlanTool);
|
|
281
|
+
this.toolRegistry.register(markTaskCompleteTool);
|
|
273
282
|
this.toolRegistry.register(webSearchTool);
|
|
274
283
|
this.toolRegistry.register(fetchUrlTool);
|
|
275
284
|
this.toolRegistry.register(taskCompleteTool);
|
|
@@ -399,6 +408,10 @@ Press Enter to continue...
|
|
|
399
408
|
if (!apiClient.isAuthenticated()) {
|
|
400
409
|
throw new Error('Authentication required. Please sign in to use AI features.');
|
|
401
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
|
+
}
|
|
402
415
|
// Add user message to history
|
|
403
416
|
this.conversationHistory.push({
|
|
404
417
|
role: 'user',
|
|
@@ -406,6 +419,9 @@ Press Enter to continue...
|
|
|
406
419
|
});
|
|
407
420
|
// Save user message to backend
|
|
408
421
|
await this.saveMessageToBackend('user', message);
|
|
422
|
+
// Start logging session and log user message
|
|
423
|
+
conversationLogger.startSession();
|
|
424
|
+
conversationLogger.logUserMessage(message);
|
|
409
425
|
try {
|
|
410
426
|
const tools = this.toolRegistry.getSchemas();
|
|
411
427
|
const context = {
|
|
@@ -434,27 +450,10 @@ Press Enter to continue...
|
|
|
434
450
|
const selectedModelConfig = ALL_MODELS.find(m => m.id === selectedModelId && m.name === config.modelName);
|
|
435
451
|
const selectedModel = selectedModelId;
|
|
436
452
|
const selectedModelThinkingConfig = selectedModelConfig?.thinkingConfig;
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
let
|
|
441
|
-
try {
|
|
442
|
-
systemPrompt = fs.readFileSync(systemPromptPath, 'utf-8');
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
// Fallback to basic prompt if file not found
|
|
446
|
-
systemPrompt = 'You are Centaurus, a Senior Site Reliability Engineer (SRE) and Full-Stack Architect embedded in a CLI.';
|
|
447
|
-
}
|
|
448
|
-
// Enhance system prompt with environment context
|
|
449
|
-
const enhancedSystemPrompt = environmentContextInjector.getEnhancedSystemPrompt(systemPrompt);
|
|
450
|
-
// Build messages array with enhanced system prompt at the beginning
|
|
451
|
-
let messages = [
|
|
452
|
-
{
|
|
453
|
-
role: 'system',
|
|
454
|
-
content: enhancedSystemPrompt,
|
|
455
|
-
},
|
|
456
|
-
...this.conversationHistory
|
|
457
|
-
];
|
|
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];
|
|
458
457
|
// Inject subshell context if in a subshell environment
|
|
459
458
|
const currentContext = this.contextManager.getCurrentContext();
|
|
460
459
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
@@ -471,44 +470,61 @@ Press Enter to continue...
|
|
|
471
470
|
let completionAttempts = 0; // Track how many times AI provided text summary without task_complete
|
|
472
471
|
let thoughtStartTime = null; // Track when thinking started
|
|
473
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
|
|
474
480
|
// Create AbortController for this request
|
|
475
481
|
this.currentAbortController = new AbortController();
|
|
476
482
|
// Multi-turn tool execution loop
|
|
477
483
|
while (turnCount < MAX_TURNS) {
|
|
478
484
|
turnCount++;
|
|
479
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
|
|
480
487
|
environmentContext = this.getEnvironmentContext();
|
|
481
|
-
// Refresh system prompt with new CWD
|
|
482
|
-
const refreshedSystemPrompt = environmentContextInjector.getEnhancedSystemPrompt(systemPrompt, this.cwd);
|
|
483
|
-
// Update the system message in the messages array
|
|
484
|
-
if (messages.length > 0 && messages[0].role === 'system') {
|
|
485
|
-
messages[0].content = refreshedSystemPrompt;
|
|
486
|
-
}
|
|
487
488
|
let assistantMessage = '';
|
|
488
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) { }
|
|
489
509
|
// Stream AI response from backend
|
|
490
510
|
// Backend will inject system prompt automatically with environment context
|
|
491
511
|
for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
|
|
492
512
|
// Handle error chunks
|
|
493
513
|
if (chunk.type === 'error') {
|
|
514
|
+
conversationLogger.logError('AI Stream', new Error(chunk.message));
|
|
494
515
|
throw new Error(chunk.message);
|
|
495
516
|
}
|
|
496
517
|
// Handle thought chunks (internal reasoning)
|
|
497
518
|
if (chunk.type === 'thought') {
|
|
498
|
-
|
|
499
|
-
fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Received thought chunk: ${chunk.content.substring(0, 100)}\n`);
|
|
519
|
+
conversationLogger.logThoughtChunk(chunk.content);
|
|
500
520
|
if (!thoughtStartTime) {
|
|
501
521
|
thoughtStartTime = Date.now();
|
|
502
522
|
}
|
|
503
523
|
thoughtContent += chunk.content;
|
|
504
524
|
// Send thought to UI callback if available
|
|
505
525
|
if (this.onThoughtStreamCallback) {
|
|
506
|
-
fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Calling onThoughtStreamCallback\n`);
|
|
507
526
|
this.onThoughtStreamCallback(chunk.content);
|
|
508
527
|
}
|
|
509
|
-
else {
|
|
510
|
-
fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] WARNING: onThoughtStreamCallback is not set!\n`);
|
|
511
|
-
}
|
|
512
528
|
continue;
|
|
513
529
|
}
|
|
514
530
|
// Handle text chunks
|
|
@@ -516,6 +532,7 @@ Press Enter to continue...
|
|
|
516
532
|
// If we were thinking and now got text, finalize the thought
|
|
517
533
|
if (thoughtStartTime) {
|
|
518
534
|
const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
|
|
535
|
+
conversationLogger.logThoughtComplete(thinkingDuration);
|
|
519
536
|
if (this.onThoughtCompleteCallback) {
|
|
520
537
|
this.onThoughtCompleteCallback(thinkingDuration);
|
|
521
538
|
}
|
|
@@ -528,6 +545,7 @@ Press Enter to continue...
|
|
|
528
545
|
filteredContent = filteredContent.trim();
|
|
529
546
|
if (filteredContent) {
|
|
530
547
|
assistantMessage += filteredContent;
|
|
548
|
+
conversationLogger.logAITextChunk(filteredContent);
|
|
531
549
|
// Send chunk to UI in real-time
|
|
532
550
|
if (this.onResponseStreamCallback) {
|
|
533
551
|
this.onResponseStreamCallback(filteredContent);
|
|
@@ -536,11 +554,17 @@ Press Enter to continue...
|
|
|
536
554
|
}
|
|
537
555
|
// Handle tool call chunks
|
|
538
556
|
if (chunk.type === 'tool_call') {
|
|
539
|
-
|
|
540
|
-
|
|
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 || {});
|
|
541
564
|
// If we were thinking and now got a tool call, finalize the thought
|
|
542
565
|
if (thoughtStartTime) {
|
|
543
566
|
const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
|
|
567
|
+
conversationLogger.logThoughtComplete(thinkingDuration);
|
|
544
568
|
if (this.onThoughtCompleteCallback) {
|
|
545
569
|
this.onThoughtCompleteCallback(thinkingDuration);
|
|
546
570
|
}
|
|
@@ -548,34 +572,48 @@ Press Enter to continue...
|
|
|
548
572
|
thoughtContent = '';
|
|
549
573
|
}
|
|
550
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) { }
|
|
551
589
|
}
|
|
552
590
|
// Handle done chunk
|
|
553
591
|
if (chunk.type === 'done') {
|
|
554
592
|
// If we were thinking and stream ended, finalize the thought
|
|
555
593
|
if (thoughtStartTime) {
|
|
556
594
|
const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
|
|
557
|
-
|
|
595
|
+
conversationLogger.logThoughtComplete(thinkingDuration);
|
|
558
596
|
if (this.onThoughtCompleteCallback) {
|
|
559
|
-
fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Calling onThoughtCompleteCallback from done handler\n`);
|
|
560
597
|
this.onThoughtCompleteCallback(thinkingDuration);
|
|
561
598
|
}
|
|
562
|
-
else {
|
|
563
|
-
fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] WARNING: onThoughtCompleteCallback not set in done handler!\n`);
|
|
564
|
-
}
|
|
565
599
|
thoughtStartTime = null;
|
|
566
600
|
thoughtContent = '';
|
|
567
601
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
602
|
+
// Log AI text completion
|
|
603
|
+
conversationLogger.logAITextComplete();
|
|
604
|
+
conversationLogger.logStreamEnd('done');
|
|
571
605
|
break;
|
|
572
606
|
}
|
|
573
607
|
} // End of stream loop
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
});
|
|
579
617
|
// If there are tool calls, execute them
|
|
580
618
|
if (toolCalls.length > 0) {
|
|
581
619
|
// CRITICAL: AI should ONLY communicate via reason_text and task_complete summary
|
|
@@ -590,16 +628,23 @@ Press Enter to continue...
|
|
|
590
628
|
toolCalls = toolCalls.slice(0, MAX_TOOL_CALLS_PER_TURN);
|
|
591
629
|
}
|
|
592
630
|
const toolResults = [];
|
|
631
|
+
const handledToolCallIds = new Set(); // Track tool calls that were handled directly (e.g., create_plan, mark_task_complete)
|
|
593
632
|
let userCancelledOperation = false;
|
|
594
633
|
let taskCompleted = false;
|
|
595
634
|
let taskCompleteSummary = '';
|
|
596
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) { }
|
|
597
641
|
const toolCall = toolCalls[i];
|
|
598
642
|
try {
|
|
599
643
|
// Check if this is task_complete FIRST (before displaying anything)
|
|
600
644
|
if (toolCall.name === 'task_complete') {
|
|
601
645
|
taskCompleted = true;
|
|
602
646
|
taskCompleteSummary = toolCall.arguments.summary || '';
|
|
647
|
+
conversationLogger.logTaskComplete(taskCompleteSummary);
|
|
603
648
|
// CRITICAL: Suppress any text that came before task_complete in this turn
|
|
604
649
|
// The AI should ONLY communicate through the task_complete summary
|
|
605
650
|
assistantMessage = '';
|
|
@@ -609,19 +654,218 @@ Press Enter to continue...
|
|
|
609
654
|
}
|
|
610
655
|
// Execute the tool for proper result handling
|
|
611
656
|
await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
|
|
657
|
+
// Clear the plan when task is complete
|
|
658
|
+
clearPlan();
|
|
612
659
|
// Stop processing remaining tools
|
|
613
660
|
break;
|
|
614
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.
|
|
615
854
|
// Extract and display reason_text if present (but skip for task_complete)
|
|
616
855
|
const reasonText = toolCall.arguments.reason_text;
|
|
617
856
|
if (reasonText && this.onResponseStreamCallback) {
|
|
618
857
|
this.onResponseStreamCallback(reasonText + '\n\n');
|
|
619
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;
|
|
620
864
|
// Notify UI: tool is executing (send before execution starts)
|
|
621
865
|
if (this.onToolExecutionUpdate) {
|
|
622
866
|
// Add cwd to arguments for execute_command tool
|
|
623
867
|
const toolArgs = toolCall.name === 'execute_command'
|
|
624
|
-
? { ...toolCall.arguments, cwd:
|
|
868
|
+
? { ...toolCall.arguments, cwd: effectiveCwd }
|
|
625
869
|
: toolCall.arguments;
|
|
626
870
|
this.onToolExecutionUpdate({
|
|
627
871
|
toolName: toolCall.name,
|
|
@@ -629,14 +873,18 @@ Press Enter to continue...
|
|
|
629
873
|
arguments: toolArgs
|
|
630
874
|
});
|
|
631
875
|
}
|
|
876
|
+
// Log tool execution start
|
|
877
|
+
conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
|
|
632
878
|
// Execute the tool (it will request approval if needed)
|
|
633
879
|
const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
|
|
634
880
|
if (result.success) {
|
|
881
|
+
// Log successful tool result
|
|
882
|
+
conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
|
|
635
883
|
// Notify UI: tool succeeded (send full result to UI)
|
|
636
884
|
if (this.onToolExecutionUpdate) {
|
|
637
885
|
// Add cwd to arguments for execute_command tool
|
|
638
886
|
const toolArgs = toolCall.name === 'execute_command'
|
|
639
|
-
? { ...toolCall.arguments, cwd:
|
|
887
|
+
? { ...toolCall.arguments, cwd: effectiveCwd }
|
|
640
888
|
: toolCall.arguments;
|
|
641
889
|
this.onToolExecutionUpdate({
|
|
642
890
|
toolName: toolCall.name,
|
|
@@ -665,6 +913,8 @@ Press Enter to continue...
|
|
|
665
913
|
});
|
|
666
914
|
}
|
|
667
915
|
else {
|
|
916
|
+
// Log failed tool result
|
|
917
|
+
conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, result.error);
|
|
668
918
|
// Check if operation was cancelled by user
|
|
669
919
|
if (result.error && result.error.includes('Operation cancelled by user')) {
|
|
670
920
|
userCancelledOperation = true;
|
|
@@ -673,7 +923,7 @@ Press Enter to continue...
|
|
|
673
923
|
if (this.onToolExecutionUpdate) {
|
|
674
924
|
// Add cwd to arguments for execute_command tool
|
|
675
925
|
const toolArgs = toolCall.name === 'execute_command'
|
|
676
|
-
? { ...toolCall.arguments, cwd:
|
|
926
|
+
? { ...toolCall.arguments, cwd: effectiveCwd }
|
|
677
927
|
: toolCall.arguments;
|
|
678
928
|
this.onToolExecutionUpdate({
|
|
679
929
|
toolName: toolCall.name,
|
|
@@ -694,6 +944,8 @@ Press Enter to continue...
|
|
|
694
944
|
}
|
|
695
945
|
}
|
|
696
946
|
catch (error) {
|
|
947
|
+
// Log tool execution error
|
|
948
|
+
conversationLogger.logError(`Tool execution: ${toolCall.name}`, error);
|
|
697
949
|
// Check if operation was cancelled by user
|
|
698
950
|
if (error.message && error.message.includes('Operation cancelled by user')) {
|
|
699
951
|
userCancelledOperation = true;
|
|
@@ -738,14 +990,10 @@ Press Enter to continue...
|
|
|
738
990
|
});
|
|
739
991
|
// Add tool results to history
|
|
740
992
|
for (const toolResult of toolResults) {
|
|
741
|
-
const toolMessage = {
|
|
742
|
-
tool_call_id: toolResult.tool_call_id,
|
|
743
|
-
name: toolResult.name,
|
|
744
|
-
result: toolResult.result,
|
|
745
|
-
};
|
|
746
993
|
this.conversationHistory.push({
|
|
747
994
|
role: 'tool',
|
|
748
|
-
|
|
995
|
+
tool_call_id: toolResult.tool_call_id,
|
|
996
|
+
content: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
|
|
749
997
|
});
|
|
750
998
|
}
|
|
751
999
|
// Set final message indicating cancellation
|
|
@@ -758,35 +1006,42 @@ Press Enter to continue...
|
|
|
758
1006
|
this.onResponseStreamCallback(assistantMessage);
|
|
759
1007
|
}
|
|
760
1008
|
// Add assistant message with tool calls to conversation history
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
+
}
|
|
766
1031
|
// Add tool results to conversation history as tool messages
|
|
767
1032
|
// Format: { tool_call_id, name, result: <object or string> }
|
|
768
|
-
// Include system instruction to remind AI to continue if needed
|
|
769
1033
|
for (const toolResult of toolResults) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
name: toolResult.name,
|
|
773
|
-
result: toolResult.result,
|
|
774
|
-
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.'
|
|
775
|
-
};
|
|
1034
|
+
// Add tool result to conversation history as tool message
|
|
1035
|
+
// IMPORTANT: tool_call_id must be a top-level property
|
|
776
1036
|
this.conversationHistory.push({
|
|
777
1037
|
role: 'tool',
|
|
778
|
-
|
|
1038
|
+
tool_call_id: toolResult.tool_call_id,
|
|
1039
|
+
content: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
|
|
779
1040
|
});
|
|
780
1041
|
}
|
|
781
1042
|
// Rebuild messages array with updated history
|
|
782
|
-
//
|
|
783
|
-
messages = [
|
|
784
|
-
{
|
|
785
|
-
role: 'system',
|
|
786
|
-
content: enhancedSystemPrompt,
|
|
787
|
-
},
|
|
788
|
-
...this.conversationHistory
|
|
789
|
-
];
|
|
1043
|
+
// Backend will inject system prompt
|
|
1044
|
+
messages = [...this.conversationHistory];
|
|
790
1045
|
// Re-inject subshell context
|
|
791
1046
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
792
1047
|
continue; // Loop back to AI service
|
|
@@ -798,12 +1053,17 @@ Press Enter to continue...
|
|
|
798
1053
|
// No tool calls and no message - AI stopped silently
|
|
799
1054
|
// This usually means the AI thinks it's done but didn't call task_complete
|
|
800
1055
|
// Prompt it to either continue or complete
|
|
1056
|
+
conversationLogger.logNarrationDetection('silent_stop', {
|
|
1057
|
+
turn: turnCount,
|
|
1058
|
+
assistantMessageLength: 0,
|
|
1059
|
+
});
|
|
801
1060
|
const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
|
|
802
1061
|
'**This is not allowed.** You must either:\n' +
|
|
803
1062
|
'1. Execute a tool call if more work is needed, OR\n' +
|
|
804
1063
|
'2. Call task_complete() with a summary of what you accomplished\n\n' +
|
|
805
1064
|
'**If you have completed the task**, call task_complete() NOW with a comprehensive summary.\n' +
|
|
806
1065
|
'**If more work is needed**, execute the next tool call immediately.';
|
|
1066
|
+
conversationLogger.logSystemPrompt('silent_stop_prompt', silentStopPrompt);
|
|
807
1067
|
this.conversationHistory.push({
|
|
808
1068
|
role: 'user',
|
|
809
1069
|
content: silentStopPrompt,
|
|
@@ -814,9 +1074,19 @@ Press Enter to continue...
|
|
|
814
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);
|
|
815
1075
|
if (isNarration) {
|
|
816
1076
|
narrationAttempts++;
|
|
1077
|
+
conversationLogger.logNarrationDetection('narration', {
|
|
1078
|
+
turn: turnCount,
|
|
1079
|
+
narrationAttempts,
|
|
1080
|
+
maxAttempts: MAX_NARRATION_ATTEMPTS,
|
|
1081
|
+
messagePreview: assistantMessage.substring(0, 200),
|
|
1082
|
+
});
|
|
817
1083
|
// If AI keeps narrating without executing, force completion immediately
|
|
818
1084
|
if (narrationAttempts >= MAX_NARRATION_ATTEMPTS) {
|
|
819
1085
|
// Force task completion with error message
|
|
1086
|
+
conversationLogger.logNarrationDetection('narration', {
|
|
1087
|
+
action: 'forced_completion',
|
|
1088
|
+
reason: 'max_narration_attempts_reached',
|
|
1089
|
+
});
|
|
820
1090
|
finalAssistantMessage = '⚠️ **Task Incomplete**: The AI repeatedly described actions without executing them.\n\n' +
|
|
821
1091
|
'**What happened**: The AI entered a narration loop, describing what it wanted to do instead of using tool calls.\n\n' +
|
|
822
1092
|
'**Suggestions**:\n' +
|
|
@@ -871,9 +1141,18 @@ Press Enter to continue...
|
|
|
871
1141
|
const isFinalAnswer = assistantMessage.length > 20;
|
|
872
1142
|
if (isFinalAnswer) {
|
|
873
1143
|
completionAttempts++;
|
|
1144
|
+
conversationLogger.logNarrationDetection('final_answer', {
|
|
1145
|
+
turn: turnCount,
|
|
1146
|
+
completionAttempts,
|
|
1147
|
+
messagePreview: assistantMessage.substring(0, 200),
|
|
1148
|
+
});
|
|
874
1149
|
// If AI keeps providing text summaries without calling task_complete, accept the text and finish
|
|
875
1150
|
// This prevents the infinite loop where the AI keeps summarizing in response to our prompt
|
|
876
1151
|
if (completionAttempts > 1) {
|
|
1152
|
+
conversationLogger.logNarrationDetection('final_answer', {
|
|
1153
|
+
action: 'accepting_text_as_final',
|
|
1154
|
+
reason: 'multiple_completion_attempts',
|
|
1155
|
+
});
|
|
877
1156
|
finalAssistantMessage = assistantMessage;
|
|
878
1157
|
break;
|
|
879
1158
|
}
|
|
@@ -898,7 +1177,8 @@ Press Enter to continue...
|
|
|
898
1177
|
}
|
|
899
1178
|
}
|
|
900
1179
|
}
|
|
901
|
-
// Rebuild messages array
|
|
1180
|
+
// Rebuild messages array with updated history
|
|
1181
|
+
// Backend will inject system prompt
|
|
902
1182
|
messages = [...this.conversationHistory];
|
|
903
1183
|
// Re-inject subshell context
|
|
904
1184
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
@@ -918,7 +1198,8 @@ Press Enter to continue...
|
|
|
918
1198
|
role: 'user',
|
|
919
1199
|
content: silentStopPrompt,
|
|
920
1200
|
});
|
|
921
|
-
// Rebuild messages array
|
|
1201
|
+
// Rebuild messages array with updated history
|
|
1202
|
+
// Backend will inject system prompt
|
|
922
1203
|
messages = [...this.conversationHistory];
|
|
923
1204
|
// Re-inject subshell context
|
|
924
1205
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
@@ -953,32 +1234,19 @@ Press Enter to continue...
|
|
|
953
1234
|
// Save assistant message to backend
|
|
954
1235
|
await this.saveMessageToBackend('assistant', finalMessage);
|
|
955
1236
|
} // End of while loop
|
|
956
|
-
//
|
|
957
|
-
|
|
958
|
-
const planData = this.parsePlanResponse(finalAssistantMessage);
|
|
959
|
-
if (planData && this.onPlanApprovalRequest) {
|
|
960
|
-
// Ask user for approval
|
|
961
|
-
const approved = await this.onPlanApprovalRequest(planData);
|
|
962
|
-
if (approved) {
|
|
963
|
-
// Switch to execution mode
|
|
964
|
-
this.planMode = false;
|
|
965
|
-
if (this.onPlanModeChange) {
|
|
966
|
-
this.onPlanModeChange(false);
|
|
967
|
-
}
|
|
968
|
-
// Send message to start execution
|
|
969
|
-
await this.handleMessage('Let\'s implement the plan we just discussed. Start with the first task.');
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
1237
|
+
// Log session end
|
|
1238
|
+
conversationLogger.endSession(finalAssistantMessage, turnCount);
|
|
974
1239
|
// Send response back to UI (only if there's a message)
|
|
975
1240
|
if (this.onResponseCallback && finalAssistantMessage) {
|
|
976
1241
|
this.onResponseCallback(finalAssistantMessage);
|
|
977
1242
|
}
|
|
978
1243
|
}
|
|
979
1244
|
catch (error) {
|
|
1245
|
+
// Log the error
|
|
1246
|
+
conversationLogger.logError('handleMessage', error);
|
|
980
1247
|
// Check if this was an abort/cancellation
|
|
981
1248
|
if (error.name === 'AbortError' || error.message?.includes('aborted')) {
|
|
1249
|
+
conversationLogger.logError('handleMessage', new Error('Request cancelled by user'));
|
|
982
1250
|
if (this.onResponseCallback) {
|
|
983
1251
|
this.onResponseCallback('⚠️ Request cancelled by user.');
|
|
984
1252
|
}
|
|
@@ -1005,16 +1273,20 @@ Press Enter to continue...
|
|
|
1005
1273
|
`/config - View current configuration\n` +
|
|
1006
1274
|
`/model - Select from available Google models\n` +
|
|
1007
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` +
|
|
1008
1278
|
`/quality - Toggle enhanced quality features (thinking protocol, validation)\n` +
|
|
1009
1279
|
`/autonomous - Toggle autonomous mode (Silent Operator with task_complete)\n` +
|
|
1010
1280
|
`/sign-in - Sign in with Google (if not already signed in)\n` +
|
|
1011
1281
|
`/logout - Sign out, clear session, and exit CLI\n` +
|
|
1012
1282
|
`/exit - Exit the application\n\n` +
|
|
1013
1283
|
`Keyboard Shortcuts:\n\n` +
|
|
1014
|
-
`Ctrl+D -
|
|
1015
|
-
`Tab - Autocomplete files/directories (in command mode)\n` +
|
|
1284
|
+
`Ctrl+D - Cycle modes (Agent → Terminal → Auto)\n` +
|
|
1016
1285
|
`Ctrl+T - Toggle auto-accept mode\n` +
|
|
1017
|
-
|
|
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`;
|
|
1018
1290
|
break;
|
|
1019
1291
|
case 'init':
|
|
1020
1292
|
try {
|
|
@@ -1402,6 +1674,22 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1402
1674
|
responseMessage = '❌ MCP is not initialized. Please restart the CLI.';
|
|
1403
1675
|
}
|
|
1404
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;
|
|
1405
1693
|
case 'exit':
|
|
1406
1694
|
process.exit(0);
|
|
1407
1695
|
break;
|
|
@@ -1442,25 +1730,41 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1442
1730
|
return 'default';
|
|
1443
1731
|
}
|
|
1444
1732
|
getPlanModeInstructions() {
|
|
1445
|
-
return `\n\n## PLAN MODE ACTIVE
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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.**`;
|
|
1464
1768
|
}
|
|
1465
1769
|
/**
|
|
1466
1770
|
* Toggle command mode on/off
|
|
@@ -1510,32 +1814,18 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1510
1814
|
// These need full terminal control
|
|
1511
1815
|
const currentContextForEditor = this.contextManager.getCurrentContext();
|
|
1512
1816
|
if (isInteractiveEditorCommand(command)) {
|
|
1513
|
-
if (
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
}
|
|
1518
|
-
else if (currentContextForEditor.type !== 'local') {
|
|
1519
|
-
// Remote context (SSH, WSL, Docker)
|
|
1520
|
-
if (isNanoEditor(command) && this.onInteractiveEditorMode) {
|
|
1521
|
-
// Nano is supported in remote contexts - pass remote context to App
|
|
1522
|
-
this.onInteractiveEditorMode(true, command, this.cwd, currentContextForEditor);
|
|
1523
|
-
return;
|
|
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);
|
|
1524
1821
|
}
|
|
1525
1822
|
else {
|
|
1526
|
-
//
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
`💡 Currently supported: nano\n` +
|
|
1531
|
-
` Try: nano <filename>\n\n` +
|
|
1532
|
-
`📋 Alternatives:\n` +
|
|
1533
|
-
` • Type 'exit' to return to local mode, then run the editor\n` +
|
|
1534
|
-
` • Use 'code <filename>' to open in VS Code\n` +
|
|
1535
|
-
` • Use 'cat' or 'less' to view files`);
|
|
1536
|
-
}
|
|
1537
|
-
return;
|
|
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);
|
|
1538
1827
|
}
|
|
1828
|
+
return;
|
|
1539
1829
|
}
|
|
1540
1830
|
}
|
|
1541
1831
|
try {
|
|
@@ -1627,15 +1917,19 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1627
1917
|
return;
|
|
1628
1918
|
}
|
|
1629
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;
|
|
1630
1925
|
// Notify UI that command is executing
|
|
1631
1926
|
if (this.onToolExecutionUpdate) {
|
|
1632
1927
|
this.onToolExecutionUpdate({
|
|
1633
1928
|
toolName: 'execute_command',
|
|
1634
1929
|
status: 'executing',
|
|
1635
|
-
arguments: { command, cwd:
|
|
1930
|
+
arguments: { command, cwd: effectiveCwd, isPty: shellUtils.isPtyAvailable() }
|
|
1636
1931
|
});
|
|
1637
1932
|
}
|
|
1638
|
-
const currentContext = this.contextManager.getCurrentContext();
|
|
1639
1933
|
// Execute with streaming support for local commands
|
|
1640
1934
|
if (currentContext.type === 'local') {
|
|
1641
1935
|
// Use interactive execution to support stdin
|
|
@@ -1673,38 +1967,161 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1673
1967
|
});
|
|
1674
1968
|
});
|
|
1675
1969
|
}
|
|
1676
|
-
else {
|
|
1677
|
-
//
|
|
1678
|
-
const
|
|
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';
|
|
1679
1974
|
let output = '';
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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');
|
|
1707
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
|
+
});
|
|
1708
2125
|
}
|
|
1709
2126
|
}
|
|
1710
2127
|
catch (error) {
|