kiku-agent 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +459 -407
  2. package/package.json +27 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +629 -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 +528 -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/lanDiscovery.js +369 -0
  23. package/server/agent/llmClient.js +111 -39
  24. package/server/agent/lmStudioWakeup.js +144 -0
  25. package/server/agent/mcpClient.js +2 -2
  26. package/server/agent/memory.js +635 -494
  27. package/server/agent/modelConfig.js +487 -0
  28. package/server/agent/newsIntelligence.js +219 -0
  29. package/server/agent/newsWatch.js +85 -0
  30. package/server/agent/prompts.js +146 -0
  31. package/server/agent/publicTools.js +7 -0
  32. package/server/agent/queryDetails.js +62 -1
  33. package/server/agent/research.js +314 -35
  34. package/server/agent/routingInterceptor.js +77 -0
  35. package/server/agent/rumoredProjects.js +15 -0
  36. package/server/agent/sanitizer.js +117 -17
  37. package/server/agent/subagent.js +52 -9
  38. package/server/agent/suggestionTui.js +33 -13
  39. package/server/agent/suggestions.js +29 -13
  40. package/server/agent/telemetry.js +0 -11
  41. package/server/agent/terminalImage.js +0 -1
  42. package/server/agent/textToolProtocol.js +190 -0
  43. package/server/agent/tokenTracker.js +220 -0
  44. package/server/agent/toolRouter.js +362 -216
  45. package/server/agent/tools.js +1155 -194
  46. package/server/agent/ui.js +144 -41
  47. package/server/agent/vramService.js +572 -0
  48. package/server/agent/webSearch.js +2279 -273
  49. package/server/agent/youtubeAuth.js +82 -73
  50. package/server/agent/youtubeTranscript.js +209 -0
  51. package/server/apiRouter.js +383 -179
  52. package/server/mcp/data/brands.js +5 -5
  53. package/server/mcp/data/discography.js +56 -45
  54. package/server/mcp/data/gallery.js +9 -0
  55. package/server/mcp/data/index.js +1 -0
  56. package/server/mcp/data/industryIndex.js +251 -0
  57. package/server/mcp/data/profile.js +5 -5
  58. package/server/mcp/data/timeline.js +2 -2
  59. package/server/mcp/data/works.js +2 -2
  60. package/server/mcp/handler.js +0 -2
  61. package/server/mcp/prompts.js +42 -2
  62. package/server/mcp/resources.js +5 -1
  63. package/server/mcp/security.js +25 -14
  64. package/server/mcp/server.js +92 -3
  65. package/server/mcp/tools.js +20 -11
  66. package/server/web-chat/core/connection.js +218 -194
  67. package/server/web-chat/core/conversationContext.js +1 -1
  68. package/server/web-chat/core/directCatalogEngine.js +190 -184
  69. package/server/web-chat/core/followUpEngine.js +88 -11
  70. package/server/web-chat/core/security.js +7 -4
  71. package/server/web-chat/router.js +54 -17
  72. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  73. package/server/web-chat/routes/chatRoute.js +488 -397
  74. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  75. package/server/web-chat/routes/exportRoute.js +2 -1
  76. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  77. package/server/web-chat/routes/newsRoute.js +36 -6
  78. package/server/web-chat/routes/providerRoute.js +87 -0
  79. package/server/web-chat/routes/staticRoute.js +61 -13
  80. package/server/web-chat/routes/statusRoute.js +23 -2
  81. package/server/web-chat/routes/vramRoute.js +22 -0
  82. package/server/web-chat/server.js +135 -106
  83. package/server/web-chat/ui/htmlBuilder.js +8 -8
  84. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  85. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  86. package/server/web-chat/ui/scripts/client.js +10526 -8397
  87. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  88. package/server/web-chat/ui/styles/main.css +7471 -4246
  89. package/server/web-chat/ui/templates/body.html +305 -179
  90. package/src/data/brandsData.js +29 -6
  91. package/src/data/discographyData.js +918 -900
  92. package/src/data/galleryData.js +66 -0
  93. package/src/data/industryIndexData.js +292 -0
  94. package/src/data/musicVideoData.js +48 -5
  95. package/src/data/profileData.js +15 -1
  96. package/src/data/timelineData.js +4 -3
  97. package/src/data/worksData.js +536 -535
  98. package/src/lib/vram/estimator.js +464 -0
  99. package/src/lib/vram/modelRegistry.js +387 -0
  100. 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,15 +2190,14 @@ 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
 
