kiku-agent 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +459 -407
  2. package/package.json +32 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +637 -0
  6. package/server/agent/citations.js +0 -21
  7. package/server/agent/clarificationAnalytics.js +1 -1
  8. package/server/agent/clarifyingQuestions.js +286 -216
  9. package/server/agent/cli.js +532 -73
  10. package/server/agent/commandFeatures.js +123 -24
  11. package/server/agent/commandRegistry.js +128 -62
  12. package/server/agent/commands.js +1410 -95
  13. package/server/agent/config.js +253 -163
  14. package/server/agent/contextCompactor.js +239 -25
  15. package/server/agent/dynamicCatalog.js +192 -0
  16. package/server/agent/envBootstrap.js +46 -0
  17. package/server/agent/envManager.js +317 -0
  18. package/server/agent/eventBus.js +3 -3
  19. package/server/agent/feedbackLearning.js +0 -8
  20. package/server/agent/gpuMetrics.js +188 -55
  21. package/server/agent/ingestionWorker.js +339 -0
  22. package/server/agent/ink/App.jsx +348 -0
  23. package/server/agent/ink/Panel.jsx +54 -0
  24. package/server/agent/ink/PromptInput.jsx +321 -0
  25. package/server/agent/ink/TaskPlanPanel.jsx +88 -0
  26. package/server/agent/ink/ThinkingPanel.jsx +68 -0
  27. package/server/agent/ink/ToolResult.jsx +133 -0
  28. package/server/agent/ink/main.jsx +195 -0
  29. package/server/agent/ink/useEventBus.js +218 -0
  30. package/server/agent/lanDiscovery.js +369 -0
  31. package/server/agent/llmClient.js +111 -39
  32. package/server/agent/lmStudioWakeup.js +144 -0
  33. package/server/agent/mcpClient.js +2 -2
  34. package/server/agent/memory/contextAllocator.js +214 -0
  35. package/server/agent/memory/hierarchicalStore.js +523 -0
  36. package/server/agent/memory/reflectionWorker.js +110 -0
  37. package/server/agent/memory.js +635 -494
  38. package/server/agent/modelConfig.js +487 -0
  39. package/server/agent/newsIntelligence.js +219 -0
  40. package/server/agent/newsWatch.js +85 -0
  41. package/server/agent/prompts.js +146 -0
  42. package/server/agent/publicTools.js +7 -0
  43. package/server/agent/queryDetails.js +62 -1
  44. package/server/agent/research.js +314 -35
  45. package/server/agent/routingInterceptor.js +77 -0
  46. package/server/agent/rumoredProjects.js +15 -0
  47. package/server/agent/sanitizer.js +117 -17
  48. package/server/agent/subagent.js +52 -9
  49. package/server/agent/suggestionTui.js +33 -13
  50. package/server/agent/suggestions.js +29 -13
  51. package/server/agent/telemetry/hardwareTiers.js +332 -0
  52. package/server/agent/telemetry/runtimeWatchdog.js +66 -0
  53. package/server/agent/telemetry/techPowerUpClient.js +682 -0
  54. package/server/agent/telemetry/techpowerup-mcp.json +22 -0
  55. package/server/agent/telemetry.js +0 -11
  56. package/server/agent/terminalImage.js +0 -1
  57. package/server/agent/textToolProtocol.js +190 -0
  58. package/server/agent/tokenTracker.js +220 -0
  59. package/server/agent/toolRouter.js +362 -216
  60. package/server/agent/tools.js +1155 -194
  61. package/server/agent/ui.js +144 -41
  62. package/server/agent/vramService.js +572 -0
  63. package/server/agent/webSearch.js +2279 -273
  64. package/server/agent/websearch/adapters/duckduckgo.js +97 -0
  65. package/server/agent/websearch/adapters/jina.js +77 -0
  66. package/server/agent/websearch/adapters/searxng.js +57 -0
  67. package/server/agent/websearch/core/canonical.js +89 -0
  68. package/server/agent/websearch/core/fusion.js +81 -0
  69. package/server/agent/websearch/core/types.js +29 -0
  70. package/server/agent/websearch/extract.js +605 -0
  71. package/server/agent/websearch/index.js +43 -0
  72. package/server/agent/websearch/orchestrator.js +266 -0
  73. package/server/agent/websearch/resilience/breaker.js +97 -0
  74. package/server/agent/websearch/resilience/cache.js +59 -0
  75. package/server/agent/websearch/resilience/limiter.js +93 -0
  76. package/server/agent/websearch/security/filter.js +36 -0
  77. package/server/agent/websearch/security/ssrf.js +289 -0
  78. package/server/agent/websearch/telemetry.js +71 -0
  79. package/server/agent/youtubeAuth.js +82 -73
  80. package/server/agent/youtubeTranscript.js +209 -0
  81. package/server/apiRouter.js +390 -190
  82. package/server/mcp/data/brands.js +5 -5
  83. package/server/mcp/data/discography.js +56 -45
  84. package/server/mcp/data/gallery.js +9 -0
  85. package/server/mcp/data/index.js +1 -0
  86. package/server/mcp/data/industryIndex.js +251 -0
  87. package/server/mcp/data/profile.js +5 -5
  88. package/server/mcp/data/timeline.js +2 -2
  89. package/server/mcp/data/works.js +2 -2
  90. package/server/mcp/handler.js +0 -2
  91. package/server/mcp/prompts.js +42 -2
  92. package/server/mcp/resources.js +5 -1
  93. package/server/mcp/security.js +25 -14
  94. package/server/mcp/server.js +92 -3
  95. package/server/mcp/tools.js +20 -11
  96. package/server/routes/agent.js +55 -0
  97. package/server/web-chat/core/connection.js +218 -194
  98. package/server/web-chat/core/conversationContext.js +1 -1
  99. package/server/web-chat/core/directCatalogEngine.js +190 -184
  100. package/server/web-chat/core/followUpEngine.js +88 -11
  101. package/server/web-chat/core/security.js +7 -4
  102. package/server/web-chat/router.js +54 -17
  103. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  104. package/server/web-chat/routes/chatRoute.js +488 -397
  105. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  106. package/server/web-chat/routes/exportRoute.js +2 -1
  107. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  108. package/server/web-chat/routes/newsRoute.js +36 -6
  109. package/server/web-chat/routes/providerRoute.js +87 -0
  110. package/server/web-chat/routes/staticRoute.js +61 -13
  111. package/server/web-chat/routes/statusRoute.js +23 -2
  112. package/server/web-chat/routes/vramRoute.js +22 -0
  113. package/server/web-chat/server.js +135 -106
  114. package/server/web-chat/ui/htmlBuilder.js +8 -8
  115. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  116. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  117. package/server/web-chat/ui/scripts/client.js +10526 -8397
  118. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  119. package/server/web-chat/ui/styles/main.css +7471 -4246
  120. package/server/web-chat/ui/templates/body.html +305 -179
  121. package/src/data/brandsData.js +29 -6
  122. package/src/data/discographyData.js +918 -900
  123. package/src/data/galleryData.js +66 -0
  124. package/src/data/industryIndexData.js +292 -0
  125. package/src/data/musicVideoData.js +48 -5
  126. package/src/data/profileData.js +15 -1
  127. package/src/data/timelineData.js +4 -3
  128. package/src/data/worksData.js +536 -535
  129. package/src/lib/vram/estimator.js +464 -0
  130. package/src/lib/vram/modelRegistry.js +387 -0
  131. package/tokens.css +33 -2
