@xagent-ai/cli 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) 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 +3 -0
  6. package/dist/ai-client.d.ts.map +1 -1
  7. package/dist/ai-client.js +115 -25
  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/memory.d.ts +5 -1
  16. package/dist/memory.d.ts.map +1 -1
  17. package/dist/memory.js +77 -37
  18. package/dist/memory.js.map +1 -1
  19. package/dist/remote-ai-client.d.ts +1 -8
  20. package/dist/remote-ai-client.d.ts.map +1 -1
  21. package/dist/remote-ai-client.js +55 -61
  22. package/dist/remote-ai-client.js.map +1 -1
  23. package/dist/retry.d.ts +35 -0
  24. package/dist/retry.d.ts.map +1 -0
  25. package/dist/retry.js +166 -0
  26. package/dist/retry.js.map +1 -0
  27. package/dist/session.d.ts +0 -5
  28. package/dist/session.d.ts.map +1 -1
  29. package/dist/session.js +186 -164
  30. package/dist/session.js.map +1 -1
  31. package/dist/slash-commands.d.ts +1 -0
  32. package/dist/slash-commands.d.ts.map +1 -1
  33. package/dist/slash-commands.js +91 -9
  34. package/dist/slash-commands.js.map +1 -1
  35. package/dist/smart-approval.d.ts.map +1 -1
  36. package/dist/smart-approval.js +5 -4
  37. package/dist/smart-approval.js.map +1 -1
  38. package/dist/system-prompt-generator.d.ts.map +1 -1
  39. package/dist/system-prompt-generator.js +149 -139
  40. package/dist/system-prompt-generator.js.map +1 -1
  41. package/dist/theme.d.ts +48 -0
  42. package/dist/theme.d.ts.map +1 -1
  43. package/dist/theme.js +254 -0
  44. package/dist/theme.js.map +1 -1
  45. package/dist/tools/edit-diff.d.ts +32 -0
  46. package/dist/tools/edit-diff.d.ts.map +1 -0
  47. package/dist/tools/edit-diff.js +185 -0
  48. package/dist/tools/edit-diff.js.map +1 -0
  49. package/dist/tools/edit.d.ts +11 -0
  50. package/dist/tools/edit.d.ts.map +1 -0
  51. package/dist/tools/edit.js +129 -0
  52. package/dist/tools/edit.js.map +1 -0
  53. package/dist/tools.d.ts +19 -5
  54. package/dist/tools.d.ts.map +1 -1
  55. package/dist/tools.js +979 -631
  56. package/dist/tools.js.map +1 -1
  57. package/dist/types.d.ts +1 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/package.json +3 -2
  60. package/src/agents.ts +504 -504
  61. package/src/ai-client.ts +133 -31
  62. package/src/auth.ts +4 -4
  63. package/src/cli.ts +195 -1
  64. package/src/config.ts +3 -3
  65. package/src/memory.ts +83 -42
  66. package/src/remote-ai-client.ts +69 -76
  67. package/src/retry.ts +217 -0
  68. package/src/session.ts +1733 -1844
  69. package/src/slash-commands.ts +98 -9
  70. package/src/smart-approval.ts +626 -625
  71. package/src/system-prompt-generator.ts +853 -843
  72. package/src/theme.ts +284 -0
  73. package/src/tools.ts +3889 -3483
  74. package/src/types.ts +1 -0
