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