neuro-cli 4.1.2 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // ============================================================
3
3
  // NeuroCLI - Advanced AI Terminal Coding Assistant
4
- // Main Entry Point - v4.1.2 with cross-platform path fix
4
+ // Main Entry Point - v4.1.3 with robust error handling
5
5
  // ============================================================
6
6
  import { Command } from 'commander';
7
7
  import { createInterface } from 'readline';
@@ -16,7 +16,20 @@ import { HeadlessMode } from './core/headless.js';
16
16
  import { ShellCompletionGenerator } from './core/shell-completion.js';
17
17
  import chalk from 'chalk';
18
18
  import { AutoUpdater } from './core/updater.js';
19
- const VERSION = '4.1.2';
19
+ const VERSION = '4.2.0';
20
+ // ---- Global Error Handlers (prevent crashes) ----
21
+ process.on('unhandledRejection', (reason) => {
22
+ console.error(chalk.red('\n⚠️ Unhandled promise rejection:'), reason);
23
+ // Don't exit — log and continue
24
+ });
25
+ process.on('uncaughtException', (error) => {
26
+ console.error(chalk.red('\n⚠️ Uncaught exception:'), error.message);
27
+ // Only exit for truly fatal errors
28
+ if (error.message.includes('ENOMEM') || error.message.includes('EADDRINUSE') || error.message.includes('EPERM')) {
29
+ process.exit(1);
30
+ }
31
+ // Otherwise, try to continue
32
+ });
20
33
  // ---- CLI Setup ----
21
34
  const program = new Command();
22
35
  program
@@ -236,7 +249,13 @@ program
236
249
  transport: transport,
237
250
  command: transport === 'stdio' ? command : undefined,
238
251
  url: isUrl ? command : undefined,
239
- headers: opts.headers ? JSON.parse(opts.headers) : undefined,
252
+ headers: opts.headers ? (() => { try {
253
+ return JSON.parse(opts.headers);
254
+ }
255
+ catch {
256
+ console.log(chalk.red('Invalid JSON for --headers'));
257
+ return undefined;
258
+ } })() : undefined,
240
259
  });
241
260
  console.log(chalk.green(`MCP server "${name}" added (${transport})`));
242
261
  }))