@@ -2052,7 +2253,59 @@ async function main() {
2052
2253
  return;
2053
2254
  }
2054
2255
 
2256
+ if (opts.vram) {
2257
+ const vramSub = typeof opts.vram === 'string' && opts.vram !== 'true' ? opts.vram : 'live';
2258
+ await routeCommand(`/vram ${vramSub}`.trim(), {});
2259
+ process.exit(0);
2260
+ }
2261
+
2262
+ if (opts.hub) {
2263
+ const hubFilter = typeof opts.hub === 'string' && opts.hub !== 'true' ? opts.hub : '';
2264
+ await routeCommand(`/hub ${hubFilter}`.trim(), {});
2265
+ process.exit(0);
2266
+ }
2267
+
2268
+ if (opts.eval) {
2269
+ const evalSub = typeof opts.eval === 'string' && opts.eval !== 'true' ? opts.eval : 'ui';
2270
+ await routeCommand(`/eval ${evalSub}`.trim(), {});
2271
+ process.exit(0);
2272
+ }
2273
+
2274
+ if (opts.portal) {
2275
+ const portalSub = typeof opts.portal === 'string' && opts.portal !== 'true' ? opts.portal : 'open';
2276
+ await routeCommand(`/portal ${portalSub}`.trim(), {});
2277
+ process.exit(0);
2278
+ }
2279
+
2055
2280
  if (opts.query) {
2281
+ const trimmedQuery = opts.query.trim();
2282
+ if (trimmedQuery.startsWith('/')) {
2283
+ const client = new LLMClient({
2284
+ provider: opts.provider || config.activeProvider,
2285
+ baseUrl: opts.url,
2286
+ apiKey: opts.apiKey,
2287
+ model: opts.model,
2288
+ });
2289
+ const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
2290
+ const parts = trimmedQuery.split(/\s+/);
2291
+ const sub = parts[1] || '';
2292
+ const extra = parts.slice(2).join(' ');
2293
+ const cmdCtx = {
2294
+ agent,
2295
+ client,
2296
+ thinkingMode: opts.thinking,
2297
+ setThinkingMode: () => {},
2298
+ theme: getTheme(),
2299
+ setTheme: () => {},
2300
+ audioPlayer,
2301
+ sub,
2302
+ extra,
2303
+ parts,
2304
+ };
2305
+ await routeCommand(trimmedQuery, cmdCtx);
2306
+ process.exit(0);
2307
+ }
2308
+
2056
2309
  if (opts.print || opts.output === 'json') {
2057
2310
  await executeHeadlessQuery(opts, opts.query);
2058
2311
  return;
@@ -2074,7 +2327,7 @@ async function main() {
2074
2327
  if (modelResolution.usedFallback) {
2075
2328
  console.log(` ${C.amber}Model ${modelResolution.requestedModel} is unavailable; using ${modelResolution.selectedModel}.${C.reset}\n`);
2076
2329
  }
2077
- console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
2330
+ await runOneShotPreflightGate(client, opts);
2078
2331
  const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
2079
2332
  await executeQueryWithUI(agent, opts.query, agent.session.thinkingMode === 'show', null, 1);
2080
2333
  process.exit(0);
@@ -2200,7 +2453,7 @@ async function main() {
2200
2453
  status: 'offline',
2201
2454
  lines: [
2202
2455
  '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.',
2456
+ 'Open LM Studio and load a model (e.g. google/gemma-4-e4b) — it will auto-connect once loaded.',
2204
2457
  'Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit',
2205
2458
  ],
2206
2459
  });
@@ -2231,7 +2484,7 @@ async function main() {
2231
2484
  status: 'warning',
2232
2485
  lines: [
2233
2486
  modelResolution.error || 'No chat-capable model loaded',
2234
- 'Load a model in LM Studio, then type /model reconnect.',
2487
+ 'Load a model in LM Studio — the CLI will auto-connect as soon as it is loaded in VRAM.',
2235
2488
  ],
2236
2489
  });
2237
2490
  catalogOnlyMode = true;
@@ -2313,6 +2566,10 @@ async function main() {
2313
2566
  });
