@xagent-ai/cli 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/README_CN.md +1 -1
  3. package/dist/agents.js +164 -164
  4. package/dist/agents.js.map +1 -1
  5. package/dist/ai-client.d.ts +4 -6
  6. package/dist/ai-client.d.ts.map +1 -1
  7. package/dist/ai-client.js +137 -115
  8. package/dist/ai-client.js.map +1 -1
  9. package/dist/auth.js +4 -4
  10. package/dist/auth.js.map +1 -1
  11. package/dist/cli.js +184 -1
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.js +3 -3
  14. package/dist/config.js.map +1 -1
  15. package/dist/context-compressor.d.ts.map +1 -1
  16. package/dist/context-compressor.js +65 -81
  17. package/dist/context-compressor.js.map +1 -1
  18. package/dist/conversation.d.ts +1 -1
  19. package/dist/conversation.d.ts.map +1 -1
  20. package/dist/conversation.js +5 -31
  21. package/dist/conversation.js.map +1 -1
  22. package/dist/memory.d.ts +5 -1
  23. package/dist/memory.d.ts.map +1 -1
  24. package/dist/memory.js +77 -37
  25. package/dist/memory.js.map +1 -1
  26. package/dist/remote-ai-client.d.ts +1 -8
  27. package/dist/remote-ai-client.d.ts.map +1 -1
  28. package/dist/remote-ai-client.js +55 -65
  29. package/dist/remote-ai-client.js.map +1 -1
  30. package/dist/retry.d.ts +35 -0
  31. package/dist/retry.d.ts.map +1 -0
  32. package/dist/retry.js +166 -0
  33. package/dist/retry.js.map +1 -0
  34. package/dist/session.d.ts +0 -5
  35. package/dist/session.d.ts.map +1 -1
  36. package/dist/session.js +243 -312
  37. package/dist/session.js.map +1 -1
  38. package/dist/slash-commands.d.ts +1 -0
  39. package/dist/slash-commands.d.ts.map +1 -1
  40. package/dist/slash-commands.js +91 -9
  41. package/dist/slash-commands.js.map +1 -1
  42. package/dist/smart-approval.d.ts.map +1 -1
  43. package/dist/smart-approval.js +18 -17
  44. package/dist/smart-approval.js.map +1 -1
  45. package/dist/system-prompt-generator.d.ts.map +1 -1
  46. package/dist/system-prompt-generator.js +149 -139
  47. package/dist/system-prompt-generator.js.map +1 -1
  48. package/dist/theme.d.ts +48 -0
  49. package/dist/theme.d.ts.map +1 -1
  50. package/dist/theme.js +254 -0
  51. package/dist/theme.js.map +1 -1
  52. package/dist/tools/edit-diff.d.ts +32 -0
  53. package/dist/tools/edit-diff.d.ts.map +1 -0
  54. package/dist/tools/edit-diff.js +185 -0
  55. package/dist/tools/edit-diff.js.map +1 -0
  56. package/dist/tools/edit.d.ts +11 -0
  57. package/dist/tools/edit.d.ts.map +1 -0
  58. package/dist/tools/edit.js +129 -0
  59. package/dist/tools/edit.js.map +1 -0
  60. package/dist/tools.d.ts +19 -5
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +979 -631
  63. package/dist/tools.js.map +1 -1
  64. package/dist/types.d.ts +6 -31
  65. package/dist/types.d.ts.map +1 -1
  66. package/package.json +3 -2
  67. package/src/agents.ts +504 -504
  68. package/src/ai-client.ts +1559 -1458
  69. package/src/auth.ts +4 -4
  70. package/src/cli.ts +195 -1
  71. package/src/config.ts +3 -3
  72. package/src/memory.ts +55 -14
  73. package/src/remote-ai-client.ts +663 -683
  74. package/src/retry.ts +217 -0
  75. package/src/session.ts +1736 -1840
  76. package/src/slash-commands.ts +98 -9
  77. package/src/smart-approval.ts +626 -625
  78. package/src/system-prompt-generator.ts +853 -843
  79. package/src/theme.ts +284 -0
  80. package/src/tools.ts +390 -70
