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