kiku-agent 1.0.10 → 1.0.11

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.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import readline from 'node:readline';
4
- import { stdin as input } from 'node:process';
5
- import { existsSync } from 'node:fs';
6
- import { spawn } from 'node:child_process';
7
- import './youtubeAuth.js';
8
- import { KikuAgent } from './agentCore.js';
3
+ import readline from 'node:readline';
4
+ import { stdin as input } from 'node:process';
5
+ import { existsSync } from 'node:fs';
6
+ import { spawn } from 'node:child_process';
7
+ import './youtubeAuth.js';
8
+ import { KikuAgent } from './agentCore.js';
9
9
  import { LLMClient, LMStudioClient } from './llmClient.js';
10
10
  import { toolsSchema, toolHandlers, executeTool } from './tools.js';
11
11
  import { config, CLOUD_PROVIDERS } from './config.js';
@@ -14,12 +14,12 @@ import { routeCommand } from './commands.js';
14
14
  import { eventBus, Events } from './eventBus.js';
15
15
  import { AgentStates } from './agentState.js';
16
16
  import { sanitizeQuery } from './sanitizer.js';
17
- import { isAbortError } from './errors.js';
17
+ import { isAbortError, formatExecutionErrorCard } from './errors.js';
18
18
  import { COMMAND_DEFINITIONS, SUB_COMMANDS, findCommandDefinition, getCommandDetails } from './commandRegistry.js';
19
19
  import { ModelManager } from './modelManager.js';
20
- import { PACKAGE_VERSION } from './version.js';
21
- import { getTheme, getThemeName } from './theme.js';
22
- import { createTerminalImageGallery } from './terminalImage.js';
20
+ import { PACKAGE_VERSION } from './version.js';
21
+ import { getTheme, getThemeName } from './theme.js';
22
+ import { createTerminalImageGallery, getWelcomeGalleryLines } from './terminalImage.js';
23
23
  import {
24
24
  classifyToolResult,
25
25
  formatDetailDuration,
@@ -39,7 +39,7 @@ import {
39
39
  printWelcomeHero, getWelcomeHeroLines, randomFanName, setupOutputInterception,
40
40
  rawWrite, INPUT_PLACEHOLDER, UI_ICONS,
41
41
  spinnerFrames, thinkingFrames, createProgressRunner,
42
- enterAlternateScreen, exitAlternateScreen, getTuiViewportLayout, getNewOutputScrollOffset, VirtualScreenBuffer,
42
+ enterAlternateScreen, exitAlternateScreen, getTuiViewportLayout, getNewOutputScrollOffset, VirtualScreenBuffer,
43
43
  } from './ui.js';
44
44
 
45
45
  let activeAbortController = null;
@@ -318,7 +318,7 @@ function printQueryDetails(reference = '') {
318
318
  });
319
319
  }
320
320
 