2314
2567
  }
2315
2568
 
2569
+ if (!catalogOnlyMode) {
2570
+ await printResourceVerificationPanel(client);
2571
+ }
2572
+
2316
2573
  if (agent.restoredMessageCount > 0) {
2317
2574
  const s = agent.session;
2318
2575
  console.log(`\n ${C.sakura}${C.bold}[SESSION RESTORED]${C.reset} ${C.dim}${s.id} — ${s.messages.length} messages${C.reset}\n`);
@@ -2348,13 +2605,74 @@ async function main() {
2348
2605
  }
2349
2606
  });
2350
2607
 
2608
+ let autoReconnectTimer = null;
2609
+ const stopAutoReconnectWatcher = () => {
2610
+ if (autoReconnectTimer) {
2611
+ clearInterval(autoReconnectTimer);
2612
+ autoReconnectTimer = null;
2613
+ }
2614
+ };
2615
+
2616
+ const startAutoReconnectWatcher = () => {
2617
+ if (autoReconnectTimer) return;
2618
+ autoReconnectTimer = setInterval(async () => {
2619
+ if (!catalogOnlyMode && tuiPrompt.isConnected) {
2620
+ stopAutoReconnectWatcher();
2621
+ return;
2622
+ }
2623
+ if (isProcessing) return;
2624
+
2625
+ try {
2626
+ const resolution = modelManager
2627
+ ? await modelManager.resolve()
2628
+ : await client.checkConnection();
2629
+
2630
+ if (resolution?.connected && (resolution.selectedModel || resolution.activeLoadedModel)) {
2631
+ const detectedModel = resolution.selectedModel || resolution.activeLoadedModel;
2632
+ catalogOnlyMode = false;
2633
+ tuiPrompt.isConnected = true;
2634
+ tuiPrompt.modelName = detectedModel;
2635
+ tuiPrompt.render();
2636
+ stopAutoReconnectWatcher();
2637
+
2638
+ printRichCard({
2639
+ title: 'Model Auto-Connected',
2640
+ status: 'connected',
2641
+ lines: [
2642
+ `• Provider: ${client.providerName}${client.provider === 'lmstudio' ? ' (Local)' : ''} • Model: ${detectedModel}`,
2643
+ '• Auto-detected model in VRAM: chat is ready without requiring /model reconnect.',
2644
+ ],
2645
+ });
2646
+ }
2647
+ } catch {
2648
+
2649
+ }
2650
+ }, 2500);
2651
+
2652
+ if (autoReconnectTimer?.unref) {
2653
+ autoReconnectTimer.unref();
2654
+ }
2655
+ };
2656
+
2351
2657
  const cmdCtx = {
2352
2658
  tuiPrompt,
2353
2659
  agent,
2354
2660
  client,
2355
2661
  baseUrl,
2356
2662
  modelManager,
2357
- catalogOnlyMode,
2663
+ get catalogOnlyMode() { return catalogOnlyMode; },
2664
+ set catalogOnlyMode(value) {
2665
+ catalogOnlyMode = Boolean(value);
2666
+ if (tuiPrompt) {
2667
+ tuiPrompt.isConnected = !catalogOnlyMode;
2668
+ tuiPrompt.render();
2669
+ }
2670
+ if (catalogOnlyMode) {
2671
+ startAutoReconnectWatcher();
2672
+ } else {
2673
+ stopAutoReconnectWatcher();
2674
+ }
2675
+ },
2358
2676
  setCatalogOnlyMode: value => {
2359
2677
  catalogOnlyMode = Boolean(value);
2360
2678
  if (tuiPrompt) {
@@ -2407,6 +2725,10 @@ async function main() {
2407
2725
  },
2408
2726
  };
2409
2727
 
2728
+ if (catalogOnlyMode) {
2729
+ startAutoReconnectWatcher();
2730
+ }
2731
+
2410
2732
  const triggerQueueProcessing = () => {
2411
2733
  if (isProcessing || queryQueue.length === 0) return;
2412
2734
  isProcessing = true;
@@ -2420,6 +2742,18 @@ async function main() {
2420
2742
  activeAbortController = new AbortController();
2421
2743
 
2422
2744
  try {
2745
+ if (catalogOnlyMode && modelManager) {
2746
+ try {
2747
+ const autoProbe = await modelManager.resolve();
2748
+ if (autoProbe?.connected && (autoProbe.selectedModel || autoProbe.activeLoadedModel)) {
2749
+ cmdCtx.setCatalogOnlyMode(false);
2750
+ if (tuiPrompt) {
2751
+ tuiPrompt.modelName = autoProbe.selectedModel || autoProbe.activeLoadedModel;
2752
+ }
2753
+ }
2754
+ } catch {}
2755
+ }
2756
+
2423
2757
  if (catalogOnlyMode) {
2424
2758
  if (nextItem.prompt.startsWith('@')) {
2425
2759
  const targetTool = nextItem.prompt.slice(1).split(/\s+/)[0];
@@ -2480,6 +2814,48 @@ async function main() {
2480
2814
  })();
2481
2815
  };
2482
2816
 
2817
+ const IMMEDIATE_COMMAND_NAMES = new Set([
2818
+ '/help', '/?', '/menu', '/commands',
2819
+ '/cancel', '/abort', '/stop',
2820
+ '/version', '/ver', '/v',
2821
+ '/status', '/doctor', '/about', '/info',
2822
+ '/jobs', '/job',
2823
+ '/theme', '/themes',
2824
+ '/ping', '/health',
2825
+ '/system', '/sys', '/os',
2826
+ '/gpu', '/gpus',
2827
+ '/vram', '/resource',
2828
+ '/details', '/detail', '/d',
2829
+ '/copy', '/cp', '/clip', '/clipboard',
2830
+ '/prompt', '/persona', '/sysprompt',
2831
+ '/config', '/settings', '/conf',
2832
+ '/sources', '/src', '/srcs',
2833
+ '/queue',
2834
+ '/ads', '/sponsor', '/ad',
2835
+ ]);
2836
+
2837
+ function isImmediateCommand(rawInput) {
2838
+ const trimmed = (rawInput || '').trim();
2839
+ if (!trimmed.startsWith('/')) return false;
2840
+ const parts = trimmed.split(/\s+/);
2841
+ const base = parts[0].toLowerCase();
2842
+ const sub = (parts[1] || '').toLowerCase();
2843
+ if (IMMEDIATE_COMMAND_NAMES.has(base)) return true;
2844
+ if (base === '/research' && ['status', 'sources', 'cancel', 'last', 'workers'].includes(sub)) return true;
2845
+ if (base === '/subagent' && ['status', 'last', 'tools'].includes(sub)) return true;
2846
+ if (base === '/debate' && ['status', 'last'].includes(sub)) return true;
2847
+ if (base === '/favorites' && ['list', 'show', 'export'].includes(sub)) return true;
2848
+ if (base === '/history' && (!sub || ['list', 'summary', 'search'].includes(sub))) return true;
2849
+ if (base === '/session' && (!sub || ['info', 'list', 'summary'].includes(sub))) return true;
2850
+ if (base === '/lang' && !sub) return true;
2851
+ if (base === '/thinking' && !sub) return true;
2852
+ if (base === '/model' && (!sub || sub === 'status' || sub === 'list' || sub === 'recommend')) return true;
2853
+ if (base === '/provider' && (!sub || sub === 'list' || sub === 'status' || sub === 'info')) return true;
2854
+ if (base === '/mcp' && (!sub || sub === 'status' || sub === 'list' || sub === 'tools')) return true;
2855
+ if (base === '/env' && (!sub || sub === 'get' || sub === 'path')) return true;
2856
+ return false;
2857
+ }
2858
+
2483
2859
  while (true) {
2484
2860
  let rawInput;
2485
2861
 
@@ -2502,10 +2878,38 @@ async function main() {
2502
2878
  const lower = trimmed.toLowerCase();
2503
2879
  if (lower === '/cancel' || lower === '/abort' || (lower === '/stop' && isProcessing)) {
2504
2880
  cancelActiveQuery();
2881
+ printRichCard({
2882
+ title: 'Cancelled',
2883
+ status: 'info',
2884
+ lines: ['• Active operation cancelled by user.'],
2885
+ });
2505
2886
  continue;
2506
2887
  }
2507
2888
 
2508
2889
  if (isProcessing) {
2890
+ if (isImmediateCommand(trimmed)) {
2891
+ const parts = trimmed.split(/\s+/);
2892
+ const sub = parts[1] || '';
2893
+ const extra = parts.slice(2).join(' ');
2894
+ try {
2895
+ await routeCommand(trimmed, {
2896
+ ...cmdCtx,
2897
+ sub,
2898
+ extra,
2899
+ parts,
2900
+ });
2901
+ } catch (cmdErr) {
2902
+ if (!isAbortError(cmdErr)) {
2903
+ printRichCard({
2904
+ title: 'Command Error',
2905
+ status: 'error',
2906
+ lines: [cmdErr.message || String(cmdErr)],
2907
+ });
2908
+ }
2909
+ }
2910
+ continue;
2911
+ }
2912
+
2509
2913
  deferredCommands.push(trimmed);
2510
2914
  printRichCard({
2511
2915
  title: 'Command Queued',
@@ -2519,45 +2923,71 @@ async function main() {
2519
2923
  const sub = parts[1] || '';
2520
2924
  const extra = parts.slice(2).join(' ');
2521
2925
 
2522
- let result;
2926
+ console.log(`\n ${C.dim}❯ ${trimmed}${C.reset}`);
2927
+ cmdCtx.setIsProcessing(true, `Running ${parts[0]}`);
2523
2928
  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)],
2929
+
2930
+ (async () => {
2931
+ try {
2932
+ const result = await routeCommand(trimmed, {
2933
+ ...cmdCtx,
2934
+ signal: activeAbortController.signal,
2935
+ sub,
2936
+ extra,
2937
+ parts,
2538
2938
  });
2939
+ if (result === 'EXIT') {
2940
+ cancelActiveQuery();
2941
+ activeAbortController = null;
2942
+ cmdCtx.setIsProcessing(false);
2943
+ tuiPrompt.abortActiveRead(null);
2944
+ return;
2945
+ }
2946
+ if (result === 'RETRY' && cmdCtx.lastQuery) {
2947
+ const retryNumber = nextQueryNumber++;
2948
+ printSolidPanel({
2949
+ title: `${C.roseGlow}${C.bold}Question ${retryNumber} • Retry${C.reset}`,
2950
+ lines: wrapText(cmdCtx.lastQuery, getFullWidth() - 8)
2951
+ .map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
2952
+ bg: C.bgPanel
2953
+ });
2954
+ queryQueue.push({ id: 'q_' + Date.now(), number: retryNumber, prompt: cmdCtx.lastQuery, addedAt: Date.now() });
2955
+ triggerQueueProcessing();
2956
+ }
2957
+ } catch (cmdErr) {
2958
+ if (!isAbortError(cmdErr)) {
2959
+ printRichCard({
2960
+ title: 'Command Error',
2961
+ status: 'error',
2962
+ lines: [cmdErr.message || String(cmdErr)],
2963
+ });
2964
+ }
2965
+ } finally {
2966
+ activeAbortController = null;
2967
+ cmdCtx.setIsProcessing(false);
2968
+ if (deferredCommands.length > 0) {
2969
+ const nextCmd = deferredCommands.shift();
2970
+ printRichCard({
2971
+ title: 'Deferred Command Executed',
2972
+ status: 'info',
2973
+ lines: [nextCmd],
2974
+ });
2975
+ setTimeout(async () => {
2976
+ try {
2977
+ const defParts = nextCmd.split(/\s+/);
2978
+ await routeCommand(nextCmd, {
2979
+ ...cmdCtx,
2980
+ parts: defParts,
2981
+ sub: defParts[1] || '',
2982
+ extra: defParts.slice(2).join(' '),
2983
+ });
2984
+ } catch (err) {
2985
+ printRichCard({ title: 'Deferred Command Error', status: 'error', lines: [err.message || String(err)] });
2986
+ }
2987
+ }, 10);
2988
+ }
2539
2989
  }
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
- }
2990
+ })();
2561
2991
  continue;
2562
2992
  }
2563
2993
 
@@ -2612,22 +3042,46 @@ async function main() {
2612
3042
  }
2613
3043
 
2614
3044
 
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;
3045
+
3046
+ try {
3047
+ const autoProbe = modelManager
3048
+ ? await modelManager.resolve()
3049
+ : await client.checkConnection();
3050
+ if (autoProbe?.connected && (autoProbe.selectedModel || autoProbe.activeLoadedModel)) {
3051
+ catalogOnlyMode = false;
3052
+ tuiPrompt.isConnected = true;
3053
+ tuiPrompt.modelName = autoProbe.selectedModel || autoProbe.activeLoadedModel;
3054
+ tuiPrompt.render();
3055
+ stopAutoReconnectWatcher();
3056
+ printRichCard({
3057
+ title: 'Model Auto-Connected',
3058
+ status: 'connected',
3059
+ lines: [
3060
+ `• Provider: ${client.providerName}${client.provider === 'lmstudio' ? ' (Local)' : ''} • Model: ${tuiPrompt.modelName}`,
3061
+ '• Loaded model detected in VRAM: chat typing unlocked.',
3062
+ ],
3063
+ });
3064
+ }
3065
+ } catch {}
3066
+
3067
+ if (catalogOnlyMode || !tuiPrompt.isConnected) {
3068
+ printSolidPanel({
3069
+ title: 'AI Chat Locked • LM Studio Offline',
3070
+ lines: [
3071
+ `${C.bold}${C.amber}LM Studio is not running or connected.${C.reset}`,
3072
+ `${C.white}Chat query typing is disabled until the AI server is active.${C.reset}`,
3073
+ '',
3074
+ `${C.bold}${C.white}How to unlock & connect:${C.reset}`,
3075
+ ` 1. Open ${C.cyberCyan}LM Studio${C.reset} on your computer`,
3076
+ ` 2. Load model ${C.gold}${opts.model || client.model || 'google/gemma-4-e4b'}${C.reset} and ensure the local server is running (port 1234)`,
3077
+ ` 3. The agent will auto-connect automatically (or type ${C.cyberCyan}/model reconnect${C.reset})`,
3078
+ '',
3079
+ `${C.dim}Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit${C.reset}`,
3080
+ ],
3081
+ bg: C.bgPanel,
3082
+ });
3083
+ continue;
3084
+ }
2631
3085
  }
2632
3086
 
2633
3087
  const { sanitized, warnings } = sanitizeQuery(trimmed);
@@ -2653,6 +3107,7 @@ async function main() {
2653
3107
  triggerQueueProcessing();
2654
3108
  }
2655
3109
 
3110
+ stopAutoReconnectWatcher();
2656
3111
  if (globalTuiPrompt) {
2657
3112
  globalTuiPrompt.dispose();
2658
3113
  globalTuiPrompt = null;