@@ -5,11 +5,11 @@ import { fileURLToPath } from 'node:url';
5
5
  import { stdin as input } from 'node:process';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { spawn } from 'node:child_process';
8
- import './youtubeAuth.js';
8
+ import './envBootstrap.js';
9
9
  import { KikuAgent } from './agentCore.js';
10
10
  import { LLMClient, LMStudioClient } from './llmClient.js';
11
11
  import { toolsSchema, toolHandlers, executeTool } from './tools.js';
12
- import { config, CLOUD_PROVIDERS } from './config.js';
12
+ import { config, CLOUD_PROVIDERS, matchModelIdentifier } from './config.js';
13
13
  import { audioPlayer } from './audioPlayer.js';
14
14
  import { routeCommand } from './commands.js';
15
15
  import { eventBus, Events } from './eventBus.js';
@@ -18,6 +18,9 @@ import { sanitizeQuery } from './sanitizer.js';
18
18
  import { isAbortError, formatExecutionErrorCard } from './errors.js';
19
19
  import { COMMAND_DEFINITIONS, COMMAND_CATEGORIES, SUB_COMMANDS, findCommandDefinition, getCommandDetails } from './commandRegistry.js';
20
20
  import { ModelManager } from './modelManager.js';
21
+ import { runVramPreflight, probeLmStudioRuntime, getHardwareTarget } from './vramService.js';
22
+ import { resolveModelConfig } from './modelConfig.js';
23
+ import { estimateVramFootprint, classifyAllocation, formatGb } from '../../src/lib/vram/estimator.js';
21
24
  import { PACKAGE_VERSION } from './version.js';
22
25
  import { getProfile, listProfiles } from './profiles.js';
23
26
  import { formatCitationsBlock } from './citations.js';
@@ -30,7 +33,7 @@ import {
30
33
  parseDetailsRequest,
31
34
  serializeDetailValue,
32
35
  summarizeQueryDetails,
33
- copyToClipboard,
36
+ copyToClipboardAsync,
34
37
  getCopyableContent,
35
38
  } from './queryDetails.js';
36
39
 