321
- function copyQueryDetails(reference = '') {
321
+ function copyQueryDetails(reference = '', session = null) {
322
322
  const request = parseDetailsRequest(reference);
323
323
  if (request.error) {
324
324
  printSolidPanel({
@@ -326,11 +326,13 @@ function copyQueryDetails(reference = '') {
326
326
  lines: [
327
327
  `${C.amber}${request.error}${C.reset}`,
328
328
  '',
329
+ `${C.white}/copy session [markdown|json|text]${C.reset} ${C.dim}— copy full session transcript${C.reset}`,
329
330
  `${C.white}/copy [Qn] [answer]${C.reset} ${C.dim}— copy model answer (default)${C.reset}`,
330
331
  `${C.white}/copy [Qn] thinking${C.reset} ${C.dim}— copy thinking trace${C.reset}`,
331
332
  `${C.white}/copy [Qn] tools${C.reset} ${C.dim}— copy tools JSON${C.reset}`,
332
333
  `${C.white}/copy [Qn] tool <n>${C.reset} ${C.dim}— copy tool <n> raw result${C.reset}`,
333
334
  `${C.white}/copy [Qn] all${C.reset} ${C.dim}— copy full question & answer markdown${C.reset}`,
335
+ `${C.white}/copy [Qn] json${C.reset} ${C.dim}— copy question diagnostics JSON${C.reset}`,
334
336
  ],
335
337
  });
336
338
  return false;
@@ -340,13 +342,13 @@ function copyQueryDetails(reference = '') {
340
342
  ? lastQueryDetails
341
343
  : queryDetails.get(request.queryNumber);
342
344
 
343
- if (!details) {
345
+ if (!details && !request.isSession && !session) {
344
346
  const target = request.queryNumber == null ? 'the latest query' : `Q${request.queryNumber}`;
345
347
  console.log(`\n ${C.amber}[NOT FOUND] No diagnostics recorded for ${target}.${C.reset}\n`);
346
348
  return false;
347
349
  }
348
350
 
349
- const copyable = getCopyableContent(details, request);
351
+ const copyable = getCopyableContent(details, request, session);
350
352
  if (!copyable || !copyable.text) {
351
353
  console.log(`\n ${C.amber}[EMPTY] Nothing to copy for ${request.view || 'answer'}.${C.reset}\n`);
352
354
  return false;
@@ -461,44 +463,112 @@ export function printHelp(specificCmd = null) {
461
463
 
462
464
  const ads = [
463
465
  {
464
- title: 'DashSecurity', subtitle: 'Cybersecurity Knowledge Cloud',
466
+ title: 'DashSecurity',
467
+ subtitle: 'Cybersecurity Knowledge Cloud',
468
+ tagline: 'Enterprise Cyber Threat Intelligence & AI-Powered DevSecOps',
469
+ badge: 'Security Platform',
465
470
  url: 'https://dashsecurity.kpltgroup.com/',
466
- features: ['AI-Powered SAST Scanner', 'OWASP Top 10 Detection', 'Certification Prep Labs'],
471
+ summary: 'Next-generation application security & vulnerability intelligence platform designed for developers, security engineers, and red teamers.',
472
+ features: [
473
+ 'AI-Powered SAST/DAST Static & Dynamic Security Code Analyzer',
474
+ 'Automated OWASP Top 10 & API Security Vulnerability Detection',
475
+ 'Interactive Certification Prep Labs (OSCP, CEH, CompTIA Security+)',
476
+ 'Real-time Threat Intelligence Feeds & CVE Remediation Advisory',
477
+ ],
478
+ highlights: [
479
+ 'AI-Powered SAST/DAST Static & Dynamic Security Code Analyzer',
480
+ 'Automated OWASP Top 10 & API Security Vulnerability Detection',
481
+ 'Interactive Certification Prep Labs (OSCP, CEH, CompTIA Security+)',
482
+ 'Real-time Threat Intelligence Feeds & CVE Remediation Advisory',
483
+ ],
484
+ cta: 'Explore security tools & interactive labs',
467
485
  },
468
486
  {
469
- title: 'Ju Jingyi Kikuuu', subtitle: 'Official Portfolio',
487
+ title: 'Ju Jingyi • 鞠婧祎 Official Portal',
488
+ subtitle: 'Official Portfolio & SSOT Knowledge Base',
489
+ tagline: 'Definitive Multimedia Knowledge Base & Autonomous Agent Ecosystem',
490
+ badge: 'Official Fan Portal',
470
491
  url: 'https://jujingyi-kikuuu.vercel.app/',
471
- features: ['45+ Music Releases', 'Complete Filmography', 'REST API v1'],
492
+ summary: 'The single source of truth for Ju Jingyi — featuring exhaustive career timelines, 45+ discography releases, fashion milestones, and full REST API integration.',
493
+ features: [
494
+ 'Complete Filmography: In Blossom, Beauty of Resilience, 2026 upcoming dramas',
495
+ 'Full Discography: 45+ Singles, Studio Albums, OSTs & live stage tracks',
496
+ 'Fashion & Awards: Versace Global Ambassador milestones & career honors',
497
+ 'Developer API: Production REST API v1, MCP Server & OpenAPI 3.1 Specification',
498
+ ],
499
+ highlights: [
500
+ 'Complete Filmography: In Blossom, Beauty of Resilience, 2026 upcoming dramas',
501
+ 'Full Discography: 45+ Singles, Studio Albums, OSTs & live stage tracks',
502
+ 'Fashion & Awards: Versace Global Ambassador milestones & career honors',
503
+ 'Developer API: Production REST API v1, MCP Server & OpenAPI 3.1 Specification',
504
+ ],
505
+ cta: 'Visit the portal & test the REST API',
472
506
  },
473
507
  {
474
- title: 'ThemeHackers', subtitle: 'Security Research',
508
+ title: 'ThemeHackers Research Group',
509
+ subtitle: 'Security Research & Tooling',
510
+ tagline: 'Elite Offensive Cyber Research & Open-Source Security Ecosystem',
511
+ badge: 'Security Research',
475
512
  url: 'https://github.com/Themehackers/',
476
- features: ['Penetration Testing', 'Open-Source Tools', '10+ Certifications'],
513
+ summary: 'Independent cybersecurity research collective specializing in red teaming, vulnerability disclosure, penetration testing frameworks, and defensive automation.',
514
+ features: [
515
+ 'Red Team Operations & Advanced Penetration Testing Toolkits',
516
+ 'Zero-Day Vulnerability Research & Coordinated Security Disclosures',
517
+ 'Open-Source Security Automation Scripts & Reverse Engineering Tools',
518
+ '10+ Industry Professional Security Certifications & Technical Writeups',
519
+ ],
520
+ highlights: [
521
+ 'Red Team Operations & Advanced Penetration Testing Toolkits',
522
+ 'Zero-Day Vulnerability Research & Coordinated Security Disclosures',
523
+ 'Open-Source Security Automation Scripts & Reverse Engineering Tools',
524
+ '10+ Industry Professional Security Certifications & Technical Writeups',
525
+ ],
526
+ cta: 'Explore open-source security repos on GitHub',
477
527
  },
478
528
  ];
479
529
 
480
530
  let adIndex = 0;
481
531
  function printAd() {
482
- const ad = ads[adIndex % ads.length];
532
+ const currentIdx = adIndex % ads.length;
533
+ const ad = ads[currentIdx];
483
534
  adIndex++;
484
- const w = getFullWidth();
485
- console.log('');
486
- printSolidBannerRow(` ${C.gold}${C.bold}${UI_ICONS.sponsor} SPONSOR${C.reset} ${C.bold}${C.white}${ad.title}${C.reset} — ${C.roseGlow}${ad.subtitle}${C.reset}`, w, C.bgPanelHeader);
487
- printSolidBannerRow(` ${ad.features.map(f => `${C.white}${f}${C.reset}`).join(' • ')}`, w, C.bgPanel);
488
- printSolidBannerRow(` ${C.cyberCyan}${C.underline}${ad.url}${C.reset}`, w, C.bgPanel);
489
- console.log('');
535
+
536
+ const lines = [
537
+ `${C.bold}${C.white}${ad.tagline || ad.subtitle}${C.reset}`,
538
+ '',
539
+ `${C.dim}${ad.summary}${C.reset}`,
540
+ '',
541
+ `${C.gold}${C.bold}Core Highlights & Features:${C.reset}`,
542
+ ...(ad.highlights || ad.features).map(f => ` ${C.emerald}◆${C.reset} ${C.white}${f}${C.reset}`),
543
+ '',
544
+ `${C.slate}${ad.cta || 'Visit'}:${C.reset} ${C.cyberCyan}${C.underline}${ad.url}${C.reset}`,
545
+ ];
546
+
547
+ printSolidPanel({
548
+ title: `${UI_ICONS.sponsor} SPONSOR SPOTLIGHT [${currentIdx + 1}/${ads.length}] • ${ad.title} (${ad.badge || 'Featured'})`,
549
+ lines,
550
+ headerBg: C.bgPanelHeader,
551
+ bg: C.bgPanel,
552
+ });
553
+ }
554
+
555
+ function getInitialViewportLines() {
556
+ const heroLines = getWelcomeHeroLines();
557
+ const galleryLines = getWelcomeGalleryLines(2);
558
+ return galleryLines.length > 0 ? [...heroLines, ...galleryLines] : heroLines;
490
559
  }
491
560
 
492
561
  class InteractiveTuiPrompt {
493
562
  constructor(options = {}) {
494
- this.defaultPromptText = options.prompt || 'Kiku ❯ ';
495
- this.promptText = this.defaultPromptText;
496
- this.inputPlaceholder = INPUT_PLACEHOLDER;
497
- this.recordInputHistory = true;
498
- this.cancelValue = null;
563
+ this.defaultPromptText = options.prompt || 'Kiku ❯ ';
564
+ this.promptText = this.defaultPromptText;
565
+ this.inputPlaceholder = INPUT_PLACEHOLDER;
566
+ this.recordInputHistory = true;
567
+ this.cancelValue = null;
499
568
  this.agent = options.agent || null;
500
569
  this.modelName = options.modelName || 'gemma-4-e4b';
501
570
  this.provider = options.provider || 'lmstudio';
571
+ this.isConnected = options.isConnected ?? true;
502
572
  this.history = [];
503
573
  this.historyIndex = -1;
504
574
  this.line = '';
@@ -518,7 +588,7 @@ class InteractiveTuiPrompt {
518
588
  this.agentStateTime = 0;
519
589
  this._lastCtrlCTime = 0;
520
590
 
521
- this.viewportLines = getWelcomeHeroLines();
591
+ this.viewportLines = getInitialViewportLines();
522
592
  this.scrollOffset = 0;
523
593
  this.screenBuffer = new VirtualScreenBuffer(
524
594
  Math.max(20, process.stdout.columns || 80),
@@ -526,28 +596,28 @@ class InteractiveTuiPrompt {
526
596
  );
527
597
  }
528
598
 
529
- addViewportLines(lines) {
530
- if (lines === null || lines === undefined) return;
531
- if (!Array.isArray(lines)) lines = [lines];
532
- const wasLive = this.scrollOffset === 0;
533
- let addedLineCount = 0;
534
- for (const l of lines) {
535
- if (typeof l === 'string') {
536
- const sublines = l.split('\n');
537
- for (const sl of sublines) {
538
- this.viewportLines.push(sl);
539
- addedLineCount += 1;
540
- }
541
- } else if (l != null) {
542
- this.viewportLines.push(String(l));
543
- addedLineCount += 1;
544
- }
599
+ addViewportLines(lines) {
600
+ if (lines === null || lines === undefined) return;
601
+ if (!Array.isArray(lines)) lines = [lines];
602
+ const wasLive = this.scrollOffset === 0;
603
+ let addedLineCount = 0;
604
+ for (const l of lines) {
605
+ if (typeof l === 'string') {
606
+ const sublines = l.split('\n');
607
+ for (const sl of sublines) {
608
+ this.viewportLines.push(sl);
609
+ addedLineCount += 1;
610
+ }
611
+ } else if (l != null) {
612
+ this.viewportLines.push(String(l));
613
+ addedLineCount += 1;
614
+ }
545
615
  }
546
616
  if (this.viewportLines.length > 5000) {
547
617
  const removedCount = this.viewportLines.length - 5000;
548
618
  this.viewportLines.splice(0, removedCount);
549
619
  }
550
- this.scrollOffset = getNewOutputScrollOffset(addedLineCount, process.stdout.rows || 24, wasLive ? 0 : this.scrollOffset);
620
+ this.scrollOffset = getNewOutputScrollOffset(addedLineCount, process.stdout.rows || 24, wasLive ? 0 : this.scrollOffset);
551
621
  this.render();
552
622
  }
553
623
 
@@ -561,7 +631,7 @@ class InteractiveTuiPrompt {
561
631
  }
562
632
 
563
633
  clearViewport() {
564
- this.viewportLines = getWelcomeHeroLines();
634
+ this.viewportLines = getInitialViewportLines();
565
635
  this.scrollOffset = 0;
566
636
  this.render();
567
637
  }
@@ -584,15 +654,15 @@ class InteractiveTuiPrompt {
584
654
  try { if (input.isTTY) input.setRawMode(false); } catch { }
585
655
  }
586
656
 
587
- async readLine(options = {}) {
588
- return new Promise((resolve, reject) => {
589
- this._cleanupInput();
590
- const inputOptions = typeof options === 'string' ? { prompt: options } : options;
591
- this.promptText = inputOptions.prompt || this.defaultPromptText;
592
- this.inputPlaceholder = inputOptions.placeholder || INPUT_PLACEHOLDER;
593
- this.recordInputHistory = inputOptions.history !== false;
594
- this.cancelValue = inputOptions.cancelValue ?? null;
595
- this.line = '';
657
+ async readLine(options = {}) {
658
+ return new Promise((resolve, reject) => {
659
+ this._cleanupInput();
660
+ const inputOptions = typeof options === 'string' ? { prompt: options } : options;
661
+ this.promptText = inputOptions.prompt || this.defaultPromptText;
662
+ this.inputPlaceholder = inputOptions.placeholder || INPUT_PLACEHOLDER;
663
+ this.recordInputHistory = inputOptions.history !== false;
664
+ this.cancelValue = inputOptions.cancelValue ?? null;
665
+ this.line = '';
596
666
  this.cursor = 0;
597
667
  this.historyIndex = -1;
598
668
  this.popoverIndex = 0;
@@ -626,7 +696,7 @@ class InteractiveTuiPrompt {
626
696
  this.cursor = 0;
627
697
  this.popoverActive = false;
628
698
  this.popoverItems = [];
629
- if (this.recordInputHistory && result && result.trim()) this.history.push(result);
699
+ if (this.recordInputHistory && result && result.trim()) this.history.push(result);
630
700
  const resolve = this.activeResolve;
631
701
  this.activeResolve = null;
632
702
  this.activeReject = null;
@@ -670,7 +740,11 @@ class InteractiveTuiPrompt {
670
740
  this.popoverActive = false;
671
741
  this.popoverItems = [];
672
742
  this.popoverIndex = 0;
673
- this.ghostText = '';
743
+ if (!this.isConnected && this.line.length > 0) {
744
+ this.ghostText = ' (LM Studio offline - type /model reconnect)';
745
+ } else {
746
+ this.ghostText = '';
747
+ }
674
748
  }
675
749
  }
676
750
 
@@ -691,7 +765,9 @@ class InteractiveTuiPrompt {
691
765
 
692
766
 
693
767
  const shortModel = (this.modelName || 'gemma-4-e4b').replace(/^.*\//, '');
694
- const modelLabel = `${C.gold}✦ ${shortModel}${C.reset}`;
768
+ const modelLabel = this.isConnected
769
+ ? `${C.gold}✦ ${shortModel}${C.reset}`
770
+ : `${C.roseGlow}✦ OFFLINE${C.reset}`;
695
771
  const mcpCount = this.agent?.mcpManager?.servers?.size || 0;
696
772
  const mcpLabel = mcpCount > 0 ? `${C.emerald}● ${mcpCount} MCP${C.reset}` : `${C.slate}○ 0 MCP${C.reset}`;
697
773
 
@@ -732,7 +808,9 @@ class InteractiveTuiPrompt {
732
808
  const pad = Math.max(1, safeCols - getStringDisplayWidth(statusLeft) - getStringDisplayWidth(statusRight));
733
809
  statusStyled = `${statusLeft}${' '.repeat(pad)}${statusRight}`;
734
810
  } else {
735
- statusStyled = statusLeft;
811
+ const hkRight = `${C.slate}Esc${C.reset}${C.dim} Cancel${C.reset} ${C.slate}F3${C.reset}${C.dim} Think${C.reset} ${C.slate}F5${C.reset}${C.dim} Copy${C.reset} `;
812
+ const pad = Math.max(1, safeCols - getStringDisplayWidth(statusLeft) - getStringDisplayWidth(hkRight));
813
+ statusStyled = `${statusLeft}${' '.repeat(pad)}${hkRight}`;
736
814
  }
737
815
  } else if (currentOffset > 0) {
738
816
  const posInfo = startIndex > 0 ? ` ${C.slate}• ↑ ${startIndex} above${C.reset}` : '';
@@ -742,8 +820,11 @@ class InteractiveTuiPrompt {
742
820
  statusStyled = `${statusLeft}${' '.repeat(pad)}${statusRight}`;
743
821
  } else if (musicStatus) {
744
822
  statusStyled = ` ${C.cyberCyan}🎵 ${musicStatus}${C.reset}`;
823
+ } else if (!this.isConnected) {
824
+ statusStyled = ` ${C.roseGlow}○ OFFLINE${C.reset} ${C.slate}• Start LM Studio & type ${C.cyberCyan}/model reconnect${C.reset}`;
745
825
  } else {
746
- statusStyled = ` ${C.emerald}● READY${C.reset}`;
826
+ const hkHints = `${C.slate}F1${C.reset}${C.dim} Help${C.reset} ${C.slate}F3${C.reset}${C.dim} Think${C.reset} ${C.slate}F4${C.reset}${C.dim} Tools${C.reset} ${C.slate}F5${C.reset}${C.dim} Copy${C.reset} ${C.slate}F7${C.reset}${C.dim} Theme${C.reset}`;
827
+ statusStyled = ` ${C.emerald}● READY${C.reset} ${C.slate}│${C.reset} ${hkHints}`;
747
828
  }
748
829
 
749
830
  this.screenBuffer.setRow(statusBarRow, statusStyled, C.bgPanelHeader);
@@ -795,7 +876,10 @@ class InteractiveTuiPrompt {
795
876
  trailingStyled = `${C.dim}${C.slate}${ghostDisplay}${C.reset}`;
796
877
  }
797
878
  } else if (!this.line) {
798
- const placeholder = sliceLineForDisplay(this.inputPlaceholder, 0, maxLineWidth).displayLine;
879
+ const defaultPlaceholder = this.isConnected
880
+ ? this.inputPlaceholder
881
+ : '[AI Offline] Start LM Studio & type /model reconnect (or /help)';
882
+ const placeholder = sliceLineForDisplay(defaultPlaceholder, 0, maxLineWidth).displayLine;
799
883
  trailingStyled = `${C.dim}${C.slate}${placeholder}${C.reset}`;
800
884
  }
801
885
 
@@ -812,12 +896,12 @@ class InteractiveTuiPrompt {
812
896
  handleKeypress(str, key) {
813
897
  if (!key && !str) return;
814
898
 
815
- if ((key?.ctrl && key?.name === 'c') || str === '\x03') {
816
- if (this.cancelValue !== null) {
817
- this.finish(this.cancelValue);
818
- return;
819
- }
820
- if (isProcessing) {
899
+ if ((key?.ctrl && key?.name === 'c') || str === '\x03') {
900
+ if (this.cancelValue !== null) {
901
+ this.finish(this.cancelValue);
902
+ return;
903
+ }
904
+ if (isProcessing) {
821
905
  if (this.onCancel) this.onCancel();
822
906
  else if (this.onSigInt) this.onSigInt();
823
907
  this.addViewportLines(` ${C.amber}[✓ Cancelled by user]${C.reset}`);
@@ -842,12 +926,12 @@ class InteractiveTuiPrompt {
842
926
  return;
843
927
  }
844
928
 
845
- if (key?.name === 'escape' || str === '\x1b') {
846
- if (this.cancelValue !== null) {
847
- this.finish(this.cancelValue);
848
- return;
849
- }
850
- if (this.popoverActive) {
929
+ if (key?.name === 'escape' || str === '\x1b') {
930
+ if (this.cancelValue !== null) {
931
+ this.finish(this.cancelValue);
932
+ return;
933
+ }
934
+ if (this.popoverActive) {
851
935
  this.popoverActive = false;
852
936
  this.popoverItems = [];
853
937
  this.ghostText = '';
@@ -869,38 +953,24 @@ class InteractiveTuiPrompt {
869
953
  return;
870
954
  }
871
955
 
872
- if (
873
- str && (
874
- str.startsWith('\x1b') ||
875
- str.includes('\x1b') ||
876
- str.includes('[<') ||
877
- str.includes('[M') ||
878
- /^<[0-9;]*[a-zA-Z]?$/.test(str) ||
879
- /^[0-9;]+[a-zA-Z]$/.test(str) ||
880
- /^[0-9;]{2,}$/.test(str) ||
881
- /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(str)
882
- )
883
- ) {
884
- if (str.includes('[<64;') || str.includes('[64;') || str.includes('<64;') || (key?.name === 'up' && key?.shift)) {
885
- const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
886
- const maxOffset = Math.max(0, this.viewportLines.length - viewportHeight);
887
- this.scrollOffset = Math.min(maxOffset, this.scrollOffset + 3);
956
+
957
+ if (str && (str.includes('[<64;') || str.includes('[64;') || str.includes('<64;') || (key?.name === 'up' && key?.shift))) {
958
+ const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
959
+ const maxOffset = Math.max(0, this.viewportLines.length - viewportHeight);
960
+ this.scrollOffset = Math.min(maxOffset, this.scrollOffset + 3);
961
+ this.render();
962
+ return;
963
+ }
964
+ if (str && (str.includes('[<65;') || str.includes('[65;') || str.includes('<65;') || (key?.name === 'down' && key?.shift))) {
965
+ if (this.scrollOffset > 0) {
966
+ this.scrollOffset = Math.max(0, this.scrollOffset - 3);
888
967
  this.render();
889
- return;
890
- }
891
- if (str.includes('[<65;') || str.includes('[65;') || str.includes('<65;') || (key?.name === 'down' && key?.shift)) {
892
- if (this.scrollOffset > 0) {
893
- this.scrollOffset = Math.max(0, this.scrollOffset - 3);
894
- this.render();
895
- }
896
- return;
897
- }
898
- if (str !== '\r' && str !== '\n' && str !== '\t' && str !== '\x08' && str !== '\x7f') {
899
- return;
900
968
  }
969
+ return;
901
970
  }
902
971
 
903
- if (key?.name === 'pageup') {
972
+
973
+ if (key?.name === 'pageup' || str === '\x1b[5~') {
904
974
  const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
905
975
  const maxOffset = Math.max(0, this.viewportLines.length - viewportHeight);
906
976
  this.scrollOffset = Math.min(maxOffset, this.scrollOffset + Math.max(1, viewportHeight - 2));
@@ -908,22 +978,48 @@ class InteractiveTuiPrompt {
908
978
  return;
909
979
  }
910
980
 
911
- if (key?.name === 'pagedown') {
981
+ if (key?.name === 'pagedown' || str === '\x1b[6~') {
912
982
  const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
913
983
  this.scrollOffset = Math.max(0, this.scrollOffset - Math.max(1, viewportHeight - 2));
914
984
  this.render();
915
985
  return;
916
986
  }
917
987
 
918
- if (key?.ctrl && key?.name === 'l') {
919
- this.render();
920
- return;
921
- }
922
988
 
923
- if (key?.name === 'f2') {
924
- this.finish('/details');
925
- return;
926
- }
989
+ const isF1 = key?.name === 'f1' || str === '\x1bOP' || str === '\x1b[[A' || str === '\x1b[11~';
990
+ const isF2 = key?.name === 'f2' || str === '\x1bOQ' || str === '\x1b[[B' || str === '\x1b[12~';
991
+ const isF3 = key?.name === 'f3' || str === '\x1bOR' || str === '\x1b[[C' || str === '\x1b[13~';
992
+ const isF4 = key?.name === 'f4' || str === '\x1bOS' || str === '\x1b[[D' || str === '\x1b[14~';
993
+ const isF5 = key?.name === 'f5' || str === '\x1b[15~' || str === '\x1b[[E';
994
+ const isF6 = key?.name === 'f6' || str === '\x1b[17~';
995
+ const isF7 = key?.name === 'f7' || str === '\x1b[18~';
996
+ const isF8 = key?.name === 'f8' || str === '\x1b[19~';
997
+ const isF9 = key?.name === 'f9' || str === '\x1b[20~';
998
+ const isF10 = key?.name === 'f10' || str === '\x1b[21~';
999
+
1000
+ if (isF1) { this.finish('/help'); return; }
1001
+ if (isF2) { this.finish('/details'); return; }
1002
+ if (isF3) { this.finish('/details thinking'); return; }
1003
+ if (isF4) { this.finish('/details tools'); return; }
1004
+ if (isF5) { this.finish('/copy'); return; }
1005
+ if (isF6) { this.finish('/gallery'); return; }
1006
+ if (isF7) { this.finish('/theme next'); return; }
1007
+ if (isF8) { this.finish('/stats'); return; }
1008
+ if (isF9) { this.finish('/provider'); return; }
1009
+ if (isF10) { this.finish('/mcp list'); return; }
1010
+
1011
+
1012
+ const isCtrlL = (key?.ctrl && key?.name === 'l') || str === '\x0c';
1013
+ const isCtrlT = (key?.ctrl && key?.name === 't') || str === '\x14';
1014
+ const isCtrlR = (key?.ctrl && key?.name === 'r') || str === '\x12';
1015
+ const isCtrlS = (key?.ctrl && key?.name === 's') || str === '\x13';
1016
+ const isCtrlG = (key?.ctrl && key?.name === 'g') || str === '\x07';
1017
+
1018
+ if (isCtrlL) { this.render(); return; }
1019
+ if (isCtrlT) { this.finish('/thinking toggle'); return; }
1020
+ if (isCtrlR) { this.finish('/retry'); return; }
1021
+ if (isCtrlS) { this.finish('/save'); return; }
1022
+ if (isCtrlG) { this.finish('/gallery'); return; }
927
1023
 
928
1024
  if (key?.name === 'tab' || str === '\t') {
929
1025
  if (this.popoverActive && this.popoverItems.length > 0) {
@@ -966,14 +1062,15 @@ class InteractiveTuiPrompt {
966
1062
  return;
967
1063
  }
968
1064
 
969
- if (key.name === 'up' || (key.ctrl && key.name === 'p')) {
1065
+ const isUp = key?.name === 'up' || (key?.ctrl && key?.name === 'p') || str === '\x10';
1066
+ if (isUp) {
970
1067
  if (this.popoverActive && this.popoverItems.length > 0) {
971
1068
  this.popoverIndex = (this.popoverIndex - 1 + this.popoverItems.length) % this.popoverItems.length;
972
1069
  this.updateState();
973
1070
  this.render();
974
1071
  return;
975
1072
  }
976
- if (key.ctrl && this.history.length > 0) {
1073
+ if (key?.ctrl && this.history.length > 0) {
977
1074
  if (this.historyIndex === -1) this.historyIndex = this.history.length - 1;
978
1075
  else if (this.historyIndex > 0) this.historyIndex--;
979
1076
  this.line = this.history[this.historyIndex] || '';
@@ -989,7 +1086,8 @@ class InteractiveTuiPrompt {
989
1086
  return;
990
1087
  }
991
1088
 
992
- if (key?.name === 'down' || (key?.ctrl && key?.name === 'n')) {
1089
+ const isDown = key?.name === 'down' || (key?.ctrl && key?.name === 'n') || str === '\x0e';
1090
+ if (isDown) {
993
1091
  if (this.popoverActive && this.popoverItems.length > 0) {
994
1092
  this.popoverIndex = (this.popoverIndex + 1) % this.popoverItems.length;
995
1093
  this.updateState();
@@ -1016,7 +1114,8 @@ class InteractiveTuiPrompt {
1016
1114
  return;
1017
1115
  }
1018
1116
 
1019
- if (key?.name === 'home' || (key?.ctrl && key?.name === 'a')) {
1117
+ const isHome = key?.name === 'home' || (key?.ctrl && key?.name === 'a') || str === '\x01' || str === '\x1b[H' || str === '\x1b[1~';
1118
+ if (isHome) {
1020
1119
  if (this.line === '') {
1021
1120
  const { viewportHeight } = getTuiViewportLayout(process.stdout.rows || 24);
1022
1121
  this.scrollOffset = Math.max(0, this.viewportLines.length - viewportHeight);
@@ -1028,7 +1127,8 @@ class InteractiveTuiPrompt {
1028
1127
  return;
1029
1128
  }
1030
1129
 
1031
- if (key?.name === 'end' || (key?.ctrl && key?.name === 'e')) {
1130
+ const isEnd = key?.name === 'end' || (key?.ctrl && key?.name === 'e') || str === '\x05' || str === '\x1b[F' || str === '\x1b[4~';
1131
+ if (isEnd) {
1032
1132
  if (this.line === '') {
1033
1133
  this.scrollOffset = 0;
1034
1134
  this.render();
@@ -1043,7 +1143,9 @@ class InteractiveTuiPrompt {
1043
1143
  if (key?.name === 'left') { if (this.cursor > 0) { this.cursor = previousGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
1044
1144
 
1045
1145
  if (key?.name === 'backspace' || str === '\x08' || str === '\x7f') { if (this.cursor > 0) { const previous = previousGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, previous) + this.line.slice(this.cursor); this.cursor = previous; this.updateState(); this.render(); } return; }
1046
- if (key?.name === 'delete') { if (this.cursor < this.line.length) { const next = nextGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, this.cursor) + this.line.slice(next); this.updateState(); this.render(); } return; }
1146
+
1147
+ const isDelete = key?.name === 'delete' || str === '\x1b[3~';
1148
+ if (isDelete) { if (this.cursor < this.line.length) { const next = nextGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, this.cursor) + this.line.slice(next); this.updateState(); this.render(); } return; }
1047
1149
 
1048
1150
  if (str && !key?.ctrl && !key?.meta) {
1049
1151
  if (
@@ -1113,16 +1215,21 @@ function getSuggestionsForLine(line, cursor = (line || '').length, agent = null)
1113
1215
  if (parts.length > 1 || (parts.length === 1 && line.includes(' '))) {
1114
1216
  const subs = SUB_COMMANDS[baseCmd] || [];
1115
1217
  const details = getCommandDetails(baseCmd);
1218
+ const def = findCommandDefinition(baseCmd);
1116
1219
  const query = parts.slice(1).join(' ').toLowerCase();
1117
1220
  return subs
1118
1221
  .filter(s => s.toLowerCase().includes(query))
1119
1222
  .map(s => {
1120
- const subKey = s.split(/\s+/)[0].toLowerCase();
1223
+ const sNorm = s.trim().toLowerCase();
1224
+ const subWords = sNorm.split(/\s+/);
1121
1225
  const matchedDetail = details?.subcommands?.find(sc => {
1122
- const scKey = sc.cmd.split(/\s+/)[0].toLowerCase();
1123
- return scKey === subKey || sc.cmd.toLowerCase().includes(subKey);
1226
+ const scNorm = (sc.cmd || '').trim().toLowerCase();
1227
+ const scWords = scNorm.split(/\s+/);
1228
+ if (scNorm === sNorm) return true;
1229
+ if (scWords.length >= 2 && subWords.length >= 2 && scWords[0] === subWords[0] && scWords[1] === subWords[1]) return true;
1230
+ return scWords[0] === subWords[0];
1124
1231
  });
1125
- const desc = matchedDetail?.desc || '';
1232
+ const desc = matchedDetail?.desc || details?.summary || def?.desc || '';
1126
1233
  return { name: `${baseCmd} ${s}`, desc, insert: `${baseCmd} ${s} ` };
1127
1234
  });
1128
1235
  }
@@ -1509,13 +1616,8 @@ async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, sign
1509
1616
  }
1510
1617
  },
1511
1618
 
1512
- onError: (err) => {
1619
+ onError: (_err) => {
1513
1620
  stopSpinner();
1514
- printRichCard({
1515
- title: 'Execution Error',
1516
- status: 'error',
1517
- lines: [err.message],
1518
- });
1519
1621
  },
1520
1622
  });
1521
1623
 
@@ -1526,11 +1628,7 @@ async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, sign
1526
1628
  stopSpinner();
1527
1629
  persistQueryDetails(isAbortError(err) ? 'aborted' : 'error', err);
1528
1630
  if (!isAbortError(err)) {
1529
- printRichCard({
1530
- title: 'Query Failed',
1531
- status: 'error',
1532
- lines: [err.message],
1533
- });
1631
+ printRichCard(formatExecutionErrorCard(err, { client: agent.client, provider: agent.client?.provider, model: agent.client?.model }));
1534
1632
  } else {
1535
1633
  printRichCard({
1536
1634
  title: 'Query Cancelled',
@@ -1560,23 +1658,23 @@ async function main() {
1560
1658
 
1561
1659
  if (opts.showStats) { printWelcomeHero(); const r = await executeTool('get_system_stats', {}); console.log(JSON.stringify(r, null, 2)); process.exit(0); }
1562
1660
 
1563
- if (opts.query) {
1564
- printWelcomeHero();
1565
- const client = new LLMClient({
1661
+ if (opts.query) {
1662
+ printWelcomeHero();
1663
+ const client = new LLMClient({
1566
1664
  provider: opts.provider || config.activeProvider,
1567
1665
  baseUrl: opts.url,
1568
1666
  apiKey: opts.apiKey,
1569
- model: opts.model
1570
- });
1571
- const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1572
- const modelResolution = await modelManager.resolve({ requestedModel: opts.model || client.model });
1573
- if (!modelResolution.connected || !modelResolution.selectedModel) {
1574
- throw new Error(modelResolution.error || `No available chat model found for ${client.providerName}`);
1575
- }
1576
- if (modelResolution.usedFallback) {
1577
- console.log(` ${C.amber}Model ${modelResolution.requestedModel} is unavailable; using ${modelResolution.selectedModel}.${C.reset}\n`);
1578
- }
1579
- console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
1667
+ model: opts.model
1668
+ });
1669
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1670
+ const modelResolution = await modelManager.resolve({ requestedModel: opts.model || client.model });
1671
+ if (!modelResolution.connected || !modelResolution.selectedModel) {
1672
+ throw new Error(modelResolution.error || `No available chat model found for ${client.providerName}`);
1673
+ }
1674
+ if (modelResolution.usedFallback) {
1675
+ console.log(` ${C.amber}Model ${modelResolution.requestedModel} is unavailable; using ${modelResolution.selectedModel}.${C.reset}\n`);
1676
+ }
1677
+ console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
1580
1678
  const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1581
1679
  await executeQueryWithUI(agent, opts.query, agent.session.thinkingMode === 'show', null, 1);
1582
1680
  process.exit(0);
@@ -1592,7 +1690,7 @@ async function main() {
1592
1690
  apiKey: opts.apiKey,
1593
1691
  model: opts.model
1594
1692
  });
1595
- const baseUrl = client.baseUrl;
1693
+ let baseUrl = client.baseUrl;
1596
1694
  const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1597
1695
 
1598
1696
  function cancelActiveQuery() {
@@ -1620,11 +1718,12 @@ async function main() {
1620
1718
  return false;
1621
1719
  }
1622
1720
 
1623
- const tuiPrompt = new InteractiveTuiPrompt({
1721
+ const tuiPrompt = new InteractiveTuiPrompt({
1624
1722
  prompt: 'Kiku ❯ ',
1625
1723
  agent,
1626
1724
  modelName: opts.model || client.model || 'gemma-4-e4b',
1627
1725
  provider: client.providerName,
1726
+ isConnected: false,
1628
1727
  onCancel: cancelActiveQuery,
1629
1728
  onSigInt: async () => {
1630
1729
  if (activeAbortController || isProcessing) {
@@ -1636,12 +1735,13 @@ async function main() {
1636
1735
  globalTuiPrompt = null;
1637
1736
  }
1638
1737
  exitAlternateScreen();
1639
- console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
1640
- process.exit(0);
1738
+ process.stdout.write(`\n ${C.sakura}Goodbye • 再见 (鞠躬 ~ 谢谢!)${C.reset}\n\n`, () => {
1739
+ process.exit(0);
1740
+ });
1641
1741
  }
1642
1742
  }
1643
- });
1644
- const terminalGallery = createTerminalImageGallery();
1743
+ });
1744
+ const terminalGallery = createTerminalImageGallery();
1645
1745
 
1646
1746
  globalTuiPrompt = tuiPrompt;
1647
1747
  setupOutputInterception(tuiPrompt, () => isProcessing);
@@ -1650,57 +1750,84 @@ async function main() {
1650
1750
  let catalogOnlyMode = false;
1651
1751
 
1652
1752
  if (!health.connected) {
1653
- printRichCard({
1654
- title: `${client.providerName} Offline`,
1655
- status: 'offline',
1656
- lines: [
1657
- `• Endpoint: ${baseUrl || 'local'} (${health.error || 'unreachable'})`,
1658
- ...(client.provider === 'lmstudio' ? ['• Action: Attempting auto-start of local LM Studio...'] : []),
1659
- ],
1660
- });
1753
+ if (client.provider !== 'lmstudio') {
1754
+ printRichCard({
1755
+ title: `${client.providerName} Unavailable • Auto-Fallback to LM Studio`,
1756
+ status: 'warning',
1757
+ lines: [
1758
+ `• ${client.providerName} is not ready (${health.error || 'unreachable'}).`,
1759
+ '• Auto-switching to local LM Studio (google/gemma-4-e4b)...',
1760
+ 'Tip: To configure keys, use /provider setkey <key> or /provider use <name>',
1761
+ ],
1762
+ });
1661
1763
 
1662
- if (client.provider === 'lmstudio') {
1663
- const autoHealth = await attemptAutoStartLMStudio(baseUrl, opts.model || client.model);
1664
- if (autoHealth) {
1665
- health = autoHealth;
1666
- printRichCard({
1667
- title: 'LM Studio Started',
1668
- status: 'ok',
1669
- lines: [
1670
- 'Local LM Studio process started and responding.',
1671
- ],
1672
- });
1764
+ client.switchProvider('lmstudio');
1765
+ if (agent?.client) agent.client.switchProvider('lmstudio');
1766
+ baseUrl = client.baseUrl;
1767
+ tuiPrompt.provider = client.providerName;
1768
+ tuiPrompt.modelName = client.model;
1769
+ tuiPrompt.render();
1770
+
1771
+ health = await client.checkConnection();
1772
+ }
1773
+
1774
+ if (!health.connected) {
1775
+ printRichCard({
1776
+ title: `${client.providerName} Offline`,
1777
+ status: 'offline',
1778
+ lines: [
1779
+ `• Endpoint: ${baseUrl || 'local'} (${health.error || 'unreachable'})`,
1780
+ ...(client.provider === 'lmstudio' ? ['• Action: Attempting auto-start of local LM Studio...'] : []),
1781
+ ],
1782
+ });
1783
+
1784
+ if (client.provider === 'lmstudio') {
1785
+ const autoHealth = await attemptAutoStartLMStudio(baseUrl, opts.model || client.model);
1786
+ if (autoHealth) {
1787
+ health = autoHealth;
1788
+ printRichCard({
1789
+ title: 'LM Studio Started',
1790
+ status: 'ok',
1791
+ lines: [
1792
+ 'Local LM Studio process started and responding.',
1793
+ ],
1794
+ });
1795
+ } else {
1796
+ catalogOnlyMode = true;
1797
+ tuiPrompt.isConnected = false;
1798
+ printRichCard({
1799
+ title: 'LM Studio Offline • AI Chat Locked',
1800
+ status: 'offline',
1801
+ lines: [
1802
+ 'LM Studio is not running. Free-form chat typing is locked to prevent errors.',
1803
+ 'Open LM Studio, load a model (e.g. google/gemma-4-e4b), then type /model reconnect.',
1804
+ 'Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit',
1805
+ ],
1806
+ });
1807
+ }
1673
1808
  } else {
1674
1809
  catalogOnlyMode = true;
1810
+ tuiPrompt.isConnected = false;
1675
1811
  printRichCard({
1676
- title: 'Catalog-Only Mode',
1677
- status: 'catalog',
1812
+ title: `${client.providerName} Offline • AI Chat Locked`,
1813
+ status: 'offline',
1678
1814
  lines: [
1679
- 'Direct database & MCP tools remain available without AI synthesis.',
1680
- 'Start LM Studio and run /model reconnect to enable conversational chat.',
1815
+ `${client.providerName} is unreachable. Chat typing is locked to prevent errors.`,
1816
+ 'Verify your provider endpoint/key and use /model reconnect or /provider.',
1817
+ 'Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit',
1681
1818
  ],
1682
1819
  });
1683
1820
  }
1684
- } else {
1685
- catalogOnlyMode = true;
1686
- printRichCard({
1687
- title: 'Catalog-Only Mode',
1688
- status: 'catalog',
1689
- lines: [
1690
- 'Direct database & MCP tools remain available without AI synthesis.',
1691
- 'Verify your provider endpoint/key and use /model reconnect.',
1692
- ],
1693
- });
1694
1821
  }
1695
1822
  }
1696
1823
 
1697
- const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1824
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1698
1825
  const modelResolution = catalogOnlyMode
1699
1826
  ? { connected: false, state: 'catalog_only' }
1700
1827
  : await modelManager.resolve({ requestedModel: opts.model || client.model });
1701
1828
  if (!catalogOnlyMode && (!modelResolution.connected || !modelResolution.selectedModel)) {
1702
1829
  printRichCard({
1703
- title: 'Model Not Loaded',
1830
+ title: 'Model Not Loaded • AI Chat Locked',
1704
1831
  status: 'warning',
1705
1832
  lines: [
1706
1833
  modelResolution.error || 'No chat-capable model loaded',
@@ -1708,13 +1835,15 @@ async function main() {
1708
1835
  ],
1709
1836
  });
1710
1837
  catalogOnlyMode = true;
1838
+ tuiPrompt.isConnected = false;
1711
1839
  } else if (!catalogOnlyMode && modelResolution.selectedModel) {
1840
+ tuiPrompt.isConnected = true;
1712
1841
  tuiPrompt.modelName = modelResolution.selectedModel;
1713
1842
  printRichCard({
1714
1843
  title: 'Connected & Ready',
1715
1844
  status: 'connected',
1716
1845
  lines: [
1717
- `• Provider: ${client.providerName}${client.provider === 'lmstudio' ? ' (Local)' : ''} • Model: ${modelResolution.selectedModel}`,
1846
+ `• Provider: ${client.providerName}${client.provider === 'lmstudio' ? ' (Local)' : ''} • Model: ${modelResolution.selectedModel}`,
1718
1847
  `• Status: Chat & Tool DAG Active • Tools: ${toolsSchema.length} Loaded`,
1719
1848
  ],
1720
1849
  });
@@ -1763,17 +1892,17 @@ async function main() {
1763
1892
 
1764
1893
  const mcpStatus = agent.mcpManager ? agent.mcpManager.getStatus() : null;
1765
1894
  if (mcpStatus && mcpStatus.activeServersCount > 0 && !opts.mcp) {
1766
- const serverSummaries = mcpStatus.servers
1767
- .filter(s => s.status === 'connected')
1768
- .map(s => {
1769
- const sources = (s.sources || [])
1770
- .map(source => String(source).split(/[\\/]/).pop())
1771
- .filter(Boolean);
1772
- const sourceLabel = sources.length > 0
1773
- ? ` • source: ${[...new Set(sources)].join(', ')}`
1774
- : '';
1775
- return `• ${s.name} (${s.type}) — ${s.toolsCount} tools${sourceLabel}`;
1776
- });
1895
+ const serverSummaries = mcpStatus.servers
1896
+ .filter(s => s.status === 'connected')
1897
+ .map(s => {
1898
+ const sources = (s.sources || [])
1899
+ .map(source => String(source).split(/[\\/]/).pop())
1900
+ .filter(Boolean);
1901
+ const sourceLabel = sources.length > 0
1902
+ ? ` • source: ${[...new Set(sources)].join(', ')}`
1903
+ : '';
1904
+ return `• ${s.name} (${s.type}) — ${s.toolsCount} tools${sourceLabel}`;
1905
+ });
1777
1906
  printRichCard({
1778
1907
  title: 'Persisted MCP Connected',
1779
1908
  status: 'mcp',
@@ -1826,22 +1955,29 @@ async function main() {
1826
1955
  baseUrl,
1827
1956
  modelManager,
1828
1957
  catalogOnlyMode,
1829
- setCatalogOnlyMode: value => { catalogOnlyMode = Boolean(value); },
1958
+ setCatalogOnlyMode: value => {
1959
+ catalogOnlyMode = Boolean(value);
1960
+ if (tuiPrompt) {
1961
+ tuiPrompt.isConnected = !catalogOnlyMode;
1962
+ tuiPrompt.render();
1963
+ }
1964
+ },
1830
1965
  get lastQuery() { return lastQuery; },
1831
1966
  set lastQuery(value) { lastQuery = value; },
1832
1967
  printHelp,
1833
1968
  printQueryDetails,
1834
- copyQueryDetails,
1835
- printWelcome: printWelcomeHero,
1836
- printAd,
1837
- showTerminalImage: () => {
1838
- const imageLines = terminalGallery.maybeRender(queryCount, { force: true });
1839
- if (imageLines.length > 0) tuiPrompt.addViewportLines(imageLines);
1840
- return imageLines.length > 0;
1841
- },
1842
- cancelActiveQuery,
1843
- readInput: options => tuiPrompt.readLine(options),
1844
- enqueueQuery: async (prompt, isRetry) => {
1969
+ copyQueryDetails: (ref) => copyQueryDetails(ref, agent?.session),
1970
+ printWelcome: printWelcomeHero,
1971
+ printAd,
1972
+ showTerminalImage: (count = 2) => {
1973
+ const num = Number(count) || 2;
1974
+ const imageLines = terminalGallery.maybeRender(queryCount, { force: true, count: num });
1975
+ if (imageLines.length > 0) tuiPrompt.addViewportLines(imageLines);
1976
+ return imageLines.length > 0;
1977
+ },
1978
+ cancelActiveQuery,
1979
+ readInput: options => tuiPrompt.readLine(options),
1980
+ enqueueQuery: async (prompt, isRetry) => {
1845
1981
  queryQueue.push({ id: 'q_' + Date.now(), number: nextQueryNumber++, prompt, addedAt: Date.now() });
1846
1982
  triggerQueueProcessing();
1847
1983
  },
@@ -1879,46 +2015,49 @@ async function main() {
1879
2015
 
1880
2016
  try {
1881
2017
  if (catalogOnlyMode) {
1882
- const targetTool = nextItem.prompt.startsWith('@')
1883
- ? nextItem.prompt.slice(1).split(/\s+/)[0]
1884
- : 'search_works';
1885
- const rawArg = nextItem.prompt.startsWith('@')
1886
- ? nextItem.prompt.slice(targetTool.length + 1).trim()
1887
- : nextItem.prompt;
1888
-
1889
- let queryArgs = {};
1890
- try { queryArgs = JSON.parse(rawArg); } catch { queryArgs = { query: rawArg }; }
1891
-
1892
- let result;
1893
- if (agent.mcpManager?.isMcpTool(targetTool)) {
1894
- result = await agent.mcpManager.callTool(targetTool, queryArgs);
2018
+ if (nextItem.prompt.startsWith('@')) {
2019
+ const targetTool = nextItem.prompt.slice(1).split(/\s+/)[0];
2020
+ const rawArg = nextItem.prompt.slice(targetTool.length + 1).trim();
2021
+
2022
+ let queryArgs = {};
2023
+ try { queryArgs = JSON.parse(rawArg); } catch { queryArgs = { query: rawArg }; }
2024
+
2025
+ let result;
2026
+ if (agent.mcpManager?.isMcpTool(targetTool)) {
2027
+ result = await agent.mcpManager.callTool(targetTool, queryArgs);
2028
+ } else {
2029
+ result = await executeTool(targetTool, queryArgs);
2030
+ }
2031
+
2032
+ const serialized = JSON.stringify(result, null, 2);
2033
+ printSolidPanel({
2034
+ title: `Direct Tool Result • ${targetTool} • Q${nextItem.number}`,
2035
+ lines: serialized.split('\n'),
2036
+ });
1895
2037
  } else {
1896
- result = await executeTool(targetTool, queryArgs);
2038
+ printRichCard({
2039
+ title: 'Query Blocked • LM Studio Offline',
2040
+ status: 'warning',
2041
+ lines: [
2042
+ 'Cannot execute chat query because LM Studio is offline.',
2043
+ 'Please start LM Studio and run /model reconnect to enable conversational chat.',
2044
+ ],
2045
+ });
1897
2046
  }
1898
-
1899
- const serialized = JSON.stringify(result, null, 2);
1900
- printSolidPanel({
1901
- title: `Direct Tool Result • ${targetTool} • Q${nextItem.number}`,
1902
- lines: serialized.split('\n'),
1903
- });
1904
2047
  } else {
1905
2048
  await executeQueryWithUI(agent, nextItem.prompt, agent.session.thinkingMode === 'show', activeAbortController.signal, nextItem.number);
1906
2049
  }
1907
2050
  } catch (err) {
1908
2051
  if (!isAbortError(err)) {
1909
- printRichCard({
1910
- title: 'Execution Error',
1911
- status: 'error',
1912
- lines: [err.message],
1913
- });
2052
+ printRichCard(formatExecutionErrorCard(err, { client: agent.client, provider: agent.client?.provider, model: agent.client?.model }));
1914
2053
  }
1915
2054
  } finally {
1916
- activeAbortController = null;
1917
- queryCount++;
1918
- printAd();
1919
- const imageLines = terminalGallery.maybeRender(queryCount);
1920
- if (imageLines.length > 0) tuiPrompt.addViewportLines(imageLines);
1921
- activeQueryNumber = null;
2055
+ activeAbortController = null;
2056
+ queryCount++;
2057
+ printAd();
2058
+ const imageLines = terminalGallery.maybeRender(queryCount);
2059
+ if (imageLines.length > 0) tuiPrompt.addViewportLines(imageLines);
2060
+ activeQueryNumber = null;
1922
2061
  if (globalTuiPrompt) {
1923
2062
  globalTuiPrompt.agentState = '';
1924
2063
  globalTuiPrompt.render();
@@ -2009,6 +2148,75 @@ async function main() {
2009
2148
  continue;
2010
2149
  }
2011
2150
 
2151
+
2152
+ if (catalogOnlyMode || !tuiPrompt.isConnected) {
2153
+ if (trimmed.startsWith('@')) {
2154
+ const targetTool = trimmed.slice(1).split(/\s+/)[0];
2155
+ const rawArg = trimmed.slice(targetTool.length + 1).trim();
2156
+
2157
+ let queryArgs = {};
2158
+ try { queryArgs = JSON.parse(rawArg); } catch { queryArgs = { query: rawArg }; }
2159
+
2160
+ try {
2161
+ let result;
2162
+ if (agent.mcpManager?.isMcpTool(targetTool)) {
2163
+ result = await agent.mcpManager.callTool(targetTool, queryArgs);
2164
+ } else {
2165
+ result = await executeTool(targetTool, queryArgs);
2166
+ }
2167
+ const serialized = JSON.stringify(result, null, 2);
2168
+ printSolidPanel({
2169
+ title: `Direct Tool Result • ${targetTool}`,
2170
+ lines: serialized.split('\n'),
2171
+ });
2172
+ } catch (err) {
2173
+ printRichCard({
2174
+ title: 'Tool Error',
2175
+ status: 'error',
2176
+ lines: [err.message],
2177
+ });
2178
+ }
2179
+ continue;
2180
+ }
2181
+
2182
+
2183
+ const lower = trimmed.toLowerCase();
2184
+ if (lower === 'exit' || lower === 'quit' || lower === 'q') {
2185
+ cancelActiveQuery();
2186
+ break;
2187
+ }
2188
+ if (lower === 'help' || lower === 'h' || lower === '?') {
2189
+ await routeCommand('/help', { ...cmdCtx, sub: '', extra: '', parts: ['/help'] });
2190
+ continue;
2191
+ }
2192
+ if (lower === 'reconnect' || lower === 'connect') {
2193
+ await routeCommand('/model', { ...cmdCtx, sub: 'reconnect', extra: '', parts: ['/model', 'reconnect'] });
2194
+ continue;
2195
+ }
2196
+ if (lower === 'doctor') {
2197
+ await routeCommand('/doctor', { ...cmdCtx, sub: '', extra: '', parts: ['/doctor'] });
2198
+ continue;
2199
+ }
2200
+
2201
+
2202
+ printSolidPanel({
2203
+ title: 'AI Chat Locked • LM Studio Offline',
2204
+ lines: [
2205
+ `${C.bold}${C.amber}LM Studio is not running or connected.${C.reset}`,
2206
+ `${C.white}Chat query typing is disabled until the AI server is active.${C.reset}`,
2207
+ '',
2208
+ `${C.bold}${C.white}How to unlock & connect:${C.reset}`,
2209
+ ` 1. Open ${C.cyberCyan}LM Studio${C.reset} on your computer`,
2210
+ ` 2. Load model ${C.gold}${opts.model || client.model || 'google/gemma-4-e4b'}${C.reset} and ensure the local server is running (port 1234)`,
2211
+ ` 3. Type ${C.cyberCyan}/model reconnect${C.reset} in this terminal (or type ${C.cyberCyan}reconnect${C.reset})`,
2212
+ '',
2213
+ `${C.dim}Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit${C.reset}`,
2214
+ ],
2215
+ bg: C.bgPanel,
2216
+ });
2217
+ continue;
2218
+ }
2219
+
2012
2220
  const { sanitized, warnings } = sanitizeQuery(trimmed);
2013
2221
  if (warnings.length > 0) {
2014
2222
  printRichCard({
@@ -2037,8 +2245,9 @@ async function main() {
2037
2245
  globalTuiPrompt = null;
2038
2246
  }
2039
2247
  exitAlternateScreen();
2040
- console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
2041
- process.exit(0);
2248
+ process.stdout.write(`\n ${C.sakura}Goodbye • 再见 (鞠躬 ~ 谢谢!)${C.reset}\n\n`, () => {
2249
+ process.exit(0);
2250
+ });
2042
2251
  }
2043
2252
 
2044
2253
  process.on('exit', () => {