@indiccoder/mentis-cli 1.1.3 → 1.1.5

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 (82) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.mentis/session.json +15 -0
  3. package/.mentis/sessions/1769189035730.json +23 -0
  4. package/.mentis/sessions/1769189569160.json +23 -0
  5. package/.mentis/sessions/1769767538672.json +23 -0
  6. package/.mentis/sessions/1769767785155.json +23 -0
  7. package/.mentis/sessions/1769768745802.json +23 -0
  8. package/.mentis/sessions/1769769600884.json +31 -0
  9. package/.mentis/sessions/1769770030160.json +31 -0
  10. package/.mentis/sessions/1769770606004.json +78 -0
  11. package/.mentis/sessions/1769771084515.json +141 -0
  12. package/.mentis/sessions/1769881926630.json +57 -0
  13. package/ARCHITECTURE.md +267 -0
  14. package/CONTRIBUTING.md +209 -0
  15. package/README.md +17 -0
  16. package/dist/checkpoint/CheckpointManager.js +92 -0
  17. package/dist/commands/Command.js +15 -1
  18. package/dist/commands/CommandManager.js +30 -5
  19. package/dist/commands/__tests__/CommandManager.test.js +70 -0
  20. package/dist/debug_google.js +61 -0
  21. package/dist/debug_lite.js +49 -0
  22. package/dist/debug_lite_headers.js +57 -0
  23. package/dist/debug_search.js +16 -0
  24. package/dist/index.js +33 -0
  25. package/dist/mcp/JsonRpcClient.js +16 -0
  26. package/dist/mcp/McpConfig.js +132 -0
  27. package/dist/mcp/McpManager.js +189 -0
  28. package/dist/repl/PersistentShell.js +20 -1
  29. package/dist/repl/ReplManager.js +410 -138
  30. package/dist/skills/Skill.js +17 -2
  31. package/dist/skills/SkillsManager.js +28 -3
  32. package/dist/skills/__tests__/SkillsManager.test.js +62 -0
  33. package/dist/tools/AskQuestionTool.js +172 -0
  34. package/dist/tools/EditFileTool.js +141 -0
  35. package/dist/tools/FileTools.js +7 -1
  36. package/dist/tools/PlanModeTool.js +53 -0
  37. package/dist/tools/WebSearchTool.js +190 -27
  38. package/dist/ui/DiffViewer.js +110 -0
  39. package/dist/ui/InputBox.js +16 -2
  40. package/dist/ui/MultiFileSelector.js +123 -0
  41. package/dist/ui/PlanModeUI.js +105 -0
  42. package/dist/ui/ToolExecutor.js +154 -0
  43. package/dist/ui/UIManager.js +12 -2
  44. package/dist/utils/__mocks__/chalk.js +20 -0
  45. package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
  46. package/docs/MCP_INTEGRATION.md +290 -0
  47. package/google_dump.html +18 -0
  48. package/lite_dump.html +176 -0
  49. package/lite_headers_dump.html +176 -0
  50. package/package.json +34 -2
  51. package/scripts/test_exa_mcp.ts +90 -0
  52. package/src/checkpoint/CheckpointManager.ts +102 -0
  53. package/src/commands/Command.ts +64 -13
  54. package/src/commands/CommandManager.ts +26 -5
  55. package/src/commands/__tests__/CommandManager.test.ts +83 -0
  56. package/src/debug_google.ts +30 -0
  57. package/src/debug_lite.ts +18 -0
  58. package/src/debug_lite_headers.ts +25 -0
  59. package/src/debug_search.ts +18 -0
  60. package/src/index.ts +45 -1
  61. package/src/mcp/JsonRpcClient.ts +19 -0
  62. package/src/mcp/McpConfig.ts +153 -0
  63. package/src/mcp/McpManager.ts +224 -0
  64. package/src/repl/PersistentShell.ts +24 -1
  65. package/src/repl/ReplManager.ts +1521 -1204
  66. package/src/skills/Skill.ts +91 -11
  67. package/src/skills/SkillsManager.ts +25 -3
  68. package/src/skills/__tests__/SkillsManager.test.ts +73 -0
  69. package/src/tools/AskQuestionTool.ts +197 -0
  70. package/src/tools/EditFileTool.ts +172 -0
  71. package/src/tools/FileTools.ts +3 -0
  72. package/src/tools/PlanModeTool.ts +50 -0
  73. package/src/tools/WebSearchTool.ts +235 -63
  74. package/src/ui/DiffViewer.ts +117 -0
  75. package/src/ui/InputBox.ts +17 -2
  76. package/src/ui/MultiFileSelector.ts +135 -0
  77. package/src/ui/PlanModeUI.ts +121 -0
  78. package/src/ui/ToolExecutor.ts +182 -0
  79. package/src/ui/UIManager.ts +15 -2
  80. package/src/utils/__mocks__/chalk.ts +19 -0
  81. package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
  82. package/console.log(tick) +0 -0
@@ -45,13 +45,16 @@ const OpenAIClient_1 = require("../llm/OpenAIClient");
45
45
  const ContextManager_1 = require("../context/ContextManager");