@@ -39,7 +42,7 @@ import {
39
42
  previousGraphemeBoundary, nextGraphemeBoundary, sliceLineForDisplay,
40
43
  printSolidBannerRow, printSolidPanel, formatSolidPanelLines, formatTerminalMarkdown, formatMarkdownLines,
41
44
  parseMarkdownTableRow, isMarkdownTableSeparator, expandMarkdownLinks,
42
- formatRichCardLines, printRichCard, printRichStatusCard, formatStatusBadge, formatRichTable,
45
+ formatRichCardLines, printRichCard, printRichStatusCard, formatStatusBadge, formatRichTable, cleanTableCell,
43
46
  printWelcomeHero, getWelcomeHeroLines, randomFanName, setupOutputInterception,
44
47
  rawWrite, INPUT_PLACEHOLDER, UI_ICONS,
45
48
  spinnerFrames, thinkingFrames, createProgressRunner,
@@ -136,6 +139,11 @@ export function parseCliArgs(customArgs = null) {
136
139
  webChat: false,
137
140
  port: null,
138
141
  noOpen: false,
142
+ force: false,
143
+ vram: null,
144
+ eval: null,
145
+ hub: null,
146
+ portal: null,
139
147
  };
140
148
 
141
149
  for (let i = 0; i < args.length; i++) {
@@ -157,6 +165,9 @@ export function parseCliArgs(customArgs = null) {
157
165
  else if (arg === '--output' || arg === '-o') options.output = (args[++i] || 'text').toLowerCase();
158
166
  else if (arg === '--plain') { options.plain = true; options.noColor = true; }
159
167
  else if (arg === '--no-color') options.noColor = true;
168
+ else if (arg === '--cn-tools' || arg === '--cn' || arg === '--enable-cn-tools') {
169
+ process.env.KIKU_ENABLE_CN_WEB_TOOLS = 'true';
170
+ }
160
171
  else if (arg === '--web-chat' || arg === '--web') options.webChat = true;
161
172
  else if (arg.startsWith('--web-chat=')) {
162
173
  options.webChat = true;
@@ -181,6 +192,19 @@ export function parseCliArgs(customArgs = null) {
181
192
  }
182
193
  else if (arg === '--no-open') options.noOpen = true;
183
194
  else if (arg === '--open') options.noOpen = false;
195
+ else if (arg === '--force') options.force = true;
196
+ else if (arg === '--vram' || arg === '--gpu') {
197
+ options.vram = (args[i + 1] && !args[i + 1].startsWith('-')) ? args[++i] : 'live';
198
+ }
199
+ else if (arg === '--hub') {
200
+ options.hub = (args[i + 1] && !args[i + 1].startsWith('-')) ? args[++i] : true;
201
+ }
202
+ else if (arg === '--eval') {
203
+ options.eval = (args[i + 1] && !args[i + 1].startsWith('-')) ? args[++i] : 'ui';
204
+ }
205
+ else if (arg === '--portal') {
206
+ options.portal = (args[i + 1] && !args[i + 1].startsWith('-')) ? args[++i] : 'open';
207
+ }
184
208
  else if (!arg.startsWith('-') && !options.query) { options.query = args.slice(i).join(' '); break; }
185
209
  else if (arg.startsWith('-')) {
186
210
  console.error(`Unknown option: ${arg}`);
@@ -431,7 +455,7 @@ function printQueryDetails(reference = '') {
431
455
  });
432
456
  }
433
457
 
434
- function copyQueryDetails(reference = '', session = null) {
458
+ async function copyQueryDetails(reference = '', session = null) {
435
459
  const request = parseDetailsRequest(reference);
436
460
  if (request.error) {
437
461
  printSolidPanel({
@@ -481,7 +505,7 @@ function copyQueryDetails(reference = '', session = null) {
481
505
  return false;
482
506
  }
483
507
 
484
- const copied = copyToClipboard(copyable.text);
508
+ const copied = await copyToClipboardAsync(copyable.text);
485
509
  const lineCount = copyable.text.split('\n').length;
486
510
  const charCount = copyable.text.length.toLocaleString();
487
511
 
@@ -597,7 +621,7 @@ export function printHelp(specificCmd = null) {
597
621
  helpLines.push(` ${C.emerald}${"-q, --query <text>".padEnd(28)}${C.reset} ${C.white}Run a one-shot query${C.reset}`);
598
622
  helpLines.push(` ${C.emerald}${"-p, --print".padEnd(28)}${C.reset} ${C.white}Headless non-interactive print mode (clean text output)${C.reset}`);
599
623
  helpLines.push(` ${C.emerald}${"-o, --output <format>".padEnd(28)}${C.reset} ${C.white}Output format: text (default) or json${C.reset}`);
600
- helpLines.push(` ${C.emerald}${"-P, --provider <name>".padEnd(28)}${C.reset} ${C.white}Select provider (lmstudio, openai, gemini, groq, etc.)${C.reset}`);
624
+ helpLines.push(` ${C.emerald}${"-P, --provider <name>".padEnd(28)}${C.reset} ${C.white}Select provider (lmstudio, vllm, openai, gemini, groq, etc.)${C.reset}`);
601
625
  helpLines.push(` ${C.emerald}${"--profile <name>".padEnd(28)}${C.reset} ${C.white}Load configuration profile (default, fast, deep-reasoning, cloud, gemini)${C.reset}`);
602
626
  helpLines.push(` ${C.emerald}${"-m, --model <id>".padEnd(28)}${C.reset} ${C.white}Model ID override${C.reset}`);
603
627
  helpLines.push(` ${C.emerald}${"-k, --api-key <key>".padEnd(28)}${C.reset} ${C.white}Provider API key override${C.reset}`);
@@ -607,7 +631,12 @@ export function printHelp(specificCmd = null) {
607
631
  helpLines.push(` ${C.emerald}${"--thinking <mode>".padEnd(28)}${C.reset} ${C.white}Show or hide reasoning process (show | hide)${C.reset}`);
608
632
  helpLines.push(` ${C.emerald}${"-t, --tools".padEnd(28)}${C.reset} ${C.white}List all registered tools${C.reset}`);
609
633
  helpLines.push(` ${C.emerald}${"-s, --stats".padEnd(28)}${C.reset} ${C.white}Show system and catalog stats${C.reset}`);
634
+ helpLines.push(` ${C.emerald}${"--hub [filter]".padEnd(28)}${C.reset} ${C.white}Show Kiku Master Command Center hub${C.reset}`);
635
+ helpLines.push(` ${C.emerald}${"--portal [open|api|chat]".padEnd(28)}${C.reset} ${C.white}Open Fan Portal SPA, REST API docs, or Web Chat${C.reset}`);
636
+ helpLines.push(` ${C.emerald}${"--eval [ui|dash|val]".padEnd(28)}${C.reset} ${C.white}Eval Studio web UI, dashboard, or validation${C.reset}`);
637
+ helpLines.push(` ${C.emerald}${"--vram [live|compare|tier]".padEnd(28)}${C.reset} ${C.white}VRAM memory telemetry & hardware classification${C.reset}`);
610
638
  helpLines.push(` ${C.emerald}${"--web-chat, --web".padEnd(28)}${C.reset} ${C.white}Start standalone web chatbot (default port 3300)${C.reset}`);
639
+ helpLines.push(` ${C.emerald}${"--cn-tools, --cn".padEnd(28)}${C.reset} ${C.white}Enable Chinese web tools (search_bilibili, Baidu)${C.reset}`);
611
640
  helpLines.push(` ${C.emerald}${"--port <n>".padEnd(28)}${C.reset} ${C.white}Web chatbot port 1-65535 (or --web-chat=<port>)${C.reset}`);
612
641
  helpLines.push(` ${C.emerald}${"--no-open, --open".padEnd(28)}${C.reset} ${C.white}Skip or force auto-open browser for --web-chat${C.reset}`);
613
642
  helpLines.push(` ${C.emerald}${"-v, --verbose".padEnd(28)}${C.reset} ${C.white}Verbose logging and thinking${C.reset}`);
@@ -863,6 +892,22 @@ class InteractiveTuiPrompt {
863
892
  if (resolve) resolve(result);
864
893
  }
865
894
 
895
+ abortActiveRead(result = null) {
896
+ if (this.isDisposed || !this.activeResolve || this.isResolving) return false;
897
+ this.isResolving = true;
898
+ this._cleanupInput();
899
+ this.line = '';
900
+ this.cursor = 0;
901
+ this.ghostText = '';
902
+ this.popoverActive = false;
903
+ this.popoverItems = [];
904
+ const resolve = this.activeResolve;
905
+ this.activeResolve = null;
906
+ this.activeReject = null;
907
+ resolve(result);
908
+ return true;
909
+ }
910
+
866
911
  fail(error) {
867
912
  if (this.isResolving) return;
868
913
  this.isResolving = true;
@@ -901,7 +946,7 @@ class InteractiveTuiPrompt {
901
946
  this.popoverItems = [];
902
947
  this.popoverIndex = 0;
903
948
  if (!this.isConnected && this.line.length > 0) {
904
- this.ghostText = ' (LM Studio offline - type /model reconnect)';
949
+ this.ghostText = ' (LM Studio offline - will auto-connect once loaded)';
905
950
  } else {
906
951
  this.ghostText = '';
907
952
  }
@@ -924,7 +969,7 @@ class InteractiveTuiPrompt {
924
969
  this.screenBuffer.clear(C.bgDark);
925
970
 
926
971
 
927
- const shortModel = (this.modelName || 'gemma-4-e4b').replace(/^.*\//, '');
972
+ const shortModel = String(this.modelName || 'gemma-4-e4b').split(/[/\\]/).pop();
928
973
  const modelLabel = this.isConnected
929
974
  ? `${C.gold}✦ ${shortModel}${C.reset}`
930
975
  : `${C.roseGlow}✦ OFFLINE${C.reset}`;
@@ -981,7 +1026,7 @@ class InteractiveTuiPrompt {
981
1026
  } else if (musicStatus) {
982
1027
  statusStyled = ` ${C.cyberCyan}🎵 ${musicStatus}${C.reset}`;
983
1028
  } else if (!this.isConnected) {
984
- statusStyled = ` ${C.roseGlow}○ OFFLINE${C.reset} ${C.slate}• Start LM Studio & type ${C.cyberCyan}/model reconnect${C.reset}`;
1029
+ statusStyled = ` ${C.roseGlow}○ OFFLINE${C.reset} ${C.slate}• Auto-detecting LM Studio... (${C.cyberCyan}/model reconnect${C.reset}${C.slate})${C.reset}`;
985
1030
  } else {
986
1031
  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}`;
987
1032
  statusStyled = ` ${C.emerald}● READY${C.reset} ${C.slate}│${C.reset} ${hkHints}`;
@@ -1038,7 +1083,7 @@ class InteractiveTuiPrompt {
1038
1083
  } else if (!this.line) {
1039
1084
  const defaultPlaceholder = this.isConnected
1040
1085
  ? this.inputPlaceholder
1041
- : '[AI Offline] Start LM Studio & type /model reconnect (or /help)';
1086
+ : '[AI Offline] Load model in LM Studio to auto-connect (or /help)';
1042
1087
  const placeholder = sliceLineForDisplay(defaultPlaceholder, 0, maxLineWidth).displayLine;
1043
1088
  trailingStyled = `${C.dim}${C.slate}${placeholder}${C.reset}`;
1044
1089
  }
@@ -1548,7 +1593,7 @@ async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, sign
1548
1593
  const maxWidth = Math.max(16, getFullWidth() - 8);
1549
1594
  for (let index = 0; index < activeTableHeaders.length; index++) {
1550
1595
  const header = activeTableHeaders[index] || `Column ${index + 1}`;
1551
- const value = expandMarkdownLinks(cells[index] || '—');
1596
+ const value = expandMarkdownLinks(cleanTableCell(cells[index] || '—'));
1552
1597
  const prefix = `${header}: `;
1553
1598
  const wrapped = wrapText(prefix + value, maxWidth);
1554
1599
  for (const line of wrapped) {
@@ -1757,6 +1802,28 @@ async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, sign
1757
1802
  }
1758
1803
  },
1759
1804
 
1805
+ onSubagentProgress: (event) => {
1806
+ if (!event || !event.type) return;
1807
+ let line = null;
1808
+ if (event.type === 'start') {
1809
+ line = ` ${C.slate}↳ deep research • ${event.workers || '?'} workers${C.reset}`;
1810
+ } else if (event.type === 'iteration') {
1811
+ line = ` ${C.slate}↳ iteration ${event.iteration}/${event.maxIterations}${C.reset}`;
1812
+ } else if (event.type === 'tool') {
1813
+ line = ` ${C.slate}↳ ${C.cyberCyan}${event.name}${C.reset}`;
1814
+ } else if (event.type === 'worker-start') {
1815
+ line = ` ${C.slate}↳ ${C.bold}${C.white}${event.label || event.id || 'worker'}${C.reset} ${C.dim}started${C.reset}`;
1816
+ } else if (event.type === 'worker-complete') {
1817
+ line = ` ${C.emerald}↳ ${event.label || event.id || 'worker'} done${C.reset}`;
1818
+ }
1819
+ if (!line) return;
1820
+ if (globalTuiPrompt?.addViewportLines) {
1821
+ globalTuiPrompt.addViewportLines(line);
1822
+ } else {
1823
+ console.log(line);
1824
+ }
1825
+ },
1826
+
1760
1827
  onTokenStart: () => {
1761
1828
  if (globalTuiPrompt) {
1762
1829
  globalTuiPrompt.agentState = 'Streaming…';
@@ -1783,6 +1850,11 @@ async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, sign
1783
1850
  }
1784
1851
  },
1785
1852
 
1853
+ onNotice: (text) => {
1854
+ stopSpinner();
1855
+ console.log(`\n ${C.amber}▸ ${text}${C.reset}`);
1856
+ },
1857
+
1786
1858
  onError: (_err) => {
1787
1859
  stopSpinner();
1788
1860
  },
@@ -1844,6 +1916,134 @@ async function readStdin() {
1844
1916
  });
1845
1917
  }
1846
1918
 
1919
+ function vramZoneTag(zone) {
1920
+ const color = zone === 'GREEN' ? C.emerald : zone === 'YELLOW' ? C.gold : zone === 'RED' ? C.roseGlow : C.slate;
1921
+ return `${color}[${zone}]${C.reset}`;
1922
+ }
1923
+
1924
+ /**
1925
+ * Startup "Resource Verification" panel: live VRAM usage plus the running
1926
+ * model's estimated footprint, so users immediately see whether the loaded
1927
+ * model is a heavy fit for their card. Informational only — never blocks.
1928
+ */
1929
+ async function printResourceVerificationPanel(client) {
1930
+ if (!client || client.provider !== 'lmstudio') return;
1931
+ try {
1932
+ const [hw, runtime] = await Promise.all([
1933
+ getHardwareTarget(),
1934
+ probeLmStudioRuntime(client.baseUrl).catch(() => null),
1935
+ ]);
1936
+ const config = await resolveModelConfig({ modelName: client.model, allowNetwork: false });
1937
+ const cfg = config?.config || null;
1938
+ const runtimeCtx = runtime?.loadedContextLength || cfg?.contextLength || null;
1939
+
1940
+ const estimate = estimateVramFootprint({
1941
+ modelId: client.model,
1942
+ quant: cfg?.quant || 'q4_k_m',
1943
+ paramCount: cfg?.paramCount,
1944
+ paramsB: cfg?.paramsB,
1945
+ layers: cfg?.layers,
1946
+ queryHeads: cfg?.queryHeads,
1947
+ kvHeads: cfg?.kvHeads,
1948
+ headDim: cfg?.headDim,
1949
+ hiddenSize: cfg?.hiddenSize,
1950
+ contextLength: runtimeCtx,
1951
+ });
1952
+
1953
+ const lines = [];
1954
+ let livePct = null;
1955
+ if (hw.available && hw.freeBytes !== null && hw.freeBytes !== undefined) {
1956
+ const usedBytes = Math.max(0, hw.totalBytes - hw.freeBytes);
1957
+ const live = classifyAllocation({ totalBytes: usedBytes, availableBytes: hw.totalBytes });
1958
+ livePct = live.usagePct;
1959
+ lines.push(`${C.gold}GPU (live):${C.reset} ${C.white}${hw.name}${C.reset} — ${C.bold}${live.usagePct}%${C.reset} ${C.dim}(${formatGb(usedBytes)} / ${formatGb(hw.totalBytes)} used)${C.reset} ${vramZoneTag(live.zone)}`);
1960
+ lines.push(`${C.gold}Free VRAM:${C.reset} ${C.emerald}${formatGb(hw.freeBytes)}${C.reset}`);
1961
+ } else if (hw.available) {
1962
+ lines.push(`${C.gold}GPU:${C.reset} ${C.white}${hw.name}${C.reset} ${C.dim}(total ${formatGb(hw.totalBytes)} — live usage unavailable)${C.reset}`);
1963
+ } else {
1964
+ lines.push(`${C.gold}GPU:${C.reset} ${C.dim}not detected — VRAM feasibility checks unavailable (KIKU_VRAM_TOTAL_GB can pin a budget)${C.reset}`);
1965
+ }
1966
+
1967
+ const quantLabel = String(runtime?.quantization || estimate.quant || 'q4_k_m').toUpperCase();
1968
+ const paramsLabel = estimate.paramCount ? `${(estimate.paramCount / 1e9).toFixed(1)}B` : '?';
1969
+ const sourceLabel = config?.source === 'gguf' ? 'gguf metadata' : config?.source === 'registry' ? 'registry approx' : config?.source || 'unknown';
1970
+ lines.push(`${C.gold}Model:${C.reset} ${C.white}${client.model}${C.reset} ${C.dim}(${quantLabel} • ${paramsLabel} params • ${sourceLabel})${C.reset}`);
1971
+
1972
+ if (estimate.totalBytes > 0 && runtimeCtx) {
1973
+ const estCls = hw.available ? classifyAllocation({ totalBytes: estimate.totalBytes, availableBytes: hw.totalBytes }) : null;
1974
+ lines.push(`${C.gold}Estimate:${C.reset} ${C.bold}${C.white}${formatGb(estimate.totalBytes)}${C.reset} @ ${runtimeCtx.toLocaleString('en-US')} ctx` +
1975
+ (estCls && estCls.zone !== 'UNKNOWN' ? ` ${vramZoneTag(estCls.zone)}` : ''));
1976
+ }
1977
+
1978
+ let verdict = null;
1979
+ if (livePct !== null) {
1980
+ if (livePct <= 80) verdict = `${C.emerald}Resource load is comfortable (${livePct}% VRAM in use).${C.reset}`;
1981
+ else if (livePct <= 95) verdict = `${C.gold}This model is heavy for the card (${livePct}% VRAM in use) — expect slower prefill on long contexts.${C.reset}`;
1982
+ else verdict = `${C.roseGlow}VRAM is critically full (${livePct}%) — lower the context or switch to a smaller quant in LM Studio.${C.reset}`;
1983
+ } else if (estimate.totalBytes > 0 && hw.available) {
1984
+ const estCls = classifyAllocation({ totalBytes: estimate.totalBytes, availableBytes: hw.totalBytes });
1985
+ if (estCls.zone === 'RED') verdict = `${C.gold}Naive estimate exceeds the card — if it loads anyway, sliding-window caching is saving you; otherwise lower ctx/quant.${C.reset}`;
1986
+ else verdict = `${C.dim}Live usage unavailable — estimate says ${estCls.zone}.${C.reset}`;
1987
+ }
1988
+ if (verdict) lines.push(`${C.gold}Verdict:${C.reset} ${verdict}`);
1989
+ lines.push(`${C.dim}Details: /vram live • /vram compare • /vram ctx${C.reset}`);
1990
+
1991
+ printSolidPanel({ title: 'Resource Verification', lines, headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m' });
1992
+ } catch { /* the startup sequence must never break because of this panel */ }
1993
+ }
1994
+
1995
+ /**
1996
+ * Pre-flight VRAM gate for one-shot runs against local GPU inference.
1997
+ * A RED verdict aborts (exit 4) before the first request is sent unless
1998
+ * --force is supplied — unless the model is already loaded in LM Studio, in
1999
+ * which case live VRAM is authoritative and the gate only warns. Best-effort:
2000
+ * probe failures never block execution.
2001
+ */
2002
+ async function runOneShotPreflightGate(client, opts) {
2003
+ if (!client || client.provider !== 'lmstudio') return null;
2004
+ const runtime = await probeLmStudioRuntime(client.baseUrl).catch(() => null);
2005
+ const alreadyLoaded = Boolean(runtime?.reachable && runtime.loadedModel && matchModelIdentifier(client.model, runtime.loadedModel));
2006
+ let preflight = null;
2007
+ try {
2008
+ preflight = await runVramPreflight({
2009
+ model: client.model,
2010
+ force: Boolean(opts?.force),
2011
+ contextLength: runtime?.loadedContextLength || undefined,
2012
+ allowNetwork: false,
2013
+ });
2014
+ } catch {
2015
+ return null;
2016
+ }
2017
+ if (!preflight || preflight.verdict === 'UNKNOWN' || preflight.verdict === 'GREEN') return preflight;
2018
+
2019
+ if (preflight.verdict === 'YELLOW') {
2020
+ if (opts?.verbose) {
2021
+ console.log(` ${C.gold}[WARN] VRAM pre-flight: ${preflight.message} (${preflight.classification.usagePct}% of ${(preflight.hardware.totalBytes / 1024 ** 3).toFixed(1)} GB)${C.reset}`);
2022
+ for (const suggestion of preflight.remediation || []) {
2023
+ console.log(` ${C.dim}↳ ${suggestion.description}${C.reset}`);
2024
+ }
2025
+ console.log('');
2026
+ }
2027
+ return preflight;
2028
+ }
2029
+
2030
+ if (alreadyLoaded) {
2031
+ return preflight;
2032
+ }
2033
+
2034
+ console.error(`\n ${C.roseGlow}${C.bold}ABORT: VRAM pre-flight failed (RED ZONE).${C.reset}`);
2035
+ console.error(` ${C.white}Estimated footprint ${formatGb(preflight.estimate?.totalBytes)} vs ${formatGb(preflight.hardware?.totalBytes)} available on ${preflight.hardware?.name || 'device'}.${C.reset}`);
2036
+ for (const suggestion of preflight.remediation || []) {
2037
+ console.error(` ${C.amber}↳ ${suggestion.description}${C.reset}`);
2038
+ }
2039
+ if (opts?.force) {
2040
+ console.error(` ${C.gold}--force supplied — continuing despite OOM risk.${C.reset}\n`);
2041
+ return preflight;
2042
+ }
2043
+ console.error(` ${C.dim}Re-run with --force to override (CPU offloading / CUDA OOM risk).${C.reset}\n`);
2044
+ process.exit(4);
2045
+ }
2046
+
1847
2047
  async function executeHeadlessQuery(opts, query) {
1848
2048
  const startTime = Date.now();
1849
2049
 
@@ -1879,6 +2079,8 @@ async function executeHeadlessQuery(opts, query) {
1879
2079
  throw new Error(modelResolution.error || `Cannot reach model server or no model found for ${client.providerName}`);
1880
2080
  }
1881
2081
 
2082
+ await runOneShotPreflightGate(client, opts);
2083
+
1882
2084
  agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1883
2085
  await agent.ready();
1884
2086
 
@@ -1958,7 +2160,7 @@ async function executeHeadlessQuery(opts, query) {
1958
2160
 
1959
2161
  async function runWebChatStandalone(opts) {
1960
2162
  const { startWebChatServer, stopWebChatServer, openBrowser } = await import('../webChatServer.js');
1961
- const port = opts.port || Number(process.env.PORT_WEB_CHAT) || 3300;
2163
+ const port = opts.port || Number(process.env.PORT_WEB_CHAT) || 3002;
1962
2164
 
1963
2165
  const client = new LLMClient({
1964
2166
  provider: opts.provider || config.activeProvider,
@@ -1970,7 +2172,7 @@ async function runWebChatStandalone(opts) {
1970
2172
 
1971
2173
  const res = await startWebChatServer({ port, agent });
1972
2174
  if (!opts.noOpen) {
1973
- try { openBrowser(res.url); } catch { /* browser is best-effort */ }
2175
+ try { openBrowser(res.url); } catch { }
1974
2176
  }
1975
2177
 
1976
2178
  if (!opts.plain && !opts.noColor) printWelcomeHero();
@@ -1988,19 +2190,22 @@ async function runWebChatStandalone(opts) {
1988
2190
  });
1989
2191
 
1990
2192
  const shutdown = async () => {
1991
- try { await stopWebChatServer(); } catch { /* best-effort */ }
1992
- try { await agent.endSession?.(); } catch { /* best-effort */ }
2193
+ try { await stopWebChatServer(); } catch { }
2194
+ try { await agent.endSession?.(); } catch { }
1993
2195
  console.log(`\n ${C.sakura}Web chatbot stopped • 再见!${C.reset}\n`);
1994
2196
  process.exit(0);
1995
2197
  };
1996
2198
  process.once('SIGINT', shutdown);
1997
2199
  process.once('SIGTERM', shutdown);
1998
2200
 
1999
- // Keep the process alive until Ctrl+C.
2000
2201
  await new Promise(() => {});
2001
2202
  }
2002
2203
 
2003
2204
  async function main() {
2205
+ if (process.argv[2] === 'kiku-mcp' || process.argv[2] === 'mcp-server') {
2206
+ await import('../mcp/stdio.js');
2207
+ return;
2208
+ }
2004
2209
  const opts = parseCliArgs();
2005
2210
 
2006
2211
  if (!opts.query && !process.stdin.isTTY) {
@@ -2052,7 +2257,59 @@ async function main() {
2052
2257
  return;
2053
2258
  }
2054
2259
 
2260
+ if (opts.vram) {
2261
+ const vramSub = typeof opts.vram === 'string' && opts.vram !== 'true' ? opts.vram : 'live';
2262
+ await routeCommand(`/vram ${vramSub}`.trim(), {});
2263
+ process.exit(0);
2264
+ }
2265
+
2266
+ if (opts.hub) {
2267
+ const hubFilter = typeof opts.hub === 'string' && opts.hub !== 'true' ? opts.hub : '';
2268
+ await routeCommand(`/hub ${hubFilter}`.trim(), {});
2269
+ process.exit(0);
2270
+ }
2271
+
2272
+ if (opts.eval) {
2273
+ const evalSub = typeof opts.eval === 'string' && opts.eval !== 'true' ? opts.eval : 'ui';
2274
+ await routeCommand(`/eval ${evalSub}`.trim(), {});
2275
+ process.exit(0);
2276
+ }
2277
+
2278
+ if (opts.portal) {
2279
+ const portalSub = typeof opts.portal === 'string' && opts.portal !== 'true' ? opts.portal : 'open';
2280
+ await routeCommand(`/portal ${portalSub}`.trim(), {});
2281
+ process.exit(0);
2282
+ }
2283
+
2055
2284
  if (opts.query) {
2285
+ const trimmedQuery = opts.query.trim();
2286
+ if (trimmedQuery.startsWith('/')) {
2287
+ const client = new LLMClient({
2288
+ provider: opts.provider || config.activeProvider,
2289
+ baseUrl: opts.url,
2290
+ apiKey: opts.apiKey,
2291
+ model: opts.model,
2292
+ });
2293
+ const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
2294
+ const parts = trimmedQuery.split(/\s+/);
2295
+ const sub = parts[1] || '';
2296
+ const extra = parts.slice(2).join(' ');
2297
+ const cmdCtx = {
2298
+ agent,
2299
+ client,
2300
+ thinkingMode: opts.thinking,
2301
+ setThinkingMode: () => {},
2302
+ theme: getTheme(),
2303
+ setTheme: () => {},
2304
+ audioPlayer,
2305
+ sub,
2306
+ extra,
2307
+ parts,
2308
+ };
2309
+ await routeCommand(trimmedQuery, cmdCtx);
2310
+ process.exit(0);
2311
+ }
2312
+
2056
2313
  if (opts.print || opts.output === 'json') {
2057
2314
  await executeHeadlessQuery(opts, opts.query);
2058
2315
  return;
@@ -2074,7 +2331,7 @@ async function main() {
2074
2331
  if (modelResolution.usedFallback) {
2075
2332
  console.log(` ${C.amber}Model ${modelResolution.requestedModel} is unavailable; using ${modelResolution.selectedModel}.${C.reset}\n`);
2076
2333
  }
2077
- console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
2334
+ await runOneShotPreflightGate(client, opts);
2078
2335
  const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
2079
2336
  await executeQueryWithUI(agent, opts.query, agent.session.thinkingMode === 'show', null, 1);
2080
2337
  process.exit(0);
@@ -2200,7 +2457,7 @@ async function main() {
2200
2457
  status: 'offline',
2201
2458
  lines: [
2202
2459
  'LM Studio is not running. Free-form chat typing is locked to prevent errors.',
2203
- 'Open LM Studio, load a model (e.g. google/gemma-4-e4b), then type /model reconnect.',
2460
+ 'Open LM Studio and load a model (e.g. google/gemma-4-e4b) — it will auto-connect once loaded.',
2204
2461
  'Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit',
2205
2462
  ],
2206
2463
  });
@@ -2231,7 +2488,7 @@ async function main() {
2231
2488
  status: 'warning',
2232
2489
  lines: [
2233
2490
  modelResolution.error || 'No chat-capable model loaded',
2234
- 'Load a model in LM Studio, then type /model reconnect.',
2491
+ 'Load a model in LM Studio — the CLI will auto-connect as soon as it is loaded in VRAM.',
2235
2492
  ],
2236
2493
  });
2237
2494
  catalogOnlyMode = true;
@@ -2313,6 +2570,10 @@ async function main() {
2313
2570
  });
2314
2571
  }
2315
2572
 
2573
+ if (!catalogOnlyMode) {
2574
+ await printResourceVerificationPanel(client);
2575
+ }
2576
+
2316
2577
  if (agent.restoredMessageCount > 0) {
2317
2578
  const s = agent.session;
2318
2579
  console.log(`\n ${C.sakura}${C.bold}[SESSION RESTORED]${C.reset} ${C.dim}${s.id} — ${s.messages.length} messages${C.reset}\n`);
@@ -2348,13 +2609,74 @@ async function main() {
2348
2609
  }
2349
2610
  });
2350
2611
 
2612
+ let autoReconnectTimer = null;
2613
+ const stopAutoReconnectWatcher = () => {
2614
+ if (autoReconnectTimer) {
2615
+ clearInterval(autoReconnectTimer);
2616
+ autoReconnectTimer = null;
2617
+ }
2618
+ };
2619
+
2620
+ const startAutoReconnectWatcher = () => {
2621
+ if (autoReconnectTimer) return;
2622
+ autoReconnectTimer = setInterval(async () => {
2623
+ if (!catalogOnlyMode && tuiPrompt.isConnected) {
2624
+ stopAutoReconnectWatcher();
2625
+ return;
2626
+ }
2627
+ if (isProcessing) return;
2628
+
2629
+ try {
2630
+ const resolution = modelManager
2631
+ ? await modelManager.resolve()
2632
+ : await client.checkConnection();
2633
+
2634
+ if (resolution?.connected && (resolution.selectedModel || resolution.activeLoadedModel)) {
2635
+ const detectedModel = resolution.selectedModel || resolution.activeLoadedModel;
2636
+ catalogOnlyMode = false;
2637
+ tuiPrompt.isConnected = true;
2638
+ tuiPrompt.modelName = detectedModel;
2639
+ tuiPrompt.render();
2640
+ stopAutoReconnectWatcher();
2641
+
2642
+ printRichCard({
2643
+ title: 'Model Auto-Connected',
2644
+ status: 'connected',
2645
+ lines: [
2646
+ `• Provider: ${client.providerName}${client.provider === 'lmstudio' ? ' (Local)' : ''} • Model: ${detectedModel}`,
2647
+ '• Auto-detected model in VRAM: chat is ready without requiring /model reconnect.',
2648
+ ],
2649
+ });
2650
+ }
2651
+ } catch {
2652
+
2653
+ }
2654
+ }, 2500);
2655
+
2656
+ if (autoReconnectTimer?.unref) {
2657
+ autoReconnectTimer.unref();
2658
+ }
2659
+ };
2660
+
2351
2661
  const cmdCtx = {
2352
2662
  tuiPrompt,
2353
2663
  agent,
2354
2664
  client,
2355
2665
  baseUrl,
2356
2666
  modelManager,
2357
- catalogOnlyMode,
2667
+ get catalogOnlyMode() { return catalogOnlyMode; },
2668
+ set catalogOnlyMode(value) {
2669
+ catalogOnlyMode = Boolean(value);
2670
+ if (tuiPrompt) {
2671
+ tuiPrompt.isConnected = !catalogOnlyMode;
2672
+ tuiPrompt.render();
2673
+ }
2674
+ if (catalogOnlyMode) {
2675
+ startAutoReconnectWatcher();
2676
+ } else {
2677
+ stopAutoReconnectWatcher();
2678
+ }
2679
+ },
2358
2680
  setCatalogOnlyMode: value => {
2359
2681
  catalogOnlyMode = Boolean(value);
2360
2682
  if (tuiPrompt) {
@@ -2407,6 +2729,10 @@ async function main() {
2407
2729
  },
2408
2730
  };
2409
2731
 
2732
+ if (catalogOnlyMode) {
2733
+ startAutoReconnectWatcher();
2734
+ }
2735
+
2410
2736
  const triggerQueueProcessing = () => {
2411
2737
  if (isProcessing || queryQueue.length === 0) return;
2412
2738
  isProcessing = true;
@@ -2420,6 +2746,18 @@ async function main() {
2420
2746
  activeAbortController = new AbortController();
2421
2747
 
2422
2748
  try {
2749
+ if (catalogOnlyMode && modelManager) {
2750
+ try {
2751
+ const autoProbe = await modelManager.resolve();
2752
+ if (autoProbe?.connected && (autoProbe.selectedModel || autoProbe.activeLoadedModel)) {
2753
+ cmdCtx.setCatalogOnlyMode(false);
2754
+ if (tuiPrompt) {
2755
+ tuiPrompt.modelName = autoProbe.selectedModel || autoProbe.activeLoadedModel;
2756
+ }
2757
+ }
2758
+ } catch {}
2759
+ }
2760
+
2423
2761
  if (catalogOnlyMode) {
2424
2762
  if (nextItem.prompt.startsWith('@')) {
2425
2763
  const targetTool = nextItem.prompt.slice(1).split(/\s+/)[0];
@@ -2480,6 +2818,48 @@ async function main() {
2480
2818
  })();
2481
2819
  };
2482
2820
 
2821
+ const IMMEDIATE_COMMAND_NAMES = new Set([
2822
+ '/help', '/?', '/menu', '/commands',
2823
+ '/cancel', '/abort', '/stop',
2824
+ '/version', '/ver', '/v',
2825
+ '/status', '/doctor', '/about', '/info',
2826
+ '/jobs', '/job',
2827
+ '/theme', '/themes',
2828
+ '/ping', '/health',
2829
+ '/system', '/sys', '/os',
2830
+ '/gpu', '/gpus',
2831
+ '/vram', '/resource',
2832
+ '/details', '/detail', '/d',
2833
+ '/copy', '/cp', '/clip', '/clipboard',
2834
+ '/prompt', '/persona', '/sysprompt',
2835
+ '/config', '/settings', '/conf',
2836
+ '/sources', '/src', '/srcs',
2837
+ '/queue',
2838
+ '/ads', '/sponsor', '/ad',
2839
+ ]);
2840
+
2841
+ function isImmediateCommand(rawInput) {
2842
+ const trimmed = (rawInput || '').trim();
2843
+ if (!trimmed.startsWith('/')) return false;
2844
+ const parts = trimmed.split(/\s+/);
2845
+ const base = parts[0].toLowerCase();
2846
+ const sub = (parts[1] || '').toLowerCase();
2847
+ if (IMMEDIATE_COMMAND_NAMES.has(base)) return true;
2848
+ if (base === '/research' && ['status', 'sources', 'cancel', 'last', 'workers'].includes(sub)) return true;
2849
+ if (base === '/subagent' && ['status', 'last', 'tools'].includes(sub)) return true;
2850
+ if (base === '/debate' && ['status', 'last'].includes(sub)) return true;
2851
+ if (base === '/favorites' && ['list', 'show', 'export'].includes(sub)) return true;
2852
+ if (base === '/history' && (!sub || ['list', 'summary', 'search'].includes(sub))) return true;
2853
+ if (base === '/session' && (!sub || ['info', 'list', 'summary'].includes(sub))) return true;
2854
+ if (base === '/lang' && !sub) return true;
2855
+ if (base === '/thinking' && !sub) return true;
2856
+ if (base === '/model' && (!sub || sub === 'status' || sub === 'list' || sub === 'recommend')) return true;
2857
+ if (base === '/provider' && (!sub || sub === 'list' || sub === 'status' || sub === 'info')) return true;
2858
+ if (base === '/mcp' && (!sub || sub === 'status' || sub === 'list' || sub === 'tools')) return true;
2859
+ if (base === '/env' && (!sub || sub === 'get' || sub === 'path')) return true;
2860
+ return false;
2861
+ }
2862
+
2483
2863
  while (true) {
2484
2864
  let rawInput;
2485
2865
 
@@ -2502,10 +2882,38 @@ async function main() {
2502
2882
  const lower = trimmed.toLowerCase();
2503
2883
  if (lower === '/cancel' || lower === '/abort' || (lower === '/stop' && isProcessing)) {
2504
2884
  cancelActiveQuery();
2885
+ printRichCard({
2886
+ title: 'Cancelled',
2887
+ status: 'info',
2888
+ lines: ['• Active operation cancelled by user.'],
2889
+ });
2505
2890
  continue;
2506
2891
  }
2507
2892
 
2508
2893
  if (isProcessing) {
2894
+ if (isImmediateCommand(trimmed)) {
2895
+ const parts = trimmed.split(/\s+/);
2896
+ const sub = parts[1] || '';
2897
+ const extra = parts.slice(2).join(' ');
2898
+ try {
2899
+ await routeCommand(trimmed, {
2900
+ ...cmdCtx,
2901
+ sub,
2902
+ extra,
2903
+ parts,
2904
+ });
2905
+ } catch (cmdErr) {
2906
+ if (!isAbortError(cmdErr)) {
2907
+ printRichCard({
2908
+ title: 'Command Error',
2909
+ status: 'error',
2910
+ lines: [cmdErr.message || String(cmdErr)],
2911
+ });
2912
+ }
2913
+ }
2914
+ continue;
2915
+ }
2916
+
2509
2917
  deferredCommands.push(trimmed);
2510
2918
  printRichCard({
2511
2919
  title: 'Command Queued',
@@ -2519,45 +2927,71 @@ async function main() {
2519
2927
  const sub = parts[1] || '';
2520
2928
  const extra = parts.slice(2).join(' ');
2521
2929
 
2522
- let result;
2930
+ console.log(`\n ${C.dim}❯ ${trimmed}${C.reset}`);
2931
+ cmdCtx.setIsProcessing(true, `Running ${parts[0]}`);
2523
2932
  activeAbortController = new AbortController();
2524
- try {
2525
- result = await routeCommand(trimmed, {
2526
- ...cmdCtx,
2527
- signal: activeAbortController.signal,
2528
- sub,
2529
- extra,
2530
- parts,
2531
- });
2532
- } catch (cmdErr) {
2533
- if (!isAbortError(cmdErr)) {
2534
- printRichCard({
2535
- title: 'Command Error',
2536
- status: 'error',
2537
- lines: [cmdErr.message || String(cmdErr)],
2933
+
2934
+ (async () => {
2935
+ try {
2936
+ const result = await routeCommand(trimmed, {
2937
+ ...cmdCtx,
2938
+ signal: activeAbortController.signal,
2939
+ sub,
2940
+ extra,
2941
+ parts,
2538
2942
  });
2943
+ if (result === 'EXIT') {
2944
+ cancelActiveQuery();
2945
+ activeAbortController = null;
2946
+ cmdCtx.setIsProcessing(false);
2947
+ tuiPrompt.abortActiveRead(null);
2948
+ return;
2949
+ }
2950
+ if (result === 'RETRY' && cmdCtx.lastQuery) {
2951
+ const retryNumber = nextQueryNumber++;
2952
+ printSolidPanel({
2953
+ title: `${C.roseGlow}${C.bold}Question ${retryNumber} • Retry${C.reset}`,
2954
+ lines: wrapText(cmdCtx.lastQuery, getFullWidth() - 8)
2955
+ .map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
2956
+ bg: C.bgPanel
2957
+ });
2958
+ queryQueue.push({ id: 'q_' + Date.now(), number: retryNumber, prompt: cmdCtx.lastQuery, addedAt: Date.now() });
2959
+ triggerQueueProcessing();
2960
+ }
2961
+ } catch (cmdErr) {
2962
+ if (!isAbortError(cmdErr)) {
2963
+ printRichCard({
2964
+ title: 'Command Error',
2965
+ status: 'error',
2966
+ lines: [cmdErr.message || String(cmdErr)],
2967
+ });
2968
+ }
2969
+ } finally {
2970
+ activeAbortController = null;
2971
+ cmdCtx.setIsProcessing(false);
2972
+ if (deferredCommands.length > 0) {
2973
+ const nextCmd = deferredCommands.shift();
2974
+ printRichCard({
2975
+ title: 'Deferred Command Executed',
2976
+ status: 'info',
2977
+ lines: [nextCmd],
2978
+ });
2979
+ setTimeout(async () => {
2980
+ try {
2981
+ const defParts = nextCmd.split(/\s+/);
2982
+ await routeCommand(nextCmd, {
2983
+ ...cmdCtx,
2984
+ parts: defParts,
2985
+ sub: defParts[1] || '',
2986
+ extra: defParts.slice(2).join(' '),
2987
+ });
2988
+ } catch (err) {
2989
+ printRichCard({ title: 'Deferred Command Error', status: 'error', lines: [err.message || String(err)] });
2990
+ }
2991
+ }, 10);
2992
+ }
2539
2993
  }
2540
- continue;
2541
- } finally {
2542
- activeAbortController = null;
2543
- cmdCtx.setIsProcessing(false);
2544
- }
2545
-
2546
- if (result === 'EXIT') {
2547
- cancelActiveQuery();
2548
- break;
2549
- }
2550
- if (result === 'RETRY' && cmdCtx.lastQuery) {
2551
- const retryNumber = nextQueryNumber++;
2552
- printSolidPanel({
2553
- title: `${C.roseGlow}${C.bold}Question ${retryNumber} • Retry${C.reset}`,
2554
- lines: wrapText(cmdCtx.lastQuery, getFullWidth() - 8)
2555
- .map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
2556
- bg: C.bgPanel
2557
- });
2558
- queryQueue.push({ id: 'q_' + Date.now(), number: retryNumber, prompt: cmdCtx.lastQuery, addedAt: Date.now() });
2559
- triggerQueueProcessing();
2560
- }
2994
+ })();
2561
2995
  continue;
2562
2996
  }
2563
2997
 
@@ -2612,22 +3046,46 @@ async function main() {
2612
3046
  }
2613
3047
 
2614
3048
 
2615
- printSolidPanel({
2616
- title: 'AI Chat Locked • LM Studio Offline',
2617
- lines: [
2618
- `${C.bold}${C.amber}LM Studio is not running or connected.${C.reset}`,
2619
- `${C.white}Chat query typing is disabled until the AI server is active.${C.reset}`,
2620
- '',
2621
- `${C.bold}${C.white}How to unlock & connect:${C.reset}`,
2622
- ` 1. Open ${C.cyberCyan}LM Studio${C.reset} on your computer`,
2623
- ` 2. Load model ${C.gold}${opts.model || client.model || 'google/gemma-4-e4b'}${C.reset} and ensure the local server is running (port 1234)`,
2624
- ` 3. Type ${C.cyberCyan}/model reconnect${C.reset} in this terminal (or type ${C.cyberCyan}reconnect${C.reset})`,
2625
- '',
2626
- `${C.dim}Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit${C.reset}`,
2627
- ],
2628
- bg: C.bgPanel,
2629
- });
2630
- continue;
3049
+
3050
+ try {
3051
+ const autoProbe = modelManager
3052
+ ? await modelManager.resolve()
3053
+ : await client.checkConnection();
3054
+ if (autoProbe?.connected && (autoProbe.selectedModel || autoProbe.activeLoadedModel)) {
3055
+ catalogOnlyMode = false;
3056
+ tuiPrompt.isConnected = true;
3057
+ tuiPrompt.modelName = autoProbe.selectedModel || autoProbe.activeLoadedModel;
3058
+ tuiPrompt.render();
3059
+ stopAutoReconnectWatcher();
3060
+ printRichCard({
3061
+ title: 'Model Auto-Connected',
3062
+ status: 'connected',
3063
+ lines: [
3064
+ `• Provider: ${client.providerName}${client.provider === 'lmstudio' ? ' (Local)' : ''} • Model: ${tuiPrompt.modelName}`,
3065
+ '• Loaded model detected in VRAM: chat typing unlocked.',
3066
+ ],
3067
+ });
3068
+ }
3069
+ } catch {}
3070
+
3071
+ if (catalogOnlyMode || !tuiPrompt.isConnected) {
3072
+ printSolidPanel({
3073
+ title: 'AI Chat Locked • LM Studio Offline',
3074
+ lines: [
3075
+ `${C.bold}${C.amber}LM Studio is not running or connected.${C.reset}`,
3076
+ `${C.white}Chat query typing is disabled until the AI server is active.${C.reset}`,
3077
+ '',
3078
+ `${C.bold}${C.white}How to unlock & connect:${C.reset}`,
3079
+ ` 1. Open ${C.cyberCyan}LM Studio${C.reset} on your computer`,
3080
+ ` 2. Load model ${C.gold}${opts.model || client.model || 'google/gemma-4-e4b'}${C.reset} and ensure the local server is running (port 1234)`,
3081
+ ` 3. The agent will auto-connect automatically (or type ${C.cyberCyan}/model reconnect${C.reset})`,
3082
+ '',
3083
+ `${C.dim}Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit${C.reset}`,
3084
+ ],
3085
+ bg: C.bgPanel,
3086
+ });
3087
+ continue;
3088
+ }
2631
3089
  }
2632
3090
 
2633
3091
  const { sanitized, warnings } = sanitizeQuery(trimmed);
@@ -2653,6 +3111,7 @@ async function main() {
2653
3111
  triggerQueueProcessing();
2654
3112
  }
2655
3113
 
3114
+ stopAutoReconnectWatcher();
2656
3115
  if (globalTuiPrompt) {
2657
3116
  globalTuiPrompt.dispose();
2658
3117
  globalTuiPrompt = null;