package/src/session.ts CHANGED
@@ -1,1840 +1,1736 @@
1
- import readline from 'readline';
2
- import chalk from 'chalk';
3
- import https from 'https';
4
- import axios from 'axios';
5
- import crypto from 'crypto';
6
- import ora from 'ora';
7
- import inquirer from 'inquirer';
8
- import { createRequire } from 'module';
9
- import { dirname, join } from 'path';
10
- import { fileURLToPath } from 'url';
11
-
12
- const require = createRequire(import.meta.url);
13
- const packageJson = require('../package.json');
14
- import { ExecutionMode, ChatMessage, ToolCall, AuthType } from './types.js';
15
- import { AIClient, Message, detectThinkingKeywords, getThinkingTokens } from './ai-client.js';
16
- import { RemoteAIClient, TokenInvalidError } from './remote-ai-client.js';
17
- import { getConfigManager, ConfigManager } from './config.js';
18
- import { AuthService, selectAuthType } from './auth.js';
19
- import { getToolRegistry } from './tools.js';
20
- import { getAgentManager, DEFAULT_AGENTS, AgentManager } from './agents.js';
21
- import { getMemoryManager, MemoryManager } from './memory.js';
22
- import { getMCPManager, MCPManager } from './mcp.js';
23
- import { getCheckpointManager, CheckpointManager } from './checkpoint.js';
24
- import { getConversationManager, ConversationManager } from './conversation.js';
25
- import { getSessionManager, SessionManager } from './session-manager.js';
26
- import { SlashCommandHandler, parseInput, detectImageInput } from './slash-commands.js';
27
- import { SystemPromptGenerator } from './system-prompt-generator.js';
28
- import { theme, icons, colors, styleHelpers, renderMarkdown } from './theme.js';
29
- import { getCancellationManager, CancellationManager } from './cancellation.js';
30
- import { getContextCompressor, ContextCompressor, CompressionResult } from './context-compressor.js';
31
- import { Logger, LogLevel, getLogger } from './logger.js';
32
-
33
- const logger = getLogger();
34
-
35
- export class InteractiveSession {
36
- private conversationManager: ConversationManager;
37
- private sessionManager: SessionManager;
38
- private contextCompressor: ContextCompressor;
39
- private aiClient: AIClient | null = null;
40
- private remoteAIClient: RemoteAIClient | null = null;
41
- private conversation: ChatMessage[] = [];
42
- private toolCalls: ToolCall[] = [];
43
- private executionMode: ExecutionMode;
44
- private slashCommandHandler: SlashCommandHandler;
45
- private configManager: ConfigManager;
46
- private agentManager: AgentManager;
47
- private memoryManager: MemoryManager;
48
- private mcpManager: MCPManager;
49
- private checkpointManager: CheckpointManager;
50
- private currentAgent: any = null;
51
- private rl: readline.Interface;
52
- private cancellationManager: CancellationManager;
53
- private indentLevel: number;
54
- private indentString: string;
55
- private remoteConversationId: string | null = null;
56
- private currentTaskId: string | null = null;
57
- private taskCompleted: boolean = false;
58
- private isFirstApiCall: boolean = true;
59
-
60
- constructor(indentLevel: number = 0) {
61
-
62
- this.rl = readline.createInterface({
63
-
64
- input: process.stdin,
65
-
66
- output: process.stdout
67
-
68
- });
69
-
70
-
71
-
72
- this.configManager = getConfigManager(process.cwd());
73
-
74
- this.agentManager = getAgentManager(process.cwd());
75
-
76
- this.memoryManager = getMemoryManager(process.cwd());
77
-
78
- this.mcpManager = getMCPManager();
79
-
80
- this.checkpointManager = getCheckpointManager(process.cwd());
81
-
82
- this.conversationManager = getConversationManager();
83
-
84
- this.sessionManager = getSessionManager(process.cwd());
85
-
86
- this.slashCommandHandler = new SlashCommandHandler();
87
-
88
-
89
-
90
- // Register /clear callback, clear local conversation when clearing dialogue
91
-
92
- this.slashCommandHandler.setClearCallback(() => {
93
-
94
- this.conversation = [];
95
-
96
- });
97
-
98
-
99
-
100
- // Register MCP update callback, update system prompt
101
-
102
- this.slashCommandHandler.setSystemPromptUpdateCallback(async () => {
103
-
104
- await this.updateSystemPrompt();
105
-
106
- });
107
-
108
-
109
-
110
- this.executionMode = ExecutionMode.DEFAULT;
111
-
112
- this.cancellationManager = getCancellationManager();
113
-
114
- this.indentLevel = indentLevel;
115
-
116
- this.indentString = ' '.repeat(indentLevel);
117
-
118
- this.contextCompressor = getContextCompressor();
119
-
120
- }
121
-
122
- private getIndent(): string {
123
- return this.indentString;
124
- }
125
-
126
- setAIClient(aiClient: AIClient): void {
127
- this.aiClient = aiClient;
128
- }
129
-
130
- setExecutionMode(mode: ExecutionMode): void {
131
- this.executionMode = mode;
132
- }
133
-
134
- /**
135
- * Update system prompt to reflect MCP changes (called after add/remove MCP)
136
- */
137
- async updateSystemPrompt(): Promise<void> {
138
- const toolRegistry = getToolRegistry();
139
- const promptGenerator = new SystemPromptGenerator(toolRegistry, this.executionMode, undefined, this.mcpManager);
140
-
141
- // Use the current agent's original system prompt as base
142
- const baseSystemPrompt = this.currentAgent?.systemPrompt || 'You are xAgent, an AI-powered CLI tool.';
143
- const newSystemPrompt = await promptGenerator.generateEnhancedSystemPrompt(baseSystemPrompt);
144
-
145
- // Replace old system prompt with new one
146
- this.conversation = this.conversation.filter(msg => msg.role !== 'system');
147
- this.conversation.unshift({
148
- role: 'system',
149
- content: newSystemPrompt,
150
- timestamp: Date.now()
151
- });
152
-
153
- // Sync to slashCommandHandler
154
- this.slashCommandHandler.setConversationHistory(this.conversation);
155
- }
156
-
157
- setAgent(agent: any): void {
158
- this.currentAgent = agent;
159
- }
160
-
161
- async start(): Promise<void> {
162
- // Set this session as the singleton for access from other modules
163
- setSingletonSession(this);
164
-
165
- // Initialize taskId for GUI operations
166
- this.currentTaskId = crypto.randomUUID();
167
-
168
- const separator = icons.separator.repeat(60);
169
- console.log('');
170
- console.log(colors.gradient('╔════════════════════════════════════════════════════════════╗'));
171
- console.log(colors.gradient('║') + ' '.repeat(58) + colors.gradient(' ║'));
172
- console.log(colors.gradient('║') + ' '.repeat(13) + '🤖 ' + colors.gradient('XAGENT CLI') + ' '.repeat(32) + colors.gradient(' ║'));
173
- console.log(colors.gradient('║') + ' '.repeat(16) + colors.textMuted(`v${packageJson.version}`) + ' '.repeat(36) + colors.gradient(' ║'));
174
- console.log(colors.gradient('║') + ' '.repeat(58) + colors.gradient(' ║'));
175
- console.log(colors.gradient('╚════════════════════════════════════════════════════════════╝'));
176
- console.log(colors.textMuted(' AI-powered command-line assistant'));
177
- console.log('');
178
-
179
- await this.initialize();
180
- this.showWelcomeMessage();
181
-
182
- // Track if an operation is in progress
183
- (this as any)._isOperationInProgress = false;
184
-
185
- // Listen for ESC cancellation - only cancel operations, don't exit the program
186
- const cancelHandler = () => {
187
- if ((this as any)._isOperationInProgress) {
188
- // An operation is running, let it be cancelled
189
- return;
190
- }
191
- // No operation running, ignore ESC or show a message
192
- };
193
- this.cancellationManager.on('cancelled', cancelHandler);
194
-
195
- this.promptLoop();
196
-
197
- // Keep the promise pending until shutdown
198
- return new Promise((resolve) => {
199
- (this as any)._shutdownResolver = resolve;
200
- });
201
- }
202
-
203
- private async initialize(): Promise<void> {
204
- logger.debug('\n[SESSION] ========== initialize() 开始 ==========\n');
205
-
206
- try {
207
- // Custom spinner for initialization (like Thinking...)
208
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
209
- let frameIndex = 0;
210
- const validatingText = colors.textMuted('Validating authentication...');
211
-
212
- const spinnerInterval = setInterval(() => {
213
- process.stdout.write(`\r${colors.primary(frames[frameIndex])} ${validatingText}`);
214
- frameIndex = (frameIndex + 1) % frames.length;
215
- }, 120);
216
- logger.debug('[SESSION] 调用 configManager.load()...');
217
- await this.configManager.load();
218
-
219
- logger.debug('[SESSION] Config loaded');
220
- let authConfig = this.configManager.getAuthConfig();
221
- let selectedAuthType = this.configManager.get('selectedAuthType');
222
-
223
- logger.debug('[SESSION] authConfig.apiKey exists:', String(!!authConfig.apiKey));
224
- logger.debug('[SESSION] selectedAuthType (initial):', String(selectedAuthType));
225
- logger.debug('[SESSION] AuthType.OAUTH_XAGENT:', String(AuthType.OAUTH_XAGENT));
226
- logger.debug('[SESSION] AuthType.OPENAI_COMPATIBLE:', String(AuthType.OPENAI_COMPATIBLE));
227
- logger.debug('[SESSION] Will validate OAuth:', String(!!(authConfig.apiKey && selectedAuthType === AuthType.OAUTH_XAGENT)));
228
-
229
- // Only validate OAuth tokens, skip validation for third-party API keys
230
- if (authConfig.apiKey && selectedAuthType === AuthType.OAUTH_XAGENT) {
231
- clearInterval(spinnerInterval);
232
- process.stdout.write('\r' + ' '.repeat(50) + '\r'); // Clear the line
233
-
234
- const baseUrl = authConfig.xagentApiBaseUrl || 'https://154.8.140.52:443';
235
- let isValid = await this.validateToken(baseUrl, authConfig.apiKey);
236
-
237
- // Try refresh token if validation failed
238
- if (!isValid && authConfig.refreshToken) {
239
- const refreshingText = colors.textMuted('Refreshing authentication...');
240
- frameIndex = 0;
241
- const refreshInterval = setInterval(() => {
242
- process.stdout.write(`\r${colors.primary(frames[frameIndex])} ${refreshingText}`);
243
- frameIndex = (frameIndex + 1) % frames.length;
244
- }, 120);
245
-
246
- const newToken = await this.refreshToken(baseUrl, authConfig.refreshToken);
247
- clearInterval(refreshInterval);
248
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
249
-
250
- if (newToken) {
251
- // Save new token and persist
252
- await this.configManager.set('apiKey', newToken);
253
- await this.configManager.save('global');
254
- authConfig.apiKey = newToken;
255
- isValid = true;
256
- }
257
- }
258
-
259
- if (!isValid) {
260
- console.log('');
261
- console.log(colors.warning('Your xAgent session has expired or is not configured'));
262
- console.log(colors.info('Please select an authentication method to continue.'));
263
- console.log('');
264
-
265
- // Clear invalid credentials and persist
266
- // Note: Do NOT overwrite selectedAuthType - let user re-select their preferred auth method
267
- await this.configManager.set('apiKey', '');
268
- await this.configManager.set('refreshToken', '');
269
- await this.configManager.save('global');
270
-
271
- await this.configManager.load();
272
- authConfig = this.configManager.getAuthConfig();
273
-
274
- await this.setupAuthentication();
275
- authConfig = this.configManager.getAuthConfig();
276
-
277
- // Recreate readline interface after inquirer
278
- this.rl.close();
279
- this.rl = readline.createInterface({
280
- input: process.stdin,
281
- output: process.stdout
282
- });
283
- this.rl.on('close', () => {
284
- // readline closed
285
- });
286
- }
287
- } else if (!authConfig.apiKey) {
288
- // No API key configured, need to set up authentication
289
- clearInterval(spinnerInterval);
290
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
291
- await this.setupAuthentication();
292
- authConfig = this.configManager.getAuthConfig();
293
- selectedAuthType = this.configManager.get('selectedAuthType');
294
- logger.debug('[SESSION] selectedAuthType (after setup):', String(selectedAuthType));
295
-
296
- // Recreate readline interface after inquirer
297
- this.rl.close();
298
- this.rl = readline.createInterface({
299
- input: process.stdin,
300
- output: process.stdout
301
- });
302
- this.rl.on('close', () => {
303
- // readline closed
304
- });
305
- } else {
306
- clearInterval(spinnerInterval);
307
- process.stdout.write('\r' + ' '.repeat(50) + '\r');
308
- }
309
- // For OPENAI_COMPATIBLE with API key, skip validation and proceed directly
310
-
311
- this.aiClient = new AIClient(authConfig);
312
- this.contextCompressor.setAIClient(this.aiClient);
313
-
314
- // Initialize remote AI client for OAuth XAGENT mode
315
- logger.debug('[SESSION] Final selectedAuthType:', String(selectedAuthType));
316
- logger.debug('[SESSION] Creating RemoteAIClient?', String(selectedAuthType === AuthType.OAUTH_XAGENT));
317
- if (selectedAuthType === AuthType.OAUTH_XAGENT) {
318
- const webBaseUrl = authConfig.xagentApiBaseUrl || 'https://154.8.140.52:443';
319
- // In OAuth XAGENT mode, we still pass apiKey (can be empty or used for other purposes)
320
- this.remoteAIClient = new RemoteAIClient(authConfig.apiKey || '', webBaseUrl, authConfig.showAIDebugInfo);
321
- logger.debug('[DEBUG Initialize] RemoteAIClient created successfully');
322
- } else {
323
- logger.debug('[DEBUG Initialize] RemoteAIClient NOT created (not OAuth XAGENT mode)');
324
- }
325
-
326
- this.executionMode = this.configManager.getApprovalMode() || this.configManager.getExecutionMode();
327
-
328
- await this.agentManager.loadAgents();
329
- await this.memoryManager.loadMemory();
330
- await this.conversationManager.initialize();
331
- await this.sessionManager.initialize();
332
-
333
- // Create a new conversation and session for this interactive session
334
- const conversation = await this.conversationManager.createConversation();
335
- await this.sessionManager.createSession(
336
- conversation.id,
337
- this.currentAgent?.name || 'general-purpose',
338
- this.executionMode
339
- );
340
-
341
- // Sync conversation history to slashCommandHandler
342
- this.slashCommandHandler.setConversationHistory(this.conversation);
343
-
344
- const mcpServers = this.configManager.getMcpServers();
345
- Object.entries(mcpServers).forEach(([name, config]) => {
346
- console.log(`📝 Registering MCP server: ${name} (${config.transport})`);
347
- this.mcpManager.registerServer(name, config);
348
- });
349
-
350
- // Eagerly connect to MCP servers to get tool definitions
351
- if (mcpServers && Object.keys(mcpServers).length > 0) {
352
- try {
353
- console.log(`${colors.info(`${icons.brain} Connecting to ${Object.keys(mcpServers).length} MCP server(s)...`)}`);
354
- await this.mcpManager.connectAllServers();
355
- const connectedCount = Array.from(this.mcpManager.getAllServers()).filter((s: any) => s.isServerConnected()).length;
356
- const mcpTools = this.mcpManager.getToolDefinitions();
357
- console.log(`${colors.success(`✓ ${connectedCount}/${Object.keys(mcpServers).length} MCP server(s) connected (${mcpTools.length} tools available)`)}`);
358
-
359
- // Register MCP tools with the tool registry (hide MCP origin from LLM)
360
- const toolRegistry = getToolRegistry();
361
- const allMcpTools = this.mcpManager.getAllTools();
362
- toolRegistry.registerMCPTools(allMcpTools);
363
- } catch (error: any) {
364
- console.log(`${colors.warning(`⚠ MCP connection failed: ${error.message}`)}`);
365
- }
366
- }
367
-
368
- const checkpointingConfig = this.configManager.getCheckpointingConfig();
369
- if (checkpointingConfig.enabled) {
370
- this.checkpointManager = getCheckpointManager(
371
- process.cwd(),
372
- checkpointingConfig.enabled,
373
- checkpointingConfig.maxCheckpoints
374
- );
375
- await this.checkpointManager.initialize();
376
- }
377
-
378
- this.currentAgent = this.agentManager.getAgent('general-purpose');
379
-
380
- console.log(colors.success('✔ Initialization complete'));
381
- } catch (error: any) {
382
- const spinner = ora({ text: '', spinner: 'dots', color: 'red' }).start();
383
- spinner.fail(colors.error(`Initialization failed: ${error.message}`));
384
- throw error;
385
- }
386
- }
387
-
388
- /**
389
- * Validate token with the backend
390
- * Returns true if token is valid, false otherwise
391
- */
392
- private async validateToken(baseUrl: string, apiKey: string): Promise<boolean> {
393
- logger.debug('[SESSION] validateToken called with baseUrl:', baseUrl);
394
- logger.debug('[SESSION] apiKey exists:', apiKey ? 'yes' : 'no');
395
-
396
- try {
397
- // For OAuth XAGENT auth, use /api/auth/me endpoint
398
- const url = `${baseUrl}/api/auth/me`;
399
- const httpsAgent = new https.Agent({ rejectUnauthorized: false });
400
-
401
- logger.debug('[SESSION] Sending validation request to:', url);
402
-
403
- const response = await axios.get(url, {
404
- headers: {
405
- 'Authorization': `Bearer ${apiKey}`,
406
- 'Content-Type': 'application/json'
407
- },
408
- httpsAgent,
409
- timeout: 10000
410
- });
411
-
412
- logger.debug('[SESSION] Validation response status:', String(response.status));
413
- return response.status === 200;
414
- } catch (error: any) {
415
- // Network error - log details but still consider token may be invalid
416
- logger.debug('[SESSION] Error:', error.message);
417
- if (error.response) {
418
- logger.debug('[SESSION] Response status:', error.response.status);
419
- }
420
- // For network errors, we still return false to trigger re-authentication
421
- // This ensures security but the user can retry
422
- return false;
423
- }
424
- }
425
-
426
- private async refreshToken(baseUrl: string, refreshToken: string): Promise<string | null> {
427
- try {
428
- const url = `${baseUrl}/api/auth/refresh`;
429
- const httpsAgent = new https.Agent({ rejectUnauthorized: false });
430
-
431
- const response = await axios.post(url, { refreshToken }, {
432
- httpsAgent,
433
- timeout: 10000
434
- });
435
-
436
- if (response.status === 200) {
437
- const data = response.data as { token?: string; refreshToken?: string };
438
- return data.token || null;
439
- } else {
440
- return null;
441
- }
442
- } catch (error: any) {
443
- return null;
444
- }
445
- }
446
-
447
- private async setupAuthentication(): Promise<void> {
448
- const separator = icons.separator.repeat(40);
449
- console.log('');
450
- console.log(colors.primaryBright(`${icons.lock} Setup Authentication`));
451
- console.log(colors.border(separator));
452
- console.log('');
453
-
454
- const authType = await selectAuthType();
455
- this.configManager.set('selectedAuthType', authType);
456
-
457
- const authService = new AuthService({
458
- type: authType,
459
- apiKey: '',
460
- baseUrl: '',
461
- modelName: ''
462
- });
463
-
464
- const success = await authService.authenticate();
465
-
466
- if (!success) {
467
- console.log('');
468
- console.log(colors.error('Authentication failed. Exiting...'));
469
- console.log('');
470
- process.exit(1);
471
- }
472
-
473
- const authConfig = authService.getAuthConfig();
474
-
475
- // VLM configuration is optional - only show for non-OAuth (local) mode
476
- // Remote mode uses backend VLM configuration
477
- if (authType !== AuthType.OAUTH_XAGENT) {
478
- console.log('');
479
- console.log(colors.info(`${icons.info} VLM configuration is optional.`));
480
- console.log(colors.info(`You can configure it later using the /vlm command if needed.`));
481
- console.log('');
482
- }
483
-
484
- // Save LLM config only, skip VLM for now
485
- await this.configManager.setAuthConfig(authConfig);
486
- }
487
-
488
- private showWelcomeMessage(): void {
489
- const language = this.configManager.getLanguage();
490
- const separator = icons.separator.repeat(40);
491
-
492
- console.log('');
493
- console.log(colors.border(separator));
494
-
495
- if (language === 'zh') {
496
- console.log(colors.primaryBright(`${icons.sparkles} Welcome to XAGENT CLI!`));
497
- console.log(colors.textMuted('Type /help to see available commands')); } else {
498
- console.log(colors.primaryBright(`${icons.sparkles} Welcome to XAGENT CLI!`));
499
- console.log(colors.textMuted('Type /help to see available commands'));
500
- }
501
-
502
- console.log(colors.border(separator));
503
- console.log('');
504
-
505
- this.showExecutionMode();
506
- }
507
-
508
- private showExecutionMode(): void {
509
- const modeConfig = {
510
- [ExecutionMode.YOLO]: {
511
- color: colors.error,
512
- icon: icons.fire,
513
- description: 'Execute commands without confirmation'
514
- },
515
- [ExecutionMode.ACCEPT_EDITS]: {
516
- color: colors.warning,
517
- icon: icons.check,
518
- description: 'Accept all edits automatically'
519
- },
520
- [ExecutionMode.PLAN]: {
521
- color: colors.info,
522
- icon: icons.brain,
523
- description: 'Plan before executing'
524
- },
525
- [ExecutionMode.DEFAULT]: {
526
- color: colors.success,
527
- icon: icons.bolt,
528
- description: 'Safe execution with confirmations'
529
- },
530
- [ExecutionMode.SMART]: {
531
- color: colors.primaryBright,
532
- icon: icons.sparkles,
533
- description: 'Smart approval with intelligent security checks'
534
- }
535
- };
536
-
537
- const config = modeConfig[this.executionMode];
538
- const modeName = this.executionMode;
539
-
540
- console.log(colors.textMuted(`${icons.info} Current Mode:`));
541
- console.log(` ${config.color(config.icon)} ${styleHelpers.text.bold(config.color(modeName))}`);
542
- console.log(` ${colors.textDim(` ${config.description}`)}`);
543
- console.log('');
544
- }
545
-
546
- private async promptLoop(): Promise<void> {
547
- // Check if we're shutting down
548
- if ((this as any)._isShuttingDown) {
549
- return;
550
- }
551
-
552
- // Recreate readline interface for input
553
- if (this.rl) {
554
- this.rl.close();
555
- }
556
-
557
- // Enable raw mode BEFORE emitKeypressEvents for better ESC detection
558
- if (process.stdin.isTTY) {
559
- process.stdin.setRawMode(true);
560
- }
561
- process.stdin.resume();
562
- readline.emitKeypressEvents(process.stdin);
563
-
564
- this.rl = readline.createInterface({
565
- input: process.stdin,
566
- output: process.stdout
567
- });
568
-
569
- const prompt = `${colors.primaryBright('❯')} `;
570
- this.rl.question(prompt, async (input: string) => {
571
- if ((this as any)._isShuttingDown) {
572
- return;
573
- }
574
-
575
- try {
576
- await this.handleInput(input);
577
- } catch (err: any) {
578
- console.log(colors.error(`Error: ${err.message}`));
579
- }
580
-
581
- this.promptLoop();
582
- });
583
- }
584
-
585
- private async handleInput(input: string): Promise<void> {
586
- const trimmedInput = input.trim();
587
-
588
- if (!trimmedInput) {
589
- return;
590
- }
591
-
592
- if (trimmedInput.startsWith('/')) {
593
- const handled = await this.slashCommandHandler.handleCommand(trimmedInput);
594
- if (handled) {
595
- this.executionMode = this.configManager.getApprovalMode() || this.configManager.getExecutionMode();
596
- // Sync conversation history to slashCommandHandler
597
- this.slashCommandHandler.setConversationHistory(this.conversation);
598
- }
599
- return;
600
- }
601
-
602
- if (trimmedInput.startsWith('$')) {
603
- await this.handleSubAgentCommand(trimmedInput);
604
- return;
605
- }
606
-
607
- await this.processUserMessage(trimmedInput);
608
- }
609
-
610
- private async handleSubAgentCommand(input: string): Promise<void> {
611
- const [agentType, ...taskParts] = input.slice(1).split(' ');
612
- const task = taskParts.join(' ');
613
-
614
- const agent = this.agentManager.getAgent(agentType);
615
-
616
- if (!agent) {
617
- console.log('');
618
- console.log(colors.warning(`Agent not found: ${agentType}`));
619
- console.log(colors.textMuted('Use /agents list to see available agents'));
620
- console.log('');
621
- return;
622
- }
623
-
624
- console.log('');
625
- console.log(colors.primaryBright(`${icons.robot} Using agent: ${agent.name || agent.agentType}`));
626
- console.log(colors.border(icons.separator.repeat(40)));
627
- console.log('');
628
-
629
- this.currentAgent = agent;
630
- await this.processUserMessage(task, agent);
631
- }
632
-
633
- public async processUserMessage(message: string, agent?: any): Promise<void> {
634
- const inputs = parseInput(message);
635
- const textInput = inputs.find(i => i.type === 'text');
636
- const fileInputs = inputs.filter(i => i.type === 'file');
637
- const commandInput = inputs.find(i => i.type === 'command');
638
-
639
- if (commandInput) {
640
- await this.executeShellCommand(commandInput.content);
641
- return;
642
- }
643
-
644
- let userContent = textInput?.content || '';
645
-
646
- if (fileInputs.length > 0) {
647
- const toolRegistry = getToolRegistry();
648
- for (const fileInput of fileInputs) {
649
- try {
650
- const content = await toolRegistry.execute('Read', { filePath: fileInput.content }, this.executionMode);
651
- userContent += `\n\n--- File: ${fileInput.content} ---\n${content}`;
652
- } catch (error: any) {
653
- console.log(chalk.yellow(`Warning: Failed to read file ${fileInput.content}: ${error.message}`));
654
- }
655
- }
656
- }
657
-
658
- // Record input to session manager
659
- const sessionInput = {
660
- type: 'text' as const,
661
- content: userContent,
662
- rawInput: message,
663
- timestamp: Date.now()
664
- };
665
- await this.sessionManager.addInput(sessionInput);
666
-
667
- // Calculate thinking tokens based on config and user input
668
- const thinkingConfig = this.configManager.getThinkingConfig();
669
- let thinkingTokens = 0;
670
-
671
- if (thinkingConfig.enabled) {
672
- // If thinking mode is enabled, detect keywords and calculate tokens
673
- const thinkingMode = detectThinkingKeywords(userContent);
674
- thinkingTokens = getThinkingTokens(thinkingMode);
675
- }
676
-
677
- const userMessage: ChatMessage = {
678
- role: 'user',
679
- content: userContent,
680
- timestamp: Date.now()
681
- };
682
-
683
- // Save last user message for recovery after compression
684
- const lastUserMessage = userMessage;
685
-
686
- this.conversation.push(userMessage);
687
- await this.conversationManager.addMessage(userMessage);
688
-
689
- // Check if context compression is needed
690
- await this.checkAndCompressContext(lastUserMessage);
691
-
692
- // Use remote AI client if available (OAuth XAGENT mode)
693
- const currentSelectedAuthType = this.configManager.get('selectedAuthType');
694
- logger.debug('[DEBUG processUserMessage] remoteAIClient exists:', !!this.remoteAIClient ? 'true' : 'false');
695
- logger.debug('[DEBUG processUserMessage] selectedAuthType:', String(currentSelectedAuthType));
696
- logger.debug('[DEBUG processUserMessage] AuthType.OAUTH_XAGENT:', String(AuthType.OAUTH_XAGENT));
697
-
698
- if (this.remoteAIClient) {
699
- logger.debug('[DEBUG processUserMessage] Using generateRemoteResponse');
700
- await this.generateRemoteResponse(thinkingTokens);
701
- } else {
702
- logger.debug('[DEBUG processUserMessage] Using generateResponse (local mode)');
703
- await this.generateResponse(thinkingTokens);
704
- }
705
- }
706
-
707
- private displayThinkingContent(reasoningContent: string): void {
708
- const indent = this.getIndent();
709
- const thinkingConfig = this.configManager.getThinkingConfig();
710
- const displayMode = thinkingConfig.displayMode || 'compact';
711
-
712
- const separator = icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length);
713
-
714
- console.log('');
715
- console.log(`${indent}${colors.border(separator)}`);
716
-
717
- switch (displayMode) {
718
- case 'full':
719
- // Full display, using small font and gray color
720
- console.log(`${indent}${colors.textDim(`${icons.brain} Thinking Process:`)}`);
721
- console.log('');
722
- console.log(`${indent}${colors.textDim(reasoningContent.replace(/^/gm, indent))}`);
723
- break;
724
-
725
- case 'compact':
726
- // Compact display, truncate partial content
727
- const maxLength = 500;
728
- const truncatedContent = reasoningContent.length > maxLength
729
- ? reasoningContent.substring(0, maxLength) + '... (truncated)'
730
- : reasoningContent;
731
-
732
- console.log(`${indent}${colors.textDim(`${icons.brain} Thinking Process:`)}`);
733
- console.log('');
734
- console.log(`${indent}${colors.textDim(truncatedContent.replace(/^/gm, indent))}`);
735
- console.log(`${indent}${colors.textDim(`[${reasoningContent.length} chars total]`)}`);
736
- break;
737
-
738
- case 'indicator':
739
- // Show indicator only
740
- console.log(`${indent}${colors.textDim(`${icons.brain} Thinking process completed`)}`);
741
- console.log(`${indent}${colors.textDim(`[${reasoningContent.length} chars of reasoning]`)}`);
742
- break;
743
-
744
- default:
745
- console.log(`${indent}${colors.textDim(`${icons.brain} Thinking:`)}`);
746
- console.log('');
747
- console.log(`${indent}${colors.textDim(reasoningContent.replace(/^/gm, indent))}`);
748
- }
749
-
750
- console.log(`${indent}${colors.border(separator)}`);
751
- console.log('');
752
- }
753
-
754
- /**
755
- * Check and compress conversation context
756
- */
757
- private async checkAndCompressContext(lastUserMessage?: ChatMessage): Promise<void> {
758
- const compressionConfig = this.configManager.getContextCompressionConfig();
759
-
760
- if (!compressionConfig.enabled) {
761
- return;
762
- }
763
-
764
- const { needsCompression, reason } = this.contextCompressor.needsCompression(
765
- this.conversation,
766
- compressionConfig
767
- );
768
-
769
- if (needsCompression) {
770
- const indent = this.getIndent();
771
- console.log('');
772
- console.log(`${indent}${colors.warning(`${icons.brain} Context compression triggered: ${reason}`)}`);
773
-
774
- const toolRegistry = getToolRegistry();
775
- const baseSystemPrompt = this.currentAgent?.systemPrompt || 'You are a helpful AI assistant.';
776
- const systemPromptGenerator = new SystemPromptGenerator(toolRegistry, this.executionMode);
777
- const enhancedSystemPrompt = await systemPromptGenerator.generateEnhancedSystemPrompt(baseSystemPrompt);
778
-
779
- const result: CompressionResult = await this.contextCompressor.compressContext(
780
- this.conversation,
781
- enhancedSystemPrompt,
782
- compressionConfig
783
- );
784
-
785
- if (result.wasCompressed) {
786
- this.conversation = result.compressedMessages;
787
- // console.log(`${indent}${colors.success(`✓ Compressed ${result.originalMessageCount} messages to ${result.compressedMessageCount} messages`)}`);
788
- console.log(`${indent}${colors.textMuted(`✓ Size: ${result.originalSize} → ${result.compressedSize} chars (${Math.round((1 - result.compressedSize / result.originalSize) * 100)}% reduction)`)}`);
789
-
790
- // Display compressed summary content
791
- const summaryMessage = result.compressedMessages.find(m => m.role === 'assistant');
792
- if (summaryMessage && summaryMessage.content) {
793
- const maxPreviewLength = 800;
794
- let summaryContent = summaryMessage.content;
795
- const isTruncated = summaryContent.length > maxPreviewLength;
796
-
797
- if (isTruncated) {
798
- summaryContent = summaryContent.substring(0, maxPreviewLength) + '\n...';
799
- }
800
-
801
- console.log('');
802
- console.log(`${indent}${theme.predefinedStyles.title(`${icons.sparkles} Conversation Summary`)}`);
803
- const separator = icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length * 2);
804
- console.log(`${indent}${colors.border(separator)}`);
805
- const renderedSummary = renderMarkdown(summaryContent, (process.stdout.columns || 80) - indent.length * 4);
806
- console.log(`${indent}${theme.predefinedStyles.dim(renderedSummary).replace(/^/gm, indent)}`);
807
- if (isTruncated) {
808
- console.log(`${indent}${colors.textMuted(`(... ${summaryMessage.content.length - maxPreviewLength} more chars hidden)`)}`);
809
- }
810
- console.log(`${indent}${colors.border(separator)}`);
811
- }
812
-
813
- // Restore user messages after compression, ensuring user message exists for API calls
814
- if (lastUserMessage) {
815
- this.conversation.push(lastUserMessage);
816
- }
817
-
818
- // Sync compressed conversation history to slashCommandHandler
819
- this.slashCommandHandler.setConversationHistory(this.conversation);
820
- }
821
- }
822
- }
823
-
824
- private async executeShellCommand(command: string): Promise<void> {
825
- const indent = this.getIndent();
826
- console.log('');
827
- console.log(`${indent}${colors.textMuted(`${icons.code} Executing:`)}`);
828
- console.log(`${indent}${colors.codeText(` $ ${command}`)}`);
829
- console.log(`${indent}${colors.border(icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length))}`);
830
- console.log('');
831
-
832
- const toolRegistry = getToolRegistry();
833
-
834
- try {
835
- const result = await toolRegistry.execute('Bash', { command }, this.executionMode);
836
-
837
- if (result.stdout) {
838
- console.log(`${indent}${result.stdout.replace(/^/gm, indent)}`);
839
- }
840
-
841
- if (result.stderr) {
842
- console.log(`${indent}${colors.warning(result.stderr.replace(/^/gm, indent))}`);
843
- }
844
-
845
- const toolCall: ToolCall = {
846
- tool: 'Bash',
847
- params: { command },
848
- result,
849
- timestamp: Date.now()
850
- };
851
-
852
- this.toolCalls.push(toolCall);
853
-
854
- // Record command execution to session manager
855
- await this.sessionManager.addInput({
856
- type: 'command',
857
- content: command,
858
- rawInput: command,
859
- timestamp: Date.now()
860
- });
861
-
862
- await this.sessionManager.addOutput({
863
- role: 'tool',
864
- content: JSON.stringify(result),
865
- toolName: 'Bash',
866
- toolParams: { command },
867
- toolResult: result,
868
- timestamp: Date.now()
869
- });
870
- } catch (error: any) {
871
- console.log(`${indent}${colors.error(`Command execution failed: ${error.message}`)}`);
872
- }
873
- }
874
-
875
- /**
876
- * Create unified LLM Caller
877
- * Implement transparency: caller doesn't need to care about remote vs local mode
878
- */
879
- private createLLMCaller(taskId: string, status: 'begin' | 'continue') {
880
- // Remote mode: use RemoteAIClient
881
- if (this.remoteAIClient) {
882
- return this.createRemoteCaller(taskId, status);
883
- }
884
-
885
- // Local mode: use AIClient
886
- if (!this.aiClient) {
887
- throw new Error('AI client not initialized');
888
- }
889
- return this.createLocalCaller();
890
- }
891
-
892
- /**
893
- * Create remote mode LLM caller
894
- */
895
- private createRemoteCaller(taskId: string, status: 'begin' | 'continue') {
896
- const client = this.remoteAIClient!;
897
- return {
898
- chatCompletion: (messages: ChatMessage[], options: any) =>
899
- client.chatCompletion(messages, { ...options, taskId, status }),
900
- isRemote: true
901
- };
902
- }
903
-
904
- /**
905
- * Create local mode LLM caller
906
- */
907
- private createLocalCaller() {
908
- const client = this.aiClient!;
909
- return {
910
- chatCompletion: (messages: ChatMessage[], options: any) =>
911
- client.chatCompletion(messages as any, options),
912
- isRemote: false
913
- };
914
- }
915
-
916
- private async generateResponse(thinkingTokens: number = 0): Promise<void> {
917
- // Create taskId for this user interaction (for remote mode tracking)
918
- const taskId = crypto.randomUUID();
919
- this.currentTaskId = taskId;
920
- this.isFirstApiCall = true;
921
-
922
- // Determine status based on whether this is the first API call
923
- const status: 'begin' | 'continue' = this.isFirstApiCall ? 'begin' : 'continue';
924
-
925
- // Use unified LLM Caller with taskId (automatically selects local or remote mode)
926
- const { chatCompletion, isRemote } = this.createLLMCaller(taskId, status);
927
-
928
- if (!isRemote && !this.aiClient) {
929
- console.log(colors.error('AI client not initialized'));
930
- return;
931
- }
932
-
933
- // Mark that an operation is in progress
934
- (this as any)._isOperationInProgress = true;
935
-
936
- const indent = this.getIndent();
937
- const thinkingText = colors.textMuted(`Thinking... (Press ESC to cancel)`);
938
- const icon = colors.primary(icons.brain);
939
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
940
- let frameIndex = 0;
941
-
942
- // Custom spinner: only icon rotates, text stays static
943
- const spinnerInterval = setInterval(() => {
944
- process.stdout.write(`\r${colors.primary(frames[frameIndex])} ${icon} ${thinkingText}`);
945
- frameIndex = (frameIndex + 1) % frames.length;
946
- }, 120);
947
-
948
- try {
949
- const memory = await this.memoryManager.loadMemory();
950
- const toolRegistry = getToolRegistry();
951
- const allowedToolNames = this.currentAgent
952
- ? this.agentManager.getAvailableToolsForAgent(this.currentAgent, this.executionMode)
953
- : [];
954
-
955
- // MCP servers are already connected during initialization (eager mode)
956
- // MCP tools are already registered as local tools via registerMCPTools
957
- const toolDefinitions = toolRegistry.getToolDefinitions();
958
-
959
- // Available tools for this session
960
- const availableTools = this.executionMode !== ExecutionMode.DEFAULT && allowedToolNames.length > 0
961
- ? toolDefinitions.filter((tool: any) => allowedToolNames.includes(tool.function.name))
962
- : toolDefinitions;
963
-
964
- const baseSystemPrompt = this.currentAgent?.systemPrompt;
965
- const systemPromptGenerator = new SystemPromptGenerator(toolRegistry, this.executionMode, undefined, this.mcpManager);
966
- const enhancedSystemPrompt = await systemPromptGenerator.generateEnhancedSystemPrompt(baseSystemPrompt);
967
-
968
- const messages: ChatMessage[] = [
969
- { role: 'system', content: `${enhancedSystemPrompt}\n\n${memory}`, timestamp: Date.now() },
970
- ...this.conversation
971
- ];
972
-
973
- const operationId = `ai-response-${Date.now()}`;
974
- const response = await this.cancellationManager.withCancellation(
975
- chatCompletion(messages, {
976
- tools: availableTools,
977
- toolChoice: availableTools.length > 0 ? 'auto' : 'none',
978
- thinkingTokens
979
- }),
980
- operationId
981
- );
982
-
983
- // Mark that first API call is complete
984
- this.isFirstApiCall = false;
985
-
986
- clearInterval(spinnerInterval);
987
- process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r'); // Clear spinner line
988
-
989
- const assistantMessage = response.choices[0].message;
990
-
991
- const content = typeof assistantMessage.content === 'string'
992
- ? assistantMessage.content
993
- : '';
994
- const reasoningContent = assistantMessage.reasoning_content || '';
995
- // Display reasoning content if available and thinking mode is enabled
996
- if (reasoningContent && this.configManager.getThinkingConfig().enabled) {
997
- this.displayThinkingContent(reasoningContent);
998
- }
999
-
1000
- console.log('');
1001
- console.log(`${indent}${colors.primaryBright(`${icons.robot} Assistant:`)}`);
1002
- console.log(`${indent}${colors.border(icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length))}`);
1003
- console.log('');
1004
- const renderedContent = renderMarkdown(content, (process.stdout.columns || 80) - indent.length * 2);
1005
- console.log(`${indent}${renderedContent.replace(/^/gm, indent)}`);
1006
- console.log('');
1007
-
1008
- this.conversation.push({
1009
- role: 'assistant',
1010
- content,
1011
- timestamp: Date.now(),
1012
- reasoningContent,
1013
- toolCalls: assistantMessage.tool_calls
1014
- });
1015
-
1016
- // Record output to session manager
1017
- await this.sessionManager.addOutput({
1018
- role: 'assistant',
1019
- content,
1020
- timestamp: Date.now(),
1021
- reasoningContent,
1022
- toolCalls: assistantMessage.tool_calls
1023
- });
1024
-
1025
- if (assistantMessage.tool_calls) {
1026
- await this.handleToolCalls(assistantMessage.tool_calls);
1027
- }
1028
-
1029
- if (this.checkpointManager.isEnabled()) {
1030
- await this.checkpointManager.createCheckpoint(
1031
- `Response generated at ${new Date().toLocaleString()}`,
1032
- [...this.conversation],
1033
- [...this.toolCalls]
1034
- );
1035
- }
1036
-
1037
- // Operation completed successfully, clear the flag
1038
- (this as any)._isOperationInProgress = false;
1039
- } catch (error: any) {
1040
- clearInterval(spinnerInterval);
1041
- process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
1042
-
1043
- // Clear the operation flag
1044
- (this as any)._isOperationInProgress = false;
1045
-
1046
- if (error.message === 'Operation cancelled by user') {
1047
- // Mark task as cancelled
1048
- if (this.remoteAIClient && this.currentTaskId) {
1049
- await this.remoteAIClient.cancelTask(this.currentTaskId);
1050
- }
1051
- return;
1052
- }
1053
-
1054
- // Mark task as cancelled when error occurs (发送 status: 'cancel')
1055
- logger.debug(`[Session] Task failed: taskId=${this.currentTaskId}, error: ${error.message}`);
1056
- if (this.remoteAIClient && this.currentTaskId) {
1057
- await this.remoteAIClient.cancelTask(this.currentTaskId);
1058
- }
1059
-
1060
- console.log(colors.error(`Error: ${error.message}`));
1061
- }
1062
- }
1063
-
1064
- /**
1065
- * Generate response using remote AI service(OAuth XAGENT 模式)
1066
- * Support full tool calling loop
1067
- * 与本地模式 generateResponse 保持一致
1068
- * @param thinkingTokens - Optional thinking tokens config
1069
- * @param existingTaskId - Optional existing taskId to reuse (for tool call continuation)
1070
- */
1071
- private async generateRemoteResponse(thinkingTokens: number = 0, existingTaskId?: string): Promise<void> {
1072
- // Reuse existing taskId or create new one for this user interaction
1073
- const taskId = existingTaskId || crypto.randomUUID();
1074
- this.currentTaskId = taskId;
1075
- logger.debug(`[Session] generateRemoteResponse: taskId=${taskId}, existingTaskId=${!!existingTaskId}`);
1076
-
1077
- // Reset isFirstApiCall for new task, keep true for continuation
1078
- if (!existingTaskId) {
1079
- this.isFirstApiCall = true;
1080
- }
1081
-
1082
- // Determine status based on whether this is the first API call
1083
- const status: 'begin' | 'continue' = this.isFirstApiCall ? 'begin' : 'continue';
1084
- logger.debug(`[Session] Status for this call: ${status}, isFirstApiCall=${this.isFirstApiCall}`);
1085
-
1086
- // 使用统一的 LLM Caller
1087
- const { chatCompletion, isRemote } = this.createLLMCaller(taskId, status);
1088
-
1089
- if (!isRemote) {
1090
- // 如果不是远程模式,回退到本地模式
1091
- return this.generateResponse(thinkingTokens);
1092
- }
1093
-
1094
- const indent = this.getIndent();
1095
- const thinkingText = colors.textMuted(`Thinking... (Press ESC to cancel)`);
1096
- const icon = colors.primary(icons.brain);
1097
- const frames = ['', '⠙', '', '⠸', '⠼', '⠴', '', '⠧', '', '⠏'];
1098
- let frameIndex = 0;
1099
-
1100
- // Mark that an operation is in progress
1101
- (this as any)._isOperationInProgress = true;
1102
-
1103
- // Custom spinner: only icon rotates, text stays static
1104
- const spinnerInterval = setInterval(() => {
1105
- process.stdout.write(`\r${colors.primary(frames[frameIndex])} ${icon} ${thinkingText}`);
1106
- frameIndex = (frameIndex + 1) % frames.length;
1107
- }, 120);
1108
-
1109
- try {
1110
- // Load memory (与本地模式一致)
1111
- const memory = await this.memoryManager.loadMemory();
1112
-
1113
- // Get tool definitions
1114
- const toolRegistry = getToolRegistry();
1115
- const allowedToolNames = this.currentAgent
1116
- ? this.agentManager.getAvailableToolsForAgent(this.currentAgent, this.executionMode)
1117
- : [];
1118
-
1119
- const allToolDefinitions = toolRegistry.getToolDefinitions();
1120
-
1121
- const availableTools = this.executionMode !== ExecutionMode.DEFAULT && allowedToolNames.length > 0
1122
- ? allToolDefinitions.filter((tool: any) => allowedToolNames.includes(tool.function.name))
1123
- : allToolDefinitions;
1124
-
1125
- // Convert to the format expected by backend (与本地模式一致使用 availableTools)
1126
- const tools = availableTools.map((tool: any) => ({
1127
- type: 'function' as const,
1128
- function: {
1129
- name: tool.function.name,
1130
- description: tool.function.description || '',
1131
- parameters: tool.function.parameters || {
1132
- type: 'object' as const,
1133
- properties: {}
1134
- }
1135
- }
1136
- }));
1137
-
1138
- // Generate system prompt (与本地模式一致)
1139
- const baseSystemPrompt = this.currentAgent?.systemPrompt || 'You are a helpful AI assistant.';
1140
- const systemPromptGenerator = new SystemPromptGenerator(toolRegistry, this.executionMode);
1141
- const enhancedSystemPrompt = await systemPromptGenerator.generateEnhancedSystemPrompt(baseSystemPrompt);
1142
-
1143
- // Build messages with system prompt (与本地模式一致)
1144
- const messages: ChatMessage[] = [
1145
- { role: 'system', content: `${enhancedSystemPrompt}\n\n${memory}`, timestamp: Date.now() },
1146
- ...this.conversation
1147
- ];
1148
-
1149
- // Call unified LLM API with cancellation support
1150
- const operationId = `remote-ai-response-${Date.now()}`;
1151
- const response = await this.cancellationManager.withCancellation(
1152
- chatCompletion(messages, {
1153
- tools,
1154
- toolChoice: tools.length > 0 ? 'auto' : 'none',
1155
- thinkingTokens
1156
- }),
1157
- operationId
1158
- );
1159
-
1160
- // Mark that first API call is complete
1161
- this.isFirstApiCall = false;
1162
-
1163
- clearInterval(spinnerInterval);
1164
- process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
1165
- console.log('');
1166
-
1167
- // 使用统一的响应格式(与本地模式一致)
1168
- const assistantMessage = response.choices[0].message;
1169
- const content = typeof assistantMessage.content === 'string'
1170
- ? assistantMessage.content
1171
- : '';
1172
- const reasoningContent = assistantMessage.reasoning_content || '';
1173
- const toolCalls = assistantMessage.tool_calls || [];
1174
-
1175
- // Display reasoning content if available and thinking mode is enabled (与本地模式一致)
1176
- if (reasoningContent && this.configManager.getThinkingConfig().enabled) {
1177
- this.displayThinkingContent(reasoningContent);
1178
- }
1179
-
1180
- console.log(`${indent}${colors.primaryBright(`${icons.robot} Assistant:`)}`);
1181
- console.log(`${indent}${colors.border(icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length))}`);
1182
- console.log('');
1183
- const renderedContent = renderMarkdown(content, (process.stdout.columns || 80) - indent.length * 2);
1184
- console.log(`${indent}${renderedContent.replace(/^/gm, indent)}`);
1185
- console.log('');
1186
-
1187
- // Add assistant message to conversation (consistent with local mode, including reasoningContent)
1188
- this.conversation.push({
1189
- role: 'assistant',
1190
- content,
1191
- timestamp: Date.now(),
1192
- reasoningContent,
1193
- toolCalls: toolCalls
1194
- });
1195
-
1196
- // Record output to session manager (consistent with local mode, including reasoningContent and toolCalls)
1197
- await this.sessionManager.addOutput({
1198
- role: 'assistant',
1199
- content,
1200
- timestamp: Date.now(),
1201
- reasoningContent,
1202
- toolCalls
1203
- });
1204
-
1205
- // Handle tool calls
1206
- if (toolCalls.length > 0) {
1207
- await this.handleRemoteToolCalls(toolCalls);
1208
- }
1209
-
1210
- // Checkpoint support (consistent with local mode)
1211
- if (this.checkpointManager.isEnabled()) {
1212
- await this.checkpointManager.createCheckpoint(
1213
- `Response generated at ${new Date().toLocaleString()}`,
1214
- [...this.conversation],
1215
- [...this.toolCalls]
1216
- );
1217
- }
1218
-
1219
- // Operation completed successfully
1220
- (this as any)._isOperationInProgress = false;
1221
-
1222
- // Mark task as completed (发送 status: 'end')
1223
- logger.debug(`[Session] Task completed: taskId=${this.currentTaskId}`);
1224
- if (this.remoteAIClient && this.currentTaskId) {
1225
- await this.remoteAIClient.completeTask(this.currentTaskId);
1226
- }
1227
-
1228
- } catch (error: any) {
1229
- clearInterval(spinnerInterval);
1230
- process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
1231
-
1232
- // Clear the operation flag
1233
- (this as any)._isOperationInProgress = false;
1234
-
1235
- if (error.message === 'Operation cancelled by user') {
1236
- return;
1237
- }
1238
-
1239
- // Handle token invalid error - trigger re-authentication
1240
- if (error instanceof TokenInvalidError) {
1241
- console.log('');
1242
- console.log(colors.warning('⚠️ Authentication expired or invalid'));
1243
- console.log(colors.info('Your browser session has been logged out. Please log in again.'));
1244
- console.log('');
1245
-
1246
- // Clear invalid credentials and persist
1247
- // Note: Do NOT overwrite selectedAuthType - preserve user's chosen auth method
1248
- await this.configManager.set('apiKey', '');
1249
- await this.configManager.set('refreshToken', '');
1250
- await this.configManager.save('global');
1251
-
1252
- logger.debug('[DEBUG generateRemoteResponse] Cleared invalid credentials, starting re-authentication...');
1253
-
1254
- // Re-authenticate
1255
- await this.setupAuthentication();
1256
-
1257
- // Reload config to ensure we have the latest authConfig
1258
- logger.debug('[DEBUG generateRemoteResponse] Re-authentication completed, reloading config...');
1259
- await this.configManager.load();
1260
- const authConfig = this.configManager.getAuthConfig();
1261
-
1262
- logger.debug('[DEBUG generateRemoteResponse] After re-auth:');
1263
- logger.debug(' - authConfig.apiKey exists:', !!authConfig.apiKey ? 'true' : 'false');
1264
- logger.debug(' - authConfig.apiKey prefix:', authConfig.apiKey ? authConfig.apiKey.substring(0, 20) + '...' : 'empty');
1265
-
1266
- // Recreate readline interface after inquirer
1267
- this.rl.close();
1268
- this.rl = readline.createInterface({
1269
- input: process.stdin,
1270
- output: process.stdout
1271
- });
1272
- this.rl.on('close', () => {
1273
- logger.debug('DEBUG: readline interface closed');
1274
- });
1275
-
1276
- // Reinitialize RemoteAIClient with new token
1277
- if (authConfig.apiKey) {
1278
- const webBaseUrl = authConfig.xagentApiBaseUrl || 'https://154.8.140.52:443';
1279
- logger.debug('[DEBUG generateRemoteResponse] Reinitializing RemoteAIClient with new token');
1280
- const newWebBaseUrl = authConfig.xagentApiBaseUrl || 'https://154.8.140.52:443';
1281
- this.remoteAIClient = new RemoteAIClient(authConfig.apiKey, newWebBaseUrl, authConfig.showAIDebugInfo);
1282
- } else {
1283
- logger.debug('[DEBUG generateRemoteResponse] WARNING: No apiKey after re-authentication!');
1284
- }
1285
-
1286
- // Retry the current operation
1287
- console.log('');
1288
- console.log(colors.info('Retrying with new authentication...'));
1289
- console.log('');
1290
- return this.generateRemoteResponse(thinkingTokens);
1291
- }
1292
-
1293
- // Mark task as cancelled when error occurs (发送 status: 'cancel')
1294
- logger.debug(`[Session] Task failed: taskId=${this.currentTaskId}, error: ${error.message}`);
1295
- if (this.remoteAIClient && this.currentTaskId) {
1296
- await this.remoteAIClient.cancelTask(this.currentTaskId);
1297
- }
1298
-
1299
- console.log(colors.error(`Error: ${error.message}`));
1300
- return;
1301
- }
1302
- }
1303
-
1304
- private async handleToolCalls(toolCalls: any[]): Promise<void> {
1305
- // Mark that tool execution is in progress
1306
- (this as any)._isOperationInProgress = true;
1307
-
1308
- const toolRegistry = getToolRegistry();
1309
- const showToolDetails = this.configManager.get('showToolDetails') || false;
1310
- const indent = this.getIndent();
1311
-
1312
- // Prepare all tool calls
1313
- const preparedToolCalls = toolCalls.map((toolCall, index) => {
1314
- const { name, arguments: params } = toolCall.function;
1315
-
1316
- let parsedParams: any;
1317
- try {
1318
- parsedParams = typeof params === 'string' ? JSON.parse(params) : params;
1319
- } catch (e) {
1320
- parsedParams = params;
1321
- }
1322
-
1323
- return { name, params: parsedParams, index, id: toolCall.id };
1324
- });
1325
-
1326
- // Display all tool calls info
1327
- for (const { name, params } of preparedToolCalls) {
1328
- if (showToolDetails) {
1329
- console.log('');
1330
- console.log(`${indent}${colors.warning(`${icons.tool} Tool Call: ${name}`)}`);
1331
- console.log(`${indent}${colors.textDim(JSON.stringify(params, null, 2))}`);
1332
- } else {
1333
- const toolDescription = this.getToolDescription(name, params);
1334
- console.log('');
1335
- console.log(`${indent}${colors.textMuted(`${icons.loading} ${toolDescription}`)}`);
1336
- }
1337
- }
1338
-
1339
- // Execute all tools in parallel
1340
- const results = await toolRegistry.executeAll(
1341
- preparedToolCalls.map(tc => ({ name: tc.name, params: tc.params })),
1342
- this.executionMode
1343
- );
1344
-
1345
- // Process results and maintain order
1346
- for (const { tool, result, error } of results) {
1347
- const toolCall = preparedToolCalls.find(tc => tc.name === tool);
1348
- if (!toolCall) continue;
1349
-
1350
- const { params } = toolCall;
1351
-
1352
- if (error) {
1353
- // Clear the operation flag
1354
- (this as any)._isOperationInProgress = false;
1355
-
1356
- if (error === 'Operation cancelled by user') {
1357
- return;
1358
- }
1359
-
1360
- console.log('');
1361
- console.log(`${indent}${colors.error(`${icons.cross} Tool Error: ${error}`)}`);
1362
-
1363
- this.conversation.push({
1364
- role: 'tool',
1365
- content: JSON.stringify({ error }),
1366
- tool_call_id: toolCall.id,
1367
- timestamp: Date.now()
1368
- });
1369
- } else {
1370
- // Use correct indent for gui-subagent tasks
1371
- const isGuiSubagent = tool === 'task' && params?.subagent_type === 'gui-subagent';
1372
- const displayIndent = isGuiSubagent ? indent + ' ' : indent;
1373
-
1374
- // Always show details for todo tools so users can see their task lists
1375
- const isTodoTool = tool === 'todo_write' || tool === 'todo_read';
1376
- if (isTodoTool) {
1377
- console.log('');
1378
- console.log(`${displayIndent}${colors.success(`${icons.check} Todo List:`)}`);
1379
- console.log(this.renderTodoList(result?.todos || [], displayIndent));
1380
- // Show summary if available
1381
- if (result?.message) {
1382
- console.log(`${displayIndent}${colors.textDim(result.message)}`);
1383
- }
1384
- } else if (showToolDetails) {
1385
- console.log('');
1386
- console.log(`${displayIndent}${colors.success(`${icons.check} Tool Result:`)}`);
1387
- console.log(`${displayIndent}${colors.textDim(JSON.stringify(result, null, 2))}`);
1388
- } else if (result && result.success === false) {
1389
- // GUI task or other tool failed
1390
- console.log(`${displayIndent}${colors.error(`${icons.cross} ${result.message || 'Failed'}`)}`);
1391
- } else if (result) {
1392
- console.log(`${displayIndent}${colors.success(`${icons.check} Completed`)}`);
1393
- } else {
1394
- console.log(`${displayIndent}${colors.textDim('(no result)')}`);
1395
- }
1396
-
1397
- const toolCallRecord: ToolCall = {
1398
- tool,
1399
- params,
1400
- result,
1401
- timestamp: Date.now()
1402
- };
1403
-
1404
- this.toolCalls.push(toolCallRecord);
1405
-
1406
- // Record tool output to session manager
1407
- await this.sessionManager.addOutput({
1408
- role: 'tool',
1409
- content: JSON.stringify(result),
1410
- toolName: tool,
1411
- toolParams: params,
1412
- toolResult: result,
1413
- timestamp: Date.now()
1414
- });
1415
-
1416
- this.conversation.push({
1417
- role: 'tool',
1418
- content: JSON.stringify(result),
1419
- tool_call_id: toolCall.id,
1420
- timestamp: Date.now()
1421
- });
1422
- }
1423
- }
1424
-
1425
- // Logic: Only skip returning results to main agent when user explicitly cancelled (ESC)
1426
- // For all other cases (success, failure, errors), always return results for further processing
1427
- const guiSubagentFailed = preparedToolCalls.some(tc => tc.name === 'task' && tc.params?.subagent_type === 'gui-subagent');
1428
- const guiSubagentCancelled = preparedToolCalls.some(tc => tc.name === 'task' && tc.params?.subagent_type === 'gui-subagent' && results.some(r => r.tool === 'task' && (r.result as any)?.cancelled === true));
1429
-
1430
- // If GUI agent was cancelled by user, don't continue generating response
1431
- // This avoids wasting API calls and tokens on cancelled tasks
1432
- if (guiSubagentCancelled) {
1433
- console.log('');
1434
- console.log(`${indent}${colors.textMuted('GUI task cancelled by user')}`);
1435
- (this as any)._isOperationInProgress = false;
1436
- return;
1437
- }
1438
-
1439
- // For all other cases (GUI success/failure, other tool errors), return results to main agent
1440
- // This allows main agent to decide how to handle failures (retry, fallback, user notification, etc.)
1441
- await this.generateResponse();
1442
- }
1443
-
1444
- /**
1445
- * Get user-friendly description for tool
1446
- */
1447
- private getToolDescription(toolName: string, params: any): string {
1448
- const descriptions: Record<string, (params: any) => string> = {
1449
- 'Read': (p) => `Read file: ${this.truncatePath(p.filePath)}`,
1450
- 'Write': (p) => `Write file: ${this.truncatePath(p.filePath)}`,
1451
- 'Grep': (p) => `Search text: "${p.pattern}"`,
1452
- 'Bash': (p) => `Execute command: ${this.truncateCommand(p.command)}`,
1453
- 'ListDirectory': (p) => `List directory: ${this.truncatePath(p.path || '.')}`,
1454
- 'SearchCodebase': (p) => `Search files: ${p.pattern}`,
1455
- 'DeleteFile': (p) => `Delete file: ${this.truncatePath(p.filePath)}`,
1456
- 'CreateDirectory': (p) => `Create directory: ${this.truncatePath(p.dirPath)}`,
1457
- 'replace': (p) => `Replace text: ${this.truncatePath(p.file_path)}`,
1458
- 'web_search': (p) => `Web search: "${p.query}"`,
1459
- 'todo_write': () => `Update todo list`,
1460
- 'todo_read': () => `Read todo list`,
1461
- 'task': (p) => `Launch subtask: ${p.description}`,
1462
- 'ReadBashOutput': (p) => `Read task output: ${p.task_id}`,
1463
- 'web_fetch': () => `Fetch web content`,
1464
- 'ask_user_question': () => `Ask user`,
1465
- 'save_memory': () => `Save memory`,
1466
- 'exit_plan_mode': () => `Complete plan`,
1467
- 'xml_escape': (p) => `XML escape: ${this.truncatePath(p.file_path)}`,
1468
- 'image_read': (p) => `Read image: ${this.truncatePath(p.image_input)}`,
1469
- // 'Skill': (p) => `Execute skill: ${p.skill}`,
1470
- // 'ListSkills': () => `List available skills`,
1471
- // 'GetSkillDetails': (p) => `Get skill details: ${p.skill}`,
1472
- 'InvokeSkill': (p) => `Invoke skill: ${p.skillId} - ${this.truncatePath(p.taskDescription || '', 40)}`
1473
- };
1474
-
1475
- const getDescription = descriptions[toolName];
1476
- return getDescription ? getDescription(params) : `Execute tool: ${toolName}`;
1477
- }
1478
-
1479
- /**
1480
- * Handle tool calls for remote AI mode
1481
- * Executes tools and then continues the conversation with results
1482
- */
1483
- private async handleRemoteToolCalls(toolCalls: any[]): Promise<void> {
1484
- // Mark that tool execution is in progress
1485
- (this as any)._isOperationInProgress = true;
1486
-
1487
- const toolRegistry = getToolRegistry();
1488
- const showToolDetails = this.configManager.get('showToolDetails') || false;
1489
- const indent = this.getIndent();
1490
-
1491
- // Prepare all tool calls (include id for tool result matching)
1492
- const preparedToolCalls = toolCalls.map((toolCall, index) => {
1493
- const { name, arguments: params } = toolCall.function;
1494
-
1495
- let parsedParams: any;
1496
- try {
1497
- parsedParams = typeof params === 'string' ? JSON.parse(params) : params;
1498
- } catch (e) {
1499
- parsedParams = params;
1500
- }
1501
-
1502
- return { name, params: parsedParams, index, id: toolCall.id };
1503
- });
1504
-
1505
- // Display all tool calls info
1506
- for (const { name, params } of preparedToolCalls) {
1507
- if (showToolDetails) {
1508
- console.log('');
1509
- console.log(`${indent}${colors.warning(`${icons.tool} Tool Call: ${name}`)}`);
1510
- console.log(`${indent}${colors.textDim(JSON.stringify(params, null, 2))}`);
1511
- } else {
1512
- const toolDescription = this.getToolDescription(name, params);
1513
- console.log('');
1514
- console.log(`${indent}${colors.textMuted(`${icons.loading} ${toolDescription}`)}`);
1515
- }
1516
- }
1517
-
1518
- // Execute all tools in parallel
1519
- const results = await toolRegistry.executeAll(
1520
- preparedToolCalls.map(tc => ({ name: tc.name, params: tc.params })),
1521
- this.executionMode
1522
- );
1523
-
1524
- // Process results and maintain order
1525
- let hasError = false;
1526
- for (const { tool, result, error } of results) {
1527
- const toolCall = preparedToolCalls.find(tc => tc.name === tool);
1528
- if (!toolCall) continue;
1529
-
1530
- const { params } = toolCall;
1531
-
1532
- if (error) {
1533
- // Clear the operation flag
1534
- (this as any)._isOperationInProgress = false;
1535
-
1536
- if (error === 'Operation cancelled by user') {
1537
- return;
1538
- }
1539
-
1540
- hasError = true;
1541
-
1542
- console.log('');
1543
- console.log(`${indent}${colors.error(`${icons.cross} Tool Error: ${error}`)}`);
1544
-
1545
- this.conversation.push({
1546
- role: 'tool',
1547
- content: JSON.stringify({ error }),
1548
- tool_call_id: toolCall.id,
1549
- timestamp: Date.now()
1550
- });
1551
- } else {
1552
- // Use correct indent for gui-subagent tasks
1553
- const isGuiSubagent = tool === 'task' && params?.subagent_type === 'gui-subagent';
1554
- const displayIndent = isGuiSubagent ? indent + ' ' : indent;
1555
-
1556
- // Always show details for todo tools so users can see their task lists
1557
- const isTodoTool = tool === 'todo_write' || tool === 'todo_read';
1558
- if (isTodoTool) {
1559
- console.log('');
1560
- console.log(`${displayIndent}${colors.success(`${icons.check} Todo List:`)}`);
1561
- console.log(this.renderTodoList(result.todos || result.todos, displayIndent));
1562
- // Show summary if available
1563
- if (result.message) {
1564
- console.log(`${displayIndent}${colors.textDim(result.message)}`);
1565
- }
1566
- } else if (showToolDetails) {
1567
- console.log('');
1568
- console.log(`${displayIndent}${colors.success(`${icons.check} Tool Result:`)}`);
1569
- console.log(`${displayIndent}${colors.textDim(JSON.stringify(result, null, 2))}`);
1570
- } else if (result.success === false) {
1571
- // GUI task or other tool failed
1572
- console.log(`${displayIndent}${colors.error(`${icons.cross} ${result.message || 'Failed'}`)}`);
1573
- } else {
1574
- console.log(`${displayIndent}${colors.success(`${icons.check} Completed`)}`);
1575
- }
1576
-
1577
- const toolCallRecord: ToolCall = {
1578
- tool,
1579
- params,
1580
- result,
1581
- timestamp: Date.now()
1582
- };
1583
-
1584
- this.toolCalls.push(toolCallRecord);
1585
-
1586
- // Record tool output to session manager
1587
- await this.sessionManager.addOutput({
1588
- role: 'tool',
1589
- content: JSON.stringify(result),
1590
- toolName: tool,
1591
- toolParams: params,
1592
- toolResult: result,
1593
- timestamp: Date.now()
1594
- });
1595
-
1596
- this.conversation.push({
1597
- role: 'tool',
1598
- content: JSON.stringify(result),
1599
- tool_call_id: toolCall.id,
1600
- timestamp: Date.now()
1601
- });
1602
- }
1603
- }
1604
-
1605
- // Logic: Only skip returning results to main agent when user explicitly cancelled (ESC)
1606
- // For all other cases (success, failure, errors), always return results for further processing
1607
- const guiSubagentFailed = preparedToolCalls.some(tc => tc.name === 'task' && tc.params?.subagent_type === 'gui-subagent');
1608
- const guiSubagentCancelled = preparedToolCalls.some(tc => tc.name === 'task' && tc.params?.subagent_type === 'gui-subagent' && results.some(r => r.tool === 'task' && (r.result as any)?.cancelled === true));
1609
-
1610
- // If GUI agent was cancelled by user, don't continue generating response
1611
- // This avoids wasting API calls and tokens on cancelled tasks
1612
- if (guiSubagentCancelled) {
1613
- console.log('');
1614
- console.log(`${indent}${colors.textMuted('GUI task cancelled by user')}`);
1615
- (this as any)._isOperationInProgress = false;
1616
- return;
1617
- }
1618
-
1619
- // If any tool call failed, throw error to mark task as cancelled
1620
- if (hasError) {
1621
- throw new Error('Tool execution failed');
1622
- }
1623
-
1624
- // For all other cases (GUI success/failure, other tool errors), return results to main agent
1625
- // This allows main agent to decide how to handle failures (retry, fallback, user notification, etc.)
1626
- // Reuse existing taskId instead of generating new one
1627
- await this.generateRemoteResponse(0, this.currentTaskId || undefined);
1628
- }
1629
-
1630
- /**
1631
- * Truncate path for display
1632
- */
1633
- private truncatePath(path: string, maxLength: number = 30): string {
1634
- if (!path) return '';
1635
- if (path.length <= maxLength) return path;
1636
- return '...' + path.slice(-(maxLength - 3));
1637
- }
1638
-
1639
- /**
1640
- * Truncate command for display
1641
- */
1642
- private truncateCommand(command: string, maxLength: number = 40): string {
1643
- if (!command) return '';
1644
- if (command.length <= maxLength) return command;
1645
- return command.slice(0, maxLength - 3) + '...';
1646
- }
1647
-
1648
- /**
1649
- * Render todo list in a user-friendly format
1650
- */
1651
- private renderTodoList(todos: any[], indent: string = ''): string {
1652
- if (!todos || todos.length === 0) {
1653
- return `${indent}${colors.textMuted('No tasks')}`;
1654
- }
1655
-
1656
- const statusConfig: Record<string, { icon: string; color: (text: string) => string; label: string }> = {
1657
- 'pending': { icon: icons.circle, color: colors.textMuted, label: 'Pending' },
1658
- 'in_progress': { icon: icons.loading, color: colors.warning, label: 'In Progress' },
1659
- 'completed': { icon: icons.success, color: colors.success, label: 'Completed' },
1660
- 'failed': { icon: icons.error, color: colors.error, label: 'Failed' }
1661
- };
1662
-
1663
- const lines: string[] = [];
1664
-
1665
- for (const todo of todos) {
1666
- const config = statusConfig[todo.status] || statusConfig['pending'];
1667
- const statusPrefix = `${config.color(config.icon)} ${config.color(config.label)}:`;
1668
- lines.push(`${indent} ${statusPrefix} ${colors.text(todo.task)}`);
1669
- }
1670
-
1671
- return lines.join('\n');
1672
- }
1673
-
1674
- /**
1675
- * Display AI debug information (input or output)
1676
- */
1677
- // AI debug info moved to ai-client.ts implementation
1678
- // private displayAIDebugInfo(type: 'INPUT' | 'OUTPUT', data: any, extra?: any): void {
1679
- // const indent = this.getIndent();
1680
- // const boxChar = {
1681
- // topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
1682
- // horizontal: '═', vertical: '║'
1683
- // };
1684
- //
1685
- // console.log('\n' + colors.border(
1686
- // `${boxChar.topLeft}${boxChar.horizontal.repeat(58)}${boxChar.topRight}`
1687
- // ));
1688
- // console.log(colors.border(`${boxChar.vertical}`) + ' ' +
1689
- // colors.primaryBright(type === 'INPUT' ? '🤖 AI INPUT DEBUG' : '📤 AI OUTPUT DEBUG') +
1690
- // ' '.repeat(36) + colors.border(boxChar.vertical));
1691
- // console.log(colors.border(
1692
- // `${boxChar.vertical}${boxChar.horizontal.repeat(58)}${boxChar.vertical}`
1693
- // ));
1694
- //
1695
- // if (type === 'INPUT') {
1696
- // const messages = data as any[];
1697
- // const tools = extra as any[];
1698
- //
1699
- // // System prompt
1700
- // const systemMsg = messages.find((m: any) => m.role === 'system');
1701
- // console.log(colors.border(`${boxChar.vertical}`) + ' 🟫 SYSTEM: ' +
1702
- // colors.textMuted(systemMsg?.content?.toString().substring(0, 50) || '(none)') + ' '.repeat(3) + colors.border(boxChar.vertical));
1703
- //
1704
- // // Messages count
1705
- // console.log(colors.border(`${boxChar.vertical}`) + ' 💬 MESSAGES: ' +
1706
- // colors.text(messages.length.toString()) + ' items' + ' '.repeat(40) + colors.border(boxChar.vertical));
1707
- //
1708
- // // Tools count
1709
- // console.log(colors.border(`${boxChar.vertical}`) + ' 🔧 TOOLS: ' +
1710
- // colors.text((tools?.length || 0).toString()) + '' + ' '.repeat(43) + colors.border(boxChar.vertical)); //
1711
- // // Show last 2 messages
1712
- // const recentMessages = messages.slice(-2);
1713
- // for (const msg of recentMessages) {
1714
- // const roleLabel: Record<string, string> = { user: '👤 USER', assistant: '🤖 ASSISTANT', tool: '🔧 TOOL' };
1715
- // const label = roleLabel[msg.role] || msg.role;
1716
- // const contentStr = typeof msg.content === 'string'
1717
- // ? msg.content.substring(0, 100)
1718
- // : JSON.stringify(msg.content).substring(0, 100);
1719
- // console.log(colors.border(`${boxChar.vertical}`) + ` ${label}: ` +
1720
- // colors.textDim(contentStr + '...') + ' '.repeat(Math.max(0, 50 - contentStr.length)) + colors.border(boxChar.vertical));
1721
- // }
1722
- // } else {
1723
- // // OUTPUT
1724
- // const response = data;
1725
- // const message = extra;
1726
- //
1727
- // console.log(colors.border(`${boxChar.vertical}`) + ' 📋 MODEL: ' +
1728
- // colors.text(response.model || 'unknown') + ' '.repeat(45) + colors.border(boxChar.vertical));
1729
- //
1730
- // console.log(colors.border(`${boxChar.vertical}`) + ' ⏱️ TOKENS: ' +
1731
- // colors.text(`Prompt: ${response.usage?.prompt_tokens || '?'}, Completion: ${response.usage?.completion_tokens || '?'}`) +
1732
- // ' '.repeat(15) + colors.border(boxChar.vertical));
1733
- //
1734
- // console.log(colors.border(`${boxChar.vertical}`) + ' 🔧 TOOL_CALLS: ' +
1735
- // colors.text((message.tool_calls?.length || 0).toString()) + '' + ' '.repeat(37) + colors.border(boxChar.vertical));
1736
- //
1737
- // // Content preview
1738
- // const contentStr = typeof message.content === 'string'
1739
- // ? message.content.substring(0, 100)
1740
- // : JSON.stringify(message.content).substring(0, 100);
1741
- // console.log(colors.border(`${boxChar.vertical}`) + ' 📝 CONTENT: ' +
1742
- // colors.textDim(contentStr + '...') + ' '.repeat(Math.max(0, 40 - contentStr.length)) + colors.border(boxChar.vertical));
1743
- // }
1744
- //
1745
- // console.log(colors.border(
1746
- // `${boxChar.bottomLeft}${boxChar.horizontal.repeat(58)}${boxChar.bottomRight}`
1747
- // ));
1748
- // }
1749
-
1750
- shutdown(): void {
1751
- this.rl.close();
1752
- this.cancellationManager.cleanup();
1753
- this.mcpManager.disconnectAllServers();
1754
-
1755
- // End the current session
1756
- this.sessionManager.completeCurrentSession();
1757
-
1758
- const separator = icons.separator.repeat(40);
1759
- console.log('');
1760
- console.log(colors.border(separator));
1761
- console.log(colors.primaryBright(`${icons.sparkles} Goodbye!`));
1762
- console.log(colors.border(separator));
1763
- console.log('');
1764
- }
1765
-
1766
- /**
1767
- * Get the RemoteAIClient instance
1768
- * Used by tools.ts to access the remote AI client for GUI operations
1769
- */
1770
- getRemoteAIClient(): RemoteAIClient | null {
1771
- return this.remoteAIClient;
1772
- }
1773
-
1774
- /**
1775
- * Get the current taskId for this user interaction
1776
- * Used by GUI operations to track the same task
1777
- */
1778
- getTaskId(): string | null {
1779
- return this.currentTaskId;
1780
- }
1781
- }
1782
-
1783
- export async function startInteractiveSession(): Promise<void> {
1784
- const session = new InteractiveSession();
1785
-
1786
- // Flag to control shutdown
1787
- (session as any)._isShuttingDown = false;
1788
-
1789
- // Also listen for raw Ctrl+C on stdin (works in Windows PowerShell)
1790
- process.stdin.on('data', (chunk: Buffer) => {
1791
- const str = chunk.toString();
1792
- // Ctrl+C is character 0x03 or string '\u0003'
1793
- if (str === '\u0003' || str.charCodeAt(0) === 3) {
1794
- if (!(session as any)._isShuttingDown) {
1795
- (session as any)._isShuttingDown = true;
1796
-
1797
- // Print goodbye immediately
1798
- const separator = icons.separator.repeat(40);
1799
- process.stdout.write('\n' + colors.border(separator) + '\n');
1800
- process.stdout.write(colors.primaryBright(`${icons.sparkles} Goodbye!`) + '\n');
1801
- process.stdout.write(colors.border(separator) + '\n\n');
1802
-
1803
- // Force exit
1804
- process.exit(0);
1805
- }
1806
- }
1807
- });
1808
-
1809
- process.on('SIGINT', () => {
1810
- if ((session as any)._isShuttingDown) {
1811
- return;
1812
- }
1813
- (session as any)._isShuttingDown = true;
1814
-
1815
- // Remove all SIGINT listeners to prevent re-entry
1816
- process.removeAllListeners('SIGINT');
1817
-
1818
- // Print goodbye immediately
1819
- const separator = icons.separator.repeat(40);
1820
- process.stdout.write('\n' + colors.border(separator) + '\n');
1821
- process.stdout.write(colors.primaryBright(`${icons.sparkles} Goodbye!`) + '\n');
1822
- process.stdout.write(colors.border(separator) + '\n\n');
1823
-
1824
- // Force exit
1825
- process.exit(0);
1826
- });
1827
-
1828
- await session.start();
1829
- }
1830
-
1831
- // Singleton session instance for access from other modules
1832
- let singletonSession: InteractiveSession | null = null;
1833
-
1834
- export function setSingletonSession(session: InteractiveSession): void {
1835
- singletonSession = session;
1836
- }
1837
-
1838
- export function getSingletonSession(): InteractiveSession | null {
1839
- return singletonSession;
1840
- }
1
+ import readline from 'readline';
2
+ import chalk from 'chalk';
3
+ import https from 'https';
4
+ import axios from 'axios';
5
+ import crypto from 'crypto';
6
+ import ora from 'ora';
7
+ import inquirer from 'inquirer';
8
+ import { createRequire } from 'module';
9
+ import { dirname, join } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const packageJson = require('../package.json');
14
+ import { ExecutionMode, ChatMessage, ToolCall, AuthType } from './types.js';
15
+ import { AIClient, Message, detectThinkingKeywords, getThinkingTokens } from './ai-client.js';
16
+ import { RemoteAIClient, TokenInvalidError } from './remote-ai-client.js';
17
+ import { getConfigManager, ConfigManager } from './config.js';
18
+ import { AuthService, selectAuthType } from './auth.js';
19
+ import { getToolRegistry } from './tools.js';
20
+ import { getAgentManager, DEFAULT_AGENTS, AgentManager } from './agents.js';
21
+ import { getMemoryManager, MemoryManager } from './memory.js';
22
+ import { getMCPManager, MCPManager } from './mcp.js';
23
+ import { getCheckpointManager, CheckpointManager } from './checkpoint.js';
24
+ import { getConversationManager, ConversationManager } from './conversation.js';
25
+ import { getSessionManager, SessionManager } from './session-manager.js';
26
+ import { SlashCommandHandler, parseInput, detectImageInput } from './slash-commands.js';
27
+ import { SystemPromptGenerator } from './system-prompt-generator.js';
28
+ import { theme, icons, colors, styleHelpers, renderMarkdown, renderDiff, renderLines, TERMINAL_BG } from './theme.js';
29
+ import { getCancellationManager, CancellationManager } from './cancellation.js';
30
+ import { getContextCompressor, ContextCompressor, CompressionResult } from './context-compressor.js';
31
+ import { Logger, LogLevel, getLogger } from './logger.js';
32
+
33
+ const logger = getLogger();
34
+
35
+ export class InteractiveSession {
36
+ private conversationManager: ConversationManager;
37
+ private sessionManager: SessionManager;
38
+ private contextCompressor: ContextCompressor;
39
+ private aiClient: AIClient | null = null;
40
+ private remoteAIClient: RemoteAIClient | null = null;
41
+ private conversation: ChatMessage[] = [];
42
+ private toolCalls: ToolCall[] = [];
43
+ private executionMode: ExecutionMode;
44
+ private slashCommandHandler: SlashCommandHandler;
45
+ private configManager: ConfigManager;
46
+ private agentManager: AgentManager;
47
+ private memoryManager: MemoryManager;
48
+ private mcpManager: MCPManager;
49
+ private checkpointManager: CheckpointManager;
50
+ private currentAgent: any = null;
51
+ private rl: readline.Interface;
52
+ private cancellationManager: CancellationManager;
53
+ private indentLevel: number;
54
+ private indentString: string;
55
+ private remoteConversationId: string | null = null;
56
+ private currentTaskId: string | null = null;
57
+ private taskCompleted: boolean = false;
58
+ private isFirstApiCall: boolean = true;
59
+
60
+ constructor(indentLevel: number = 0) {
61
+
62
+ this.rl = readline.createInterface({
63
+
64
+ input: process.stdin,
65
+
66
+ output: process.stdout
67
+
68
+ });
69
+
70
+
71
+
72
+ this.configManager = getConfigManager(process.cwd());
73
+
74
+ this.agentManager = getAgentManager(process.cwd());
75
+
76
+ this.memoryManager = getMemoryManager(process.cwd());
77
+
78
+ this.mcpManager = getMCPManager();
79
+
80
+ this.checkpointManager = getCheckpointManager(process.cwd());
81
+
82
+ this.conversationManager = getConversationManager();
83
+
84
+ this.sessionManager = getSessionManager(process.cwd());
85
+
86
+ this.slashCommandHandler = new SlashCommandHandler();
87
+
88
+ // Register /clear callback, clear local conversation when clearing dialogue
89
+ this.slashCommandHandler.setClearCallback(() => {
90
+ this.conversation = [];
91
+ this.toolCalls = [];
92
+ this.currentTaskId = null;
93
+ this.taskCompleted = false;
94
+ this.isFirstApiCall = true;
95
+ this.slashCommandHandler.setConversationHistory([]);
96
+ });
97
+
98
+
99
+
100
+ // Register MCP update callback, update system prompt
101
+
102
+ this.slashCommandHandler.setSystemPromptUpdateCallback(async () => {
103
+
104
+ await this.updateSystemPrompt();
105
+
106
+ });
107
+
108
+
109
+
110
+ this.executionMode = ExecutionMode.DEFAULT;
111
+
112
+ this.cancellationManager = getCancellationManager();
113
+
114
+ this.indentLevel = indentLevel;
115
+
116
+ this.indentString = ' '.repeat(indentLevel);
117
+
118
+ this.contextCompressor = getContextCompressor();
119
+
120
+ }
121
+
122
+ private getIndent(): string {
123
+ return this.indentString;
124
+ }
125
+
126
+ setAIClient(aiClient: AIClient): void {
127
+ this.aiClient = aiClient;
128
+ }
129
+
130
+ setExecutionMode(mode: ExecutionMode): void {
131
+ this.executionMode = mode;
132
+ }
133
+
134
+ /**
135
+ * Update system prompt to reflect MCP changes (called after add/remove MCP)
136
+ */
137
+ async updateSystemPrompt(): Promise<void> {
138
+ const toolRegistry = getToolRegistry();
139
+ const promptGenerator = new SystemPromptGenerator(toolRegistry, this.executionMode, undefined, this.mcpManager);
140
+
141
+ // Use the current agent's original system prompt as base
142
+ const baseSystemPrompt = this.currentAgent?.systemPrompt || 'You are xAgent, an AI-powered CLI tool.';
143
+ const newSystemPrompt = await promptGenerator.generateEnhancedSystemPrompt(baseSystemPrompt);
144
+
145
+ // Replace old system prompt with new one
146
+ this.conversation = this.conversation.filter(msg => msg.role !== 'system');
147
+ this.conversation.unshift({
148
+ role: 'system',
149
+ content: newSystemPrompt,
150
+ timestamp: Date.now()
151
+ });
152
+
153
+ // Sync to slashCommandHandler
154
+ this.slashCommandHandler.setConversationHistory(this.conversation);
155
+ }
156
+
157
+ setAgent(agent: any): void {
158
+ this.currentAgent = agent;
159
+ }
160
+
161
+ async start(): Promise<void> {
162
+ // Set this session as the singleton for access from other modules
163
+ setSingletonSession(this);
164
+
165
+ // Initialize taskId for GUI operations
166
+ this.currentTaskId = crypto.randomUUID();
167
+
168
+ const separator = icons.separator.repeat(60);
169
+ console.log('');
170
+ console.log(colors.gradient('╔════════════════════════════════════════════════════════════╗'));
171
+ console.log(colors.gradient('║') + ' '.repeat(58) + colors.gradient(' ║'));
172
+ console.log(colors.gradient('║') + ' '.repeat(13) + '🤖 ' + colors.gradient('XAGENT CLI') + ' '.repeat(32) + colors.gradient(' ║'));
173
+ console.log(colors.gradient('║') + ' '.repeat(16) + colors.textMuted(`v${packageJson.version}`) + ' '.repeat(36) + colors.gradient(' ║'));
174
+ console.log(colors.gradient('║') + ' '.repeat(58) + colors.gradient(' ║'));
175
+ console.log(colors.gradient('╚════════════════════════════════════════════════════════════╝'));
176
+ console.log(colors.textMuted(' AI-powered command-line assistant'));
177
+ console.log('');
178
+
179
+ await this.initialize();
180
+ this.showWelcomeMessage();
181
+
182
+ // Track if an operation is in progress
183
+ (this as any)._isOperationInProgress = false;
184
+
185
+ // Listen for ESC cancellation - only cancel operations, don't exit the program
186
+ const cancelHandler = () => {
187
+ if ((this as any)._isOperationInProgress) {
188
+ // An operation is running, let it be cancelled
189
+ return;
190
+ }
191
+ // No operation running, ignore ESC or show a message
192
+ };
193
+ this.cancellationManager.on('cancelled', cancelHandler);
194
+
195
+ this.promptLoop();
196
+
197
+ // Keep the promise pending until shutdown
198
+ return new Promise((resolve) => {
199
+ (this as any)._shutdownResolver = resolve;
200
+ });
201
+ }
202
+
203
+ private async initialize(): Promise<void> {
204
+ logger.debug('\n[SESSION] ========== initialize() 开始 ==========\n');
205
+
206
+ try {
207
+ // Custom spinner for initialization (like Thinking...)
208
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
209
+ let frameIndex = 0;
210
+ const validatingText = colors.textMuted('Validating authentication...');
211
+
212
+ const spinnerInterval = setInterval(() => {
213
+ process.stdout.write(`\r${colors.primary(frames[frameIndex])} ${validatingText}`);
214
+ frameIndex = (frameIndex + 1) % frames.length;
215
+ }, 120);
216
+ logger.debug('[SESSION] 调用 configManager.load()...');
217
+ await this.configManager.load();
218
+
219
+ logger.debug('[SESSION] Config loaded');
220
+ let authConfig = this.configManager.getAuthConfig();
221
+ let selectedAuthType = this.configManager.get('selectedAuthType');
222
+
223
+ logger.debug('[SESSION] authConfig.apiKey exists:', String(!!authConfig.apiKey));
224
+ logger.debug('[SESSION] selectedAuthType (initial):', String(selectedAuthType));
225
+ logger.debug('[SESSION] AuthType.OAUTH_XAGENT:', String(AuthType.OAUTH_XAGENT));
226
+ logger.debug('[SESSION] AuthType.OPENAI_COMPATIBLE:', String(AuthType.OPENAI_COMPATIBLE));
227
+ logger.debug('[SESSION] Will validate OAuth:', String(!!(authConfig.apiKey && selectedAuthType === AuthType.OAUTH_XAGENT)));
228
+
229
+ // Only validate OAuth tokens, skip validation for third-party API keys
230
+ if (authConfig.apiKey && selectedAuthType === AuthType.OAUTH_XAGENT) {
231
+ clearInterval(spinnerInterval);
232
+ process.stdout.write('\r' + ' '.repeat(50) + '\r'); // Clear the line
233
+
234
+ const baseUrl = authConfig.xagentApiBaseUrl || 'https://www.xagent-colife.net';
235
+ let isValid = await this.validateToken(baseUrl, authConfig.apiKey);
236
+
237
+ // Try refresh token if validation failed
238
+ if (!isValid && authConfig.refreshToken) {
239
+ const refreshingText = colors.textMuted('Refreshing authentication...');
240
+ frameIndex = 0;
241
+ const refreshInterval = setInterval(() => {
242
+ process.stdout.write(`\r${colors.primary(frames[frameIndex])} ${refreshingText}`);
243
+ frameIndex = (frameIndex + 1) % frames.length;
244
+ }, 120);
245
+
246
+ const newToken = await this.refreshToken(baseUrl, authConfig.refreshToken);
247
+ clearInterval(refreshInterval);
248
+ process.stdout.write('\r' + ' '.repeat(50) + '\r');
249
+
250
+ if (newToken) {
251
+ // Save new token and persist
252
+ await this.configManager.set('apiKey', newToken);
253
+ await this.configManager.save('global');
254
+ authConfig.apiKey = newToken;
255
+ isValid = true;
256
+ }
257
+ }
258
+
259
+ if (!isValid) {
260
+ console.log('');
261
+ console.log(colors.warning('Your xAgent session has expired or is not configured'));
262
+ console.log(colors.info('Please select an authentication method to continue.'));
263
+ console.log('');
264
+
265
+ // Clear invalid credentials and persist
266
+ // Note: Do NOT overwrite selectedAuthType - let user re-select their preferred auth method
267
+ await this.configManager.set('apiKey', '');
268
+ await this.configManager.set('refreshToken', '');
269
+ await this.configManager.save('global');
270
+
271
+ await this.configManager.load();
272
+ authConfig = this.configManager.getAuthConfig();
273
+
274
+ await this.setupAuthentication();
275
+ authConfig = this.configManager.getAuthConfig();
276
+
277
+ // Recreate readline interface after inquirer
278
+ this.rl.close();
279
+ this.rl = readline.createInterface({
280
+ input: process.stdin,
281
+ output: process.stdout
282
+ });
283
+ this.rl.on('close', () => {
284
+ // readline closed
285
+ });
286
+ }
287
+ } else if (!authConfig.apiKey) {
288
+ // No API key configured, need to set up authentication
289
+ clearInterval(spinnerInterval);
290
+ process.stdout.write('\r' + ' '.repeat(50) + '\r');
291
+ await this.setupAuthentication();
292
+ authConfig = this.configManager.getAuthConfig();
293
+ selectedAuthType = this.configManager.get('selectedAuthType');
294
+ logger.debug('[SESSION] selectedAuthType (after setup):', String(selectedAuthType));
295
+
296
+ // Recreate readline interface after inquirer
297
+ this.rl.close();
298
+ this.rl = readline.createInterface({
299
+ input: process.stdin,
300
+ output: process.stdout
301
+ });
302
+ this.rl.on('close', () => {
303
+ // readline closed
304
+ });
305
+ } else {
306
+ clearInterval(spinnerInterval);
307
+ process.stdout.write('\r' + ' '.repeat(50) + '\r');
308
+ }
309
+ // For OPENAI_COMPATIBLE with API key, skip validation and proceed directly
310
+
311
+ this.aiClient = new AIClient(authConfig);
312
+ this.contextCompressor.setAIClient(this.aiClient);
313
+
314
+ // Initialize remote AI client for OAuth XAGENT mode
315
+ logger.debug('[SESSION] Final selectedAuthType:', String(selectedAuthType));
316
+ logger.debug('[SESSION] Creating RemoteAIClient?', String(selectedAuthType === AuthType.OAUTH_XAGENT));
317
+ if (selectedAuthType === AuthType.OAUTH_XAGENT) {
318
+ const webBaseUrl = authConfig.xagentApiBaseUrl || 'https://www.xagent-colife.net';
319
+ // In OAuth XAGENT mode, we still pass apiKey (can be empty or used for other purposes)
320
+ this.remoteAIClient = new RemoteAIClient(authConfig.apiKey || '', webBaseUrl, authConfig.showAIDebugInfo);
321
+ logger.debug('[DEBUG Initialize] RemoteAIClient created successfully');
322
+ } else {
323
+ logger.debug('[DEBUG Initialize] RemoteAIClient NOT created (not OAuth XAGENT mode)');
324
+ }
325
+
326
+ this.executionMode = this.configManager.getApprovalMode() || this.configManager.getExecutionMode();
327
+
328
+ await this.agentManager.loadAgents();
329
+ await this.memoryManager.loadMemory();
330
+ await this.conversationManager.initialize();
331
+ await this.sessionManager.initialize();
332
+
333
+ // Create a new conversation and session for this interactive session
334
+ const conversation = await this.conversationManager.createConversation();
335
+ await this.sessionManager.createSession(
336
+ conversation.id,
337
+ this.currentAgent?.name || 'general-purpose',
338
+ this.executionMode
339
+ );
340
+
341
+ // Sync conversation history to slashCommandHandler
342
+ this.slashCommandHandler.setConversationHistory(this.conversation);
343
+
344
+ const mcpServers = this.configManager.getMcpServers();
345
+ Object.entries(mcpServers).forEach(([name, config]) => {
346
+ console.log(`📝 Registering MCP server: ${name} (${config.transport})`);
347
+ this.mcpManager.registerServer(name, config);
348
+ });
349
+
350
+ // Eagerly connect to MCP servers to get tool definitions
351
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
352
+ try {
353
+ console.log(`${colors.info(`${icons.brain} Connecting to ${Object.keys(mcpServers).length} MCP server(s)...`)}`);
354
+ await this.mcpManager.connectAllServers();
355
+ const connectedCount = Array.from(this.mcpManager.getAllServers()).filter((s: any) => s.isServerConnected()).length;
356
+ const mcpTools = this.mcpManager.getToolDefinitions();
357
+ console.log(`${colors.success(`✓ ${connectedCount}/${Object.keys(mcpServers).length} MCP server(s) connected (${mcpTools.length} tools available)`)}`);
358
+
359
+ // Register MCP tools with the tool registry (hide MCP origin from LLM)
360
+ const toolRegistry = getToolRegistry();
361
+ const allMcpTools = this.mcpManager.getAllTools();
362
+ toolRegistry.registerMCPTools(allMcpTools);
363
+ } catch (error: any) {
364
+ console.log(`${colors.warning(`⚠ MCP connection failed: ${error.message}`)}`);
365
+ }
366
+ }
367
+
368
+ const checkpointingConfig = this.configManager.getCheckpointingConfig();
369
+ if (checkpointingConfig.enabled) {
370
+ this.checkpointManager = getCheckpointManager(
371
+ process.cwd(),
372
+ checkpointingConfig.enabled,
373
+ checkpointingConfig.maxCheckpoints
374
+ );
375
+ await this.checkpointManager.initialize();
376
+ }
377
+
378
+ this.currentAgent = this.agentManager.getAgent('general-purpose');
379
+
380
+ console.log(colors.success('✔ Initialization complete'));
381
+ } catch (error: any) {
382
+ const spinner = ora({ text: '', spinner: 'dots', color: 'red' }).start();
383
+ spinner.fail(colors.error(`Initialization failed: ${error.message}`));
384
+ throw error;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Validate token with the backend
390
+ * Returns true if token is valid, false otherwise
391
+ */
392
+ private async validateToken(baseUrl: string, apiKey: string): Promise<boolean> {
393
+ logger.debug('[SESSION] validateToken called with baseUrl:', baseUrl);
394
+ logger.debug('[SESSION] apiKey exists:', apiKey ? 'yes' : 'no');
395
+
396
+ try {
397
+ // For OAuth XAGENT auth, use /api/auth/me endpoint
398
+ const url = `${baseUrl}/api/auth/me`;
399
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
400
+
401
+ logger.debug('[SESSION] Sending validation request to:', url);
402
+
403
+ const response = await axios.get(url, {
404
+ headers: {
405
+ 'Authorization': `Bearer ${apiKey}`,
406
+ 'Content-Type': 'application/json'
407
+ },
408
+ httpsAgent,
409
+ timeout: 10000
410
+ });
411
+
412
+ logger.debug('[SESSION] Validation response status:', String(response.status));
413
+ return response.status === 200;
414
+ } catch (error: any) {
415
+ // Network error - log details but still consider token may be invalid
416
+ logger.debug('[SESSION] Error:', error.message);
417
+ if (error.response) {
418
+ logger.debug('[SESSION] Response status:', error.response.status);
419
+ }
420
+ // For network errors, we still return false to trigger re-authentication
421
+ // This ensures security but the user can retry
422
+ return false;
423
+ }
424
+ }
425
+
426
+ private async refreshToken(baseUrl: string, refreshToken: string): Promise<string | null> {
427
+ try {
428
+ const url = `${baseUrl}/api/auth/refresh`;
429
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
430
+
431
+ const response = await axios.post(url, { refreshToken }, {
432
+ httpsAgent,
433
+ timeout: 10000
434
+ });
435
+
436
+ if (response.status === 200) {
437
+ const data = response.data as { token?: string; refreshToken?: string };
438
+ return data.token || null;
439
+ } else {
440
+ return null;
441
+ }
442
+ } catch (error: any) {
443
+ return null;
444
+ }
445
+ }
446
+
447
+ private async setupAuthentication(): Promise<void> {
448
+ const separator = icons.separator.repeat(40);
449
+ console.log('');
450
+ console.log(colors.primaryBright(`${icons.lock} Setup Authentication`));
451
+ console.log(colors.border(separator));
452
+ console.log('');
453
+
454
+ const authType = await selectAuthType();
455
+ this.configManager.set('selectedAuthType', authType);
456
+
457
+ const authService = new AuthService({
458
+ type: authType,
459
+ apiKey: '',
460
+ baseUrl: '',
461
+ modelName: ''
462
+ });
463
+
464
+ const success = await authService.authenticate();
465
+
466
+ if (!success) {
467
+ console.log('');
468
+ console.log(colors.error('Authentication failed. Exiting...'));
469
+ console.log('');
470
+ process.exit(1);
471
+ }
472
+
473
+ const authConfig = authService.getAuthConfig();
474
+
475
+ // VLM configuration is optional - only show for non-OAuth (local) mode
476
+ // Remote mode uses backend VLM configuration
477
+ if (authType !== AuthType.OAUTH_XAGENT) {
478
+ console.log('');
479
+ console.log(colors.info(`${icons.info} VLM configuration is optional.`));
480
+ console.log(colors.info(`You can configure it later using the /vlm command if needed.`));
481
+ console.log('');
482
+ }
483
+
484
+ // Save LLM config only, skip VLM for now
485
+ await this.configManager.setAuthConfig(authConfig);
486
+ }
487
+
488
+ private showWelcomeMessage(): void {
489
+ const language = this.configManager.getLanguage();
490
+ const separator = icons.separator.repeat(40);
491
+
492
+ console.log('');
493
+ console.log(colors.border(separator));
494
+
495
+ if (language === 'zh') {
496
+ console.log(colors.primaryBright(`${icons.sparkles} Welcome to XAGENT CLI!`));
497
+ console.log(colors.textMuted('Type /help to see available commands')); } else {
498
+ console.log(colors.primaryBright(`${icons.sparkles} Welcome to XAGENT CLI!`));
499
+ console.log(colors.textMuted('Type /help to see available commands'));
500
+ }
501
+
502
+ console.log(colors.border(separator));
503
+ console.log('');
504
+
505
+ this.showExecutionMode();
506
+ }
507
+
508
+ private showExecutionMode(): void {
509
+ const modeConfig = {
510
+ [ExecutionMode.YOLO]: {
511
+ color: colors.error,
512
+ icon: icons.fire,
513
+ description: 'Execute commands without confirmation'
514
+ },
515
+ [ExecutionMode.ACCEPT_EDITS]: {
516
+ color: colors.warning,
517
+ icon: icons.check,
518
+ description: 'Accept all edits automatically'
519
+ },
520
+ [ExecutionMode.PLAN]: {
521
+ color: colors.info,
522
+ icon: icons.brain,
523
+ description: 'Plan before executing'
524
+ },
525
+ [ExecutionMode.DEFAULT]: {
526
+ color: colors.success,
527
+ icon: icons.bolt,
528
+ description: 'Safe execution with confirmations'
529
+ },
530
+ [ExecutionMode.SMART]: {
531
+ color: colors.primaryBright,
532
+ icon: icons.sparkles,
533
+ description: 'Smart approval with intelligent security checks'
534
+ }
535
+ };
536
+
537
+ const config = modeConfig[this.executionMode];
538
+ const modeName = this.executionMode;
539
+
540
+ console.log(colors.textMuted(`${icons.info} Current Mode:`));
541
+ console.log(` ${config.color(config.icon)} ${styleHelpers.text.bold(config.color(modeName))}`);
542
+ console.log(` ${colors.textDim(` ${config.description}`)}`);
543
+ console.log('');
544
+ }
545
+
546
+ private async promptLoop(): Promise<void> {
547
+ // Check if we're shutting down
548
+ if ((this as any)._isShuttingDown) {
549
+ return;
550
+ }
551
+
552
+ // Recreate readline interface for input
553
+ if (this.rl) {
554
+ this.rl.close();
555
+ }
556
+
557
+ // Enable raw mode BEFORE emitKeypressEvents for better ESC detection
558
+ if (process.stdin.isTTY) {
559
+ process.stdin.setRawMode(true);
560
+ }
561
+ process.stdin.resume();
562
+ readline.emitKeypressEvents(process.stdin);
563
+
564
+ this.rl = readline.createInterface({
565
+ input: process.stdin,
566
+ output: process.stdout
567
+ });
568
+
569
+ const prompt = `${colors.primaryBright('❯')} `;
570
+ this.rl.question(prompt, async (input: string) => {
571
+ if ((this as any)._isShuttingDown) {
572
+ return;
573
+ }
574
+
575
+ try {
576
+ await this.handleInput(input);
577
+ } catch (err: any) {
578
+ console.log(colors.error(`Error: ${err.message}`));
579
+ }
580
+
581
+ this.promptLoop();
582
+ });
583
+ }
584
+
585
+ private async handleInput(input: string): Promise<void> {
586
+ const trimmedInput = input.trim();
587
+
588
+ if (!trimmedInput) {
589
+ return;
590
+ }
591
+
592
+ if (trimmedInput.startsWith('/')) {
593
+ const handled = await this.slashCommandHandler.handleCommand(trimmedInput);
594
+ if (handled) {
595
+ this.executionMode = this.configManager.getApprovalMode() || this.configManager.getExecutionMode();
596
+ // Sync conversation history to slashCommandHandler
597
+ this.slashCommandHandler.setConversationHistory(this.conversation);
598
+ }
599
+ return;
600
+ }
601
+
602
+ if (trimmedInput.startsWith('$')) {
603
+ await this.handleSubAgentCommand(trimmedInput);
604
+ return;
605
+ }
606
+
607
+ await this.processUserMessage(trimmedInput);
608
+ }
609
+
610
+ private async handleSubAgentCommand(input: string): Promise<void> {
611
+ const [agentType, ...taskParts] = input.slice(1).split(' ');
612
+ const task = taskParts.join(' ');
613
+
614
+ const agent = this.agentManager.getAgent(agentType);
615
+
616
+ if (!agent) {
617
+ console.log('');
618
+ console.log(colors.warning(`Agent not found: ${agentType}`));
619
+ console.log(colors.textMuted('Use /agents list to see available agents'));
620
+ console.log('');
621
+ return;
622
+ }
623
+
624
+ console.log('');
625
+ console.log(colors.primaryBright(`${icons.robot} Using agent: ${agent.name || agent.agentType}`));
626
+ console.log(colors.border(icons.separator.repeat(40)));
627
+ console.log('');
628
+
629
+ this.currentAgent = agent;
630
+ await this.processUserMessage(task, agent);
631
+ }
632
+
633
+ public async processUserMessage(message: string, agent?: any): Promise<void> {
634
+ const inputs = parseInput(message);
635
+ const textInput = inputs.find(i => i.type === 'text');
636
+ const fileInputs = inputs.filter(i => i.type === 'file');
637
+ const commandInput = inputs.find(i => i.type === 'command');
638
+
639
+ if (commandInput) {
640
+ await this.executeShellCommand(commandInput.content);
641
+ return;
642
+ }
643
+
644
+ let userContent = textInput?.content || '';
645
+
646
+ if (fileInputs.length > 0) {
647
+ const toolRegistry = getToolRegistry();
648
+ for (const fileInput of fileInputs) {
649
+ try {
650
+ const content = await toolRegistry.execute('Read', { filePath: fileInput.content }, this.executionMode);
651
+ userContent += `\n\n--- File: ${fileInput.content} ---\n${content}`;
652
+ } catch (error: any) {
653
+ console.log(chalk.yellow(`Warning: Failed to read file ${fileInput.content}: ${error.message}`));
654
+ }
655
+ }
656
+ }
657
+
658
+ // Record input to session manager
659
+ const sessionInput = {
660
+ type: 'text' as const,
661
+ content: userContent,
662
+ rawInput: message,
663
+ timestamp: Date.now()
664
+ };
665
+ await this.sessionManager.addInput(sessionInput);
666
+
667
+ // Calculate thinking tokens based on config and user input
668
+ const thinkingConfig = this.configManager.getThinkingConfig();
669
+ let thinkingTokens = 0;
670
+
671
+ if (thinkingConfig.enabled) {
672
+ // If thinking mode is enabled, detect keywords and calculate tokens
673
+ const thinkingMode = detectThinkingKeywords(userContent);
674
+ thinkingTokens = getThinkingTokens(thinkingMode);
675
+ }
676
+
677
+ const userMessage: ChatMessage = {
678
+ role: 'user',
679
+ content: userContent,
680
+ timestamp: Date.now()
681
+ };
682
+
683
+ // Save last user message for recovery after compression
684
+ const lastUserMessage = userMessage;
685
+
686
+ this.conversation.push(userMessage);
687
+ await this.conversationManager.addMessage(userMessage);
688
+
689
+ // Check if context compression is needed
690
+ await this.checkAndCompressContext(lastUserMessage);
691
+
692
+ // Use remote AI client if available (OAuth XAGENT mode)
693
+ const currentSelectedAuthType = this.configManager.get('selectedAuthType');
694
+ logger.debug('[DEBUG processUserMessage] remoteAIClient exists:', !!this.remoteAIClient ? 'true' : 'false');
695
+ logger.debug('[DEBUG processUserMessage] selectedAuthType:', String(currentSelectedAuthType));
696
+ logger.debug('[DEBUG processUserMessage] AuthType.OAUTH_XAGENT:', String(AuthType.OAUTH_XAGENT));
697
+
698
+ if (this.remoteAIClient) {
699
+ logger.debug('[DEBUG processUserMessage] Using generateRemoteResponse');
700
+ await this.generateRemoteResponse(thinkingTokens);
701
+ } else {
702
+ logger.debug('[DEBUG processUserMessage] Using generateResponse (local mode)');
703
+ await this.generateResponse(thinkingTokens);
704
+ }
705
+ }
706
+
707
+ private displayThinkingContent(reasoningContent: string): void {
708
+ const indent = this.getIndent();
709
+ const thinkingConfig = this.configManager.getThinkingConfig();
710
+ const displayMode = thinkingConfig.displayMode || 'compact';
711
+
712
+ const separator = icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length);
713
+
714
+ console.log('');
715
+ console.log(`${indent}${colors.border(separator)}`);
716
+
717
+ switch (displayMode) {
718
+ case 'full':
719
+ // Full display, using small font and gray color
720
+ console.log(`${indent}${colors.textDim(`${icons.brain} Thinking Process:`)}`);
721
+ console.log('');
722
+ console.log(`${indent}${colors.textDim(reasoningContent.replace(/^/gm, indent))}`);
723
+ break;
724
+
725
+ case 'compact':
726
+ // Compact display, truncate partial content
727
+ const maxLength = 500;
728
+ const truncatedContent = reasoningContent.length > maxLength
729
+ ? reasoningContent.substring(0, maxLength) + '... (truncated)'
730
+ : reasoningContent;
731
+
732
+ console.log(`${indent}${colors.textDim(`${icons.brain} Thinking Process:`)}`);
733
+ console.log('');
734
+ console.log(`${indent}${colors.textDim(truncatedContent.replace(/^/gm, indent))}`);
735
+ console.log(`${indent}${colors.textDim(`[${reasoningContent.length} chars total]`)}`);
736
+ break;
737
+
738
+ case 'indicator':
739
+ // Show indicator only
740
+ console.log(`${indent}${colors.textDim(`${icons.brain} Thinking process completed`)}`);
741
+ console.log(`${indent}${colors.textDim(`[${reasoningContent.length} chars of reasoning]`)}`);
742
+ break;
743
+
744
+ default:
745
+ console.log(`${indent}${colors.textDim(`${icons.brain} Thinking:`)}`);
746
+ console.log('');
747
+ console.log(`${indent}${colors.textDim(reasoningContent.replace(/^/gm, indent))}`);
748
+ }
749
+
750
+ console.log(`${indent}${colors.border(separator)}`);
751
+ console.log('');
752
+ }
753
+
754
+ /**
755
+ * Check and compress conversation context
756
+ */
757
+ private async checkAndCompressContext(lastUserMessage?: ChatMessage): Promise<void> {
758
+ const compressionConfig = this.configManager.getContextCompressionConfig();
759
+
760
+ if (!compressionConfig.enabled) {
761
+ return;
762
+ }
763
+
764
+ const { needsCompression, reason } = this.contextCompressor.needsCompression(
765
+ this.conversation,
766
+ compressionConfig
767
+ );
768
+
769
+ if (needsCompression) {
770
+ const indent = this.getIndent();
771
+ console.log('');
772
+ console.log(`${indent}${colors.warning(`${icons.brain} Context compression triggered: ${reason}`)}`);
773
+
774
+ const toolRegistry = getToolRegistry();
775
+ const baseSystemPrompt = this.currentAgent?.systemPrompt || 'You are a helpful AI assistant.';
776
+ const systemPromptGenerator = new SystemPromptGenerator(toolRegistry, this.executionMode);
777
+ const enhancedSystemPrompt = await systemPromptGenerator.generateEnhancedSystemPrompt(baseSystemPrompt);
778
+
779
+ const result: CompressionResult = await this.contextCompressor.compressContext(
780
+ this.conversation,
781
+ enhancedSystemPrompt,
782
+ compressionConfig
783
+ );
784
+
785
+ if (result.wasCompressed) {
786
+ this.conversation = result.compressedMessages;
787
+ // console.log(`${indent}${colors.success(`✓ Compressed ${result.originalMessageCount} messages to ${result.compressedMessageCount} messages`)}`);
788
+ console.log(`${indent}${colors.textMuted(`✓ Size: ${result.originalSize} → ${result.compressedSize} chars (${Math.round((1 - result.compressedSize / result.originalSize) * 100)}% reduction)`)}`);
789
+
790
+ // Display compressed summary content
791
+ const summaryMessage = result.compressedMessages.find(m => m.role === 'assistant');
792
+ if (summaryMessage && summaryMessage.content) {
793
+ const maxPreviewLength = 800;
794
+ let summaryContent = summaryMessage.content;
795
+ const isTruncated = summaryContent.length > maxPreviewLength;
796
+
797
+ if (isTruncated) {
798
+ summaryContent = summaryContent.substring(0, maxPreviewLength) + '\n...';
799
+ }
800
+
801
+ console.log('');
802
+ console.log(`${indent}${theme.predefinedStyles.title(`${icons.sparkles} Conversation Summary`)}`);
803
+ const separator = icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length * 2);
804
+ console.log(`${indent}${colors.border(separator)}`);
805
+ const renderedSummary = renderMarkdown(summaryContent, (process.stdout.columns || 80) - indent.length * 4);
806
+ console.log(`${indent}${theme.predefinedStyles.dim(renderedSummary).replace(/^/gm, indent)}`);
807
+ if (isTruncated) {
808
+ console.log(`${indent}${colors.textMuted(`(... ${summaryMessage.content.length - maxPreviewLength} more chars hidden)`)}`);
809
+ }
810
+ console.log(`${indent}${colors.border(separator)}`);
811
+ }
812
+
813
+ // Restore user messages after compression, ensuring user message exists for API calls
814
+ if (lastUserMessage) {
815
+ this.conversation.push(lastUserMessage);
816
+ }
817
+
818
+ // Sync compressed conversation history to slashCommandHandler
819
+ this.slashCommandHandler.setConversationHistory(this.conversation);
820
+ }
821
+ }
822
+ }
823
+
824
+ private async executeShellCommand(command: string): Promise<void> {
825
+ const indent = this.getIndent();
826
+ console.log('');
827
+ console.log(`${indent}${colors.textMuted(`${icons.code} Executing:`)}`);
828
+ console.log(`${indent}${colors.codeText(` $ ${command}`)}`);
829
+ console.log(`${indent}${colors.border(icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length))}`);
830
+ console.log('');
831
+
832
+ const toolRegistry = getToolRegistry();
833
+
834
+ try {
835
+ const result = await toolRegistry.execute('Bash', { command }, this.executionMode);
836
+
837
+ if (result.stdout) {
838
+ console.log(`${indent}${result.stdout.replace(/^/gm, indent)}`);
839
+ }
840
+
841
+ if (result.stderr) {
842
+ console.log(`${indent}${colors.warning(result.stderr.replace(/^/gm, indent))}`);
843
+ }
844
+
845
+ const toolCall: ToolCall = {
846
+ tool: 'Bash',
847
+ params: { command },
848
+ result,
849
+ timestamp: Date.now()
850
+ };
851
+
852
+ this.toolCalls.push(toolCall);
853
+
854
+ // Record command execution to session manager
855
+ await this.sessionManager.addInput({
856
+ type: 'command',
857
+ content: command,
858
+ rawInput: command,
859
+ timestamp: Date.now()
860
+ });
861
+
862
+ await this.sessionManager.addOutput({
863
+ role: 'tool',
864
+ content: JSON.stringify(result),
865
+ toolName: 'Bash',
866
+ toolParams: { command },
867
+ toolResult: result,
868
+ timestamp: Date.now()
869
+ });
870
+ } catch (error: any) {
871
+ console.log(`${indent}${colors.error(`Command execution failed: ${error.message}`)}`);
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Create unified LLM Caller
877
+ * Implement transparency: caller doesn't need to care about remote vs local mode
878
+ */
879
+ private createLLMCaller(taskId: string, status: 'begin' | 'continue') {
880
+ // Remote mode: use RemoteAIClient
881
+ if (this.remoteAIClient) {
882
+ return this.createRemoteCaller(taskId, status);
883
+ }
884
+
885
+ // Local mode: use AIClient
886
+ if (!this.aiClient) {
887
+ throw new Error('AI client not initialized');
888
+ }
889
+ return this.createLocalCaller();
890
+ }
891
+
892
+ /**
893
+ * Create remote mode LLM caller
894
+ */
895
+ private createRemoteCaller(taskId: string, status: 'begin' | 'continue') {
896
+ const client = this.remoteAIClient!;
897
+ return {
898
+ chatCompletion: (messages: ChatMessage[], options: any) =>
899
+ client.chatCompletion(messages, { ...options, taskId, status }),
900
+ isRemote: true
901
+ };
902
+ }
903
+
904
+ /**
905
+ * Create local mode LLM caller
906
+ */
907
+ private createLocalCaller() {
908
+ const client = this.aiClient!;
909
+ return {
910
+ chatCompletion: (messages: ChatMessage[], options: any) =>
911
+ client.chatCompletion(messages as any, options),
912
+ isRemote: false
913
+ };
914
+ }
915
+
916
+ private async generateResponse(thinkingTokens: number = 0, customAIClient?: AIClient, existingTaskId?: string): Promise<void> {
917
+ // Use existing taskId or create new one for this user interaction
918
+ // If taskId already exists (e.g., from tool calls), reuse it
919
+ const taskId = existingTaskId || this.currentTaskId || crypto.randomUUID();
920
+ this.currentTaskId = taskId;
921
+ this.isFirstApiCall = true;
922
+
923
+ // Determine status based on whether this is the first API call
924
+ const status: 'begin' | 'continue' = this.isFirstApiCall ? 'begin' : 'continue';
925
+
926
+ // Use custom AI client if provided, otherwise use default logic
927
+ let chatCompletion: (messages: ChatMessage[], options: any) => Promise<any>;
928
+ let isRemote = false;
929
+
930
+ if (customAIClient) {
931
+ // Custom client (used by remote mode) - pass taskId and status
932
+ chatCompletion = (messages: ChatMessage[], options: any) =>
933
+ customAIClient.chatCompletion(messages as any, { ...options, taskId, status });
934
+ isRemote = true;
935
+ } else {
936
+ // Use unified LLM Caller with taskId (automatically selects local or remote mode)
937
+ const caller = this.createLLMCaller(taskId, status);
938
+ chatCompletion = caller.chatCompletion;
939
+ isRemote = caller.isRemote;
940
+ }
941
+
942
+ if (!isRemote && !this.aiClient && !customAIClient) {
943
+ console.log(colors.error('AI client not initialized'));
944
+ return;
945
+ }
946
+
947
+ // Mark that an operation is in progress
948
+ (this as any)._isOperationInProgress = true;
949
+
950
+ const indent = this.getIndent();
951
+ const thinkingText = colors.textMuted(`Thinking... (Press ESC to cancel)`);
952
+ const icon = colors.primary(icons.brain);
953
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
954
+ let frameIndex = 0;
955
+
956
+ // Custom spinner: only icon rotates, text stays static
957
+ const spinnerInterval = setInterval(() => {
958
+ process.stdout.write(`\r${colors.primary(frames[frameIndex])} ${icon} ${thinkingText}`);
959
+ frameIndex = (frameIndex + 1) % frames.length;
960
+ }, 120);
961
+
962
+ try {
963
+ const memory = await this.memoryManager.loadMemory();
964
+ const toolRegistry = getToolRegistry();
965
+ const allowedToolNames = this.currentAgent
966
+ ? this.agentManager.getAvailableToolsForAgent(this.currentAgent, this.executionMode)
967
+ : [];
968
+
969
+ // MCP servers are already connected during initialization (eager mode)
970
+ // MCP tools are already registered as local tools via registerMCPTools
971
+ const toolDefinitions = toolRegistry.getToolDefinitions();
972
+
973
+ // Available tools for this session
974
+ const availableTools = this.executionMode !== ExecutionMode.DEFAULT && allowedToolNames.length > 0
975
+ ? toolDefinitions.filter((tool: any) => allowedToolNames.includes(tool.function.name))
976
+ : toolDefinitions;
977
+
978
+ const baseSystemPrompt = this.currentAgent?.systemPrompt;
979
+ const systemPromptGenerator = new SystemPromptGenerator(toolRegistry, this.executionMode, undefined, this.mcpManager);
980
+ const enhancedSystemPrompt = await systemPromptGenerator.generateEnhancedSystemPrompt(baseSystemPrompt);
981
+
982
+ const messages: ChatMessage[] = [
983
+ { role: 'system', content: `${enhancedSystemPrompt}\n\n${memory}`, timestamp: Date.now() },
984
+ ...this.conversation
985
+ ];
986
+
987
+ const operationId = `ai-response-${Date.now()}`;
988
+ const response = await this.cancellationManager.withCancellation(
989
+ chatCompletion(messages, {
990
+ tools: availableTools,
991
+ toolChoice: availableTools.length > 0 ? 'auto' : 'none',
992
+ thinkingTokens
993
+ }),
994
+ operationId
995
+ );
996
+
997
+ // Mark that first API call is complete
998
+ this.isFirstApiCall = false;
999
+
1000
+ clearInterval(spinnerInterval);
1001
+ process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r'); // Clear spinner line
1002
+
1003
+ const assistantMessage = response.choices[0].message;
1004
+
1005
+ const content = typeof assistantMessage.content === 'string'
1006
+ ? assistantMessage.content
1007
+ : '';
1008
+ const reasoningContent = assistantMessage.reasoning_content || '';
1009
+ // Display reasoning content if available and thinking mode is enabled
1010
+ if (reasoningContent && this.configManager.getThinkingConfig().enabled) {
1011
+ this.displayThinkingContent(reasoningContent);
1012
+ }
1013
+
1014
+ console.log('');
1015
+ console.log(`${indent}${colors.primaryBright(`${icons.robot} Assistant:`)}`);
1016
+ console.log(`${indent}${colors.border(icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length))}`);
1017
+ console.log('');
1018
+ const renderedContent = renderMarkdown(content, (process.stdout.columns || 80) - indent.length * 2);
1019
+ console.log(`${indent}${renderedContent.replace(/^/gm, indent)}`);
1020
+ console.log('');
1021
+
1022
+ this.conversation.push({
1023
+ role: 'assistant',
1024
+ content,
1025
+ timestamp: Date.now(),
1026
+ reasoningContent,
1027
+ toolCalls: assistantMessage.tool_calls
1028
+ });
1029
+
1030
+ // Record output to session manager
1031
+ await this.sessionManager.addOutput({
1032
+ role: 'assistant',
1033
+ content,
1034
+ timestamp: Date.now(),
1035
+ reasoningContent,
1036
+ toolCalls: assistantMessage.tool_calls
1037
+ });
1038
+
1039
+ if (assistantMessage.tool_calls) {
1040
+ await this.handleToolCalls(assistantMessage.tool_calls);
1041
+ }
1042
+
1043
+ if (this.checkpointManager.isEnabled()) {
1044
+ await this.checkpointManager.createCheckpoint(
1045
+ `Response generated at ${new Date().toLocaleString()}`,
1046
+ [...this.conversation],
1047
+ [...this.toolCalls]
1048
+ );
1049
+ }
1050
+
1051
+ // Operation completed successfully, clear the flag
1052
+ (this as any)._isOperationInProgress = false;
1053
+ } catch (error: any) {
1054
+ clearInterval(spinnerInterval);
1055
+ process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
1056
+
1057
+ // Clear the operation flag
1058
+ (this as any)._isOperationInProgress = false;
1059
+
1060
+ if (error.message === 'Operation cancelled by user') {
1061
+ // Mark task as cancelled
1062
+ if (this.remoteAIClient && this.currentTaskId) {
1063
+ await this.remoteAIClient.cancelTask(this.currentTaskId).catch(() => {});
1064
+ }
1065
+ return;
1066
+ }
1067
+
1068
+ // Mark task as cancelled when error occurs (发送 status: 'cancel')
1069
+ logger.debug(`[Session] Task failed: taskId=${this.currentTaskId}, error: ${error.message}`);
1070
+ if (this.remoteAIClient && this.currentTaskId) {
1071
+ await this.remoteAIClient.cancelTask(this.currentTaskId).catch(() => {});
1072
+ }
1073
+
1074
+ console.log(colors.error(`Error: ${error.message}`));
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Generate response using remote AI service(OAuth XAGENT 模式)
1080
+ * Support full tool calling loop
1081
+ * 与本地模式 generateResponse 保持一致
1082
+ * @param thinkingTokens - Optional thinking tokens config
1083
+ * @param existingTaskId - Optional existing taskId to reuse (for tool call continuation)
1084
+ */
1085
+ private async generateRemoteResponse(thinkingTokens: number = 0, existingTaskId?: string): Promise<void> {
1086
+ // Reuse existing taskId or create new one for this user interaction
1087
+ const taskId = existingTaskId || crypto.randomUUID();
1088
+ this.currentTaskId = taskId;
1089
+ logger.debug(`[Session] generateRemoteResponse: taskId=${taskId}, existingTaskId=${!!existingTaskId}`);
1090
+
1091
+ // Reset isFirstApiCall for new task, keep true for continuation
1092
+ if (!existingTaskId) {
1093
+ this.isFirstApiCall = true;
1094
+ }
1095
+
1096
+ // Determine status based on whether this is the first API call
1097
+ const status: 'begin' | 'continue' = this.isFirstApiCall ? 'begin' : 'continue';
1098
+ logger.debug(`[Session] Status for this call: ${status}, isFirstApiCall=${this.isFirstApiCall}`);
1099
+
1100
+ // Check if remote client is available
1101
+ if (!this.remoteAIClient) {
1102
+ console.log(colors.error('Remote AI client not initialized'));
1103
+ return;
1104
+ }
1105
+
1106
+ try {
1107
+ // Reuse generateResponse with remote client, pass taskId to avoid generating new one
1108
+ await this.generateResponse(thinkingTokens, this.remoteAIClient as any, taskId);
1109
+
1110
+ // Mark task as completed (发送 status: 'end')
1111
+ logger.debug(`[Session] Task completed: taskId=${this.currentTaskId}`);
1112
+ if (this.currentTaskId) {
1113
+ await this.remoteAIClient.completeTask(this.currentTaskId);
1114
+ }
1115
+
1116
+ } catch (error: any) {
1117
+ // Clear the operation flag
1118
+ (this as any)._isOperationInProgress = false;
1119
+
1120
+ if (error.message === 'Operation cancelled by user') {
1121
+ return;
1122
+ }
1123
+
1124
+ // Handle token invalid error - trigger re-authentication
1125
+ if (error instanceof TokenInvalidError) {
1126
+ console.log('');
1127
+ console.log(colors.warning('⚠️ Authentication expired or invalid'));
1128
+ console.log(colors.info('Your browser session has been logged out. Please log in again.'));
1129
+ console.log('');
1130
+
1131
+ // Clear invalid credentials and persist
1132
+ await this.configManager.set('apiKey', '');
1133
+ await this.configManager.set('refreshToken', '');
1134
+ await this.configManager.save('global');
1135
+
1136
+ logger.debug('[DEBUG generateRemoteResponse] Cleared invalid credentials, starting re-authentication...');
1137
+
1138
+ // Re-authenticate
1139
+ await this.setupAuthentication();
1140
+
1141
+ // Reload config to ensure we have the latest authConfig
1142
+ logger.debug('[DEBUG generateRemoteResponse] Re-authentication completed, reloading config...');
1143
+ await this.configManager.load();
1144
+ const authConfig = this.configManager.getAuthConfig();
1145
+
1146
+ logger.debug('[DEBUG generateRemoteResponse] After re-auth:');
1147
+ logger.debug(' - authConfig.apiKey exists:', !!authConfig.apiKey ? 'true' : 'false');
1148
+
1149
+ // Recreate readline interface after inquirer
1150
+ this.rl.close();
1151
+ this.rl = readline.createInterface({
1152
+ input: process.stdin,
1153
+ output: process.stdout
1154
+ });
1155
+ this.rl.on('close', () => {
1156
+ logger.debug('DEBUG: readline interface closed');
1157
+ });
1158
+
1159
+ // Reinitialize RemoteAIClient with new token
1160
+ if (authConfig.apiKey) {
1161
+ const webBaseUrl = authConfig.xagentApiBaseUrl || 'https://www.xagent-colife.net';
1162
+ logger.debug('[DEBUG generateRemoteResponse] Reinitializing RemoteAIClient with new token');
1163
+ this.remoteAIClient = new RemoteAIClient(authConfig.apiKey, webBaseUrl, authConfig.showAIDebugInfo);
1164
+ } else {
1165
+ logger.debug('[DEBUG generateRemoteResponse] WARNING: No apiKey after re-authentication!');
1166
+ }
1167
+
1168
+ // Retry the current operation
1169
+ console.log('');
1170
+ console.log(colors.info('Retrying with new authentication...'));
1171
+ console.log('');
1172
+ return this.generateRemoteResponse(thinkingTokens);
1173
+ }
1174
+
1175
+ // Mark task as cancelled when error occurs (发送 status: 'cancel')
1176
+ logger.debug(`[Session] Task failed: taskId=${this.currentTaskId}, error: ${error.message}`);
1177
+ if (this.remoteAIClient && this.currentTaskId) {
1178
+ await this.remoteAIClient.cancelTask(this.currentTaskId).catch(() => {});
1179
+ }
1180
+
1181
+ console.log(colors.error(`Error: ${error.message}`));
1182
+ return;
1183
+ }
1184
+ }
1185
+
1186
+ private async handleToolCalls(toolCalls: any[], onComplete?: () => Promise<void>): Promise<void> {
1187
+ // Mark that tool execution is in progress
1188
+ (this as any)._isOperationInProgress = true;
1189
+
1190
+ const toolRegistry = getToolRegistry();
1191
+ const showToolDetails = this.configManager.get('showToolDetails') || false;
1192
+ const indent = this.getIndent();
1193
+
1194
+ // Prepare all tool calls
1195
+ const preparedToolCalls = toolCalls.map((toolCall, index) => {
1196
+ const { name, arguments: params } = toolCall.function;
1197
+
1198
+ let parsedParams: any;
1199
+ try {
1200
+ parsedParams = typeof params === 'string' ? JSON.parse(params) : params;
1201
+ } catch (e) {
1202
+ parsedParams = params;
1203
+ }
1204
+
1205
+ return { name, params: parsedParams, index, id: toolCall.id };
1206
+ });
1207
+
1208
+ // Display all tool calls info
1209
+ for (const { name, params } of preparedToolCalls) {
1210
+ if (showToolDetails) {
1211
+ console.log('');
1212
+ console.log(`${indent}${colors.warning(`${icons.tool} Tool Call: ${name}`)}`);
1213
+ console.log(`${indent}${colors.textDim(JSON.stringify(params, null, 2))}`);
1214
+ } else {
1215
+ const toolDescription = this.getToolDescription(name, params);
1216
+ console.log(`${indent}${colors.textMuted(`${icons.loading} ${toolDescription}`)}`);
1217
+ }
1218
+ }
1219
+
1220
+ // Execute all tools in parallel
1221
+ const results = await toolRegistry.executeAll(
1222
+ preparedToolCalls.map(tc => ({ name: tc.name, params: tc.params })),
1223
+ this.executionMode
1224
+ );
1225
+
1226
+ // Process results and maintain order
1227
+ let hasError = false;
1228
+ for (const { tool, result, error } of results) {
1229
+ const toolCall = preparedToolCalls.find(tc => tc.name === tool);
1230
+ if (!toolCall) continue;
1231
+
1232
+ const { params } = toolCall;
1233
+
1234
+ if (error) {
1235
+ if (error === 'Operation cancelled by user') {
1236
+ (this as any)._isOperationInProgress = false;
1237
+ return;
1238
+ }
1239
+
1240
+ hasError = true;
1241
+
1242
+ console.log('');
1243
+ console.log(`${indent}${colors.error(`${icons.cross} Tool Error: ${tool} - ${error}`)}`);
1244
+
1245
+ // 添加详细的错误信息,包含工具名称和参数,便于 AI 理解和修正
1246
+ this.conversation.push({
1247
+ role: 'tool',
1248
+ content: JSON.stringify({
1249
+ name: tool,
1250
+ parameters: params,
1251
+ error: error
1252
+ }),
1253
+ tool_call_id: toolCall.id,
1254
+ timestamp: Date.now()
1255
+ });
1256
+ } else {
1257
+ // Use correct indent for gui-subagent tasks
1258
+ const isGuiSubagent = tool === 'task' && params?.subagent_type === 'gui-subagent';
1259
+ const displayIndent = isGuiSubagent ? indent + ' ' : indent;
1260
+
1261
+ // Always show details for todo tools so users can see their task lists
1262
+ const isTodoTool = tool === 'todo_write' || tool === 'todo_read';
1263
+
1264
+ // Special handling for edit tool with diff
1265
+ const isEditTool = tool === 'Edit';
1266
+ const hasDiff = isEditTool && result?.diff;
1267
+
1268
+ // Special handling for Write tool with file preview
1269
+ const isWriteTool = tool === 'Write';
1270
+ const hasFilePreview = isWriteTool && result?.preview;
1271
+
1272
+ // Special handling for DeleteFile tool
1273
+ const isDeleteTool = tool === 'DeleteFile';
1274
+ const hasDeleteInfo = isDeleteTool && result?.filePath;
1275
+
1276
+ // Special handling for task tool (subagent)
1277
+ const isTaskTool = tool === 'task' && params?.subagent_type;
1278
+
1279
+ // Check if tool is an MCP wrapper tool by looking up in tool registry
1280
+ const { getToolRegistry } = await import('./tools.js');
1281
+ const toolRegistry = getToolRegistry();
1282
+ const toolDef = toolRegistry.get(tool);
1283
+ const isMcpTool = toolDef && (toolDef as any)._isMcpTool === true;
1284
+
1285
+ if (isTodoTool) {
1286
+ console.log('');
1287
+ console.log(`${displayIndent}${colors.success(`${icons.check} Todo List:`)}`);
1288
+ console.log(this.renderTodoList(result?.todos || [], displayIndent));
1289
+ // Show summary if available
1290
+ if (result?.message) {
1291
+ console.log(`${displayIndent}${colors.textDim(result.message)}`);
1292
+ }
1293
+ } else if (hasDiff) {
1294
+ // Show edit result with diff
1295
+ console.log('');
1296
+ const diffOutput = renderDiff(result.diff);
1297
+ const indentedDiff = diffOutput.split('\n').map(line => `${displayIndent} ${line}`).join('\n');
1298
+ console.log(`${indentedDiff}`);
1299
+ } else if (hasFilePreview) {
1300
+ // Show new file content in diff-like style
1301
+ console.log('');
1302
+ console.log(`${displayIndent}${colors.success(`${icons.file} ${result.filePath}`)}`);
1303
+ console.log(`${displayIndent}${colors.textDim(` ${result.lineCount} lines`)}`);
1304
+ console.log('');
1305
+ console.log(renderLines(result.preview, { maxLines: 10, indent: displayIndent + ' ' }));
1306
+ } else if (hasDeleteInfo) {
1307
+ // Show DeleteFile result
1308
+ console.log('');
1309
+ console.log(`${displayIndent}${colors.success(`${icons.check} Deleted: ${result.filePath}`)}`);
1310
+ } else if (isTaskTool) {
1311
+ // Special handling for task tool (subagent) - show friendly summary
1312
+ console.log('');
1313
+ const subagentType = params.subagent_type;
1314
+ const subagentName = params.description || (params.prompt ? params.prompt.substring(0, 50).replace(/\n/g, ' ') : 'Unknown task');
1315
+
1316
+ if (result?.success) {
1317
+ console.log(`${displayIndent}${colors.success(`${icons.check} ${subagentType}: Completed`)}`);
1318
+ console.log(`${displayIndent}${colors.textDim(` Task: ${subagentName}`)}`);
1319
+ if (result.message) {
1320
+ console.log(`${displayIndent}${colors.textDim(` ${result.message}`)}`);
1321
+ }
1322
+ } else if (result?.cancelled) {
1323
+ console.log(`${displayIndent}${colors.warning(`${icons.cross} ${subagentType}: Cancelled`)}`);
1324
+ console.log(`${displayIndent}${colors.textDim(` Task: ${subagentName}`)}`);
1325
+ } else {
1326
+ console.log(`${displayIndent}${colors.error(`${icons.cross} ${subagentType}: Failed`)}`);
1327
+ console.log(`${displayIndent}${colors.textDim(` Task: ${subagentName}`)}`);
1328
+ if (result?.message) {
1329
+ console.log(`${displayIndent}${colors.textDim(` ${result.message}`)}`);
1330
+ }
1331
+ }
1332
+ } else if (isMcpTool) {
1333
+ // Special handling for MCP tools - show friendly summary
1334
+ console.log('');
1335
+ // Extract server name and tool name from tool name (format: serverName__toolName)
1336
+ let serverName = 'MCP';
1337
+ let toolDisplayName = tool;
1338
+ if (tool.includes('__')) {
1339
+ const parts = tool.split('__');
1340
+ serverName = parts[0];
1341
+ toolDisplayName = parts.slice(1).join('__');
1342
+ }
1343
+
1344
+ // Try to extract meaningful content from MCP result
1345
+ let summary = '';
1346
+ if (result?.content && Array.isArray(result.content) && result.content.length > 0) {
1347
+ const firstBlock = result.content[0];
1348
+ if (firstBlock?.type === 'text' && firstBlock?.text) {
1349
+ const text = firstBlock.text;
1350
+ if (typeof text === 'string') {
1351
+ // Detect HTML content
1352
+ if (text.trim().startsWith('<!DOCTYPE') || text.trim().startsWith('<html')) {
1353
+ summary = '[HTML content fetched]';
1354
+ } else {
1355
+ // Try to parse if it's JSON
1356
+ try {
1357
+ const parsed = JSON.parse(text);
1358
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed[0]?.title) {
1359
+ // Search results format
1360
+ summary = `Found ${parsed.length} result(s)`;
1361
+ } else if (parsed?.message) {
1362
+ summary = parsed.message;
1363
+ } else if (typeof parsed === 'string') {
1364
+ summary = parsed.substring(0, 100);
1365
+ }
1366
+ } catch {
1367
+ // Not JSON, use as-is with truncation
1368
+ summary = text.substring(0, 100);
1369
+ }
1370
+ }
1371
+ }
1372
+ }
1373
+ } else if (result?.message) {
1374
+ summary = result.message;
1375
+ }
1376
+
1377
+ if (result?.success !== false) {
1378
+ console.log(`${displayIndent}${colors.success(`${icons.check} ${serverName}: Success`)}`);
1379
+ console.log(`${displayIndent}${colors.textDim(` Tool: ${toolDisplayName}`)}`);
1380
+ if (summary) {
1381
+ console.log(`${displayIndent}${colors.textDim(` ${summary}`)}`);
1382
+ }
1383
+ } else {
1384
+ console.log(`${displayIndent}${colors.error(`${icons.cross} ${serverName}: Failed`)}`);
1385
+ console.log(`${displayIndent}${colors.textDim(` Tool: ${toolDisplayName}`)}`);
1386
+ if (result?.message || result?.error) {
1387
+ console.log(`${displayIndent}${colors.textDim(` ${result?.message || result?.error}`)}`);
1388
+ }
1389
+ }
1390
+ } else if (tool === 'InvokeSkill') {
1391
+ // Special handling for InvokeSkill - show friendly summary
1392
+ console.log('');
1393
+ const skillName = params?.skillId || 'Unknown skill';
1394
+ const taskDesc = params?.taskDescription || '';
1395
+
1396
+ if (result?.success) {
1397
+ console.log(`${displayIndent}${colors.success(`${icons.check} Skill: Completed`)}`);
1398
+ console.log(`${displayIndent}${colors.textDim(` Skill: ${skillName}`)}`);
1399
+ if (taskDesc) {
1400
+ const truncatedTask = taskDesc.length > 60 ? taskDesc.substring(0, 60) + '...' : taskDesc;
1401
+ console.log(`${displayIndent}${colors.textDim(` Task: ${truncatedTask}`)}`);
1402
+ }
1403
+ } else {
1404
+ console.log(`${displayIndent}${colors.error(`${icons.cross} Skill: Failed`)}`);
1405
+ console.log(`${displayIndent}${colors.textDim(` Skill: ${skillName}`)}`);
1406
+ if (result?.message) {
1407
+ console.log(`${displayIndent}${colors.textDim(` ${result.message}`)}`);
1408
+ }
1409
+ }
1410
+ } else if (showToolDetails) {
1411
+ console.log('');
1412
+ console.log(`${displayIndent}${colors.success(`${icons.check} Tool Result:`)}`);
1413
+ console.log(`${displayIndent}${colors.textDim(JSON.stringify(result, null, 2))}`);
1414
+ } else if (result && result.success === false) {
1415
+ // GUI task or other tool failed
1416
+ console.log(`${displayIndent}${colors.error(`${icons.cross} ${result.message || 'Failed'}`)}`);
1417
+ } else if (result) {
1418
+ // Show brief preview by default (consistent with subagent behavior)
1419
+ const resultPreview = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1420
+ const truncatedPreview = resultPreview.length > 200 ? resultPreview.substring(0, 200) + '...' : resultPreview;
1421
+ // Indent the preview
1422
+ const indentedPreview = truncatedPreview.split('\n').map(line => `${displayIndent} ${line}`).join('\n');
1423
+ console.log(`${indentedPreview}`);
1424
+ } else {
1425
+ console.log(`${displayIndent}${colors.textDim('(no result)')}`);
1426
+ }
1427
+
1428
+ const toolCallRecord: ToolCall = {
1429
+ tool,
1430
+ params,
1431
+ result,
1432
+ timestamp: Date.now()
1433
+ };
1434
+
1435
+ this.toolCalls.push(toolCallRecord);
1436
+
1437
+ // Record tool output to session manager
1438
+ await this.sessionManager.addOutput({
1439
+ role: 'tool',
1440
+ content: JSON.stringify(result),
1441
+ toolName: tool,
1442
+ toolParams: params,
1443
+ toolResult: result,
1444
+ timestamp: Date.now()
1445
+ });
1446
+
1447
+ // 统一消息格式,包含工具名称和参数
1448
+ this.conversation.push({
1449
+ role: 'tool',
1450
+ content: JSON.stringify({
1451
+ name: tool,
1452
+ parameters: params,
1453
+ result: result
1454
+ }),
1455
+ tool_call_id: toolCall.id,
1456
+ timestamp: Date.now()
1457
+ });
1458
+ }
1459
+ }
1460
+
1461
+ // Logic: Only skip returning results to main agent when user explicitly cancelled (ESC)
1462
+ // For all other cases (success, failure, errors), always return results for further processing
1463
+ const guiSubagentCancelled = preparedToolCalls.some(tc => tc.name === 'task' && tc.params?.subagent_type === 'gui-subagent' && results.some(r => r.tool === 'task' && (r.result as any)?.cancelled === true));
1464
+
1465
+ // If GUI agent was cancelled by user, don't continue generating response
1466
+ // This avoids wasting API calls and tokens on cancelled tasks
1467
+ if (guiSubagentCancelled) {
1468
+ console.log('');
1469
+ console.log(`${indent}${colors.textMuted('GUI task cancelled by user')}`);
1470
+ (this as any)._isOperationInProgress = false;
1471
+ return;
1472
+ }
1473
+
1474
+ // Handle errors and completion based on whether onComplete callback is provided
1475
+ if (hasError) {
1476
+ (this as any)._isOperationInProgress = false;
1477
+ // 不再抛出异常,而是将错误结果返回给 AI,让 AI 决定如何处理
1478
+ // 这样可以避免工具错误导致程序退出
1479
+ }
1480
+
1481
+ // Continue based on mode - 统一处理,无论是否有错误
1482
+ if (onComplete) {
1483
+ // Remote mode: use provided callback
1484
+ await onComplete();
1485
+ } else {
1486
+ // Local mode: default behavior - continue with generateResponse
1487
+ await this.generateResponse();
1488
+ }
1489
+ }
1490
+
1491
+ /**
1492
+ * Get user-friendly description for tool
1493
+ */
1494
+ private getToolDescription(toolName: string, params: any): string {
1495
+ const descriptions: Record<string, (params: any) => string> = {
1496
+ 'Read': (p) => `Read file: ${this.truncatePath(p.filePath)}`,
1497
+ 'Write': (p) => `Write file: ${this.truncatePath(p.filePath)}`,
1498
+ 'Grep': (p) => `Search text: "${p.pattern}"`,
1499
+ 'Bash': (p) => `Execute command: ${this.truncateCommand(p.command)}`,
1500
+ 'ListDirectory': (p) => `List directory: ${this.truncatePath(p.path || '.')}`,
1501
+ 'SearchFiles': (p) => `Search files: ${p.pattern}`,
1502
+ 'DeleteFile': (p) => `Delete file: ${this.truncatePath(p.filePath)}`,
1503
+ 'CreateDirectory': (p) => `Create directory: ${this.truncatePath(p.dirPath)}`,
1504
+ 'Edit': (p) => `Edit text: ${this.truncatePath(p.file_path)}`,
1505
+ 'web_search': (p) => `Web search: "${p.query}"`,
1506
+ 'todo_write': () => `Update todo list`,
1507
+ 'todo_read': () => `Read todo list`,
1508
+ 'task': (p) => `Launch subtask: ${p.description}`,
1509
+ 'ReadBashOutput': (p) => `Read task output: ${p.task_id}`,
1510
+ 'web_fetch': () => `Fetch web content`,
1511
+ 'ask_user_question': () => `Ask user`,
1512
+ 'save_memory': () => `Save memory`,
1513
+ 'exit_plan_mode': () => `Complete plan`,
1514
+ 'xml_escape': (p) => `XML escape: ${this.truncatePath(p.file_path)}`,
1515
+ 'image_read': (p) => `Read image: ${this.truncatePath(p.image_input)}`,
1516
+ // 'Skill': (p) => `Execute skill: ${p.skill}`,
1517
+ // 'ListSkills': () => `List available skills`,
1518
+ // 'GetSkillDetails': (p) => `Get skill details: ${p.skill}`,
1519
+ 'InvokeSkill': (p) => `Invoke skill: ${p.skillId} - ${this.truncatePath(p.taskDescription || '', 40)}`
1520
+ };
1521
+
1522
+ const getDescription = descriptions[toolName];
1523
+ return getDescription ? getDescription(params) : `Execute tool: ${toolName}`;
1524
+ }
1525
+
1526
+ /**
1527
+ * Truncate path for display
1528
+ */
1529
+ private truncatePath(path: string, maxLength: number = 30): string {
1530
+ if (!path) return '';
1531
+ if (path.length <= maxLength) return path;
1532
+ return '...' + path.slice(-(maxLength - 3));
1533
+ }
1534
+
1535
+ /**
1536
+ * Truncate command for display
1537
+ */
1538
+ private truncateCommand(command: string, maxLength: number = 40): string {
1539
+ if (!command) return '';
1540
+ if (command.length <= maxLength) return command;
1541
+ return command.slice(0, maxLength - 3) + '...';
1542
+ }
1543
+
1544
+ /**
1545
+ * Render todo list in a user-friendly format
1546
+ */
1547
+ private renderTodoList(todos: any[], indent: string = ''): string {
1548
+ if (!todos || todos.length === 0) {
1549
+ return `${indent}${colors.textMuted('No tasks')}`;
1550
+ }
1551
+
1552
+ const statusConfig: Record<string, { icon: string; color: (text: string) => string; label: string }> = {
1553
+ 'pending': { icon: icons.circle, color: colors.textMuted, label: 'Pending' },
1554
+ 'in_progress': { icon: icons.loading, color: colors.warning, label: 'In Progress' },
1555
+ 'completed': { icon: icons.success, color: colors.success, label: 'Completed' },
1556
+ 'failed': { icon: icons.error, color: colors.error, label: 'Failed' }
1557
+ };
1558
+
1559
+ const lines: string[] = [];
1560
+
1561
+ for (const todo of todos) {
1562
+ const config = statusConfig[todo.status] || statusConfig['pending'];
1563
+ const statusPrefix = `${config.color(config.icon)} ${config.color(config.label)}:`;
1564
+ lines.push(`${indent} ${statusPrefix} ${colors.text(todo.task)}`);
1565
+ }
1566
+
1567
+ return lines.join('\n');
1568
+ }
1569
+
1570
+ /**
1571
+ * Display AI debug information (input or output)
1572
+ */
1573
+ // AI debug info moved to ai-client.ts implementation
1574
+ // private displayAIDebugInfo(type: 'INPUT' | 'OUTPUT', data: any, extra?: any): void {
1575
+ // const indent = this.getIndent();
1576
+ // const boxChar = {
1577
+ // topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
1578
+ // horizontal: '═', vertical: '║'
1579
+ // };
1580
+ //
1581
+ // console.log('\n' + colors.border(
1582
+ // `${boxChar.topLeft}${boxChar.horizontal.repeat(58)}${boxChar.topRight}`
1583
+ // ));
1584
+ // console.log(colors.border(`${boxChar.vertical}`) + ' ' +
1585
+ // colors.primaryBright(type === 'INPUT' ? '🤖 AI INPUT DEBUG' : '📤 AI OUTPUT DEBUG') +
1586
+ // ' '.repeat(36) + colors.border(boxChar.vertical));
1587
+ // console.log(colors.border(
1588
+ // `${boxChar.vertical}${boxChar.horizontal.repeat(58)}${boxChar.vertical}`
1589
+ // ));
1590
+ //
1591
+ // if (type === 'INPUT') {
1592
+ // const messages = data as any[];
1593
+ // const tools = extra as any[];
1594
+ //
1595
+ // // System prompt
1596
+ // const systemMsg = messages.find((m: any) => m.role === 'system');
1597
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 🟫 SYSTEM: ' +
1598
+ // colors.textMuted(systemMsg?.content?.toString().substring(0, 50) || '(none)') + ' '.repeat(3) + colors.border(boxChar.vertical));
1599
+ //
1600
+ // // Messages count
1601
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 💬 MESSAGES: ' +
1602
+ // colors.text(messages.length.toString()) + ' items' + ' '.repeat(40) + colors.border(boxChar.vertical));
1603
+ //
1604
+ // // Tools count
1605
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 🔧 TOOLS: ' +
1606
+ // colors.text((tools?.length || 0).toString()) + '' + ' '.repeat(43) + colors.border(boxChar.vertical)); //
1607
+ // // Show last 2 messages
1608
+ // const recentMessages = messages.slice(-2);
1609
+ // for (const msg of recentMessages) {
1610
+ // const roleLabel: Record<string, string> = { user: '👤 USER', assistant: '🤖 ASSISTANT', tool: '🔧 TOOL' };
1611
+ // const label = roleLabel[msg.role] || msg.role;
1612
+ // const contentStr = typeof msg.content === 'string'
1613
+ // ? msg.content.substring(0, 100)
1614
+ // : JSON.stringify(msg.content).substring(0, 100);
1615
+ // console.log(colors.border(`${boxChar.vertical}`) + ` ${label}: ` +
1616
+ // colors.textDim(contentStr + '...') + ' '.repeat(Math.max(0, 50 - contentStr.length)) + colors.border(boxChar.vertical));
1617
+ // }
1618
+ // } else {
1619
+ // // OUTPUT
1620
+ // const response = data;
1621
+ // const message = extra;
1622
+ //
1623
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 📋 MODEL: ' +
1624
+ // colors.text(response.model || 'unknown') + ' '.repeat(45) + colors.border(boxChar.vertical));
1625
+ //
1626
+ // console.log(colors.border(`${boxChar.vertical}`) + ' ⏱️ TOKENS: ' +
1627
+ // colors.text(`Prompt: ${response.usage?.prompt_tokens || '?'}, Completion: ${response.usage?.completion_tokens || '?'}`) +
1628
+ // ' '.repeat(15) + colors.border(boxChar.vertical));
1629
+ //
1630
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 🔧 TOOL_CALLS: ' +
1631
+ // colors.text((message.tool_calls?.length || 0).toString()) + '' + ' '.repeat(37) + colors.border(boxChar.vertical));
1632
+ //
1633
+ // // Content preview
1634
+ // const contentStr = typeof message.content === 'string'
1635
+ // ? message.content.substring(0, 100)
1636
+ // : JSON.stringify(message.content).substring(0, 100);
1637
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 📝 CONTENT: ' +
1638
+ // colors.textDim(contentStr + '...') + ' '.repeat(Math.max(0, 40 - contentStr.length)) + colors.border(boxChar.vertical));
1639
+ // }
1640
+ //
1641
+ // console.log(colors.border(
1642
+ // `${boxChar.bottomLeft}${boxChar.horizontal.repeat(58)}${boxChar.bottomRight}`
1643
+ // ));
1644
+ // }
1645
+
1646
+ shutdown(): void {
1647
+ this.rl.close();
1648
+ this.cancellationManager.cleanup();
1649
+ this.mcpManager.disconnectAllServers();
1650
+
1651
+ // End the current session
1652
+ this.sessionManager.completeCurrentSession();
1653
+
1654
+ const separator = icons.separator.repeat(40);
1655
+ console.log('');
1656
+ console.log(colors.border(separator));
1657
+ console.log(colors.primaryBright(`${icons.sparkles} Goodbye!`));
1658
+ console.log(colors.border(separator));
1659
+ console.log('');
1660
+ }
1661
+
1662
+ /**
1663
+ * Get the RemoteAIClient instance
1664
+ * Used by tools.ts to access the remote AI client for GUI operations
1665
+ */
1666
+ getRemoteAIClient(): RemoteAIClient | null {
1667
+ return this.remoteAIClient;
1668
+ }
1669
+
1670
+ /**
1671
+ * Get the current taskId for this user interaction
1672
+ * Used by GUI operations to track the same task
1673
+ */
1674
+ getTaskId(): string | null {
1675
+ return this.currentTaskId;
1676
+ }
1677
+ }
1678
+
1679
+ export async function startInteractiveSession(): Promise<void> {
1680
+ const session = new InteractiveSession();
1681
+
1682
+ // Flag to control shutdown
1683
+ (session as any)._isShuttingDown = false;
1684
+
1685
+ // Also listen for raw Ctrl+C on stdin (works in Windows PowerShell)
1686
+ process.stdin.on('data', (chunk: Buffer) => {
1687
+ const str = chunk.toString();
1688
+ // Ctrl+C is character 0x03 or string '\u0003'
1689
+ if (str === '\u0003' || str.charCodeAt(0) === 3) {
1690
+ if (!(session as any)._isShuttingDown) {
1691
+ (session as any)._isShuttingDown = true;
1692
+
1693
+ // Print goodbye immediately
1694
+ const separator = icons.separator.repeat(40);
1695
+ process.stdout.write('\n' + colors.border(separator) + '\n');
1696
+ process.stdout.write(colors.primaryBright(`${icons.sparkles} Goodbye!`) + '\n');
1697
+ process.stdout.write(colors.border(separator) + '\n\n');
1698
+
1699
+ // Force exit
1700
+ process.exit(0);
1701
+ }
1702
+ }
1703
+ });
1704
+
1705
+ process.on('SIGINT', () => {
1706
+ if ((session as any)._isShuttingDown) {
1707
+ return;
1708
+ }
1709
+ (session as any)._isShuttingDown = true;
1710
+
1711
+ // Remove all SIGINT listeners to prevent re-entry
1712
+ process.removeAllListeners('SIGINT');
1713
+
1714
+ // Print goodbye immediately
1715
+ const separator = icons.separator.repeat(40);
1716
+ process.stdout.write('\n' + colors.border(separator) + '\n');
1717
+ process.stdout.write(colors.primaryBright(`${icons.sparkles} Goodbye!`) + '\n');
1718
+ process.stdout.write(colors.border(separator) + '\n\n');
1719
+
1720
+ // Force exit
1721
+ process.exit(0);
1722
+ });
1723
+
1724
+ await session.start();
1725
+ }
1726
+
1727
+ // Singleton session instance for access from other modules
1728
+ let singletonSession: InteractiveSession | null = null;
1729
+
1730
+ export function setSingletonSession(session: InteractiveSession): void {
1731
+ singletonSession = session;
1732
+ }
1733
+
1734
+ export function getSingletonSession(): InteractiveSession | null {
1735
+ return singletonSession;
1736
+ }