46
46
  const UIManager_1 = require("../ui/UIManager");
47
47
  const InputBox_1 = require("../ui/InputBox");
48
+ const DiffViewer_1 = require("../ui/DiffViewer");
49
+ const ToolExecutor_1 = require("../ui/ToolExecutor");
50
+ const PlanModeUI_1 = require("../ui/PlanModeUI");
48
51
  const FileTools_1 = require("../tools/FileTools");
49
52
  const SearchTools_1 = require("../tools/SearchTools");
50
53
  const PersistentShellTool_1 = require("../tools/PersistentShellTool");
51
54
  const PersistentShell_1 = require("./PersistentShell");
52
55
  const WebSearchTool_1 = require("../tools/WebSearchTool");
53
56
  const GitTools_1 = require("../tools/GitTools");
54
- const McpClient_1 = require("../mcp/McpClient");
57
+ const McpManager_1 = require("../mcp/McpManager");
55
58
  const CheckpointManager_1 = require("../checkpoint/CheckpointManager");
56
59
  const SkillsManager_1 = require("../skills/SkillsManager");
57
60
  const LoadSkillTool_1 = require("../skills/LoadSkillTool");
@@ -83,10 +86,14 @@ class ReplManager {
83
86
  this.contextVisualizer = new ContextVisualizer_1.ContextVisualizer();
84
87
  this.conversationCompacter = new ConversationCompacter_1.ConversationCompacter();
85
88
  this.commandManager = new CommandManager_1.CommandManager();
89
+ this.mcpManager = new McpManager_1.McpManager();
86
90
  this.shell = new PersistentShell_1.PersistentShell();
87
91
  // Create tools array without skill tools first
88
92
  this.tools = [
93
+ new FileTools_1.PlanModeTool(), // AI can suggest plan mode for complex tasks
94
+ new FileTools_1.AskQuestionTool(), // For plan mode questions
89
95
  new FileTools_1.WriteFileTool(),
96
+ new FileTools_1.EditFileTool(),
90
97
  new FileTools_1.ReadFileTool(),
91
98
  new FileTools_1.ListDirTool(),
92
99
  new SearchTools_1.SearchFileTool(), // grep
@@ -123,6 +130,24 @@ class ReplManager {
123
130
  this.tools.push(new LoadSkillTool_1.LoadSkillTool(this.skillsManager, (skill) => {
124
131
  this.activeSkill = skill ? skill.name : null;
125
132
  }), new LoadSkillTool_1.ListSkillsTool(this.skillsManager), new LoadSkillTool_1.ReadSkillFileTool(this.skillsManager), new SlashCommandTool_1.SlashCommandTool(this.commandManager), new SlashCommandTool_1.ListCommandsTool(this.commandManager));
133
+ // Auto-connect to MCP servers
134
+ await this.mcpManager.autoConnect();
135
+ this.refreshToolsFromMcp();
136
+ }
137
+ /**
138
+ * Refresh tools list from MCP connections
139
+ */
140
+ refreshToolsFromMcp() {
141
+ // Remove existing MCP tools (keep core tools)
142
+ this.tools = this.tools.filter(tool => !tool.name.startsWith('mcp_') &&
143
+ !['load_skill', 'list_skills', 'read_skill_file', 'slash_command', 'list_commands'].includes(tool.name));
144
+ // Add MCP tools
145
+ const mcpTools = this.mcpManager.getAllTools();
146
+ this.tools.push(...mcpTools);
147
+ // Re-add skill tools
148
+ this.tools.push(new LoadSkillTool_1.LoadSkillTool(this.skillsManager, (skill) => {
149
+ this.activeSkill = skill ? skill.name : null;
150
+ }), new LoadSkillTool_1.ListSkillsTool(this.skillsManager), new LoadSkillTool_1.ReadSkillFileTool(this.skillsManager), new SlashCommandTool_1.SlashCommandTool(this.commandManager), new SlashCommandTool_1.ListCommandsTool(this.commandManager));
126
151
  }
127
152
  /**
128
153
  * Check if a tool is allowed by the currently active skill
@@ -147,7 +172,7 @@ class ReplManager {
147
172
  'list_dir': 'ListDir',
148
173
  'search_file': 'SearchFile',
149
174
  'run_shell': 'RunShell',
150
- 'web_search': 'WebSearch',
175
+ 'search_web': 'WebSearch',
151
176
  'git_status': 'GitStatus',
152
177
  'git_diff': 'GitDiff',
153
178
  'git_commit': 'GitCommit',
@@ -207,10 +232,17 @@ class ReplManager {
207
232
  });
208
233
  // Auto-resume if --resume flag is set
209
234
  if (this.options.resume) {
210
- const cp = this.checkpointManager.load('latest');
235
+ // Prefer local (per-directory) session, fall back to global
236
+ const cwd = process.cwd();
237
+ let cp = this.checkpointManager.loadLocalSession(cwd);
238
+ let source = 'local';
239
+ if (!cp) {
240
+ cp = this.checkpointManager.load('latest');
241
+ source = 'global';
242
+ }
211
243
  if (cp) {
212
244
  this.history = cp.history;
213
- console.log(chalk_1.default.green(`\n✓ Resumed session from ${new Date(cp.timestamp).toLocaleString()}`));
245
+ console.log(chalk_1.default.green(`\n✓ Resumed ${source} session from ${new Date(cp.timestamp).toLocaleString()}`));
214
246
  console.log(chalk_1.default.dim(` Messages: ${this.history.length}\n`));
215
247
  }
216
248
  else {
@@ -256,7 +288,14 @@ class ReplManager {
256
288
  await this.handleCommand(input);
257
289
  continue;
258
290
  }
259
- await this.handleChat(input);
291
+ // Wrap handleChat in try-catch to prevent REPL from entering inconsistent state
292
+ try {
293
+ await this.handleChat(input);
294
+ }
295
+ catch (error) {
296
+ console.error(chalk_1.default.red(`Error: ${error.message}`));
297
+ console.error(chalk_1.default.dim('The REPL has recovered. You can continue using the CLI.'));
298
+ }
260
299
  }
261
300
  }
262
301
  async handleCommand(input) {
@@ -279,7 +318,6 @@ class ReplManager {
279
318
  console.log(' /skills <list|show|create|validate> - Manage Agent Skills');
280
319
  console.log(' /commands <list|create|validate> - Manage Custom Commands');
281
320
  console.log(' /resume - Resume last session');
282
- console.log(' /checkpoint <save|load|list> [name] - Manage checkpoints');
283
321
  console.log(' /search <query> - Search codebase');
284
322
  console.log(' /run <cmd> - Run shell command');
285
323
  console.log(' /commit [msg] - Git commit all changes');
@@ -287,15 +325,15 @@ class ReplManager {
287
325
  break;
288
326
  case '/plan':
289
327
  this.mode = 'PLAN';
290
- console.log(chalk_1.default.blue('Switched to PLAN mode.'));
291
- break;
292
- case '/build':
293
- this.mode = 'BUILD';
294
- console.log(chalk_1.default.yellow('Switched to BUILD mode.'));
328
+ UIManager_1.UIManager.logBullet('Entered plan mode', 'magenta');
329
+ PlanModeUI_1.PlanModeUI.showPlanHeader();
330
+ PlanModeUI_1.PlanModeUI.showQAHistory();
295
331
  break;
296
332
  case '/build':
297
333
  this.mode = 'BUILD';
298
- console.log(chalk_1.default.yellow('Switched to BUILD mode.'));
334
+ UIManager_1.UIManager.logBullet('Entered build mode', 'green');
335
+ PlanModeUI_1.PlanModeUI.showPlanSummary();
336
+ UIManager_1.UIManager.logSystem('Mentis is building the solution...');
299
337
  break;
300
338
  case '/model':
301
339
  await this.handleModelCommand(args);
@@ -313,9 +351,6 @@ class ReplManager {
313
351
  case '/resume':
314
352
  await this.handleResumeCommand();
315
353
  break;
316
- case '/checkpoint':
317
- await this.handleCheckpointCommand(args);
318
- break;
319
354
  case '/clear':
320
355
  this.history = [];
321
356
  this.contextManager.clear();
@@ -344,10 +379,13 @@ class ReplManager {
344
379
  await this.handleConfigCommand();
345
380
  break;
346
381
  case '/exit':
347
- // Auto-save on exit
382
+ // Auto-save on exit (both local and global)
383
+ const cwd = process.cwd();
384
+ this.checkpointManager.saveLocalSession(cwd, this.history, this.contextManager.getFiles());
348
385
  this.checkpointManager.save('latest', this.history, this.contextManager.getFiles());
349
386
  this.shell.kill(); // Kill the shell process
350
- console.log(chalk_1.default.green('Session saved. Goodbye!'));
387
+ this.mcpManager.disconnectAll(); // Disconnect all MCP connections
388
+ console.log(chalk_1.default.green('Session saved to .mentis/sessions/. Goodbye!'));
351
389
  process.exit(0);
352
390
  break;
353
391
  case '/update':
@@ -372,6 +410,26 @@ class ReplManager {
372
410
  console.log(chalk_1.default.red(`Unknown command: ${command}`));
373
411
  }
374
412
  }
413
+ getLoadingMessage() {
414
+ const messages = [
415
+ "Reticulating splines...",
416
+ "Consulting the silicon oracle...",
417
+ "Compiling neural pathways...",
418
+ "Optimizing flux capacitors...",
419
+ "Analyzing project structure...",
420
+ "Deciphering your intent...",
421
+ "Brewing digital coffee...",
422
+ "Checking for infinite loops...",
423
+ "Connecting to the matrix...",
424
+ "Calculating the answer (42?)...",
425
+ "Refactoring the universe...",
426
+ "Downloading more RAM...",
427
+ "Searching for bugs...",
428
+ "Asking the rubber duck...",
429
+ "Hyperspacing..."
430
+ ];
431
+ return messages[Math.floor(Math.random() * messages.length)];
432
+ }
375
433
  async handleChat(input) {
376
434
  const context = this.contextManager.getContextString();
377
435
  const skillsContext = this.skillsManager.getSkillsContext();
@@ -397,7 +455,8 @@ class ReplManager {
397
455
  fullInput = `${context}\n\nUser Question: ${fullInput}`;
398
456
  }
399
457
  this.history.push({ role: 'user', content: fullInput });
400
- let spinner = (0, ora_1.default)('Thinking... (Press Esc to cancel)').start();
458
+ const msg = this.getLoadingMessage();
459
+ let spinner = (0, ora_1.default)({ text: ` ${msg}`, color: 'cyan' }).start();
401
460
  const controller = new AbortController();
402
461
  // Setup cancellation listener
403
462
  const keyListener = (str, key) => {
@@ -438,59 +497,63 @@ class ReplManager {
438
497
  const toolName = toolCall.function.name;
439
498
  const toolArgsStr = toolCall.function.arguments;
440
499
  const toolArgs = JSON.parse(toolArgsStr);
441
- // Truncate long arguments
442
- let displayArgs = toolArgsStr;
443
- if (displayArgs.length > 100) {
444
- displayArgs = displayArgs.substring(0, 100) + '...';
445
- }
446
- console.log(chalk_1.default.dim(` [Action] ${toolName}(${displayArgs})`));
447
- // Safety check for write_file
500
+ // Show tool execution with visual feedback
501
+ ToolExecutor_1.ToolExecutor.showInline(toolName, toolArgs);
502
+ // Safety checks for write/edit operations
448
503
  // Skip confirmation if tool is allowed by active skill
449
- if (toolName === 'write_file' && !this.isToolAllowedBySkill('Write')) {
504
+ let approved = true;
505
+ const needsApproval = (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'read_file')
506
+ && !this.isToolAllowedBySkill(toolName === 'read_file' ? 'Read' : 'Write');
507
+ if (needsApproval) {
450
508
  // Pause cancellation listener during user interaction
451
509
  if (process.stdin.isTTY) {
452
510
  process.stdin.removeListener('keypress', keyListener);
453
511
  process.stdin.setRawMode(false);
454
- process.stdin.pause(); // Explicitly pause before inquirer
512
+ process.stdin.pause();
513
+ }
514
+ // Handle write_file with diff preview
515
+ if (toolName === 'write_file') {
516
+ approved = await this.handleWriteApproval(toolArgs.filePath, toolArgs.content);
517
+ }
518
+ // Handle edit_file with diff preview
519
+ if (toolName === 'edit_file') {
520
+ approved = await this.handleEditApproval(toolArgs);
521
+ }
522
+ // Handle read_file with multi-file selector
523
+ if (toolName === 'read_file') {
524
+ approved = await this.handleReadApproval(toolArgs.filePath);
455
525
  }
456
- spinner.stop(); // Stop spinner to allow input
457
- const { confirm } = await inquirer_1.default.prompt([
458
- {
459
- type: 'confirm',
460
- name: 'confirm',
461
- message: `Allow writing to ${chalk_1.default.yellow(toolArgs.filePath)}?`,
462
- default: true
463
- }
464
- ]);
465
526
  // Resume cancellation listener
466
527
  if (process.stdin.isTTY) {
467
528
  process.stdin.setRawMode(true);
468
- process.stdin.resume(); // Explicitly resume
529
+ process.stdin.resume();
469
530
  process.stdin.on('keypress', keyListener);
470
531
  }
471
- if (!confirm) {
472
- this.history.push({
473
- role: 'tool',
474
- tool_call_id: toolCall.id,
475
- name: toolName,
476
- content: 'Error: User rejected write operation.'
477
- });
478
- console.log(chalk_1.default.red(' Action cancelled by user.'));
479
- // Do not restart spinner here. Let the outer loop logic or next step handle it.
480
- // If we continue, we go to next tool or finish loop.
481
- // If finished, lines following loop will start spinner.
482
- continue;
483
- }
484
- spinner = (0, ora_1.default)('Executing...').start();
532
+ }
533
+ if (!approved) {
534
+ this.history.push({
535
+ role: 'tool',
536
+ tool_call_id: toolCall.id,
537
+ name: toolName,
538
+ content: 'Error: User rejected operation.'
539
+ });
540
+ console.log(chalk_1.default.red(' Action cancelled by user.'));
541
+ continue;
485
542
  }
486
543
  const tool = this.tools.find(t => t.name === toolName);
487
544
  let result = '';
488
545
  if (tool) {
489
546
  try {
490
- // Tools typically run synchronously or promise-based.
491
- // Verify if we want Tools to be cancellable?
492
- // For now, if aborted during tool, we let tool finish but stop loop.
493
547
  result = await tool.execute(toolArgs);
548
+ // Record Q&A for ask_question tool in plan mode
549
+ if (toolName === 'ask_question' && this.mode === 'PLAN') {
550
+ PlanModeUI_1.PlanModeUI.recordQA(toolArgs.question, result);
551
+ }
552
+ // Handle plan mode switch approval
553
+ if (toolName === 'enter_plan_mode' && result.includes('User approved')) {
554
+ this.mode = 'PLAN';
555
+ UIManager_1.UIManager.logBullet('Auto-switched to plan mode', 'magenta');
556
+ }
494
557
  }
495
558
  catch (e) {
496
559
  result = `Error: ${e.message}`;
@@ -499,9 +562,6 @@ class ReplManager {
499
562
  else {
500
563
  result = `Error: Tool ${toolName} not found.`;
501
564
  }
502
- if (spinner.isSpinning) {
503
- spinner.stop();
504
- }
505
565
  this.history.push({
506
566
  role: 'tool',
507
567
  tool_call_id: toolCall.id,
@@ -511,7 +571,8 @@ class ReplManager {
511
571
  }
512
572
  if (controller.signal.aborted)
513
573
  throw new Error('Request cancelled by user');
514
- spinner = (0, ora_1.default)('Thinking (processing tools)...').start();
574
+ const msg = this.getLoadingMessage();
575
+ spinner = (0, ora_1.default)({ text: ` ${msg}`, color: 'cyan' }).start();
515
576
  // Get next response
516
577
  response = await this.modelClient.chat(this.history, this.tools.map(t => ({
517
578
  type: 'function',
@@ -525,7 +586,7 @@ class ReplManager {
525
586
  spinner.stop();
526
587
  console.log('');
527
588
  if (response.content) {
528
- console.log(chalk_1.default.bold.blue('Mentis:'));
589
+ UIManager_1.UIManager.logBullet('Mentis:', 'magenta');
529
590
  console.log((0, marked_1.marked)(response.content));
530
591
  if (response.usage) {
531
592
  const { input_tokens, output_tokens } = response.usage;
@@ -818,100 +879,251 @@ class ReplManager {
818
879
  console.log(chalk_1.default.green(`Switched to ${provider} ${model ? `using model ${model}` : ''}`));
819
880
  }
820
881
  async handleMcpCommand(args) {
821
- if (args.length < 1) {
822
- console.log(chalk_1.default.red('Usage: /mcp <connect|list|disconnect> [args]'));
882
+ if (args.length === 0) {
883
+ console.log(chalk_1.default.red('Usage: /mcp <list|connect|disconnect|add|remove|test|config> [args]'));
884
+ console.log(chalk_1.default.dim('\nExamples:'));
885
+ console.log(chalk_1.default.dim(' /mcp list - List all MCP servers'));
886
+ console.log(chalk_1.default.dim(' /mcp connect Exa\\ Search - Connect to Exa Search'));
887
+ console.log(chalk_1.default.dim(' /mcp disconnect all - Disconnect all servers'));
888
+ console.log(chalk_1.default.dim(' /mcp add MyServer npx -y @my/mcp-server'));
889
+ console.log(chalk_1.default.dim(' /mcp test Exa\\ Search - Test connection'));
890
+ console.log(chalk_1.default.dim(' /mcp config - Show configuration'));
823
891
  return;
824
892
  }
825
893
  const action = args[0];
826
- if (action === 'connect') {
827
- const commandParts = args.slice(1);
828
- if (commandParts.length === 0) {
829
- console.log(chalk_1.default.red('Usage: /mcp connect <command> [args...]'));
830
- return;
831
- }
832
- // Example: /mcp connect npx -y @modelcontextprotocol/server-memory
833
- // On Windows, npx might be npx.cmd
834
- const cmd = process.platform === 'win32' && commandParts[0] === 'npx' ? 'npx.cmd' : commandParts[0];
835
- const cmdArgs = commandParts.slice(1);
836
- const spinner = (0, ora_1.default)(`Connecting to MCP server: ${cmd} ${cmdArgs.join(' ')}...`).start();
837
- try {
838
- const client = new McpClient_1.McpClient(cmd, cmdArgs);
839
- await client.initialize();
840
- const mcpTools = await client.listTools();
841
- this.mcpClients.push(client);
842
- this.tools.push(...mcpTools);
843
- spinner.succeed(chalk_1.default.green(`Connected to ${client.serverName}!`));
844
- console.log(chalk_1.default.green(`Added ${mcpTools.length} tools:`));
845
- mcpTools.forEach(t => console.log(chalk_1.default.dim(` - ${t.name}: ${t.description.substring(0, 50)}...`)));
846
- }
847
- catch (e) {
848
- spinner.fail(chalk_1.default.red(`Failed to connect: ${e.message}`));
849
- }
894
+ switch (action) {
895
+ case 'list':
896
+ await this.mcpManager.listServers();
897
+ break;
898
+ case 'connect':
899
+ if (args.length < 2) {
900
+ // Show interactive list of available servers
901
+ const availableServers = this.mcpManager.getAvailableServers();
902
+ if (availableServers.length === 0) {
903
+ console.log(chalk_1.default.yellow('No MCP servers configured. Use /mcp add to add one.'));
904
+ return;
905
+ }
906
+ const { serverName } = await inquirer_1.default.prompt([{
907
+ type: 'list',
908
+ name: 'serverName',
909
+ message: 'Select server to connect:',
910
+ choices: availableServers.map(s => s.name)
911
+ }]);
912
+ await this.mcpManager.connectToServer(serverName);
913
+ this.refreshToolsFromMcp();
914
+ }
915
+ else {
916
+ const serverName = args.slice(1).join(' ');
917
+ await this.mcpManager.connectToServer(serverName);
918
+ this.refreshToolsFromMcp();
919
+ }
920
+ break;
921
+ case 'disconnect':
922
+ if (args.length < 2) {
923
+ // Interactive disconnect
924
+ const connectedServers = this.mcpManager.getServerNames();
925
+ if (connectedServers.length === 0) {
926
+ console.log(chalk_1.default.yellow('No MCP connections to disconnect.'));
927
+ return;
928
+ }
929
+ const { serverName } = await inquirer_1.default.prompt([{
930
+ type: 'list',
931
+ name: 'serverName',
932
+ message: 'Select server to disconnect:',
933
+ choices: [...connectedServers, 'all']
934
+ }]);
935
+ if (serverName === 'all') {
936
+ this.mcpManager.disconnectAll();
937
+ this.refreshToolsFromMcp();
938
+ }
939
+ else {
940
+ await this.mcpManager.disconnectFromServer(serverName);
941
+ this.refreshToolsFromMcp();
942
+ }
943
+ }
944
+ else {
945
+ const serverName = args.slice(1).join(' ');
946
+ if (serverName === 'all') {
947
+ this.mcpManager.disconnectAll();
948
+ this.refreshToolsFromMcp();
949
+ }
950
+ else {
951
+ await this.mcpManager.disconnectFromServer(serverName);
952
+ this.refreshToolsFromMcp();
953
+ }
954
+ }
955
+ break;
956
+ case 'add':
957
+ if (args.length < 3) {
958
+ console.log(chalk_1.default.red('Usage: /mcp add <name> <command> [args...]'));
959
+ console.log(chalk_1.default.dim('\nExamples:'));
960
+ console.log(chalk_1.default.dim(' /mcp add "My Server" npx -y @my/mcp-server'));
961
+ console.log(chalk_1.default.dim(' /mcp add "Local Server" node /path/to/server.js'));
962
+ return;
963
+ }
964
+ const name = args[1];
965
+ const command = args[2];
966
+ const mcpArgs = args.slice(3);
967
+ const { description } = await inquirer_1.default.prompt([{
968
+ type: 'input',
969
+ name: 'description',
970
+ message: 'Description (optional):',
971
+ default: ''
972
+ }]);
973
+ await this.mcpManager.addServer(name, command, mcpArgs, description);
974
+ break;
975
+ case 'remove':
976
+ if (args.length < 2) {
977
+ // Interactive remove
978
+ const availableServers = this.mcpManager.getAvailableServers();
979
+ if (availableServers.length === 0) {
980
+ console.log(chalk_1.default.yellow('No MCP servers configured.'));
981
+ return;
982
+ }
983
+ const { serverName } = await inquirer_1.default.prompt([{
984
+ type: 'list',
985
+ name: 'serverName',
986
+ message: 'Select server to remove:',
987
+ choices: availableServers.map(s => s.name)
988
+ }]);
989
+ await this.mcpManager.removeServer(serverName);
990
+ }
991
+ else {
992
+ const serverName = args.slice(1).join(' ');
993
+ await this.mcpManager.removeServer(serverName);
994
+ }
995
+ break;
996
+ case 'test':
997
+ if (args.length < 2) {
998
+ // Interactive test
999
+ const connectedServers = this.mcpManager.getServerNames();
1000
+ if (connectedServers.length === 0) {
1001
+ console.log(chalk_1.default.yellow('No MCP connections to test.'));
1002
+ return;
1003
+ }
1004
+ const { serverName } = await inquirer_1.default.prompt([{
1005
+ type: 'list',
1006
+ name: 'serverName',
1007
+ message: 'Select server to test:',
1008
+ choices: connectedServers
1009
+ }]);
1010
+ await this.mcpManager.testConnection(serverName);
1011
+ }
1012
+ else {
1013
+ const serverName = args.slice(1).join(' ');
1014
+ await this.mcpManager.testConnection(serverName);
1015
+ }
1016
+ break;
1017
+ case 'config':
1018
+ const config = this.mcpManager.getConfig().getConfig();
1019
+ console.log(chalk_1.default.cyan('\nMCP Configuration:\n'));
1020
+ console.log(JSON.stringify(config, null, 2));
1021
+ console.log(chalk_1.default.dim(`\nConfig file: ${require('os').homedir()}/.mentis/mcp.json`));
1022
+ break;
1023
+ default:
1024
+ console.log(chalk_1.default.red(`Unknown MCP action: ${action}`));
1025
+ console.log(chalk_1.default.yellow('Available actions: list, connect, disconnect, add, remove, test, config'));
850
1026
  }
851
- else if (action === 'list') {
852
- if (this.mcpClients.length === 0) {
853
- console.log('No active MCP connections.');
854
- }
855
- else {
856
- console.log(chalk_1.default.cyan('Active MCP Connections:'));
857
- this.mcpClients.forEach((client, idx) => {
858
- console.log(`${idx + 1}. ${client.serverName}`);
1027
+ }
1028
+ async handleResumeCommand() {
1029
+ const cwd = process.cwd();
1030
+ const localSessions = this.checkpointManager.listLocalSessions(cwd);
1031
+ const globalCheckpoints = this.checkpointManager.list();
1032
+ if (localSessions.length === 0 && globalCheckpoints.length === 0) {
1033
+ console.log(chalk_1.default.yellow('No previous sessions found to resume.'));
1034
+ return;
1035
+ }
1036
+ // Header like Claude's "Resume Session"
1037
+ console.log('');
1038
+ console.log(chalk_1.default.cyan.bold('Resume Session'));
1039
+ console.log('');
1040
+ // Helper for relative time
1041
+ const relativeTime = (ts) => {
1042
+ const diff = Date.now() - ts;
1043
+ const mins = Math.floor(diff / 60000);
1044
+ const hours = Math.floor(diff / 3600000);
1045
+ const days = Math.floor(diff / 86400000);
1046
+ if (days > 0)
1047
+ return `${days} day${days > 1 ? 's' : ''} ago`;
1048
+ if (hours > 0)
1049
+ return `${hours} hour${hours > 1 ? 's' : ''} ago`;
1050
+ if (mins > 0)
1051
+ return `${mins} min${mins > 1 ? 's' : ''} ago`;
1052
+ return 'just now';
1053
+ };
1054
+ // Build choices with simple single-line formatting
1055
+ const choices = [];
1056
+ // Local sessions first
1057
+ for (const session of localSessions) {
1058
+ const timeAgo = relativeTime(session.timestamp);
1059
+ const shortPreview = session.preview.substring(0, 30).replace(/\n/g, ' ');
1060
+ choices.push({
1061
+ name: `${shortPreview}... (${timeAgo}, ${session.messageCount} msgs)`,
1062
+ value: { type: 'local', id: session.id }
1063
+ });
1064
+ }
1065
+ // Global checkpoints as fallback
1066
+ if (localSessions.length === 0 && globalCheckpoints.length > 0) {
1067
+ for (const name of globalCheckpoints) {
1068
+ const cp = this.checkpointManager.load(name);
1069
+ const timeAgo = cp ? relativeTime(cp.timestamp) : '';
1070
+ const msgCount = cp?.history?.length || 0;
1071
+ const preview = cp?.history?.find(m => m.role === 'user')?.content?.substring(0, 40)?.replace(/\n/g, ' ') || name;
1072
+ choices.push({
1073
+ name: `${preview}${preview.length >= 40 ? '...' : ''} — ${timeAgo} · ${msgCount} msgs`,
1074
+ value: { type: 'global', id: name }
859
1075
  });
860
1076
  }
861
1077
  }
862
- else if (action === 'disconnect') {
863
- // Basic disconnect all for now or by index if we wanted
864
- console.log(chalk_1.default.yellow('Disconnecting all MCP clients...'));
865
- this.mcpClients.forEach(c => c.disconnect());
866
- this.mcpClients = [];
867
- // Re-init core tools
868
- this.tools = [
869
- new FileTools_1.WriteFileTool(),
870
- new FileTools_1.ReadFileTool(),
871
- new FileTools_1.ListDirTool(),
872
- new SearchTools_1.SearchFileTool(),
873
- new PersistentShellTool_1.PersistentShellTool(this.shell),
874
- new WebSearchTool_1.WebSearchTool(),
875
- new GitTools_1.GitStatusTool(),
876
- new GitTools_1.GitDiffTool(),
877
- new GitTools_1.GitCommitTool(),
878
- new GitTools_1.GitPushTool(),
879
- new GitTools_1.GitPullTool()
880
- ];
1078
+ // Show hint
1079
+ console.log(chalk_1.default.dim(' ↑/↓ to navigate · Enter to select · Esc to cancel'));
1080
+ console.log('');
1081
+ // Guard against empty choices (would crash inquirer)
1082
+ if (choices.length === 0) {
1083
+ console.log(chalk_1.default.yellow('No sessions available. Start a conversation and /exit to save.'));
1084
+ return;
881
1085
  }
882
- else {
883
- console.log(chalk_1.default.red(`Unknown MCP action: ${action}`));
1086
+ const { selected } = await inquirer_1.default.prompt([{
1087
+ type: 'rawlist',
1088
+ name: 'selected',
1089
+ message: 'Pick a session:',
1090
+ choices,
1091
+ pageSize: 10
1092
+ }]);
1093
+ if (selected.type === 'local') {
1094
+ await this.loadLocalCheckpoint(cwd, selected.id);
884
1095
  }
885
- }
886
- async handleResumeCommand() {
887
- if (!this.checkpointManager.exists('latest')) {
888
- console.log(chalk_1.default.yellow('No previous session found to resume.'));
889
- return;
1096
+ else if (selected.type === 'global') {
1097
+ await this.loadCheckpoint(selected.id);
890
1098
  }
891
- await this.loadCheckpoint('latest');
892
1099
  }
893
- async handleCheckpointCommand(args) {
894
- if (args.length < 1) {
895
- console.log(chalk_1.default.red('Usage: /checkpoint <save|load|list> [name]'));
1100
+ async loadLocalCheckpoint(cwd, sessionId) {
1101
+ const cp = this.checkpointManager.loadLocalSession(cwd, sessionId);
1102
+ if (!cp) {
1103
+ console.log(chalk_1.default.red('Session not found.'));
896
1104
  return;
897
1105
  }
898
- const action = args[0];
899
- const name = args[1] || 'default';
900
- if (action === 'save') {
901
- this.checkpointManager.save(name, this.history, this.contextManager.getFiles());
902
- console.log(chalk_1.default.green(`Checkpoint '${name}' saved.`));
903
- }
904
- else if (action === 'load') {
905
- await this.loadCheckpoint(name);
906
- }
907
- else if (action === 'list') {
908
- const points = this.checkpointManager.list();
909
- console.log(chalk_1.default.cyan('Available Checkpoints:'));
910
- points.forEach(p => console.log(` - ${p}`));
1106
+ this.history = cp.history;
1107
+ this.contextManager.clear();
1108
+ // Restore context files
1109
+ if (cp.files && cp.files.length > 0) {
1110
+ console.log(chalk_1.default.dim('Restoring context files...'));
1111
+ for (const file of cp.files) {
1112
+ await this.contextManager.addFile(file);
1113
+ }
911
1114
  }
912
- else {
913
- console.log(chalk_1.default.red(`Unknown action: ${action}`));
1115
+ console.log(chalk_1.default.green(`✓ Resumed session (${new Date(cp.timestamp).toLocaleString()})`));
1116
+ console.log(chalk_1.default.dim(` Messages: ${this.history.length}`));
1117
+ // Re-display last assistant message if any
1118
+ const lastMsg = this.history[this.history.length - 1];
1119
+ if (lastMsg && lastMsg.role === 'assistant' && lastMsg.content) {
1120
+ console.log(chalk_1.default.blue('\nLast response:'));
1121
+ const preview = lastMsg.content.length > 200
1122
+ ? lastMsg.content.substring(0, 200) + '...'
1123
+ : lastMsg.content;
1124
+ console.log(chalk_1.default.dim(preview));
914
1125
  }
1126
+ console.log('');
915
1127
  }
916
1128
  async loadCheckpoint(name) {
917
1129
  const cp = this.checkpointManager.load(name);
@@ -1080,6 +1292,66 @@ class ReplManager {
1080
1292
  const { validateCommands } = await Promise.resolve().then(() => __importStar(require('../commands/CommandCreator')));
1081
1293
  await validateCommands(this.commandManager);
1082
1294
  }
1295
+ /**
1296
+ * Handle write_file approval with diff preview
1297
+ */
1298
+ async handleWriteApproval(filePath, content) {
1299
+ // Check if file exists and show diff
1300
+ const fullPath = path.resolve(filePath);
1301
+ if (fs.existsSync(fullPath)) {
1302
+ const oldContent = fs.readFileSync(fullPath, 'utf-8');
1303
+ DiffViewer_1.DiffViewer.showDiff(filePath, oldContent, content);
1304
+ }
1305
+ else {
1306
+ console.log('');
1307
+ console.log(chalk_1.default.cyan(`📄 Creating new file: ${filePath}`));
1308
+ console.log(chalk_1.default.dim('─'.repeat(60)));
1309
+ console.log(chalk_1.default.green('New file content:'));
1310
+ const preview = content.split('\n').slice(0, 10).join('\n');
1311
+ console.log(chalk_1.default.dim(preview));
1312
+ if (content.split('\n').length > 10) {
1313
+ console.log(chalk_1.default.dim('...'));
1314
+ }
1315
+ console.log(chalk_1.default.dim('─'.repeat(60)));
1316
+ }
1317
+ const { confirm } = await inquirer_1.default.prompt([
1318
+ {
1319
+ type: 'confirm',
1320
+ name: 'confirm',
1321
+ message: `Allow writing to ${chalk_1.default.yellow(filePath)}?`,
1322
+ default: true
1323
+ }
1324
+ ]);
1325
+ return confirm;
1326
+ }
1327
+ /**
1328
+ * Handle edit_file approval with diff preview
1329
+ */
1330
+ async handleEditApproval(args) {
1331
+ // Get diff from EditFileTool (which shows preview)
1332
+ const editTool = this.tools.find(t => t.name === 'edit_file');
1333
+ if (editTool) {
1334
+ const diffResult = await editTool.execute(args);
1335
+ console.log(diffResult);
1336
+ }
1337
+ const { confirm } = await inquirer_1.default.prompt([
1338
+ {
1339
+ type: 'confirm',
1340
+ name: 'confirm',
1341
+ message: `Apply edit to ${chalk_1.default.yellow(args.file_path)}?`,
1342
+ default: true
1343
+ }
1344
+ ]);
1345
+ return confirm;
1346
+ }
1347
+ /**
1348
+ * Handle read_file approval (currently auto-approves, but can be enhanced)
1349
+ */
1350
+ async handleReadApproval(filePath) {
1351
+ // For now, auto-approve single file reads
1352
+ // In the future, could batch multiple reads into a single selector
1353
+ return true;
1354
+ }
1083
1355
  estimateCost(input, output) {
1084
1356
  const config = this.configManager.getConfig();
1085
1357
  const provider = config.defaultProvider;