package/src/session.ts CHANGED
@@ -1,1844 +1,1733 @@
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(' '.repeat(14) + '🤖 ' + colors.gradient('XAGENT CLI') + ' '.repeat(32) + colors.gradient(' ║'));
173
- console.log(' '.repeat(17) + 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.map(msg => ({
971
- role: msg.role,
972
- content: msg.content,
973
- timestamp: msg.timestamp
974
- }))
975
- ];
976
-
977
- const operationId = `ai-response-${Date.now()}`;
978
- const response = await this.cancellationManager.withCancellation(
979
- chatCompletion(messages, {
980
- tools: availableTools,
981
- toolChoice: availableTools.length > 0 ? 'auto' : 'none',
982
- thinkingTokens
983
- }),
984
- operationId
985
- );
986
-
987
- // Mark that first API call is complete
988
- this.isFirstApiCall = false;
989
-
990
- clearInterval(spinnerInterval);
991
- process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r'); // Clear spinner line
992
-
993
- const assistantMessage = response.choices[0].message;
994
-
995
- const content = typeof assistantMessage.content === 'string'
996
- ? assistantMessage.content
997
- : '';
998
- const reasoningContent = assistantMessage.reasoning_content || '';
999
- // Display reasoning content if available and thinking mode is enabled
1000
- if (reasoningContent && this.configManager.getThinkingConfig().enabled) {
1001
- this.displayThinkingContent(reasoningContent);
1002
- }
1003
-
1004
- console.log('');
1005
- console.log(`${indent}${colors.primaryBright(`${icons.robot} Assistant:`)}`);
1006
- console.log(`${indent}${colors.border(icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length))}`);
1007
- console.log('');
1008
- const renderedContent = renderMarkdown(content, (process.stdout.columns || 80) - indent.length * 2);
1009
- console.log(`${indent}${renderedContent.replace(/^/gm, indent)}`);
1010
- console.log('');
1011
-
1012
- this.conversation.push({
1013
- role: 'assistant',
1014
- content,
1015
- timestamp: Date.now(),
1016
- reasoningContent,
1017
- toolCalls: assistantMessage.tool_calls
1018
- });
1019
-
1020
- // Record output to session manager
1021
- await this.sessionManager.addOutput({
1022
- role: 'assistant',
1023
- content,
1024
- timestamp: Date.now(),
1025
- reasoningContent,
1026
- toolCalls: assistantMessage.tool_calls
1027
- });
1028
-
1029
- if (assistantMessage.tool_calls) {
1030
- await this.handleToolCalls(assistantMessage.tool_calls);
1031
- }
1032
-
1033
- if (this.checkpointManager.isEnabled()) {
1034
- await this.checkpointManager.createCheckpoint(
1035
- `Response generated at ${new Date().toLocaleString()}`,
1036
- [...this.conversation],
1037
- [...this.toolCalls]
1038
- );
1039
- }
1040
-
1041
- // Operation completed successfully, clear the flag
1042
- (this as any)._isOperationInProgress = false;
1043
- } catch (error: any) {
1044
- clearInterval(spinnerInterval);
1045
- process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
1046
-
1047
- // Clear the operation flag
1048
- (this as any)._isOperationInProgress = false;
1049
-
1050
- if (error.message === 'Operation cancelled by user') {
1051
- // Mark task as cancelled
1052
- if (this.remoteAIClient && this.currentTaskId) {
1053
- await this.remoteAIClient.cancelTask(this.currentTaskId);
1054
- }
1055
- return;
1056
- }
1057
-
1058
- // Mark task as cancelled when error occurs (发送 status: 'cancel')
1059
- logger.debug(`[Session] Task failed: taskId=${this.currentTaskId}, error: ${error.message}`);
1060
- if (this.remoteAIClient && this.currentTaskId) {
1061
- await this.remoteAIClient.cancelTask(this.currentTaskId);
1062
- }
1063
-
1064
- console.log(colors.error(`Error: ${error.message}`));
1065
- }
1066
- }
1067
-
1068
- /**
1069
- * Generate response using remote AI service(OAuth XAGENT 模式)
1070
- * Support full tool calling loop
1071
- * 与本地模式 generateResponse 保持一致
1072
- * @param thinkingTokens - Optional thinking tokens config
1073
- * @param existingTaskId - Optional existing taskId to reuse (for tool call continuation)
1074
- */
1075
- private async generateRemoteResponse(thinkingTokens: number = 0, existingTaskId?: string): Promise<void> {
1076
- // Reuse existing taskId or create new one for this user interaction
1077
- const taskId = existingTaskId || crypto.randomUUID();
1078
- this.currentTaskId = taskId;
1079
- logger.debug(`[Session] generateRemoteResponse: taskId=${taskId}, existingTaskId=${!!existingTaskId}`);
1080
-
1081
- // Reset isFirstApiCall for new task, keep true for continuation
1082
- if (!existingTaskId) {
1083
- this.isFirstApiCall = true;
1084
- }
1085
-
1086
- // Determine status based on whether this is the first API call
1087
- const status: 'begin' | 'continue' = this.isFirstApiCall ? 'begin' : 'continue';
1088
- logger.debug(`[Session] Status for this call: ${status}, isFirstApiCall=${this.isFirstApiCall}`);
1089
-
1090
- // 使用统一的 LLM Caller
1091
- const { chatCompletion, isRemote } = this.createLLMCaller(taskId, status);
1092
-
1093
- if (!isRemote) {
1094
- // 如果不是远程模式,回退到本地模式
1095
- return this.generateResponse(thinkingTokens);
1096
- }
1097
-
1098
- const indent = this.getIndent();
1099
- const thinkingText = colors.textMuted(`Thinking... (Press ESC to cancel)`);
1100
- const icon = colors.primary(icons.brain);
1101
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
1102
- let frameIndex = 0;
1103
-
1104
- // Mark that an operation is in progress
1105
- (this as any)._isOperationInProgress = true;
1106
-
1107
- // Custom spinner: only icon rotates, text stays static
1108
- const spinnerInterval = setInterval(() => {
1109
- process.stdout.write(`\r${colors.primary(frames[frameIndex])} ${icon} ${thinkingText}`);
1110
- frameIndex = (frameIndex + 1) % frames.length;
1111
- }, 120);
1112
-
1113
- try {
1114
- // Load memory (与本地模式一致)
1115
- const memory = await this.memoryManager.loadMemory();
1116
-
1117
- // Get tool definitions
1118
- const toolRegistry = getToolRegistry();
1119
- const allowedToolNames = this.currentAgent
1120
- ? this.agentManager.getAvailableToolsForAgent(this.currentAgent, this.executionMode)
1121
- : [];
1122
-
1123
- const allToolDefinitions = toolRegistry.getToolDefinitions();
1124
-
1125
- const availableTools = this.executionMode !== ExecutionMode.DEFAULT && allowedToolNames.length > 0
1126
- ? allToolDefinitions.filter((tool: any) => allowedToolNames.includes(tool.function.name))
1127
- : allToolDefinitions;
1128
-
1129
- // Convert to the format expected by backend (与本地模式一致使用 availableTools)
1130
- const tools = availableTools.map((tool: any) => ({
1131
- type: 'function' as const,
1132
- function: {
1133
- name: tool.function.name,
1134
- description: tool.function.description || '',
1135
- parameters: tool.function.parameters || {
1136
- type: 'object' as const,
1137
- properties: {}
1138
- }
1139
- }
1140
- }));
1141
-
1142
- // Generate system prompt (与本地模式一致)
1143
- const baseSystemPrompt = this.currentAgent?.systemPrompt || 'You are a helpful AI assistant.';
1144
- const systemPromptGenerator = new SystemPromptGenerator(toolRegistry, this.executionMode);
1145
- const enhancedSystemPrompt = await systemPromptGenerator.generateEnhancedSystemPrompt(baseSystemPrompt);
1146
-
1147
- // Build messages with system prompt (与本地模式一致)
1148
- const messages: ChatMessage[] = [
1149
- { role: 'system', content: `${enhancedSystemPrompt}\n\n${memory}`, timestamp: Date.now() },
1150
- ...this.conversation.map(msg => ({
1151
- role: msg.role,
1152
- content: msg.content,
1153
- timestamp: msg.timestamp
1154
- }))
1155
- ];
1156
-
1157
- // Call unified LLM API with cancellation support
1158
- const operationId = `remote-ai-response-${Date.now()}`;
1159
- const response = await this.cancellationManager.withCancellation(
1160
- chatCompletion(messages, {
1161
- tools,
1162
- toolChoice: tools.length > 0 ? 'auto' : 'none',
1163
- thinkingTokens
1164
- }),
1165
- operationId
1166
- );
1167
-
1168
- // Mark that first API call is complete
1169
- this.isFirstApiCall = false;
1170
-
1171
- clearInterval(spinnerInterval);
1172
- process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
1173
- console.log('');
1174
-
1175
- // 使用统一的响应格式(与本地模式一致)
1176
- const assistantMessage = response.choices[0].message;
1177
- const content = typeof assistantMessage.content === 'string'
1178
- ? assistantMessage.content
1179
- : '';
1180
- const reasoningContent = assistantMessage.reasoning_content || '';
1181
- const toolCalls = assistantMessage.tool_calls || [];
1182
-
1183
- // Display reasoning content if available and thinking mode is enabled (与本地模式一致)
1184
- if (reasoningContent && this.configManager.getThinkingConfig().enabled) {
1185
- this.displayThinkingContent(reasoningContent);
1186
- }
1187
-
1188
- console.log(`${indent}${colors.primaryBright(`${icons.robot} Assistant:`)}`);
1189
- console.log(`${indent}${colors.border(icons.separator.repeat(Math.min(60, process.stdout.columns || 80) - indent.length))}`);
1190
- console.log('');
1191
- const renderedContent = renderMarkdown(content, (process.stdout.columns || 80) - indent.length * 2);
1192
- console.log(`${indent}${renderedContent.replace(/^/gm, indent)}`);
1193
- console.log('');
1194
-
1195
- // Add assistant message to conversation (consistent with local mode, including reasoningContent)
1196
- this.conversation.push({
1197
- role: 'assistant',
1198
- content,
1199
- timestamp: Date.now(),
1200
- reasoningContent,
1201
- toolCalls: toolCalls
1202
- });
1203
-
1204
- // Record output to session manager (consistent with local mode, including reasoningContent and toolCalls)
1205
- await this.sessionManager.addOutput({
1206
- role: 'assistant',
1207
- content,
1208
- timestamp: Date.now(),
1209
- reasoningContent,
1210
- toolCalls
1211
- });
1212
-
1213
- // Handle tool calls
1214
- if (toolCalls.length > 0) {
1215
- await this.handleRemoteToolCalls(toolCalls);
1216
- }
1217
-
1218
- // Checkpoint support (consistent with local mode)
1219
- if (this.checkpointManager.isEnabled()) {
1220
- await this.checkpointManager.createCheckpoint(
1221
- `Response generated at ${new Date().toLocaleString()}`,
1222
- [...this.conversation],
1223
- [...this.toolCalls]
1224
- );
1225
- }
1226
-
1227
- // Operation completed successfully
1228
- (this as any)._isOperationInProgress = false;
1229
-
1230
- // Mark task as completed (发送 status: 'end')
1231
- logger.debug(`[Session] Task completed: taskId=${this.currentTaskId}`);
1232
- if (this.remoteAIClient && this.currentTaskId) {
1233
- await this.remoteAIClient.completeTask(this.currentTaskId);
1234
- }
1235
-
1236
- } catch (error: any) {
1237
- clearInterval(spinnerInterval);
1238
- process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
1239
-
1240
- // Clear the operation flag
1241
- (this as any)._isOperationInProgress = false;
1242
-
1243
- if (error.message === 'Operation cancelled by user') {
1244
- return;
1245
- }
1246
-
1247
- // Handle token invalid error - trigger re-authentication
1248
- if (error instanceof TokenInvalidError) {
1249
- console.log('');
1250
- console.log(colors.warning('⚠️ Authentication expired or invalid'));
1251
- console.log(colors.info('Your browser session has been logged out. Please log in again.'));
1252
- console.log('');
1253
-
1254
- // Clear invalid credentials and persist
1255
- // Note: Do NOT overwrite selectedAuthType - preserve user's chosen auth method
1256
- await this.configManager.set('apiKey', '');
1257
- await this.configManager.set('refreshToken', '');
1258
- await this.configManager.save('global');
1259
-
1260
- logger.debug('[DEBUG generateRemoteResponse] Cleared invalid credentials, starting re-authentication...');
1261
-
1262
- // Re-authenticate
1263
- await this.setupAuthentication();
1264
-
1265
- // Reload config to ensure we have the latest authConfig
1266
- logger.debug('[DEBUG generateRemoteResponse] Re-authentication completed, reloading config...');
1267
- await this.configManager.load();
1268
- const authConfig = this.configManager.getAuthConfig();
1269
-
1270
- logger.debug('[DEBUG generateRemoteResponse] After re-auth:');
1271
- logger.debug(' - authConfig.apiKey exists:', !!authConfig.apiKey ? 'true' : 'false');
1272
- logger.debug(' - authConfig.apiKey prefix:', authConfig.apiKey ? authConfig.apiKey.substring(0, 20) + '...' : 'empty');
1273
-
1274
- // Recreate readline interface after inquirer
1275
- this.rl.close();
1276
- this.rl = readline.createInterface({
1277
- input: process.stdin,
1278
- output: process.stdout
1279
- });
1280
- this.rl.on('close', () => {
1281
- logger.debug('DEBUG: readline interface closed');
1282
- });
1283
-
1284
- // Reinitialize RemoteAIClient with new token
1285
- if (authConfig.apiKey) {
1286
- const webBaseUrl = authConfig.xagentApiBaseUrl || 'https://154.8.140.52:443';
1287
- logger.debug('[DEBUG generateRemoteResponse] Reinitializing RemoteAIClient with new token');
1288
- const newWebBaseUrl = authConfig.xagentApiBaseUrl || 'https://154.8.140.52:443';
1289
- this.remoteAIClient = new RemoteAIClient(authConfig.apiKey, newWebBaseUrl, authConfig.showAIDebugInfo);
1290
- } else {
1291
- logger.debug('[DEBUG generateRemoteResponse] WARNING: No apiKey after re-authentication!');
1292
- }
1293
-
1294
- // Retry the current operation
1295
- console.log('');
1296
- console.log(colors.info('Retrying with new authentication...'));
1297
- console.log('');
1298
- return this.generateRemoteResponse(thinkingTokens);
1299
- }
1300
-
1301
- // Mark task as cancelled when error occurs (发送 status: 'cancel')
1302
- logger.debug(`[Session] Task failed: taskId=${this.currentTaskId}, error: ${error.message}`);
1303
- if (this.remoteAIClient && this.currentTaskId) {
1304
- await this.remoteAIClient.cancelTask(this.currentTaskId);
1305
- }
1306
-
1307
- console.log(colors.error(`Error: ${error.message}`));
1308
- return;
1309
- }
1310
- }
1311
-
1312
- private async handleToolCalls(toolCalls: any[]): Promise<void> {
1313
- // Mark that tool execution is in progress
1314
- (this as any)._isOperationInProgress = true;
1315
-
1316
- const toolRegistry = getToolRegistry();
1317
- const showToolDetails = this.configManager.get('showToolDetails') || false;
1318
- const indent = this.getIndent();
1319
-
1320
- // Prepare all tool calls
1321
- const preparedToolCalls = toolCalls.map((toolCall, index) => {
1322
- const { name, arguments: params } = toolCall.function;
1323
-
1324
- let parsedParams: any;
1325
- try {
1326
- parsedParams = typeof params === 'string' ? JSON.parse(params) : params;
1327
- } catch (e) {
1328
- parsedParams = params;
1329
- }
1330
-
1331
- return { name, params: parsedParams, index };
1332
- });
1333
-
1334
- // Display all tool calls info
1335
- for (const { name, params } of preparedToolCalls) {
1336
- if (showToolDetails) {
1337
- console.log('');
1338
- console.log(`${indent}${colors.warning(`${icons.tool} Tool Call: ${name}`)}`);
1339
- console.log(`${indent}${colors.textDim(JSON.stringify(params, null, 2))}`);
1340
- } else {
1341
- const toolDescription = this.getToolDescription(name, params);
1342
- console.log('');
1343
- console.log(`${indent}${colors.textMuted(`${icons.loading} ${toolDescription}`)}`);
1344
- }
1345
- }
1346
-
1347
- // Execute all tools in parallel
1348
- const results = await toolRegistry.executeAll(
1349
- preparedToolCalls.map(tc => ({ name: tc.name, params: tc.params })),
1350
- this.executionMode
1351
- );
1352
-
1353
- // Process results and maintain order
1354
- for (const { tool, result, error } of results) {
1355
- const toolCall = preparedToolCalls.find(tc => tc.name === tool);
1356
- if (!toolCall) continue;
1357
-
1358
- const { params } = toolCall;
1359
-
1360
- if (error) {
1361
- // Clear the operation flag
1362
- (this as any)._isOperationInProgress = false;
1363
-
1364
- if (error === 'Operation cancelled by user') {
1365
- return;
1366
- }
1367
-
1368
- console.log('');
1369
- console.log(`${indent}${colors.error(`${icons.cross} Tool Error: ${error}`)}`);
1370
-
1371
- this.conversation.push({
1372
- role: 'tool',
1373
- content: JSON.stringify({ error }),
1374
- timestamp: Date.now()
1375
- });
1376
- } else {
1377
- // Use correct indent for gui-subagent tasks
1378
- const isGuiSubagent = tool === 'task' && params?.subagent_type === 'gui-subagent';
1379
- const displayIndent = isGuiSubagent ? indent + ' ' : indent;
1380
-
1381
- // Always show details for todo tools so users can see their task lists
1382
- const isTodoTool = tool === 'todo_write' || tool === 'todo_read';
1383
- if (isTodoTool) {
1384
- console.log('');
1385
- console.log(`${displayIndent}${colors.success(`${icons.check} Todo List:`)}`);
1386
- console.log(this.renderTodoList(result?.todos || [], displayIndent));
1387
- // Show summary if available
1388
- if (result?.message) {
1389
- console.log(`${displayIndent}${colors.textDim(result.message)}`);
1390
- }
1391
- } else if (showToolDetails) {
1392
- console.log('');
1393
- console.log(`${displayIndent}${colors.success(`${icons.check} Tool Result:`)}`);
1394
- console.log(`${displayIndent}${colors.textDim(JSON.stringify(result, null, 2))}`);
1395
- } else if (result && result.success === false) {
1396
- // GUI task or other tool failed
1397
- console.log(`${displayIndent}${colors.error(`${icons.cross} ${result.message || 'Failed'}`)}`);
1398
- } else if (result) {
1399
- console.log(`${displayIndent}${colors.success(`${icons.check} Completed`)}`);
1400
- } else {
1401
- console.log(`${displayIndent}${colors.textDim('(no result)')}`);
1402
- }
1403
-
1404
- const toolCallRecord: ToolCall = {
1405
- tool,
1406
- params,
1407
- result,
1408
- timestamp: Date.now()
1409
- };
1410
-
1411
- this.toolCalls.push(toolCallRecord);
1412
-
1413
- // Record tool output to session manager
1414
- await this.sessionManager.addOutput({
1415
- role: 'tool',
1416
- content: JSON.stringify(result),
1417
- toolName: tool,
1418
- toolParams: params,
1419
- toolResult: result,
1420
- timestamp: Date.now()
1421
- });
1422
-
1423
- this.conversation.push({
1424
- role: 'tool',
1425
- content: JSON.stringify(result),
1426
- timestamp: Date.now()
1427
- });
1428
- }
1429
- }
1430
-
1431
- // Logic: Only skip returning results to main agent when user explicitly cancelled (ESC)
1432
- // For all other cases (success, failure, errors), always return results for further processing
1433
- const guiSubagentFailed = preparedToolCalls.some(tc => tc.name === 'task' && tc.params?.subagent_type === 'gui-subagent');
1434
- 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));
1435
-
1436
- // If GUI agent was cancelled by user, don't continue generating response
1437
- // This avoids wasting API calls and tokens on cancelled tasks
1438
- if (guiSubagentCancelled) {
1439
- console.log('');
1440
- console.log(`${indent}${colors.textMuted('GUI task cancelled by user')}`);
1441
- (this as any)._isOperationInProgress = false;
1442
- return;
1443
- }
1444
-
1445
- // For all other cases (GUI success/failure, other tool errors), return results to main agent
1446
- // This allows main agent to decide how to handle failures (retry, fallback, user notification, etc.)
1447
- await this.generateResponse();
1448
- }
1449
-
1450
- /**
1451
- * Get user-friendly description for tool
1452
- */
1453
- private getToolDescription(toolName: string, params: any): string {
1454
- const descriptions: Record<string, (params: any) => string> = {
1455
- 'Read': (p) => `Read file: ${this.truncatePath(p.filePath)}`,
1456
- 'Write': (p) => `Write file: ${this.truncatePath(p.filePath)}`,
1457
- 'Grep': (p) => `Search text: "${p.pattern}"`,
1458
- 'Bash': (p) => `Execute command: ${this.truncateCommand(p.command)}`,
1459
- 'ListDirectory': (p) => `List directory: ${this.truncatePath(p.path || '.')}`,
1460
- 'SearchCodebase': (p) => `Search files: ${p.pattern}`,
1461
- 'DeleteFile': (p) => `Delete file: ${this.truncatePath(p.filePath)}`,
1462
- 'CreateDirectory': (p) => `Create directory: ${this.truncatePath(p.dirPath)}`,
1463
- 'replace': (p) => `Replace text: ${this.truncatePath(p.file_path)}`,
1464
- 'web_search': (p) => `Web search: "${p.query}"`,
1465
- 'todo_write': () => `Update todo list`,
1466
- 'todo_read': () => `Read todo list`,
1467
- 'task': (p) => `Launch subtask: ${p.description}`,
1468
- 'ReadBashOutput': (p) => `Read task output: ${p.task_id}`,
1469
- 'web_fetch': () => `Fetch web content`,
1470
- 'ask_user_question': () => `Ask user`,
1471
- 'save_memory': () => `Save memory`,
1472
- 'exit_plan_mode': () => `Complete plan`,
1473
- 'xml_escape': (p) => `XML escape: ${this.truncatePath(p.file_path)}`,
1474
- 'image_read': (p) => `Read image: ${this.truncatePath(p.image_input)}`,
1475
- // 'Skill': (p) => `Execute skill: ${p.skill}`,
1476
- // 'ListSkills': () => `List available skills`,
1477
- // 'GetSkillDetails': (p) => `Get skill details: ${p.skill}`,
1478
- 'InvokeSkill': (p) => `Invoke skill: ${p.skillId} - ${this.truncatePath(p.taskDescription || '', 40)}`
1479
- };
1480
-
1481
- const getDescription = descriptions[toolName];
1482
- return getDescription ? getDescription(params) : `Execute tool: ${toolName}`;
1483
- }
1484
-
1485
- /**
1486
- * Handle tool calls for remote AI mode
1487
- * Executes tools and then continues the conversation with results
1488
- */
1489
- private async handleRemoteToolCalls(toolCalls: any[]): Promise<void> {
1490
- // Mark that tool execution is in progress
1491
- (this as any)._isOperationInProgress = true;
1492
-
1493
- const toolRegistry = getToolRegistry();
1494
- const showToolDetails = this.configManager.get('showToolDetails') || false;
1495
- const indent = this.getIndent();
1496
-
1497
- // Prepare all tool calls
1498
- const preparedToolCalls = toolCalls.map((toolCall, index) => {
1499
- const { name, arguments: params } = toolCall.function;
1500
-
1501
- let parsedParams: any;
1502
- try {
1503
- parsedParams = typeof params === 'string' ? JSON.parse(params) : params;
1504
- } catch (e) {
1505
- parsedParams = params;
1506
- }
1507
-
1508
- return { name, params: parsedParams, index };
1509
- });
1510
-
1511
- // Display all tool calls info
1512
- for (const { name, params } of preparedToolCalls) {
1513
- if (showToolDetails) {
1514
- console.log('');
1515
- console.log(`${indent}${colors.warning(`${icons.tool} Tool Call: ${name}`)}`);
1516
- console.log(`${indent}${colors.textDim(JSON.stringify(params, null, 2))}`);
1517
- } else {
1518
- const toolDescription = this.getToolDescription(name, params);
1519
- console.log('');
1520
- console.log(`${indent}${colors.textMuted(`${icons.loading} ${toolDescription}`)}`);
1521
- }
1522
- }
1523
-
1524
- // Execute all tools in parallel
1525
- const results = await toolRegistry.executeAll(
1526
- preparedToolCalls.map(tc => ({ name: tc.name, params: tc.params })),
1527
- this.executionMode
1528
- );
1529
-
1530
- // Process results and maintain order
1531
- let hasError = false;
1532
- for (const { tool, result, error } of results) {
1533
- const toolCall = preparedToolCalls.find(tc => tc.name === tool);
1534
- if (!toolCall) continue;
1535
-
1536
- const { params } = toolCall;
1537
-
1538
- if (error) {
1539
- // Clear the operation flag
1540
- (this as any)._isOperationInProgress = false;
1541
-
1542
- if (error === 'Operation cancelled by user') {
1543
- return;
1544
- }
1545
-
1546
- hasError = true;
1547
-
1548
- console.log('');
1549
- console.log(`${indent}${colors.error(`${icons.cross} Tool Error: ${error}`)}`);
1550
-
1551
- this.conversation.push({
1552
- role: 'tool',
1553
- content: JSON.stringify({ error }),
1554
- timestamp: Date.now()
1555
- });
1556
- } else {
1557
- // Use correct indent for gui-subagent tasks
1558
- const isGuiSubagent = tool === 'task' && params?.subagent_type === 'gui-subagent';
1559
- const displayIndent = isGuiSubagent ? indent + ' ' : indent;
1560
-
1561
- // Always show details for todo tools so users can see their task lists
1562
- const isTodoTool = tool === 'todo_write' || tool === 'todo_read';
1563
- if (isTodoTool) {
1564
- console.log('');
1565
- console.log(`${displayIndent}${colors.success(`${icons.check} Todo List:`)}`);
1566
- console.log(this.renderTodoList(result.todos || result.todos, displayIndent));
1567
- // Show summary if available
1568
- if (result.message) {
1569
- console.log(`${displayIndent}${colors.textDim(result.message)}`);
1570
- }
1571
- } else if (showToolDetails) {
1572
- console.log('');
1573
- console.log(`${displayIndent}${colors.success(`${icons.check} Tool Result:`)}`);
1574
- console.log(`${displayIndent}${colors.textDim(JSON.stringify(result, null, 2))}`);
1575
- } else if (result.success === false) {
1576
- // GUI task or other tool failed
1577
- console.log(`${displayIndent}${colors.error(`${icons.cross} ${result.message || 'Failed'}`)}`);
1578
- } else {
1579
- console.log(`${displayIndent}${colors.success(`${icons.check} Completed`)}`);
1580
- }
1581
-
1582
- const toolCallRecord: ToolCall = {
1583
- tool,
1584
- params,
1585
- result,
1586
- timestamp: Date.now()
1587
- };
1588
-
1589
- this.toolCalls.push(toolCallRecord);
1590
-
1591
- // Record tool output to session manager
1592
- await this.sessionManager.addOutput({
1593
- role: 'tool',
1594
- content: JSON.stringify(result),
1595
- toolName: tool,
1596
- toolParams: params,
1597
- toolResult: result,
1598
- timestamp: Date.now()
1599
- });
1600
-
1601
- this.conversation.push({
1602
- role: 'tool',
1603
- content: JSON.stringify(result),
1604
- timestamp: Date.now()
1605
- });
1606
- }
1607
- }
1608
-
1609
- // Logic: Only skip returning results to main agent when user explicitly cancelled (ESC)
1610
- // For all other cases (success, failure, errors), always return results for further processing
1611
- const guiSubagentFailed = preparedToolCalls.some(tc => tc.name === 'task' && tc.params?.subagent_type === 'gui-subagent');
1612
- 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));
1613
-
1614
- // If GUI agent was cancelled by user, don't continue generating response
1615
- // This avoids wasting API calls and tokens on cancelled tasks
1616
- if (guiSubagentCancelled) {
1617
- console.log('');
1618
- console.log(`${indent}${colors.textMuted('GUI task cancelled by user')}`);
1619
- (this as any)._isOperationInProgress = false;
1620
- return;
1621
- }
1622
-
1623
- // If any tool call failed, throw error to mark task as cancelled
1624
- if (hasError) {
1625
- throw new Error('Tool execution failed');
1626
- }
1627
-
1628
- // For all other cases (GUI success/failure, other tool errors), return results to main agent
1629
- // This allows main agent to decide how to handle failures (retry, fallback, user notification, etc.)
1630
- // Reuse existing taskId instead of generating new one
1631
- await this.generateRemoteResponse(0, this.currentTaskId || undefined);
1632
- }
1633
-
1634
- /**
1635
- * Truncate path for display
1636
- */
1637
- private truncatePath(path: string, maxLength: number = 30): string {
1638
- if (!path) return '';
1639
- if (path.length <= maxLength) return path;
1640
- return '...' + path.slice(-(maxLength - 3));
1641
- }
1642
-
1643
- /**
1644
- * Truncate command for display
1645
- */
1646
- private truncateCommand(command: string, maxLength: number = 40): string {
1647
- if (!command) return '';
1648
- if (command.length <= maxLength) return command;
1649
- return command.slice(0, maxLength - 3) + '...';
1650
- }
1651
-
1652
- /**
1653
- * Render todo list in a user-friendly format
1654
- */
1655
- private renderTodoList(todos: any[], indent: string = ''): string {
1656
- if (!todos || todos.length === 0) {
1657
- return `${indent}${colors.textMuted('No tasks')}`;
1658
- }
1659
-
1660
- const statusConfig: Record<string, { icon: string; color: (text: string) => string; label: string }> = {
1661
- 'pending': { icon: icons.circle, color: colors.textMuted, label: 'Pending' },
1662
- 'in_progress': { icon: icons.loading, color: colors.warning, label: 'In Progress' },
1663
- 'completed': { icon: icons.success, color: colors.success, label: 'Completed' },
1664
- 'failed': { icon: icons.error, color: colors.error, label: 'Failed' }
1665
- };
1666
-
1667
- const lines: string[] = [];
1668
-
1669
- for (const todo of todos) {
1670
- const config = statusConfig[todo.status] || statusConfig['pending'];
1671
- const statusPrefix = `${config.color(config.icon)} ${config.color(config.label)}:`;
1672
- lines.push(`${indent} ${statusPrefix} ${colors.text(todo.task)}`);
1673
- }
1674
-
1675
- return lines.join('\n');
1676
- }
1677
-
1678
- /**
1679
- * Display AI debug information (input or output)
1680
- */
1681
- // AI debug info moved to ai-client.ts implementation
1682
- // private displayAIDebugInfo(type: 'INPUT' | 'OUTPUT', data: any, extra?: any): void {
1683
- // const indent = this.getIndent();
1684
- // const boxChar = {
1685
- // topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '',
1686
- // horizontal: '', vertical: '║'
1687
- // };
1688
- //
1689
- // console.log('\n' + colors.border(
1690
- // `${boxChar.topLeft}${boxChar.horizontal.repeat(58)}${boxChar.topRight}`
1691
- // ));
1692
- // console.log(colors.border(`${boxChar.vertical}`) + ' ' +
1693
- // colors.primaryBright(type === 'INPUT' ? '🤖 AI INPUT DEBUG' : '📤 AI OUTPUT DEBUG') +
1694
- // ' '.repeat(36) + colors.border(boxChar.vertical));
1695
- // console.log(colors.border(
1696
- // `${boxChar.vertical}${boxChar.horizontal.repeat(58)}${boxChar.vertical}`
1697
- // ));
1698
- //
1699
- // if (type === 'INPUT') {
1700
- // const messages = data as any[];
1701
- // const tools = extra as any[];
1702
- //
1703
- // // System prompt
1704
- // const systemMsg = messages.find((m: any) => m.role === 'system');
1705
- // console.log(colors.border(`${boxChar.vertical}`) + ' 🟫 SYSTEM: ' +
1706
- // colors.textMuted(systemMsg?.content?.toString().substring(0, 50) || '(none)') + ' '.repeat(3) + colors.border(boxChar.vertical));
1707
- //
1708
- // // Messages count
1709
- // console.log(colors.border(`${boxChar.vertical}`) + ' 💬 MESSAGES: ' +
1710
- // colors.text(messages.length.toString()) + ' items' + ' '.repeat(40) + colors.border(boxChar.vertical));
1711
- //
1712
- // // Tools count
1713
- // console.log(colors.border(`${boxChar.vertical}`) + ' 🔧 TOOLS: ' +
1714
- // colors.text((tools?.length || 0).toString()) + '' + ' '.repeat(43) + colors.border(boxChar.vertical)); //
1715
- // // Show last 2 messages
1716
- // const recentMessages = messages.slice(-2);
1717
- // for (const msg of recentMessages) {
1718
- // const roleLabel: Record<string, string> = { user: '👤 USER', assistant: '🤖 ASSISTANT', tool: '🔧 TOOL' };
1719
- // const label = roleLabel[msg.role] || msg.role;
1720
- // const contentStr = typeof msg.content === 'string'
1721
- // ? msg.content.substring(0, 100)
1722
- // : JSON.stringify(msg.content).substring(0, 100);
1723
- // console.log(colors.border(`${boxChar.vertical}`) + ` ${label}: ` +
1724
- // colors.textDim(contentStr + '...') + ' '.repeat(Math.max(0, 50 - contentStr.length)) + colors.border(boxChar.vertical));
1725
- // }
1726
- // } else {
1727
- // // OUTPUT
1728
- // const response = data;
1729
- // const message = extra;
1730
- //
1731
- // console.log(colors.border(`${boxChar.vertical}`) + ' 📋 MODEL: ' +
1732
- // colors.text(response.model || 'unknown') + ' '.repeat(45) + colors.border(boxChar.vertical));
1733
- //
1734
- // console.log(colors.border(`${boxChar.vertical}`) + ' ⏱️ TOKENS: ' +
1735
- // colors.text(`Prompt: ${response.usage?.prompt_tokens || '?'}, Completion: ${response.usage?.completion_tokens || '?'}`) +
1736
- // ' '.repeat(15) + colors.border(boxChar.vertical));
1737
- //
1738
- // console.log(colors.border(`${boxChar.vertical}`) + ' 🔧 TOOL_CALLS: ' +
1739
- // colors.text((message.tool_calls?.length || 0).toString()) + '' + ' '.repeat(37) + colors.border(boxChar.vertical));
1740
- //
1741
- // // Content preview
1742
- // const contentStr = typeof message.content === 'string'
1743
- // ? message.content.substring(0, 100)
1744
- // : JSON.stringify(message.content).substring(0, 100);
1745
- // console.log(colors.border(`${boxChar.vertical}`) + ' 📝 CONTENT: ' +
1746
- // colors.textDim(contentStr + '...') + ' '.repeat(Math.max(0, 40 - contentStr.length)) + colors.border(boxChar.vertical));
1747
- // }
1748
- //
1749
- // console.log(colors.border(
1750
- // `${boxChar.bottomLeft}${boxChar.horizontal.repeat(58)}${boxChar.bottomRight}`
1751
- // ));
1752
- // }
1753
-
1754
- shutdown(): void {
1755
- this.rl.close();
1756
- this.cancellationManager.cleanup();
1757
- this.mcpManager.disconnectAllServers();
1758
-
1759
- // End the current session
1760
- this.sessionManager.completeCurrentSession();
1761
-
1762
- const separator = icons.separator.repeat(40);
1763
- console.log('');
1764
- console.log(colors.border(separator));
1765
- console.log(colors.primaryBright(`${icons.sparkles} Goodbye!`));
1766
- console.log(colors.border(separator));
1767
- console.log('');
1768
- }
1769
-
1770
- /**
1771
- * Get the RemoteAIClient instance
1772
- * Used by tools.ts to access the remote AI client for GUI operations
1773
- */
1774
- getRemoteAIClient(): RemoteAIClient | null {
1775
- return this.remoteAIClient;
1776
- }
1777
-
1778
- /**
1779
- * Get the current taskId for this user interaction
1780
- * Used by GUI operations to track the same task
1781
- */
1782
- getTaskId(): string | null {
1783
- return this.currentTaskId;
1784
- }
1785
- }
1786
-
1787
- export async function startInteractiveSession(): Promise<void> {
1788
- const session = new InteractiveSession();
1789
-
1790
- // Flag to control shutdown
1791
- (session as any)._isShuttingDown = false;
1792
-
1793
- // Also listen for raw Ctrl+C on stdin (works in Windows PowerShell)
1794
- process.stdin.on('data', (chunk: Buffer) => {
1795
- const str = chunk.toString();
1796
- // Ctrl+C is character 0x03 or string '\u0003'
1797
- if (str === '\u0003' || str.charCodeAt(0) === 3) {
1798
- if (!(session as any)._isShuttingDown) {
1799
- (session as any)._isShuttingDown = true;
1800
-
1801
- // Print goodbye immediately
1802
- const separator = icons.separator.repeat(40);
1803
- process.stdout.write('\n' + colors.border(separator) + '\n');
1804
- process.stdout.write(colors.primaryBright(`${icons.sparkles} Goodbye!`) + '\n');
1805
- process.stdout.write(colors.border(separator) + '\n\n');
1806
-
1807
- // Force exit
1808
- process.exit(0);
1809
- }
1810
- }
1811
- });
1812
-
1813
- process.on('SIGINT', () => {
1814
- if ((session as any)._isShuttingDown) {
1815
- return;
1816
- }
1817
- (session as any)._isShuttingDown = true;
1818
-
1819
- // Remove all SIGINT listeners to prevent re-entry
1820
- process.removeAllListeners('SIGINT');
1821
-
1822
- // Print goodbye immediately
1823
- const separator = icons.separator.repeat(40);
1824
- process.stdout.write('\n' + colors.border(separator) + '\n');
1825
- process.stdout.write(colors.primaryBright(`${icons.sparkles} Goodbye!`) + '\n');
1826
- process.stdout.write(colors.border(separator) + '\n\n');
1827
-
1828
- // Force exit
1829
- process.exit(0);
1830
- });
1831
-
1832
- await session.start();
1833
- }
1834
-
1835
- // Singleton session instance for access from other modules
1836
- let singletonSession: InteractiveSession | null = null;
1837
-
1838
- export function setSingletonSession(session: InteractiveSession): void {
1839
- singletonSession = session;
1840
- }
1841
-
1842
- export function getSingletonSession(): InteractiveSession | null {
1843
- return singletonSession;
1844
- }
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);
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);
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);
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
+ // Clear the operation flag
1236
+ (this as any)._isOperationInProgress = false;
1237
+
1238
+ if (error === 'Operation cancelled by user') {
1239
+ return;
1240
+ }
1241
+
1242
+ hasError = true;
1243
+
1244
+ console.log('');
1245
+ console.log(`${indent}${colors.error(`${icons.cross} Tool Error: ${error}`)}`);
1246
+
1247
+ this.conversation.push({
1248
+ role: 'tool',
1249
+ content: JSON.stringify({ error }),
1250
+ tool_call_id: toolCall.id,
1251
+ timestamp: Date.now()
1252
+ });
1253
+ } else {
1254
+ // Use correct indent for gui-subagent tasks
1255
+ const isGuiSubagent = tool === 'task' && params?.subagent_type === 'gui-subagent';
1256
+ const displayIndent = isGuiSubagent ? indent + ' ' : indent;
1257
+
1258
+ // Always show details for todo tools so users can see their task lists
1259
+ const isTodoTool = tool === 'todo_write' || tool === 'todo_read';
1260
+
1261
+ // Special handling for edit tool with diff
1262
+ const isEditTool = tool === 'Edit';
1263
+ const hasDiff = isEditTool && result?.diff;
1264
+
1265
+ // Special handling for Write tool with file preview
1266
+ const isWriteTool = tool === 'Write';
1267
+ const hasFilePreview = isWriteTool && result?.preview;
1268
+
1269
+ // Special handling for DeleteFile tool
1270
+ const isDeleteTool = tool === 'DeleteFile';
1271
+ const hasDeleteInfo = isDeleteTool && result?.filePath;
1272
+
1273
+ // Special handling for task tool (subagent)
1274
+ const isTaskTool = tool === 'task' && params?.subagent_type;
1275
+
1276
+ // Check if tool is an MCP wrapper tool by looking up in tool registry
1277
+ const { getToolRegistry } = await import('./tools.js');
1278
+ const toolRegistry = getToolRegistry();
1279
+ const toolDef = toolRegistry.get(tool);
1280
+ const isMcpTool = toolDef && (toolDef as any)._isMcpTool === true;
1281
+
1282
+ if (isTodoTool) {
1283
+ console.log('');
1284
+ console.log(`${displayIndent}${colors.success(`${icons.check} Todo List:`)}`);
1285
+ console.log(this.renderTodoList(result?.todos || [], displayIndent));
1286
+ // Show summary if available
1287
+ if (result?.message) {
1288
+ console.log(`${displayIndent}${colors.textDim(result.message)}`);
1289
+ }
1290
+ } else if (hasDiff) {
1291
+ // Show edit result with diff
1292
+ console.log('');
1293
+ const diffOutput = renderDiff(result.diff);
1294
+ const indentedDiff = diffOutput.split('\n').map(line => `${displayIndent} ${line}`).join('\n');
1295
+ console.log(`${indentedDiff}`);
1296
+ } else if (hasFilePreview) {
1297
+ // Show new file content in diff-like style
1298
+ console.log('');
1299
+ console.log(`${displayIndent}${colors.success(`${icons.file} ${result.filePath}`)}`);
1300
+ console.log(`${displayIndent}${colors.textDim(` ${result.lineCount} lines`)}`);
1301
+ console.log('');
1302
+ console.log(renderLines(result.preview, { maxLines: 10, indent: displayIndent + ' ' }));
1303
+ } else if (hasDeleteInfo) {
1304
+ // Show DeleteFile result
1305
+ console.log('');
1306
+ console.log(`${displayIndent}${colors.success(`${icons.check} Deleted: ${result.filePath}`)}`);
1307
+ } else if (isTaskTool) {
1308
+ // Special handling for task tool (subagent) - show friendly summary
1309
+ console.log('');
1310
+ const subagentType = params.subagent_type;
1311
+ const subagentName = params.description || (params.prompt ? params.prompt.substring(0, 50).replace(/\n/g, ' ') : 'Unknown task');
1312
+
1313
+ if (result?.success) {
1314
+ console.log(`${displayIndent}${colors.success(`${icons.check} ${subagentType}: Completed`)}`);
1315
+ console.log(`${displayIndent}${colors.textDim(` Task: ${subagentName}`)}`);
1316
+ if (result.message) {
1317
+ console.log(`${displayIndent}${colors.textDim(` ${result.message}`)}`);
1318
+ }
1319
+ } else if (result?.cancelled) {
1320
+ console.log(`${displayIndent}${colors.warning(`${icons.cross} ${subagentType}: Cancelled`)}`);
1321
+ console.log(`${displayIndent}${colors.textDim(` Task: ${subagentName}`)}`);
1322
+ } else {
1323
+ console.log(`${displayIndent}${colors.error(`${icons.cross} ${subagentType}: Failed`)}`);
1324
+ console.log(`${displayIndent}${colors.textDim(` Task: ${subagentName}`)}`);
1325
+ if (result?.message) {
1326
+ console.log(`${displayIndent}${colors.textDim(` ${result.message}`)}`);
1327
+ }
1328
+ }
1329
+ } else if (isMcpTool) {
1330
+ // Special handling for MCP tools - show friendly summary
1331
+ console.log('');
1332
+ // Extract server name and tool name from tool name (format: serverName__toolName)
1333
+ let serverName = 'MCP';
1334
+ let toolDisplayName = tool;
1335
+ if (tool.includes('__')) {
1336
+ const parts = tool.split('__');
1337
+ serverName = parts[0];
1338
+ toolDisplayName = parts.slice(1).join('__');
1339
+ }
1340
+
1341
+ // Try to extract meaningful content from MCP result
1342
+ let summary = '';
1343
+ if (result?.content && Array.isArray(result.content) && result.content.length > 0) {
1344
+ const firstBlock = result.content[0];
1345
+ if (firstBlock?.type === 'text' && firstBlock?.text) {
1346
+ const text = firstBlock.text;
1347
+ if (typeof text === 'string') {
1348
+ // Detect HTML content
1349
+ if (text.trim().startsWith('<!DOCTYPE') || text.trim().startsWith('<html')) {
1350
+ summary = '[HTML content fetched]';
1351
+ } else {
1352
+ // Try to parse if it's JSON
1353
+ try {
1354
+ const parsed = JSON.parse(text);
1355
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed[0]?.title) {
1356
+ // Search results format
1357
+ summary = `Found ${parsed.length} result(s)`;
1358
+ } else if (parsed?.message) {
1359
+ summary = parsed.message;
1360
+ } else if (typeof parsed === 'string') {
1361
+ summary = parsed.substring(0, 100);
1362
+ }
1363
+ } catch {
1364
+ // Not JSON, use as-is with truncation
1365
+ summary = text.substring(0, 100);
1366
+ }
1367
+ }
1368
+ }
1369
+ }
1370
+ } else if (result?.message) {
1371
+ summary = result.message;
1372
+ }
1373
+
1374
+ if (result?.success !== false) {
1375
+ console.log(`${displayIndent}${colors.success(`${icons.check} ${serverName}: Success`)}`);
1376
+ console.log(`${displayIndent}${colors.textDim(` Tool: ${toolDisplayName}`)}`);
1377
+ if (summary) {
1378
+ console.log(`${displayIndent}${colors.textDim(` ${summary}`)}`);
1379
+ }
1380
+ } else {
1381
+ console.log(`${displayIndent}${colors.error(`${icons.cross} ${serverName}: Failed`)}`);
1382
+ console.log(`${displayIndent}${colors.textDim(` Tool: ${toolDisplayName}`)}`);
1383
+ if (result?.message || result?.error) {
1384
+ console.log(`${displayIndent}${colors.textDim(` ${result?.message || result?.error}`)}`);
1385
+ }
1386
+ }
1387
+ } else if (tool === 'InvokeSkill') {
1388
+ // Special handling for InvokeSkill - show friendly summary
1389
+ console.log('');
1390
+ const skillName = params?.skillId || 'Unknown skill';
1391
+ const taskDesc = params?.taskDescription || '';
1392
+
1393
+ if (result?.success) {
1394
+ console.log(`${displayIndent}${colors.success(`${icons.check} Skill: Completed`)}`);
1395
+ console.log(`${displayIndent}${colors.textDim(` Skill: ${skillName}`)}`);
1396
+ if (taskDesc) {
1397
+ const truncatedTask = taskDesc.length > 60 ? taskDesc.substring(0, 60) + '...' : taskDesc;
1398
+ console.log(`${displayIndent}${colors.textDim(` Task: ${truncatedTask}`)}`);
1399
+ }
1400
+ } else {
1401
+ console.log(`${displayIndent}${colors.error(`${icons.cross} Skill: Failed`)}`);
1402
+ console.log(`${displayIndent}${colors.textDim(` Skill: ${skillName}`)}`);
1403
+ if (result?.message) {
1404
+ console.log(`${displayIndent}${colors.textDim(` ${result.message}`)}`);
1405
+ }
1406
+ }
1407
+ } else if (showToolDetails) {
1408
+ console.log('');
1409
+ console.log(`${displayIndent}${colors.success(`${icons.check} Tool Result:`)}`);
1410
+ console.log(`${displayIndent}${colors.textDim(JSON.stringify(result, null, 2))}`);
1411
+ } else if (result && result.success === false) {
1412
+ // GUI task or other tool failed
1413
+ console.log(`${displayIndent}${colors.error(`${icons.cross} ${result.message || 'Failed'}`)}`);
1414
+ } else if (result) {
1415
+ // Show brief preview by default (consistent with subagent behavior)
1416
+ const resultPreview = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1417
+ const truncatedPreview = resultPreview.length > 200 ? resultPreview.substring(0, 200) + '...' : resultPreview;
1418
+ // Indent the preview
1419
+ const indentedPreview = truncatedPreview.split('\n').map(line => `${displayIndent} ${line}`).join('\n');
1420
+ console.log(`${indentedPreview}`);
1421
+ } else {
1422
+ console.log(`${displayIndent}${colors.textDim('(no result)')}`);
1423
+ }
1424
+
1425
+ const toolCallRecord: ToolCall = {
1426
+ tool,
1427
+ params,
1428
+ result,
1429
+ timestamp: Date.now()
1430
+ };
1431
+
1432
+ this.toolCalls.push(toolCallRecord);
1433
+
1434
+ // Record tool output to session manager
1435
+ await this.sessionManager.addOutput({
1436
+ role: 'tool',
1437
+ content: JSON.stringify(result),
1438
+ toolName: tool,
1439
+ toolParams: params,
1440
+ toolResult: result,
1441
+ timestamp: Date.now()
1442
+ });
1443
+
1444
+ this.conversation.push({
1445
+ role: 'tool',
1446
+ content: JSON.stringify(result),
1447
+ tool_call_id: toolCall.id,
1448
+ timestamp: Date.now()
1449
+ });
1450
+ }
1451
+ }
1452
+
1453
+ // Logic: Only skip returning results to main agent when user explicitly cancelled (ESC)
1454
+ // For all other cases (success, failure, errors), always return results for further processing
1455
+ 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));
1456
+
1457
+ // If GUI agent was cancelled by user, don't continue generating response
1458
+ // This avoids wasting API calls and tokens on cancelled tasks
1459
+ if (guiSubagentCancelled) {
1460
+ console.log('');
1461
+ console.log(`${indent}${colors.textMuted('GUI task cancelled by user')}`);
1462
+ (this as any)._isOperationInProgress = false;
1463
+ return;
1464
+ }
1465
+
1466
+ // Handle errors and completion based on whether onComplete callback is provided
1467
+ if (hasError) {
1468
+ (this as any)._isOperationInProgress = false;
1469
+ if (onComplete) {
1470
+ // Remote mode: callback handles error state (throws to mark task cancelled)
1471
+ throw new Error('Tool execution failed');
1472
+ } else {
1473
+ // Local mode: throw error to mark task as cancelled
1474
+ throw new Error('Tool execution failed');
1475
+ }
1476
+ }
1477
+
1478
+ // Continue based on mode
1479
+ if (onComplete) {
1480
+ // Remote mode: use provided callback
1481
+ await onComplete();
1482
+ } else {
1483
+ // Local mode: default behavior
1484
+ await this.generateResponse();
1485
+ }
1486
+ }
1487
+
1488
+ /**
1489
+ * Get user-friendly description for tool
1490
+ */
1491
+ private getToolDescription(toolName: string, params: any): string {
1492
+ const descriptions: Record<string, (params: any) => string> = {
1493
+ 'Read': (p) => `Read file: ${this.truncatePath(p.filePath)}`,
1494
+ 'Write': (p) => `Write file: ${this.truncatePath(p.filePath)}`,
1495
+ 'Grep': (p) => `Search text: "${p.pattern}"`,
1496
+ 'Bash': (p) => `Execute command: ${this.truncateCommand(p.command)}`,
1497
+ 'ListDirectory': (p) => `List directory: ${this.truncatePath(p.path || '.')}`,
1498
+ 'SearchFiles': (p) => `Search files: ${p.pattern}`,
1499
+ 'DeleteFile': (p) => `Delete file: ${this.truncatePath(p.filePath)}`,
1500
+ 'CreateDirectory': (p) => `Create directory: ${this.truncatePath(p.dirPath)}`,
1501
+ 'Edit': (p) => `Edit text: ${this.truncatePath(p.file_path)}`,
1502
+ 'web_search': (p) => `Web search: "${p.query}"`,
1503
+ 'todo_write': () => `Update todo list`,
1504
+ 'todo_read': () => `Read todo list`,
1505
+ 'task': (p) => `Launch subtask: ${p.description}`,
1506
+ 'ReadBashOutput': (p) => `Read task output: ${p.task_id}`,
1507
+ 'web_fetch': () => `Fetch web content`,
1508
+ 'ask_user_question': () => `Ask user`,
1509
+ 'save_memory': () => `Save memory`,
1510
+ 'exit_plan_mode': () => `Complete plan`,
1511
+ 'xml_escape': (p) => `XML escape: ${this.truncatePath(p.file_path)}`,
1512
+ 'image_read': (p) => `Read image: ${this.truncatePath(p.image_input)}`,
1513
+ // 'Skill': (p) => `Execute skill: ${p.skill}`,
1514
+ // 'ListSkills': () => `List available skills`,
1515
+ // 'GetSkillDetails': (p) => `Get skill details: ${p.skill}`,
1516
+ 'InvokeSkill': (p) => `Invoke skill: ${p.skillId} - ${this.truncatePath(p.taskDescription || '', 40)}`
1517
+ };
1518
+
1519
+ const getDescription = descriptions[toolName];
1520
+ return getDescription ? getDescription(params) : `Execute tool: ${toolName}`;
1521
+ }
1522
+
1523
+ /**
1524
+ * Truncate path for display
1525
+ */
1526
+ private truncatePath(path: string, maxLength: number = 30): string {
1527
+ if (!path) return '';
1528
+ if (path.length <= maxLength) return path;
1529
+ return '...' + path.slice(-(maxLength - 3));
1530
+ }
1531
+
1532
+ /**
1533
+ * Truncate command for display
1534
+ */
1535
+ private truncateCommand(command: string, maxLength: number = 40): string {
1536
+ if (!command) return '';
1537
+ if (command.length <= maxLength) return command;
1538
+ return command.slice(0, maxLength - 3) + '...';
1539
+ }
1540
+
1541
+ /**
1542
+ * Render todo list in a user-friendly format
1543
+ */
1544
+ private renderTodoList(todos: any[], indent: string = ''): string {
1545
+ if (!todos || todos.length === 0) {
1546
+ return `${indent}${colors.textMuted('No tasks')}`;
1547
+ }
1548
+
1549
+ const statusConfig: Record<string, { icon: string; color: (text: string) => string; label: string }> = {
1550
+ 'pending': { icon: icons.circle, color: colors.textMuted, label: 'Pending' },
1551
+ 'in_progress': { icon: icons.loading, color: colors.warning, label: 'In Progress' },
1552
+ 'completed': { icon: icons.success, color: colors.success, label: 'Completed' },
1553
+ 'failed': { icon: icons.error, color: colors.error, label: 'Failed' }
1554
+ };
1555
+
1556
+ const lines: string[] = [];
1557
+
1558
+ for (const todo of todos) {
1559
+ const config = statusConfig[todo.status] || statusConfig['pending'];
1560
+ const statusPrefix = `${config.color(config.icon)} ${config.color(config.label)}:`;
1561
+ lines.push(`${indent} ${statusPrefix} ${colors.text(todo.task)}`);
1562
+ }
1563
+
1564
+ return lines.join('\n');
1565
+ }
1566
+
1567
+ /**
1568
+ * Display AI debug information (input or output)
1569
+ */
1570
+ // AI debug info moved to ai-client.ts implementation
1571
+ // private displayAIDebugInfo(type: 'INPUT' | 'OUTPUT', data: any, extra?: any): void {
1572
+ // const indent = this.getIndent();
1573
+ // const boxChar = {
1574
+ // topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
1575
+ // horizontal: '═', vertical: '║'
1576
+ // };
1577
+ //
1578
+ // console.log('\n' + colors.border(
1579
+ // `${boxChar.topLeft}${boxChar.horizontal.repeat(58)}${boxChar.topRight}`
1580
+ // ));
1581
+ // console.log(colors.border(`${boxChar.vertical}`) + ' ' +
1582
+ // colors.primaryBright(type === 'INPUT' ? '🤖 AI INPUT DEBUG' : '📤 AI OUTPUT DEBUG') +
1583
+ // ' '.repeat(36) + colors.border(boxChar.vertical));
1584
+ // console.log(colors.border(
1585
+ // `${boxChar.vertical}${boxChar.horizontal.repeat(58)}${boxChar.vertical}`
1586
+ // ));
1587
+ //
1588
+ // if (type === 'INPUT') {
1589
+ // const messages = data as any[];
1590
+ // const tools = extra as any[];
1591
+ //
1592
+ // // System prompt
1593
+ // const systemMsg = messages.find((m: any) => m.role === 'system');
1594
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 🟫 SYSTEM: ' +
1595
+ // colors.textMuted(systemMsg?.content?.toString().substring(0, 50) || '(none)') + ' '.repeat(3) + colors.border(boxChar.vertical));
1596
+ //
1597
+ // // Messages count
1598
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 💬 MESSAGES: ' +
1599
+ // colors.text(messages.length.toString()) + ' items' + ' '.repeat(40) + colors.border(boxChar.vertical));
1600
+ //
1601
+ // // Tools count
1602
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 🔧 TOOLS: ' +
1603
+ // colors.text((tools?.length || 0).toString()) + '' + ' '.repeat(43) + colors.border(boxChar.vertical)); //
1604
+ // // Show last 2 messages
1605
+ // const recentMessages = messages.slice(-2);
1606
+ // for (const msg of recentMessages) {
1607
+ // const roleLabel: Record<string, string> = { user: '👤 USER', assistant: '🤖 ASSISTANT', tool: '🔧 TOOL' };
1608
+ // const label = roleLabel[msg.role] || msg.role;
1609
+ // const contentStr = typeof msg.content === 'string'
1610
+ // ? msg.content.substring(0, 100)
1611
+ // : JSON.stringify(msg.content).substring(0, 100);
1612
+ // console.log(colors.border(`${boxChar.vertical}`) + ` ${label}: ` +
1613
+ // colors.textDim(contentStr + '...') + ' '.repeat(Math.max(0, 50 - contentStr.length)) + colors.border(boxChar.vertical));
1614
+ // }
1615
+ // } else {
1616
+ // // OUTPUT
1617
+ // const response = data;
1618
+ // const message = extra;
1619
+ //
1620
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 📋 MODEL: ' +
1621
+ // colors.text(response.model || 'unknown') + ' '.repeat(45) + colors.border(boxChar.vertical));
1622
+ //
1623
+ // console.log(colors.border(`${boxChar.vertical}`) + ' ⏱️ TOKENS: ' +
1624
+ // colors.text(`Prompt: ${response.usage?.prompt_tokens || '?'}, Completion: ${response.usage?.completion_tokens || '?'}`) +
1625
+ // ' '.repeat(15) + colors.border(boxChar.vertical));
1626
+ //
1627
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 🔧 TOOL_CALLS: ' +
1628
+ // colors.text((message.tool_calls?.length || 0).toString()) + '' + ' '.repeat(37) + colors.border(boxChar.vertical));
1629
+ //
1630
+ // // Content preview
1631
+ // const contentStr = typeof message.content === 'string'
1632
+ // ? message.content.substring(0, 100)
1633
+ // : JSON.stringify(message.content).substring(0, 100);
1634
+ // console.log(colors.border(`${boxChar.vertical}`) + ' 📝 CONTENT: ' +
1635
+ // colors.textDim(contentStr + '...') + ' '.repeat(Math.max(0, 40 - contentStr.length)) + colors.border(boxChar.vertical));
1636
+ // }
1637
+ //
1638
+ // console.log(colors.border(
1639
+ // `${boxChar.bottomLeft}${boxChar.horizontal.repeat(58)}${boxChar.bottomRight}`
1640
+ // ));
1641
+ // }
1642
+
1643
+ shutdown(): void {
1644
+ this.rl.close();
1645
+ this.cancellationManager.cleanup();
1646
+ this.mcpManager.disconnectAllServers();
1647
+
1648
+ // End the current session
1649
+ this.sessionManager.completeCurrentSession();
1650
+
1651
+ const separator = icons.separator.repeat(40);
1652
+ console.log('');
1653
+ console.log(colors.border(separator));
1654
+ console.log(colors.primaryBright(`${icons.sparkles} Goodbye!`));
1655
+ console.log(colors.border(separator));
1656
+ console.log('');
1657
+ }
1658
+
1659
+ /**
1660
+ * Get the RemoteAIClient instance
1661
+ * Used by tools.ts to access the remote AI client for GUI operations
1662
+ */
1663
+ getRemoteAIClient(): RemoteAIClient | null {
1664
+ return this.remoteAIClient;
1665
+ }
1666
+
1667
+ /**
1668
+ * Get the current taskId for this user interaction
1669
+ * Used by GUI operations to track the same task
1670
+ */
1671
+ getTaskId(): string | null {
1672
+ return this.currentTaskId;
1673
+ }
1674
+ }
1675
+
1676
+ export async function startInteractiveSession(): Promise<void> {
1677
+ const session = new InteractiveSession();
1678
+
1679
+ // Flag to control shutdown
1680
+ (session as any)._isShuttingDown = false;
1681
+
1682
+ // Also listen for raw Ctrl+C on stdin (works in Windows PowerShell)
1683
+ process.stdin.on('data', (chunk: Buffer) => {
1684
+ const str = chunk.toString();
1685
+ // Ctrl+C is character 0x03 or string '\u0003'
1686
+ if (str === '\u0003' || str.charCodeAt(0) === 3) {
1687
+ if (!(session as any)._isShuttingDown) {
1688
+ (session as any)._isShuttingDown = true;
1689
+
1690
+ // Print goodbye immediately
1691
+ const separator = icons.separator.repeat(40);
1692
+ process.stdout.write('\n' + colors.border(separator) + '\n');
1693
+ process.stdout.write(colors.primaryBright(`${icons.sparkles} Goodbye!`) + '\n');
1694
+ process.stdout.write(colors.border(separator) + '\n\n');
1695
+
1696
+ // Force exit
1697
+ process.exit(0);
1698
+ }
1699
+ }
1700
+ });
1701
+
1702
+ process.on('SIGINT', () => {
1703
+ if ((session as any)._isShuttingDown) {
1704
+ return;
1705
+ }
1706
+ (session as any)._isShuttingDown = true;
1707
+
1708
+ // Remove all SIGINT listeners to prevent re-entry
1709
+ process.removeAllListeners('SIGINT');
1710
+
1711
+ // Print goodbye immediately
1712
+ const separator = icons.separator.repeat(40);
1713
+ process.stdout.write('\n' + colors.border(separator) + '\n');
1714
+ process.stdout.write(colors.primaryBright(`${icons.sparkles} Goodbye!`) + '\n');
1715
+ process.stdout.write(colors.border(separator) + '\n\n');
1716
+
1717
+ // Force exit
1718
+ process.exit(0);
1719
+ });
1720
+
1721
+ await session.start();
1722
+ }
1723
+
1724
+ // Singleton session instance for access from other modules
1725
+ let singletonSession: InteractiveSession | null = null;
1726
+
1727
+ export function setSingletonSession(session: InteractiveSession): void {
1728
+ singletonSession = session;
1729
+ }
1730
+
1731
+ export function getSingletonSession(): InteractiveSession | null {
1732
+ return singletonSession;
1733
+ }