@@ -437,710 +456,729 @@ async function startInteractive(options) {
437
456
  let currentAgent;
438
457
  let isProcessing = false;
439
458
  rl.on('line', async (line) => {
440
- const input = line.trim();
441
- if (!input) {
442
- rl.prompt();
443
- return;
444
- }
445
- // Prevent input while processing (approval prompt may be active)
446
- if (isProcessing) {
447
- return;
448
- }
449
- // Add to history
450
- completionEngine.addHistory(input);
451
- // Handle slash commands
452
- if (input.startsWith('/')) {
453
- const [cmd, ...args] = input.slice(1).split(' ');
454
- switch (cmd) {
455
- case 'help':
456
- printHelp(engine);
457
- break;
458
- case 'model':
459
- if (args[0]) {
460
- engine.switchModel(args[0]);
461
- engine.fallback.addFallbackModel(config.defaultModel);
462
- }
463
- else {
464
- engine.ui.modelList(config.defaultModel);
465
- }
466
- break;
467
- case 'agent':
468
- if (args[0]) {
469
- const agentNames = Array.from(engine.agents.keys());
470
- const found = agentNames.find(n => n.toLowerCase() === args[0].toLowerCase());
471
- if (found) {
472
- currentAgent = found;
473
- currentMode = 'direct';
474
- engine.ui.success(`Switched to agent: ${found}`);
459
+ try {
460
+ const input = line.trim();
461
+ if (!input) {
462
+ rl.prompt();
463
+ return;
464
+ }
465
+ // Add to history
466
+ completionEngine.addHistory(input);
467
+ // Handle slash commands
468
+ if (input.startsWith('/')) {
469
+ const [cmd, ...args] = input.slice(1).split(' ');
470
+ switch (cmd) {
471
+ case 'help':
472
+ printHelp(engine);
473
+ break;
474
+ case 'model':
475
+ if (args[0]) {
476
+ engine.switchModel(args[0]);
477
+ engine.fallback.addFallbackModel(config.defaultModel);
475
478
  }
476
479
  else {
477
- engine.ui.error(`Agent not found. Available: ${agentNames.join(', ')}`);
478
- }
479
- }
480
- else {
481
- const agents = Array.from(engine.agents.entries()).map(([name]) => ({ name, description: '', model: config.defaultModel }));
482
- engine.ui.agentList(agents);
483
- }
484
- break;
485
- case 'auto':
486
- currentMode = 'auto';
487
- currentAgent = undefined;
488
- engine.ui.success('Mode: Auto (smart orchestration)');
489
- break;
490
- case 'orchestrate':
491
- case 'plan':
492
- currentMode = 'agent';
493
- currentAgent = undefined;
494
- engine.ui.success('Mode: Multi-agent orchestration');
495
- break;
496
- case 'direct':
497
- currentMode = 'direct';
498
- engine.ui.success('Mode: Direct (single agent)');
499
- break;
500
- case 'plan-mode':
501
- engine.approval.setMode('plan');
502
- engine.ui.success(`${engine.approval.getModeIcon()} Permission: plan (read-only)`);
503
- break;
504
- case 'stats':
505
- const stats = engine.getSessionStats();
506
- engine.ui.sessionStats(stats.inputTokens, stats.outputTokens, stats.cost);
507
- break;
508
- case 'clear':
509
- console.clear();
510
- engine.ui.banner();
511
- break;
512
- case 'theme':
513
- if (args[0]) {
514
- config.ui.theme = args[0];
515
- engine.ui = new TerminalUI(config.ui.theme, config.ui.showTokenCount, config.ui.showCost);
516
- engine.ui.success(`Theme: ${args[0]}`);
517
- }
518
- else {
519
- console.log('Available themes: dracula, dark, nord, light');
520
- }
521
- break;
522
- case 'resume':
523
- if (args[0]) {
524
- const sessionId = args[0] === 'latest' ? engine.sessionManager.list()[0]?.id : args[0];
525
- if (sessionId) {
526
- const session = engine.sessionManager.load(sessionId);
527
- if (session)
528
- engine.ui.success(`Resumed session: ${sessionId.slice(0, 20)}...`);
529
- else
530
- engine.ui.error(`Session not found: ${sessionId}`);
480
+ engine.ui.modelList(config.defaultModel);
531
481
  }
532
- else {
533
- engine.ui.error('No sessions found');
482
+ break;
483
+ case 'agent':
484
+ if (args[0]) {
485
+ const agentNames = Array.from(engine.agents.keys());
486
+ const found = agentNames.find(n => n.toLowerCase() === args[0].toLowerCase());
487
+ if (found) {
488
+ currentAgent = found;
489
+ currentMode = 'direct';
490
+ engine.ui.success(`Switched to agent: ${found}`);
491
+ }
492
+ else {
493
+ engine.ui.error(`Agent not found. Available: ${agentNames.join(', ')}`);
494
+ }
534
495
  }
535
- }
536
- else {
537
- const sessions = engine.sessionManager.list();
538
- if (sessions.length === 0) {
539
- engine.ui.info('No sessions found');
540
- break;
496
+ else {
497
+ const agents = Array.from(engine.agents.entries()).map(([name]) => ({ name, description: '', model: config.defaultModel }));
498
+ engine.ui.agentList(agents);
541
499
  }
542
- console.log(chalk.bold('\nRecent sessions:\n'));
543
- for (const s of sessions.slice(0, 10)) {
544
- const date = new Date(s.createdAt).toLocaleString();
545
- console.log(` ${chalk.cyan(s.id.slice(0, 20))} ${chalk.gray(date)} ${chalk.gray(`${s.messageCount} msgs`)}`);
546
- }
547
- console.log(chalk.gray('\nUse /resume <id> to resume a session'));
548
- }
549
- break;
550
- case 'compact':
551
- engine.ui.info('Compacting conversation context...');
552
- engine.contextManager.manage(engine.sessionManager.getCurrent()?.messages || []);
553
- engine.ui.success('Context compacted');
554
- break;
555
- case 'undo':
556
- engine.ui.info('Undoing last change...');
557
- const undoAction = engine.undoRedo.undo();
558
- if (undoAction) {
559
- engine.ui.success(`Undone: ${undoAction.description}`);
560
- }
561
- else {
562
- engine.ui.warning('Nothing to undo');
563
- }
564
- break;
565
- case 'redo':
566
- engine.ui.info('Redoing...');
567
- const redoAction = engine.undoRedo.redo();
568
- if (redoAction) {
569
- engine.ui.success(`Redone: ${redoAction.description}`);
570
- }
571
- else {
572
- engine.ui.warning('Nothing to redo');
573
- }
574
- break;
575
- case 'rewind':
576
- const rewindN = args[0] ? parseInt(args[0]) : 1;
577
- if (isNaN(rewindN) || rewindN < 1) {
578
- engine.ui.error('Usage: /rewind <n>');
579
500
  break;
580
- }
581
- const undone = engine.undoRedo.undoN(rewindN);
582
- engine.ui.success(`Rewound ${undone.length} action(s)`);
583
- break;
584
- case 'fork':
585
- engine.ui.info('Forking current session...');
586
- const currentSession = engine.sessionManager.getCurrent();
587
- if (currentSession) {
588
- const forked = engine.sessionManager.create(process.cwd(), config.defaultModel);
589
- forked.messages = [...currentSession.messages];
590
- forked.forkedFrom = currentSession.id;
591
- engine.sessionManager.save();
592
- engine.ui.success(`Forked to new session: ${forked.id.slice(0, 20)}...`);
593
- }
594
- else {
595
- engine.ui.error('No active session to fork');
596
- }
597
- break;
598
- case 'mcp':
599
- const mcpSub = args[0];
600
- if (mcpSub === 'list') {
601
- const servers = engine.mcpClient.listServers();
602
- if (servers.length === 0) {
603
- engine.ui.info('No MCP servers configured');
604
- break;
501
+ case 'auto':
502
+ currentMode = 'auto';
503
+ currentAgent = undefined;
504
+ engine.ui.success('Mode: Auto (smart orchestration)');
505
+ break;
506
+ case 'orchestrate':
507
+ case 'plan':
508
+ currentMode = 'agent';
509
+ currentAgent = undefined;
510
+ engine.ui.success('Mode: Multi-agent orchestration');
511
+ break;
512
+ case 'direct':
513
+ currentMode = 'direct';
514
+ engine.ui.success('Mode: Direct (single agent)');
515
+ break;
516
+ case 'plan-mode':
517
+ engine.approval.setMode('plan');
518
+ engine.ui.success(`${engine.approval.getModeIcon()} Permission: plan (read-only)`);
519
+ break;
520
+ case 'stats':
521
+ const stats = engine.getSessionStats();
522
+ engine.ui.sessionStats(stats.inputTokens, stats.outputTokens, stats.cost);
523
+ break;
524
+ case 'clear':
525
+ console.clear();
526
+ engine.ui.banner();
527
+ break;
528
+ case 'theme':
529
+ if (args[0]) {
530
+ config.ui.theme = args[0];
531
+ engine.ui = new TerminalUI(config.ui.theme, config.ui.showTokenCount, config.ui.showCost);
532
+ engine.ui.success(`Theme: ${args[0]}`);
605
533
  }
606
- console.log(chalk.bold('\nMCP Servers:\n'));
607
- for (const s of servers) {
608
- const status = s.connected ? chalk.green('connected') : chalk.gray('disconnected');
609
- console.log(` ${chalk.cyan(s.name.padEnd(20))} ${status} ${chalk.gray(`${s.toolCount} tools`)} ${chalk.gray(s.config.transport)}`);
534
+ else {
535
+ console.log('Available themes: dracula, dark, nord, light');
610
536
  }
611
- }
612
- else if (mcpSub === 'connect' && args[1]) {
613
- try {
614
- const cfg = engine.mcpClient.loadConfig();
615
- if (cfg.mcpServers[args[1]]) {
616
- await engine.mcpClient.connect(args[1], cfg.mcpServers[args[1]]);
617
- engine.ui.success(`Connected to MCP server: ${args[1]}`);
537
+ break;
538
+ case 'resume':
539
+ if (args[0]) {
540
+ const sessionId = args[0] === 'latest' ? engine.sessionManager.list()[0]?.id : args[0];
541
+ if (sessionId) {
542
+ const session = engine.sessionManager.load(sessionId);
543
+ if (session)
544
+ engine.ui.success(`Resumed session: ${sessionId.slice(0, 20)}...`);
545
+ else
546
+ engine.ui.error(`Session not found: ${sessionId}`);
618
547
  }
619
548
  else {
620
- engine.ui.error(`MCP server "${args[1]}" not found`);
549
+ engine.ui.error('No sessions found');
621
550
  }
622
551
  }
623
- catch (e) {
624
- engine.ui.error(`Failed to connect: ${e instanceof Error ? e.message : String(e)}`);
625
- }
626
- }
627
- else if (mcpSub === 'disconnect' && args[1]) {
628
- await engine.mcpClient.disconnect(args[1]);
629
- engine.ui.success(`Disconnected from ${args[1]}`);
630
- }
631
- else if (mcpSub === 'health') {
632
- engine.mcpClient.healthReport();
633
- }
634
- else {
635
- console.log(chalk.bold('\nMCP Commands:\n'));
636
- console.log(' /mcp list List MCP servers');
637
- console.log(' /mcp connect <name> Connect to a server');
638
- console.log(' /mcp disconnect <name> Disconnect from a server');
639
- console.log(' /mcp health Show MCP health report');
640
- }
641
- break;
642
- case 'permission':
643
- case 'perm':
644
- if (args[0]) {
645
- const validModes = ['manual', 'auto', 'plan', 'yolo'];
646
- if (validModes.includes(args[0])) {
647
- engine.approval.setMode(args[0]);
648
- engine.ui.success(`${engine.approval.getModeIcon()} Permission: ${args[0]} (${engine.approval.getModeDescription()})`);
552
+ else {
553
+ const sessions = engine.sessionManager.list();
554
+ if (sessions.length === 0) {
555
+ engine.ui.info('No sessions found');
556
+ break;
557
+ }
558
+ console.log(chalk.bold('\nRecent sessions:\n'));
559
+ for (const s of sessions.slice(0, 10)) {
560
+ const date = new Date(s.createdAt).toLocaleString();
561
+ console.log(` ${chalk.cyan(s.id.slice(0, 20))} ${chalk.gray(date)} ${chalk.gray(`${s.messageCount} msgs`)}`);
562
+ }
563
+ console.log(chalk.gray('\nUse /resume <id> to resume a session'));
564
+ }
565
+ break;
566
+ case 'compact':
567
+ engine.ui.info('Compacting conversation context...');
568
+ engine.contextManager.manage(engine.sessionManager.getCurrent()?.messages || []);
569
+ engine.ui.success('Context compacted');
570
+ break;
571
+ case 'undo':
572
+ engine.ui.info('Undoing last change...');
573
+ const undoAction = engine.undoRedo.undo();
574
+ if (undoAction) {
575
+ engine.ui.success(`Undone: ${undoAction.description}`);
649
576
  }
650
577
  else {
651
- engine.ui.error(`Invalid mode. Use: ${validModes.join(', ')}`);
652
- }
653
- }
654
- else {
655
- const newMode = engine.approval.cycleMode();
656
- engine.ui.success(`${engine.approval.getModeIcon()} Permission: ${newMode} (${engine.approval.getModeDescription()})`);
657
- }
658
- break;
659
- case 'init':
660
- engine.ui.info('Initializing NEURO.md for this project...');
661
- try {
662
- const { NeuroMdSystem } = await import('./context/neuro-md.js');
663
- const nmd = new NeuroMdSystem(process.cwd());
664
- nmd.load();
665
- const content = `# Project Context\n\nThis file provides persistent context for NeuroCLI.\n\n## Tech Stack\n- [Detected automatically]\n\n## Conventions\n- Follow existing code patterns\n- Use TypeScript for all new files\n\n## Notes\n- This file is auto-generated by /init\n`;
666
- const { writeFileSync } = await import('fs');
667
- const { join } = await import('path');
668
- writeFileSync(join(process.cwd(), 'NEURO.md'), content, 'utf-8');
669
- engine.ui.success('NEURO.md created');
670
- }
671
- catch (e) {
672
- engine.ui.error('Could not create NEURO.md: ' + (e instanceof Error ? e.message : String(e)));
673
- }
674
- break;
675
- case 'unpause':
676
- engine.doomLoop.unpause();
677
- break;
678
- case 'sandbox':
679
- if (args[0] === 'on' || args[0] === 'enable') {
680
- engine.sandbox.enable();
681
- engine.ui.success('Sandbox mode enabled');
682
- }
683
- else if (args[0] === 'off' || args[0] === 'disable') {
684
- engine.sandbox.disable();
685
- engine.ui.success('Sandbox mode disabled');
686
- }
687
- else if (args[0] === 'status') {
688
- engine.sandbox.printStatus();
689
- }
690
- else if (args[0] === 'undo') {
691
- const undone = engine.sandbox.undoAll();
692
- engine.ui.success(`Undone ${undone} file modifications`);
693
- }
694
- else {
695
- const enabled = engine.sandbox.toggle();
696
- engine.ui.success(enabled ? 'Sandbox mode enabled' : 'Sandbox mode disabled');
697
- }
698
- break;
699
- case 'plugins':
700
- case 'plugin':
701
- const pluginSub = args[0];
702
- if (pluginSub === 'list') {
703
- const plugins = engine.pluginManager.listPlugins();
704
- if (plugins.length === 0) {
705
- engine.ui.info('No plugins loaded');
578
+ engine.ui.warning('Nothing to undo');
579
+ }
580
+ break;
581
+ case 'redo':
582
+ engine.ui.info('Redoing...');
583
+ const redoAction = engine.undoRedo.redo();
584
+ if (redoAction) {
585
+ engine.ui.success(`Redone: ${redoAction.description}`);
586
+ }
587
+ else {
588
+ engine.ui.warning('Nothing to redo');
589
+ }
590
+ break;
591
+ case 'rewind':
592
+ const rewindN = args[0] ? parseInt(args[0]) : 1;
593
+ if (isNaN(rewindN) || rewindN < 1) {
594
+ engine.ui.error('Usage: /rewind <n>');
706
595
  break;
707
596
  }
708
- console.log(chalk.bold('\nPlugins:\n'));
709
- for (const p of plugins) {
710
- console.log(` ${chalk.cyan(p.name)} v${p.version} - ${chalk.gray(p.description)} ${chalk.green(`${p.toolCount} tools`)}`);
597
+ const undone = engine.undoRedo.undoN(rewindN);
598
+ engine.ui.success(`Rewound ${undone.length} action(s)`);
599
+ break;
600
+ case 'fork':
601
+ engine.ui.info('Forking current session...');
602
+ const currentSession = engine.sessionManager.getCurrent();
603
+ if (currentSession) {
604
+ const forked = engine.sessionManager.create(process.cwd(), config.defaultModel);
605
+ forked.messages = [...currentSession.messages];
606
+ forked.forkedFrom = currentSession.id;
607
+ engine.sessionManager.save();
608
+ engine.ui.success(`Forked to new session: ${forked.id.slice(0, 20)}...`);
609
+ }
610
+ else {
611
+ engine.ui.error('No active session to fork');
612
+ }
613
+ break;
614
+ case 'mcp':
615
+ const mcpSub = args[0];
616
+ if (mcpSub === 'list') {
617
+ const servers = engine.mcpClient.listServers();
618
+ if (servers.length === 0) {
619
+ engine.ui.info('No MCP servers configured');
620
+ break;
621
+ }
622
+ console.log(chalk.bold('\nMCP Servers:\n'));
623
+ for (const s of servers) {
624
+ const status = s.connected ? chalk.green('connected') : chalk.gray('disconnected');
625
+ console.log(` ${chalk.cyan(s.name.padEnd(20))} ${status} ${chalk.gray(`${s.toolCount} tools`)} ${chalk.gray(s.config.transport)}`);
626
+ }
711
627
  }
712
- }
713
- else if (pluginSub === 'load' && args[1]) {
628
+ else if (mcpSub === 'connect' && args[1]) {
629
+ try {
630
+ const cfg = engine.mcpClient.loadConfig();
631
+ if (cfg.mcpServers[args[1]]) {
632
+ await engine.mcpClient.connect(args[1], cfg.mcpServers[args[1]]);
633
+ engine.ui.success(`Connected to MCP server: ${args[1]}`);
634
+ }
635
+ else {
636
+ engine.ui.error(`MCP server "${args[1]}" not found`);
637
+ }
638
+ }
639
+ catch (e) {
640
+ engine.ui.error(`Failed to connect: ${e instanceof Error ? e.message : String(e)}`);
641
+ }
642
+ }
643
+ else if (mcpSub === 'disconnect' && args[1]) {
644
+ await engine.mcpClient.disconnect(args[1]);
645
+ engine.ui.success(`Disconnected from ${args[1]}`);
646
+ }
647
+ else if (mcpSub === 'health') {
648
+ engine.mcpClient.healthReport();
649
+ }
650
+ else {
651
+ console.log(chalk.bold('\nMCP Commands:\n'));
652
+ console.log(' /mcp list List MCP servers');
653
+ console.log(' /mcp connect <name> Connect to a server');
654
+ console.log(' /mcp disconnect <name> Disconnect from a server');
655
+ console.log(' /mcp health Show MCP health report');
656
+ }
657
+ break;
658
+ case 'permission':
659
+ case 'perm':
660
+ if (args[0]) {
661
+ const validModes = ['manual', 'auto', 'plan', 'yolo'];
662
+ if (validModes.includes(args[0])) {
663
+ engine.approval.setMode(args[0]);
664
+ engine.ui.success(`${engine.approval.getModeIcon()} Permission: ${args[0]} (${engine.approval.getModeDescription()})`);
665
+ }
666
+ else {
667
+ engine.ui.error(`Invalid mode. Use: ${validModes.join(', ')}`);
668
+ }
669
+ }
670
+ else {
671
+ const newMode = engine.approval.cycleMode();
672
+ engine.ui.success(`${engine.approval.getModeIcon()} Permission: ${newMode} (${engine.approval.getModeDescription()})`);
673
+ }
674
+ break;
675
+ case 'init':
676
+ engine.ui.info('Initializing NEURO.md for this project...');
714
677
  try {
715
- await engine.pluginManager.loadByName(args[1]);
716
- engine.ui.success(`Plugin "${args[1]}" loaded`);
678
+ const { NeuroMdSystem } = await import('./context/neuro-md.js');
679
+ const nmd = new NeuroMdSystem(process.cwd());
680
+ nmd.load();
681
+ const content = `# Project Context\n\nThis file provides persistent context for NeuroCLI.\n\n## Tech Stack\n- [Detected automatically]\n\n## Conventions\n- Follow existing code patterns\n- Use TypeScript for all new files\n\n## Notes\n- This file is auto-generated by /init\n`;
682
+ const { writeFileSync } = await import('fs');
683
+ const { join } = await import('path');
684
+ writeFileSync(join(process.cwd(), 'NEURO.md'), content, 'utf-8');
685
+ engine.ui.success('NEURO.md created');
717
686
  }
718
687
  catch (e) {
719
- engine.ui.error(`Failed to load plugin: ${e instanceof Error ? e.message : String(e)}`);
720
- }
721
- }
722
- else {
723
- console.log(chalk.bold('\nPlugin Commands:\n'));
724
- console.log(' /plugins list List loaded plugins');
725
- console.log(' /plugins load <name> Load a plugin');
726
- }
727
- break;
728
- case 'whitelist':
729
- if (args[0] === 'add' && args[1]) {
730
- engine.approval.addToWhitelist(args[1]);
731
- engine.ui.success(`Added "${args[1]}" to whitelist`);
732
- }
733
- else if (args[0] === 'remove' && args[1]) {
734
- engine.approval.removeFromWhitelist(args[1]);
735
- engine.ui.success(`Removed "${args[1]}" from whitelist`);
736
- }
737
- else if (args[0] === 'list') {
738
- const wl = engine.approval.getWhitelist();
739
- console.log(chalk.bold('\nWhitelisted tools:\n'));
740
- for (const t of wl)
741
- console.log(` ${chalk.green('+')} ${t}`);
742
- }
743
- else {
744
- console.log('Usage: /whitelist add|remove|list <tool>');
745
- }
746
- break;
747
- case 'blacklist':
748
- if (args[0] === 'add' && args[1]) {
749
- engine.approval.addToBlacklist(args[1]);
750
- engine.ui.success(`Added "${args[1]}" to blacklist`);
751
- }
752
- else if (args[0] === 'remove' && args[1]) {
753
- engine.approval.removeFromBlacklist(args[1]);
754
- engine.ui.success(`Removed "${args[1]}" from blacklist`);
755
- }
756
- else if (args[0] === 'list') {
757
- const bl = engine.approval.getBlacklist();
758
- console.log(chalk.bold('\nBlacklisted tools:\n'));
759
- for (const t of bl)
760
- console.log(` ${chalk.red('-')} ${t}`);
761
- }
762
- else {
763
- console.log('Usage: /blacklist add|remove|list <tool>');
764
- }
765
- break;
766
- // --- v3.0 New Commands ---
767
- case 'style':
768
- if (args[0]) {
769
- if (engine.styleManager.setStyle(args[0])) {
770
- engine.ui.success(`Output style: ${args[0]}`);
688
+ engine.ui.error('Could not create NEURO.md: ' + (e instanceof Error ? e.message : String(e)));
689
+ }
690
+ break;
691
+ case 'unpause':
692
+ engine.doomLoop.unpause();
693
+ break;
694
+ case 'sandbox':
695
+ if (args[0] === 'on' || args[0] === 'enable') {
696
+ engine.sandbox.enable();
697
+ engine.ui.success('Sandbox mode enabled');
698
+ }
699
+ else if (args[0] === 'off' || args[0] === 'disable') {
700
+ engine.sandbox.disable();
701
+ engine.ui.success('Sandbox mode disabled');
702
+ }
703
+ else if (args[0] === 'status') {
704
+ engine.sandbox.printStatus();
705
+ }
706
+ else if (args[0] === 'undo') {
707
+ const undone = engine.sandbox.undoAll();
708
+ engine.ui.success(`Undone ${undone} file modifications`);
771
709
  }
772
710
  else {
773
- engine.ui.error(`Unknown style. Available: ${engine.styleManager.listStyles().map(s => s.name).join(', ')}`);
774
- }
775
- }
776
- else {
777
- engine.styleManager.printStyles();
778
- }
779
- break;
780
- case 'thinking':
781
- if (args[0]) {
782
- const modes = ['none', 'brief', 'full', 'ultrathink'];
783
- if (modes.includes(args[0])) {
784
- engine.extendedThinking.setMode(args[0]);
785
- if (args[0] !== 'none' && !engine.extendedThinking.isDisplayEnabled()) {
786
- engine.extendedThinking.toggleDisplay();
711
+ const enabled = engine.sandbox.toggle();
712
+ engine.ui.success(enabled ? 'Sandbox mode enabled' : 'Sandbox mode disabled');
713
+ }
714
+ break;
715
+ case 'plugins':
716
+ case 'plugin':
717
+ const pluginSub = args[0];
718
+ if (pluginSub === 'list') {
719
+ const plugins = engine.pluginManager.listPlugins();
720
+ if (plugins.length === 0) {
721
+ engine.ui.info('No plugins loaded');
722
+ break;
723
+ }
724
+ console.log(chalk.bold('\nPlugins:\n'));
725
+ for (const p of plugins) {
726
+ console.log(` ${chalk.cyan(p.name)} v${p.version} - ${chalk.gray(p.description)} ${chalk.green(`${p.toolCount} tools`)}`);
727
+ }
728
+ }
729
+ else if (pluginSub === 'load' && args[1]) {
730
+ try {
731
+ await engine.pluginManager.loadByName(args[1]);
732
+ engine.ui.success(`Plugin "${args[1]}" loaded`);
733
+ }
734
+ catch (e) {
735
+ engine.ui.error(`Failed to load plugin: ${e instanceof Error ? e.message : String(e)}`);
787
736
  }
788
- engine.ui.success(`Thinking mode: ${args[0]}`);
789
737
  }
790
738
  else {
791
- engine.ui.error(`Invalid mode. Use: ${modes.join(', ')}`);
792
- }
793
- }
794
- else {
795
- const currentMode = engine.extendedThinking.getMode();
796
- const showing = engine.extendedThinking.isDisplayEnabled() ? 'visible' : 'hidden';
797
- console.log(chalk.bold(`\nThinking Mode: ${chalk.cyan(currentMode)} (display: ${showing})`));
798
- console.log(chalk.gray(' Toggle display: /thinking toggle'));
799
- console.log(chalk.gray(' Set mode: /thinking none|brief|full|ultrathink'));
800
- console.log();
801
- }
802
- break;
803
- case 'skills':
804
- const skillSub = args[0];
805
- if (skillSub === 'list') {
806
- engine.skillSystem.listSkills();
807
- }
808
- else if (skillSub === 'activate' && args[1]) {
809
- const activated = engine.skillSystem.activate(args[1]);
810
- if (activated) {
811
- engine.ui.success(`Skill activated: ${args[1]}`);
739
+ console.log(chalk.bold('\nPlugin Commands:\n'));
740
+ console.log(' /plugins list List loaded plugins');
741
+ console.log(' /plugins load <name> Load a plugin');
742
+ }
743
+ break;
744
+ case 'whitelist':
745
+ if (args[0] === 'add' && args[1]) {
746
+ engine.approval.addToWhitelist(args[1]);
747
+ engine.ui.success(`Added "${args[1]}" to whitelist`);
748
+ }
749
+ else if (args[0] === 'remove' && args[1]) {
750
+ engine.approval.removeFromWhitelist(args[1]);
751
+ engine.ui.success(`Removed "${args[1]}" from whitelist`);
752
+ }
753
+ else if (args[0] === 'list') {
754
+ const wl = engine.approval.getWhitelist();
755
+ console.log(chalk.bold('\nWhitelisted tools:\n'));
756
+ for (const t of wl)
757
+ console.log(` ${chalk.green('+')} ${t}`);
758
+ }
759
+ else {
760
+ console.log('Usage: /whitelist add|remove|list <tool>');
761
+ }
762
+ break;
763
+ case 'blacklist':
764
+ if (args[0] === 'add' && args[1]) {
765
+ engine.approval.addToBlacklist(args[1]);
766
+ engine.ui.success(`Added "${args[1]}" to blacklist`);
767
+ }
768
+ else if (args[0] === 'remove' && args[1]) {
769
+ engine.approval.removeFromBlacklist(args[1]);
770
+ engine.ui.success(`Removed "${args[1]}" from blacklist`);
771
+ }
772
+ else if (args[0] === 'list') {
773
+ const bl = engine.approval.getBlacklist();
774
+ console.log(chalk.bold('\nBlacklisted tools:\n'));
775
+ for (const t of bl)
776
+ console.log(` ${chalk.red('-')} ${t}`);
812
777
  }
813
778
  else {
814
- engine.ui.error(`Skill not found: ${args[1]}`);
779
+ console.log('Usage: /blacklist add|remove|list <tool>');
815
780
  }
816
- }
817
- else if (skillSub === 'deactivate' && args[1]) {
818
- if (engine.skillSystem.deactivate(args[1])) {
819
- engine.ui.success(`Skill deactivated: ${args[1]}`);
781
+ break;
782
+ // --- v3.0 New Commands ---
783
+ case 'style':
784
+ if (args[0]) {
785
+ if (engine.styleManager.setStyle(args[0])) {
786
+ engine.ui.success(`Output style: ${args[0]}`);
787
+ }
788
+ else {
789
+ engine.ui.error(`Unknown style. Available: ${engine.styleManager.listStyles().map(s => s.name).join(', ')}`);
790
+ }
820
791
  }
821
792
  else {
822
- engine.ui.error(`Skill not active: ${args[1]}`);
823
- }
824
- }
825
- else if (skillSub === 'clear') {
826
- engine.skillSystem.deactivateAll();
827
- engine.ui.success('All skills deactivated');
828
- }
829
- else {
830
- console.log(chalk.bold('\nSkill Commands:\n'));
831
- console.log(' /skills list List all skills');
832
- console.log(' /skills activate <name> Activate a skill');
833
- console.log(' /skills deactivate <name> Deactivate a skill');
834
- console.log(' /skills clear Deactivate all skills');
835
- }
836
- break;
837
- case 'effort':
838
- if (args[0]) {
839
- const levels = ['low', 'medium', 'high', 'ultrathink'];
840
- if (levels.includes(args[0])) {
841
- engine.modelRouter.setEffort(args[0]);
842
- engine.ui.success(`Effort level: ${args[0]}`);
793
+ engine.styleManager.printStyles();
794
+ }
795
+ break;
796
+ case 'thinking':
797
+ if (args[0]) {
798
+ const modes = ['none', 'brief', 'full', 'ultrathink'];
799
+ if (modes.includes(args[0])) {
800
+ engine.extendedThinking.setMode(args[0]);
801
+ if (args[0] !== 'none' && !engine.extendedThinking.isDisplayEnabled()) {
802
+ engine.extendedThinking.toggleDisplay();
803
+ }
804
+ engine.ui.success(`Thinking mode: ${args[0]}`);
805
+ }
806
+ else {
807
+ engine.ui.error(`Invalid mode. Use: ${modes.join(', ')}`);
808
+ }
843
809
  }
844
810
  else {
845
- engine.ui.error(`Invalid level. Use: ${levels.join(', ')}`);
811
+ const currentMode = engine.extendedThinking.getMode();
812
+ const showing = engine.extendedThinking.isDisplayEnabled() ? 'visible' : 'hidden';
813
+ console.log(chalk.bold(`\nThinking Mode: ${chalk.cyan(currentMode)} (display: ${showing})`));
814
+ console.log(chalk.gray(' Toggle display: /thinking toggle'));
815
+ console.log(chalk.gray(' Set mode: /thinking none|brief|full|ultrathink'));
816
+ console.log();
846
817
  }
847
- }
848
- else {
849
- console.log(chalk.bold(`\nEffort Level: ${chalk.cyan(engine.modelRouter.getEffort())}`));
850
- console.log(chalk.gray(' Set level: /effort low|medium|high|ultrathink'));
851
- console.log();
852
- }
853
- break;
854
- case 'cache':
855
- const cacheSub = args[0];
856
- if (cacheSub === 'on') {
857
- config.promptCache.enabled = true;
858
- engine.ui.success('Prompt cache enabled');
859
- }
860
- else if (cacheSub === 'off') {
861
- config.promptCache.enabled = false;
862
- engine.ui.success('Prompt cache disabled');
863
- }
864
- else if (cacheSub === 'clear') {
865
- engine.promptCache.clear();
866
- engine.ui.success('Cache cleared');
867
- }
868
- else if (cacheSub === 'stats') {
869
- engine.promptCache.printStats();
870
- }
871
- else {
872
- const status = config.promptCache.enabled ? chalk.green('enabled') : chalk.gray('disabled');
873
- console.log(chalk.bold(`\nPrompt Cache: ${status}`));
874
- console.log(chalk.gray(' /cache on|off|clear|stats'));
818
+ break;
819
+ case 'skills':
820
+ const skillSub = args[0];
821
+ if (skillSub === 'list') {
822
+ engine.skillSystem.listSkills();
823
+ }
824
+ else if (skillSub === 'activate' && args[1]) {
825
+ const activated = engine.skillSystem.activate(args[1]);
826
+ if (activated) {
827
+ engine.ui.success(`Skill activated: ${args[1]}`);
828
+ }
829
+ else {
830
+ engine.ui.error(`Skill not found: ${args[1]}`);
831
+ }
832
+ }
833
+ else if (skillSub === 'deactivate' && args[1]) {
834
+ if (engine.skillSystem.deactivate(args[1])) {
835
+ engine.ui.success(`Skill deactivated: ${args[1]}`);
836
+ }
837
+ else {
838
+ engine.ui.error(`Skill not active: ${args[1]}`);
839
+ }
840
+ }
841
+ else if (skillSub === 'clear') {
842
+ engine.skillSystem.deactivateAll();
843
+ engine.ui.success('All skills deactivated');
844
+ }
845
+ else {
846
+ console.log(chalk.bold('\nSkill Commands:\n'));
847
+ console.log(' /skills list List all skills');
848
+ console.log(' /skills activate <name> Activate a skill');
849
+ console.log(' /skills deactivate <name> Deactivate a skill');
850
+ console.log(' /skills clear Deactivate all skills');
851
+ }
852
+ break;
853
+ case 'effort':
854
+ if (args[0]) {
855
+ const levels = ['low', 'medium', 'high', 'ultrathink'];
856
+ if (levels.includes(args[0])) {
857
+ engine.modelRouter.setEffort(args[0]);
858
+ engine.ui.success(`Effort level: ${args[0]}`);
859
+ }
860
+ else {
861
+ engine.ui.error(`Invalid level. Use: ${levels.join(', ')}`);
862
+ }
863
+ }
864
+ else {
865
+ console.log(chalk.bold(`\nEffort Level: ${chalk.cyan(engine.modelRouter.getEffort())}`));
866
+ console.log(chalk.gray(' Set level: /effort low|medium|high|ultrathink'));
867
+ console.log();
868
+ }
869
+ break;
870
+ case 'cache':
871
+ const cacheSub = args[0];
872
+ if (cacheSub === 'on') {
873
+ config.promptCache.enabled = true;
874
+ engine.ui.success('Prompt cache enabled');
875
+ }
876
+ else if (cacheSub === 'off') {
877
+ config.promptCache.enabled = false;
878
+ engine.ui.success('Prompt cache disabled');
879
+ }
880
+ else if (cacheSub === 'clear') {
881
+ engine.promptCache.clear();
882
+ engine.ui.success('Cache cleared');
883
+ }
884
+ else if (cacheSub === 'stats') {
885
+ engine.promptCache.printStats();
886
+ }
887
+ else {
888
+ const status = config.promptCache.enabled ? chalk.green('enabled') : chalk.gray('disabled');
889
+ console.log(chalk.bold(`\nPrompt Cache: ${status}`));
890
+ console.log(chalk.gray(' /cache on|off|clear|stats'));
891
+ console.log();
892
+ }
893
+ break;
894
+ case 'spending':
895
+ engine.spendingMonitor.printReport();
896
+ break;
897
+ case 'ignore':
898
+ if (args[0] === 'list') {
899
+ engine.neuroIgnore.printRules();
900
+ }
901
+ else if (args[0] === 'add' && args[1]) {
902
+ engine.neuroIgnore.addRule(args[1], 'manual');
903
+ engine.ui.success(`Added ignore rule: ${args[1]}`);
904
+ }
905
+ else if (args[0] === 'check' && args[1]) {
906
+ const ignored = engine.neuroIgnore.isIgnored(args[1]);
907
+ console.log(` ${args[1]}: ${ignored ? chalk.red('ignored') : chalk.green('allowed')}`);
908
+ }
909
+ else {
910
+ console.log(chalk.bold('\nIgnore Commands:\n'));
911
+ console.log(' /ignore list List ignore rules');
912
+ console.log(' /ignore add <pattern> Add an ignore pattern');
913
+ console.log(' /ignore check <path> Check if a path is ignored');
914
+ }
915
+ break;
916
+ case 'ollama':
917
+ try {
918
+ const available = await engine.ollamaProvider.isAvailable();
919
+ if (!available) {
920
+ engine.ui.error('Ollama is not running. Start it with: ollama serve');
921
+ break;
922
+ }
923
+ const models = await engine.ollamaProvider.listModels();
924
+ console.log(chalk.bold('\nOllama Local Models:\n'));
925
+ for (const m of models) {
926
+ const isActive = config.defaultModel === m.name;
927
+ console.log(` ${isActive ? chalk.green('*') : ' '} ${chalk.cyan(m.name.padEnd(40))} ${chalk.gray(m.details?.parameter_size || '')} ${chalk.gray(m.details?.quantization_level || '')}`);
928
+ }
929
+ console.log(chalk.gray('\nSwitch model: /model <name>'));
930
+ }
931
+ catch (e) {
932
+ engine.ui.error('Could not connect to Ollama');
933
+ }
934
+ break;
935
+ case 'doctor':
936
+ console.log(chalk.bold('\nNeuroCLI v' + VERSION + ' Health Check:\n'));
937
+ console.log(` API Key: ${config.apiKey ? chalk.green('configured') : chalk.red('MISSING')}`);
938
+ console.log(` Default Model: ${chalk.cyan(config.defaultModel)} ${MODELS[config.defaultModel] ? chalk.green('valid') : chalk.red('INVALID')}`);
939
+ console.log(` MCP Servers: ${chalk.cyan(String(engine.mcpClient.listServers().length))}`);
940
+ console.log(` Permission Mode: ${chalk.cyan(engine.approval.getMode())}`);
941
+ console.log(` Diff Preview: ${config.diffPreview ? chalk.green('enabled') : chalk.gray('disabled')}`);
942
+ console.log(` Fallback Chain: ${config.fallbackChain.models.length > 0 ? chalk.green(config.fallbackChain.models.length + ' models') : chalk.yellow('none')}`);
943
+ console.log(` Doom Loop Protection: ${config.doomLoop.autoBreak ? chalk.green('enabled') : chalk.yellow('disabled')}`);
944
+ console.log(` Sandbox: ${engine.sandbox.isEnabled() ? chalk.green('enabled') : chalk.gray('disabled')}`);
945
+ console.log(` Plugins: ${chalk.cyan(String(engine.pluginManager.listPlugins().length))}`);
946
+ console.log(` Custom Agents: ${chalk.cyan(String(engine.customAgentLoader.getAll().length))}`);
947
+ console.log(` Custom Tools: ${chalk.cyan(String(engine.customToolLoader.getAll().length))}`);
948
+ console.log(` Skills: ${chalk.cyan(String(engine.skillSystem.getAllSkills().length))} (${chalk.cyan(String(engine.skillSystem.getActiveSkills().length))} active)`);
949
+ console.log(` Prompt Cache: ${config.promptCache.enabled ? chalk.green('enabled') : chalk.gray('disabled')}`);
950
+ console.log(` Output Style: ${chalk.cyan(engine.styleManager.getStyle().name)}`);
951
+ console.log(` Thinking Mode: ${chalk.cyan(engine.extendedThinking.getMode())}`);
952
+ console.log(` Effort Level: ${chalk.cyan(engine.modelRouter.getEffort())}`);
953
+ console.log(` Sessions: ${chalk.cyan(String(engine.sessionManager.list().length))}`);
954
+ console.log(` Spending Limit: ${config.spendingLimit > 0 ? chalk.cyan('$' + config.spendingLimit.toFixed(2)) : chalk.gray('unlimited')}`);
955
+ console.log(` Ignore Rules: ${chalk.cyan(String(engine.neuroIgnore.getRules().length))}`);
956
+ // Check Ollama availability
957
+ const ollamaAvail = await engine.ollamaProvider.isAvailable().catch(() => false);
958
+ console.log(` Ollama: ${ollamaAvail ? chalk.green('available') : chalk.gray('not running')}`);
959
+ // Update check status
960
+ const lastUpdateCheck = updater.getLastCheck();
961
+ const updateStatus = lastUpdateCheck?.hasUpdate ? chalk.yellow(`update available (v${lastUpdateCheck.latestVersion})`) : chalk.green('up to date');
962
+ console.log(` Auto-Update: ${updateStatus}`);
963
+ const nextCheck = updater.timeUntilNextCheck();
964
+ const nextCheckStr = nextCheck > 0 ? ` (next check in ${Math.floor(nextCheck / 3600000)}h)` : '';
965
+ console.log(` Update Check: ${chalk.gray('enabled' + nextCheckStr)}`);
875
966
  console.log();
876
- }
877
- break;
878
- case 'spending':
879
- engine.spendingMonitor.printReport();
880
- break;
881
- case 'ignore':
882
- if (args[0] === 'list') {
883
- engine.neuroIgnore.printRules();
884
- }
885
- else if (args[0] === 'add' && args[1]) {
886
- engine.neuroIgnore.addRule(args[1], 'manual');
887
- engine.ui.success(`Added ignore rule: ${args[1]}`);
888
- }
889
- else if (args[0] === 'check' && args[1]) {
890
- const ignored = engine.neuroIgnore.isIgnored(args[1]);
891
- console.log(` ${args[1]}: ${ignored ? chalk.red('ignored') : chalk.green('allowed')}`);
892
- }
893
- else {
894
- console.log(chalk.bold('\nIgnore Commands:\n'));
895
- console.log(' /ignore list List ignore rules');
896
- console.log(' /ignore add <pattern> Add an ignore pattern');
897
- console.log(' /ignore check <path> Check if a path is ignored');
898
- }
899
- break;
900
- case 'ollama':
901
- try {
902
- const available = await engine.ollamaProvider.isAvailable();
903
- if (!available) {
904
- engine.ui.error('Ollama is not running. Start it with: ollama serve');
967
+ break;
968
+ case 'export':
969
+ const exportSession = engine.sessionManager.getCurrent();
970
+ if (exportSession) {
971
+ const exportPath = args[0] || 'neuro-session-export.json';
972
+ const exportData = {
973
+ version: VERSION,
974
+ exportedAt: Date.now(),
975
+ session: exportSession,
976
+ neuroVersion: VERSION,
977
+ };
978
+ try {
979
+ writeFileSync(exportPath, JSON.stringify(exportData, null, 2), 'utf-8');
980
+ engine.ui.success(`Session exported to ${exportPath}`);
981
+ }
982
+ catch (e) {
983
+ console.log(JSON.stringify(exportSession, null, 2));
984
+ }
985
+ }
986
+ else {
987
+ engine.ui.warning('No active session to export');
988
+ }
989
+ break;
990
+ case 'import':
991
+ const importPath = args[0];
992
+ if (!importPath) {
993
+ engine.ui.error('Usage: /import <path-to-json-file>');
905
994
  break;
906
995
  }
907
- const models = await engine.ollamaProvider.listModels();
908
- console.log(chalk.bold('\nOllama Local Models:\n'));
909
- for (const m of models) {
910
- const isActive = config.defaultModel === m.name;
911
- console.log(` ${isActive ? chalk.green('*') : ' '} ${chalk.cyan(m.name.padEnd(40))} ${chalk.gray(m.details?.parameter_size || '')} ${chalk.gray(m.details?.quantization_level || '')}`);
912
- }
913
- console.log(chalk.gray('\nSwitch model: /model <name>'));
914
- }
915
- catch (e) {
916
- engine.ui.error('Could not connect to Ollama');
917
- }
918
- break;
919
- case 'doctor':
920
- console.log(chalk.bold('\nNeuroCLI v4.1.2 Health Check:\n'));
921
- console.log(` API Key: ${config.apiKey ? chalk.green('configured') : chalk.red('MISSING')}`);
922
- console.log(` Default Model: ${chalk.cyan(config.defaultModel)} ${MODELS[config.defaultModel] ? chalk.green('valid') : chalk.red('INVALID')}`);
923
- console.log(` MCP Servers: ${chalk.cyan(String(engine.mcpClient.listServers().length))}`);
924
- console.log(` Permission Mode: ${chalk.cyan(engine.approval.getMode())}`);
925
- console.log(` Diff Preview: ${config.diffPreview ? chalk.green('enabled') : chalk.gray('disabled')}`);
926
- console.log(` Fallback Chain: ${config.fallbackChain.models.length > 0 ? chalk.green(config.fallbackChain.models.length + ' models') : chalk.yellow('none')}`);
927
- console.log(` Doom Loop Protection: ${config.doomLoop.autoBreak ? chalk.green('enabled') : chalk.yellow('disabled')}`);
928
- console.log(` Sandbox: ${engine.sandbox.isEnabled() ? chalk.green('enabled') : chalk.gray('disabled')}`);
929
- console.log(` Plugins: ${chalk.cyan(String(engine.pluginManager.listPlugins().length))}`);
930
- console.log(` Custom Agents: ${chalk.cyan(String(engine.customAgentLoader.getAll().length))}`);
931
- console.log(` Custom Tools: ${chalk.cyan(String(engine.customToolLoader.getAll().length))}`);
932
- console.log(` Skills: ${chalk.cyan(String(engine.skillSystem.getAllSkills().length))} (${chalk.cyan(String(engine.skillSystem.getActiveSkills().length))} active)`);
933
- console.log(` Prompt Cache: ${config.promptCache.enabled ? chalk.green('enabled') : chalk.gray('disabled')}`);
934
- console.log(` Output Style: ${chalk.cyan(engine.styleManager.getStyle().name)}`);
935
- console.log(` Thinking Mode: ${chalk.cyan(engine.extendedThinking.getMode())}`);
936
- console.log(` Effort Level: ${chalk.cyan(engine.modelRouter.getEffort())}`);
937
- console.log(` Sessions: ${chalk.cyan(String(engine.sessionManager.list().length))}`);
938
- console.log(` Spending Limit: ${config.spendingLimit > 0 ? chalk.cyan('$' + config.spendingLimit.toFixed(2)) : chalk.gray('unlimited')}`);
939
- console.log(` Ignore Rules: ${chalk.cyan(String(engine.neuroIgnore.getRules().length))}`);
940
- // Check Ollama availability
941
- const ollamaAvail = await engine.ollamaProvider.isAvailable().catch(() => false);
942
- console.log(` Ollama: ${ollamaAvail ? chalk.green('available') : chalk.gray('not running')}`);
943
- // Update check status
944
- const lastUpdateCheck = updater.getLastCheck();
945
- const updateStatus = lastUpdateCheck?.hasUpdate ? chalk.yellow(`update available (v${lastUpdateCheck.latestVersion})`) : chalk.green('up to date');
946
- console.log(` Auto-Update: ${updateStatus}`);
947
- const nextCheck = updater.timeUntilNextCheck();
948
- const nextCheckStr = nextCheck > 0 ? ` (next check in ${Math.floor(nextCheck / 3600000)}h)` : '';
949
- console.log(` Update Check: ${chalk.gray('enabled' + nextCheckStr)}`);
950
- console.log();
951
- break;
952
- case 'export':
953
- const exportSession = engine.sessionManager.getCurrent();
954
- if (exportSession) {
955
- const exportPath = args[0] || 'neuro-session-export.json';
956
- const exportData = {
957
- version: VERSION,
958
- exportedAt: Date.now(),
959
- session: exportSession,
960
- neuroVersion: VERSION,
961
- };
962
996
  try {
963
- writeFileSync(exportPath, JSON.stringify(exportData, null, 2), 'utf-8');
964
- engine.ui.success(`Session exported to ${exportPath}`);
997
+ const importData = JSON.parse(readFileSync(importPath, 'utf-8'));
998
+ const sessionData = importData.session || importData;
999
+ const newSession = engine.sessionManager.create(process.cwd(), sessionData.model || config.defaultModel);
1000
+ if (sessionData.messages)
1001
+ newSession.messages = sessionData.messages;
1002
+ if (sessionData.totalInputTokens)
1003
+ newSession.totalInputTokens = sessionData.totalInputTokens;
1004
+ if (sessionData.totalOutputTokens)
1005
+ newSession.totalOutputTokens = sessionData.totalOutputTokens;
1006
+ if (sessionData.totalCost)
1007
+ newSession.totalCost = sessionData.totalCost;
1008
+ engine.sessionManager.save();
1009
+ engine.ui.success(`Session imported: ${newSession.id.slice(0, 20)}... (${newSession.messages.length} messages)`);
1010
+ }
1011
+ catch (e) {
1012
+ engine.ui.error(`Failed to import: ${e instanceof Error ? e.message : String(e)}`);
1013
+ }
1014
+ break;
1015
+ case 'commit-push-pr':
1016
+ engine.ui.info('Running commit + push + PR workflow...');
1017
+ try {
1018
+ const { execSync } = await import('child_process');
1019
+ // Stage all
1020
+ execSync('git add -A', { cwd: process.cwd(), encoding: 'utf-8' });
1021
+ // Commit
1022
+ const commitMsg = args.join(' ') || 'Update from NeuroCLI';
1023
+ execSync(`git commit -m "${commitMsg}" --no-gpg-sign`, { cwd: process.cwd(), encoding: 'utf-8' });
1024
+ // Push
1025
+ execSync('git push', { cwd: process.cwd(), encoding: 'utf-8' });
1026
+ engine.ui.success('Changes committed and pushed');
1027
+ // Try to create PR with gh CLI
1028
+ try {
1029
+ const prResult = execSync(`gh pr create --title "${commitMsg}" --body "Auto-generated by NeuroCLI"`, { cwd: process.cwd(), encoding: 'utf-8' });
1030
+ engine.ui.success(`PR created: ${prResult.trim()}`);
1031
+ }
1032
+ catch {
1033
+ engine.ui.info('Could not create PR (gh CLI not available or not a GitHub repo)');
1034
+ }
965
1035
  }
966
1036
  catch (e) {
967
- console.log(JSON.stringify(exportSession, null, 2));
968
- }
969
- }
970
- else {
971
- engine.ui.warning('No active session to export');
972
- }
973
- break;
974
- case 'import':
975
- const importPath = args[0];
976
- if (!importPath) {
977
- engine.ui.error('Usage: /import <path-to-json-file>');
1037
+ engine.ui.error(`Git operation failed: ${e instanceof Error ? e.message : String(e)}`);
1038
+ }
978
1039
  break;
979
- }
980
- try {
981
- const importData = JSON.parse(readFileSync(importPath, 'utf-8'));
982
- const sessionData = importData.session || importData;
983
- const newSession = engine.sessionManager.create(process.cwd(), sessionData.model || config.defaultModel);
984
- if (sessionData.messages)
985
- newSession.messages = sessionData.messages;
986
- if (sessionData.totalInputTokens)
987
- newSession.totalInputTokens = sessionData.totalInputTokens;
988
- if (sessionData.totalOutputTokens)
989
- newSession.totalOutputTokens = sessionData.totalOutputTokens;
990
- if (sessionData.totalCost)
991
- newSession.totalCost = sessionData.totalCost;
992
- engine.sessionManager.save();
993
- engine.ui.success(`Session imported: ${newSession.id.slice(0, 20)}... (${newSession.messages.length} messages)`);
994
- }
995
- catch (e) {
996
- engine.ui.error(`Failed to import: ${e instanceof Error ? e.message : String(e)}`);
997
- }
998
- break;
999
- case 'commit-push-pr':
1000
- engine.ui.info('Running commit + push + PR workflow...');
1001
- try {
1002
- const { execSync } = await import('child_process');
1003
- // Stage all
1004
- execSync('git add -A', { cwd: process.cwd(), encoding: 'utf-8' });
1005
- // Commit
1006
- const commitMsg = args.join(' ') || 'Update from NeuroCLI';
1007
- execSync(`git commit -m "${commitMsg}" --no-gpg-sign`, { cwd: process.cwd(), encoding: 'utf-8' });
1008
- // Push
1009
- execSync('git push', { cwd: process.cwd(), encoding: 'utf-8' });
1010
- engine.ui.success('Changes committed and pushed');
1011
- // Try to create PR with gh CLI
1040
+ case 'code-review':
1041
+ engine.ui.info('Starting multi-agent code review...');
1012
1042
  try {
1013
- const prResult = execSync(`gh pr create --title "${commitMsg}" --body "Auto-generated by NeuroCLI"`, { cwd: process.cwd(), encoding: 'utf-8' });
1014
- engine.ui.success(`PR created: ${prResult.trim()}`);
1015
- }
1016
- catch {
1017
- engine.ui.info('Could not create PR (gh CLI not available or not a GitHub repo)');
1018
- }
1019
- }
1020
- catch (e) {
1021
- engine.ui.error(`Git operation failed: ${e instanceof Error ? e.message : String(e)}`);
1022
- }
1023
- break;
1024
- case 'code-review':
1025
- engine.ui.info('Starting multi-agent code review...');
1026
- try {
1027
- const reviewResult = await engine.processMessage('Perform a thorough code review of all recent changes in this repository. Check for: bugs, security issues, performance problems, code style, test coverage. Provide findings with severity levels (CRITICAL, WARNING, INFO).', 'agent');
1028
- }
1029
- catch (e) {
1030
- engine.ui.error(`Code review failed: ${e instanceof Error ? e.message : String(e)}`);
1031
- }
1032
- break;
1033
- case 'feedback':
1034
- console.log(chalk.bold('\nFeedback:\n'));
1035
- console.log(' Report issues: https://github.com/neuro-cli/neuro/issues');
1036
- console.log(' Discussions: https://github.com/neuro-cli/neuro/discussions');
1037
- console.log(chalk.gray('\n Your feedback helps make NeuroCLI better!'));
1038
- console.log();
1039
- break;
1040
- case 'cost':
1041
- engine.spendingMonitor.printReport();
1042
- if (engine.promptCache) {
1043
- console.log(chalk.bold('\nCache Savings:'));
1044
- engine.promptCache.printStats();
1045
- }
1046
- break;
1047
- case 'update':
1048
- case 'upgrade':
1049
- const updateSub = args[0];
1050
- if (updateSub === 'now') {
1051
- engine.ui.info('Updating NeuroCLI...');
1052
- const updateResult = await updater.performUpdate();
1053
- if (updateResult.success) {
1054
- engine.ui.success(updateResult.message);
1055
- console.log(chalk.yellow(' Please restart NeuroCLI to use the new version.'));
1043
+ const reviewResult = await engine.processMessage('Perform a thorough code review of all recent changes in this repository. Check for: bugs, security issues, performance problems, code style, test coverage. Provide findings with severity levels (CRITICAL, WARNING, INFO).', 'agent');
1056
1044
  }
1057
- else {
1058
- engine.ui.error(updateResult.message);
1045
+ catch (e) {
1046
+ engine.ui.error(`Code review failed: ${e instanceof Error ? e.message : String(e)}`);
1059
1047
  }
1060
- }
1061
- else if (updateSub === 'check') {
1062
- engine.ui.info('Checking for updates...');
1063
- const checkResult = await updater.checkForUpdate(true);
1064
- if (checkResult.hasUpdate) {
1065
- updater.showUpdateDetails(checkResult);
1066
- updater.showUpdateNotification(checkResult);
1048
+ break;
1049
+ case 'feedback':
1050
+ console.log(chalk.bold('\nFeedback:\n'));
1051
+ console.log(' Report issues: https://github.com/neuro-cli/neuro/issues');
1052
+ console.log(' Discussions: https://github.com/neuro-cli/neuro/discussions');
1053
+ console.log(chalk.gray('\n Your feedback helps make NeuroCLI better!'));
1054
+ console.log();
1055
+ break;
1056
+ case 'cost':
1057
+ engine.spendingMonitor.printReport();
1058
+ if (engine.promptCache) {
1059
+ console.log(chalk.bold('\nCache Savings:'));
1060
+ engine.promptCache.printStats();
1067
1061
  }
1068
- else {
1069
- updater.showUpToDate();
1062
+ break;
1063
+ case 'update':
1064
+ case 'upgrade':
1065
+ const updateSub = args[0];
1066
+ if (updateSub === 'now') {
1067
+ engine.ui.info('Updating NeuroCLI...');
1068
+ const updateResult = await updater.performUpdate();
1069
+ if (updateResult.success) {
1070
+ engine.ui.success(updateResult.message);
1071
+ console.log(chalk.yellow(' Please restart NeuroCLI to use the new version.'));
1072
+ }
1073
+ else {
1074
+ engine.ui.error(updateResult.message);
1075
+ }
1076
+ }
1077
+ else if (updateSub === 'check') {
1078
+ engine.ui.info('Checking for updates...');
1079
+ const checkResult = await updater.checkForUpdate(true);
1080
+ if (checkResult.hasUpdate) {
1081
+ updater.showUpdateDetails(checkResult);
1082
+ updater.showUpdateNotification(checkResult);
1083
+ }
1084
+ else {
1085
+ updater.showUpToDate();
1086
+ }
1070
1087
  }
1071
- }
1072
- else if (updateSub === 'dismiss') {
1073
- const checkResult = await updater.checkForUpdate(true);
1074
- if (checkResult.hasUpdate) {
1075
- updater.dismissVersion(checkResult.latestVersion);
1076
- engine.ui.success(`Dismissed update notification for v${checkResult.latestVersion}`);
1088
+ else if (updateSub === 'dismiss') {
1089
+ const checkResult = await updater.checkForUpdate(true);
1090
+ if (checkResult.hasUpdate) {
1091
+ updater.dismissVersion(checkResult.latestVersion);
1092
+ engine.ui.success(`Dismissed update notification for v${checkResult.latestVersion}`);
1093
+ }
1094
+ else {
1095
+ engine.ui.info('No update to dismiss');
1096
+ }
1077
1097
  }
1078
- else {
1079
- engine.ui.info('No update to dismiss');
1080
- }
1081
- }
1082
- else if (updateSub === 'auto') {
1083
- const enableAuto = args[1] !== 'off';
1084
- updater.setAutoUpdate(enableAuto);
1085
- engine.ui.success(`Auto-update: ${enableAuto ? 'enabled' : 'disabled'}`);
1086
- }
1087
- else if (updateSub === 'interval') {
1088
- const hours = args[1] ? parseFloat(args[1]) : 24;
1089
- if (isNaN(hours) || hours < 1) {
1090
- engine.ui.error('Interval must be at least 1 hour');
1098
+ else if (updateSub === 'auto') {
1099
+ const enableAuto = args[1] !== 'off';
1100
+ updater.setAutoUpdate(enableAuto);
1101
+ engine.ui.success(`Auto-update: ${enableAuto ? 'enabled' : 'disabled'}`);
1102
+ }
1103
+ else if (updateSub === 'interval') {
1104
+ const hours = args[1] ? parseFloat(args[1]) : 24;
1105
+ if (isNaN(hours) || hours < 1) {
1106
+ engine.ui.error('Interval must be at least 1 hour');
1107
+ }
1108
+ else {
1109
+ updater.setCheckInterval(hours);
1110
+ engine.ui.success(`Update check interval set to ${hours} hours`);
1111
+ }
1112
+ }
1113
+ else if (updateSub === 'reset') {
1114
+ updater.resetDismissed();
1115
+ updater.forceNextCheck();
1116
+ engine.ui.success('Update preferences reset');
1091
1117
  }
1092
1118
  else {
1093
- updater.setCheckInterval(hours);
1094
- engine.ui.success(`Update check interval set to ${hours} hours`);
1095
- }
1096
- }
1097
- else if (updateSub === 'reset') {
1098
- updater.resetDismissed();
1099
- updater.forceNextCheck();
1100
- engine.ui.success('Update preferences reset');
1101
- }
1102
- else {
1103
- // Default: interactive update flow
1104
- await updater.interactiveUpdate();
1105
- }
1106
- break;
1107
- case 'exit':
1108
- case 'quit':
1109
- case 'q':
1110
- engine.mcpClient.disconnectAll().catch(() => { });
1111
- engine.approval.close();
1112
- engine.ui.info('Goodbye!');
1113
- process.exit(0);
1114
- break;
1115
- default:
1116
- engine.ui.error(`Unknown command: /${cmd}. Type /help for available commands.`);
1119
+ // Default: interactive update flow
1120
+ await updater.interactiveUpdate();
1121
+ }
1122
+ break;
1123
+ case 'exit':
1124
+ case 'quit':
1125
+ case 'q':
1126
+ engine.mcpClient.disconnectAll().catch(() => { });
1127
+ engine.approval.close();
1128
+ engine.ui.info('Goodbye!');
1129
+ process.exit(0);
1130
+ break;
1131
+ default:
1132
+ engine.ui.error(`Unknown command: /${cmd}. Type /help for available commands.`);
1133
+ }
1134
+ rl.prompt();
1135
+ return;
1136
+ }
1137
+ // Process message with the engine
1138
+ if (isProcessing) {
1139
+ engine.ui.warning('Still processing previous request. Please wait...');
1140
+ rl.prompt();
1141
+ return;
1142
+ }
1143
+ try {
1144
+ isProcessing = true;
1145
+ await engine.processMessage(input, currentMode, currentAgent);
1146
+ }
1147
+ catch (error) {
1148
+ engine.ui.error(error instanceof Error ? error.message : String(error));
1149
+ }
1150
+ finally {
1151
+ isProcessing = false;
1117
1152
  }
1118
1153
  rl.prompt();
1119
- return;
1120
- }
1121
- // Process message with the engine
1122
- try {
1123
- isProcessing = true;
1124
- await engine.processMessage(input, currentMode, currentAgent);
1125
1154
  }
1126
1155
  catch (error) {
1127
- engine.ui.error(error instanceof Error ? error.message : String(error));
1128
- }
1129
- finally {
1156
+ // Catch-all for any unhandled error in the line handler
1157
+ console.error(chalk.red(`\n⚠️ Error: ${error instanceof Error ? error.message : String(error)}`));
1130
1158
  isProcessing = false;
1159
+ rl.prompt();
1131
1160
  }
1132
- rl.prompt();
1133
1161
  });
1134
- rl.on('close', () => {
1135
- engine.mcpClient.disconnectAll().catch(() => { });
1136
- engine.approval.close();
1162
+ rl.on('close', async () => {
1163
+ try {
1164
+ await engine.mcpClient.disconnectAll();
1165
+ }
1166
+ catch { }
1167
+ try {
1168
+ engine.approval.close();
1169
+ }
1170
+ catch { }
1171
+ try {
1172
+ engine.sessionManager.save();
1173
+ }
1174
+ catch { }
1137
1175
  engine.ui.info('Goodbye!');
1138
1176
  process.exit(0);
1139
1177
  });
1140
1178
  }
1141
1179
  function printHelp(engine) {
1142
1180
  const t = engine.ui.theme;
1143
- console.log(`\n ${t.bold('NeuroCLI v4.1.2 Commands:')}\n`);
1181
+ console.log(`\n ${t.bold(`NeuroCLI v${VERSION} Commands:`)}\n`);
1144
1182
  console.log(` ${t.tool('/help')} Show this help message`);
1145
1183
  console.log(` ${t.tool('/model [id]')} Switch or list models`);
1146
1184
  console.log(` ${t.tool('/agent [name]')} Switch or list agents`);