kiku-agent 1.3.2 → 1.3.4

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 (152) hide show
  1. package/README.md +69 -12
  2. package/package.json +2 -1
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/modules/evaluation/runner.js +4 -14
  18. package/server/agent/eval/modules/evaluation/scoring.js +15 -3
  19. package/server/agent/eval/modules/server/evalApi.js +5 -5
  20. package/server/agent/eval/modules/ui/evalUi.js +7 -4
  21. package/server/agent/eventBus.js +1 -0
  22. package/server/agent/ink/main.jsx +196 -191
  23. package/server/agent/jobManager.js +11 -1
  24. package/server/agent/lanDiscovery.js +721 -23
  25. package/server/agent/llmClient.js +317 -71
  26. package/server/agent/lmStudioWakeup.js +517 -65
  27. package/server/agent/locationService.js +797 -0
  28. package/server/agent/mcpQuizData.js +509 -0
  29. package/server/agent/memory/contextAllocator.js +19 -7
  30. package/server/agent/memory.js +3 -1
  31. package/server/agent/modelCompatibility.js +352 -0
  32. package/server/agent/modelDefaults.js +1 -0
  33. package/server/agent/modelManager.js +46 -2
  34. package/server/agent/newsWatch.js +9 -3
  35. package/server/agent/processManager.js +229 -0
  36. package/server/agent/prompts.js +174 -8
  37. package/server/agent/routingInterceptor.js +9 -1
  38. package/server/agent/sanitizer.js +41 -6
  39. package/server/agent/searchQueryPlanner.js +202 -0
  40. package/server/agent/session.js +59 -4
  41. package/server/agent/tests/agentFollowUps.test.js +128 -2
  42. package/server/agent/tests/agentMusicControl.test.js +245 -0
  43. package/server/agent/tests/apiRouter.test.js +1 -1
  44. package/server/agent/tests/autoContext.test.js +176 -0
  45. package/server/agent/tests/bilibiliEmbed.test.js +45 -0
  46. package/server/agent/tests/chatHardening.test.js +121 -0
  47. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  48. package/server/agent/tests/clarifyingQuestions.test.js +33 -0
  49. package/server/agent/tests/cliAutomation.test.js +38 -1
  50. package/server/agent/tests/cloudProviders.test.js +91 -3
  51. package/server/agent/tests/commands.test.js +92 -3
  52. package/server/agent/tests/contextIntelligence.test.js +50 -3
  53. package/server/agent/tests/cpuMetrics.test.js +101 -0
  54. package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
  55. package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
  56. package/server/agent/tests/engineService.test.js +436 -0
  57. package/server/agent/tests/engineUnload.test.js +94 -0
  58. package/server/agent/tests/envCommand.test.js +69 -0
  59. package/server/agent/tests/evalMetrics.test.js +8 -0
  60. package/server/agent/tests/fetchApi.test.js +99 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +2 -0
  62. package/server/agent/tests/lanDiscovery.test.js +5 -0
  63. package/server/agent/tests/lanInterfaces.test.js +510 -0
  64. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  65. package/server/agent/tests/locationTool.test.js +278 -0
  66. package/server/agent/tests/memoryRoute.test.js +173 -0
  67. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  68. package/server/agent/tests/modelCompatibility.test.js +148 -0
  69. package/server/agent/tests/modelManager.test.js +32 -0
  70. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +4 -2
  72. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  73. package/server/agent/tests/openDomainSearch.test.js +121 -0
  74. package/server/agent/tests/processManager.test.js +144 -0
  75. package/server/agent/tests/providerPersistence.test.js +10 -5
  76. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  77. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  78. package/server/agent/tests/securityAuditRemediation.test.js +16 -9
  79. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  80. package/server/agent/tests/securityHardening.test.js +4 -4
  81. package/server/agent/tests/sessionAndCopy.test.js +39 -4
  82. package/server/agent/tests/sidebarControls.test.js +34 -0
  83. package/server/agent/tests/statefulMemory.test.js +8 -1
  84. package/server/agent/tests/textProtocolLoop.test.js +71 -0
  85. package/server/agent/tests/textToolParsing.test.js +48 -0
  86. package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
  87. package/server/agent/tests/unifiedEndpoints.test.js +13 -6
  88. package/server/agent/tests/v110Features.test.js +1 -1
  89. package/server/agent/tests/vramEstimator.test.js +75 -0
  90. package/server/agent/tests/webChatDocs.test.js +199 -0
  91. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  92. package/server/agent/tests/webChatInspection.test.js +256 -5
  93. package/server/agent/tests/webChatMcp.test.js +94 -0
  94. package/server/agent/tests/webExtract.test.js +23 -0
  95. package/server/agent/tests/webSources.test.js +46 -12
  96. package/server/agent/textToolProtocol.js +173 -46
  97. package/server/agent/toolRouter.js +318 -44
  98. package/server/agent/tools.js +877 -96
  99. package/server/agent/ui.js +39 -34
  100. package/server/agent/vramService.js +35 -54
  101. package/server/agent/webSearch.js +441 -162
  102. package/server/agent/websearch/orchestrator.js +15 -5
  103. package/server/agent/websearch/resilience/cache.js +9 -2
  104. package/server/agent/websearch/security/ssrf.js +41 -10
  105. package/server/agent/youtubeAuth.js +5 -20
  106. package/server/apiRouter.js +5558 -4422
  107. package/server/mcp/data/discography.js +2 -2
  108. package/server/mcp/examples/antigravity.example.json +17 -0
  109. package/server/mcp/handler.js +8 -4
  110. package/server/mcp/security.js +220 -21
  111. package/server/mcp/server.js +2 -1
  112. package/server/mcp/tests/connection.test.js +14 -8
  113. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  114. package/server/mcp/tools.js +7 -1
  115. package/server/supabase_migration_prefix.sql +102 -0
  116. package/server/supabase_schema.sql +18 -18
  117. package/server/web-chat/core/connection.js +179 -218
  118. package/server/web-chat/core/conversationContext.js +9 -2
  119. package/server/web-chat/core/directCatalogEngine.js +52 -28
  120. package/server/web-chat/core/dramaKnowledge.js +33 -22
  121. package/server/web-chat/core/followUpEngine.js +735 -81
  122. package/server/web-chat/core/futureEventGuard.js +5 -1
  123. package/server/web-chat/core/musicControlAction.js +70 -0
  124. package/server/web-chat/core/security.js +32 -21
  125. package/server/web-chat/core/toolInspector.js +6 -6
  126. package/server/web-chat/core/urlGuard.js +45 -0
  127. package/server/web-chat/router.js +204 -157
  128. package/server/web-chat/routes/chatRoute.js +885 -488
  129. package/server/web-chat/routes/engineRoute.js +270 -0
  130. package/server/web-chat/routes/exportRoute.js +1 -1
  131. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  132. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  133. package/server/web-chat/routes/mcpRoute.js +386 -0
  134. package/server/web-chat/routes/memoryRoute.js +253 -0
  135. package/server/web-chat/routes/providerRoute.js +49 -16
  136. package/server/web-chat/routes/staticRoute.js +306 -17
  137. package/server/web-chat/routes/statusRoute.js +194 -73
  138. package/server/web-chat/routes/suggestRoute.js +11 -3
  139. package/server/web-chat/routes/verifyRoute.js +26 -4
  140. package/server/web-chat/server.js +7 -6
  141. package/server/web-chat/ui/docsBuilder.js +3558 -0
  142. package/server/web-chat/ui/htmlBuilder.js +12 -0
  143. package/server/web-chat/ui/scripts/client.js +3401 -1302
  144. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  145. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  146. package/server/web-chat/ui/styles/lan-card.css +165 -0
  147. package/server/web-chat/ui/styles/main.css +4289 -447
  148. package/server/web-chat/ui/sw.js +64 -0
  149. package/server/web-chat/ui/templates/body.html +407 -281
  150. package/src/data/awardsData.js +146 -146
  151. package/src/data/musicVideoData.js +6 -4
  152. package/src/lib/vram/estimator.js +195 -19
@@ -6,6 +6,8 @@ import { stdin as input } from 'node:process';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { spawn } from 'node:child_process';
8
8
  import './envBootstrap.js';
9
+ import { resolveDynamicWindowsAppPaths } from './appPaths.js';
10
+ import { registerBackgroundProcess } from './processManager.js';
9
11
  import { KikuAgent } from './agentCore.js';
10
12
  import { LLMClient, LMStudioClient } from './llmClient.js';
11
13
  import { toolsSchema, toolHandlers, executeTool } from './tools.js';
@@ -116,6 +118,15 @@ function recordQueryDiagnostics(details) {
116
118
  return lastQueryDetails;
117
119
  }
118
120
 
121
+ export function resolveModelFallbackWarning(modelResolution = {}, opts = {}) {
122
+ if (!modelResolution.usedFallback) return null;
123
+ if (!opts.model) return null;
124
+ return {
125
+ requested: modelResolution.requestedModel || opts.model,
126
+ selected: modelResolution.selectedModel,
127
+ };
128
+ }
129
+
119
130
  export function parseCliArgs(customArgs = null) {
120
131
  const args = customArgs || process.argv.slice(2);
121
132
  const options = {
@@ -144,6 +155,9 @@ export function parseCliArgs(customArgs = null) {
144
155
  eval: null,
145
156
  hub: null,
146
157
  portal: null,
158
+ gpuOffload: null,
159
+ cpuThreads: null,
160
+ nGpuLayers: null,
147
161
  };
148
162
 
149
163
  for (let i = 0; i < args.length; i++) {
@@ -205,6 +219,20 @@ export function parseCliArgs(customArgs = null) {
205
219
  else if (arg === '--portal') {
206
220
  options.portal = (args[i + 1] && !args[i + 1].startsWith('-')) ? args[++i] : 'open';
207
221
  }
222
+ else if (arg === '--n-gpu-layers' || arg === '--gpu-layers' || arg === '-ngl') {
223
+ const val = args[++i];
224
+ options.nGpuLayers = val;
225
+ options.gpuOffload = val;
226
+ }
227
+ else if (arg === '--gpu-offload') {
228
+ options.gpuOffload = args[++i];
229
+ }
230
+ else if (arg === '--threads' || arg === '--cpu-threads') {
231
+ const maybeThreads = Number(args[++i]);
232
+ if (Number.isFinite(maybeThreads) && maybeThreads >= 1) {
233
+ options.cpuThreads = Math.trunc(maybeThreads);
234
+ }
235
+ }
208
236
  else if (!arg.startsWith('-') && !options.query) { options.query = args.slice(i).join(' '); break; }
209
237
  else if (arg.startsWith('-')) {
210
238
  console.error(`Unknown option: ${arg}`);
@@ -243,11 +271,7 @@ async function attemptAutoStartLMStudio(baseUrl, model) {
243
271
  const isMac = process.platform === 'darwin';
244
272
 
245
273
  const candidates = isWin
246
- ? [
247
- `${process.env.LOCALAPPDATA}\\LM Studio\\LM Studio.exe`,
248
- `${process.env.LOCALAPPDATA}\\Programs\\LM Studio\\LM Studio.exe`,
249
- 'C:\\Program Files\\LM Studio\\LM Studio.exe'
250
- ]
274
+ ? resolveDynamicWindowsAppPaths('LM Studio', 'LM Studio.exe')
251
275
  : isMac
252
276
  ? ['/Applications/LM Studio.app/Contents/MacOS/LM Studio']
253
277
  : ['lmstudio', '/usr/local/bin/lmstudio', '/opt/lmstudio/bin/lmstudio'];
@@ -275,6 +299,13 @@ async function attemptAutoStartLMStudio(baseUrl, model) {
275
299
  : ['serve', '--model', model, '--port', port],
276
300
  { detached: true, stdio: 'ignore', windowsHide: true }
277
301
  );
302
+ if (child.pid) {
303
+ registerBackgroundProcess('lmstudio', child, {
304
+ command: exe,
305
+ args: isWin ? ['--model', model, '--serve', '--port', port] : ['serve', '--model', model, '--port', port],
306
+ metadata: { model, port },
307
+ });
308
+ }
278
309
  child.unref();
279
310
 
280
311
  return await waitForLMStudio(baseUrl, 45000);
@@ -638,6 +669,8 @@ export function printHelp(specificCmd = null) {
638
669
  helpLines.push(` ${C.emerald}${"--web-chat, --web".padEnd(28)}${C.reset} ${C.white}Start standalone web chatbot (default port 3300)${C.reset}`);
639
670
  helpLines.push(` ${C.emerald}${"--cn-tools, --cn".padEnd(28)}${C.reset} ${C.white}Enable Chinese web tools (search_bilibili, Baidu)${C.reset}`);
640
671
  helpLines.push(` ${C.emerald}${"--port <n>".padEnd(28)}${C.reset} ${C.white}Web chatbot port 1-65535 (or --web-chat=<port>)${C.reset}`);
672
+ helpLines.push(` ${C.emerald}${"--n-gpu-layers <n>".padEnd(28)}${C.reset} ${C.white}GPU offload layers/mode (max, off, 0.0-1.0, or layer count)${C.reset}`);
673
+ helpLines.push(` ${C.emerald}${"--threads <n>".padEnd(28)}${C.reset} ${C.white}CPU thread pool size (1-256) for local model execution${C.reset}`);
641
674
  helpLines.push(` ${C.emerald}${"--no-open, --open".padEnd(28)}${C.reset} ${C.white}Skip or force auto-open browser for --web-chat${C.reset}`);
642
675
  helpLines.push(` ${C.emerald}${"-v, --verbose".padEnd(28)}${C.reset} ${C.white}Verbose logging and thinking${C.reset}`);
643
676
  helpLines.push(` ${C.emerald}${"-V, --version".padEnd(28)}${C.reset} ${C.white}Print version${C.reset}`);
@@ -742,7 +775,7 @@ function printAd() {
742
775
 
743
776
  function getInitialViewportLines() {
744
777
  const heroLines = getWelcomeHeroLines();
745
- const galleryLines = getWelcomeGalleryLines(2);
778
+ const galleryLines = process.env.KIKU_WELCOME_GALLERY === 'true' ? getWelcomeGalleryLines(2) : [];
746
779
  return galleryLines.length > 0 ? [...heroLines, ...galleryLines] : heroLines;
747
780
  }
748
781
 
@@ -768,6 +801,7 @@ class InteractiveTuiPrompt {
768
801
  this.isResolving = false;
769
802
  this.activeResolve = null;
770
803
  this.activeReject = null;
804
+ this.parkedReads = [];
771
805
  this.onCancel = options.onCancel || null;
772
806
  this.onSigInt = options.onSigInt || null;
773
807
  this._keyHandler = null;
@@ -844,6 +878,24 @@ class InteractiveTuiPrompt {
844
878
 
845
879
  async readLine(options = {}) {
846
880
  return new Promise((resolve, reject) => {
881
+ if (this.activeResolve && !this.isResolving) {
882
+ // A read is already active (the main input loop). Park it instead of
883
+ // leaking its promise: modal callers (/env init, /quiz, clarifying
884
+ // answers) own the terminal until they finish, then the parked read
885
+ // resumes via _resumeParked().
886
+ this.parkedReads.push({
887
+ resolve: this.activeResolve,
888
+ reject: this.activeReject,
889
+ line: this.line,
890
+ cursor: this.cursor,
891
+ promptText: this.promptText,
892
+ placeholder: this.inputPlaceholder,
893
+ cancelValue: this.cancelValue,
894
+ recordInputHistory: this.recordInputHistory,
895
+ });
896
+ this.activeResolve = null;
897
+ this.activeReject = null;
898
+ }
847
899
  this._cleanupInput();
848
900
  const inputOptions = typeof options === 'string' ? { prompt: options } : options;
849
901
  this.promptText = inputOptions.prompt || this.defaultPromptText;
@@ -890,6 +942,38 @@ class InteractiveTuiPrompt {
890
942
  this.activeResolve = null;
891
943
  this.activeReject = null;
892
944
  if (resolve) resolve(result);
945
+ this._resumeParked();
946
+ }
947
+
948
+ _resumeParked() {
949
+ const parked = this.parkedReads.pop();
950
+ if (!parked || this.isDisposed || this.activeResolve) return;
951
+ this.isResolving = false;
952
+ this.activeResolve = parked.resolve;
953
+ this.activeReject = parked.reject;
954
+ this.line = parked.line;
955
+ this.cursor = parked.cursor;
956
+ this.promptText = parked.promptText;
957
+ this.inputPlaceholder = parked.placeholder;
958
+ this.cancelValue = parked.cancelValue;
959
+ this.recordInputHistory = parked.recordInputHistory;
960
+ this.historyIndex = -1;
961
+ this.popoverIndex = 0;
962
+ this.popoverActive = false;
963
+ this.popoverItems = [];
964
+ this.ghostText = '';
965
+ try {
966
+ this.updateState();
967
+ readline.emitKeypressEvents(input);
968
+ if (input.isTTY) input.setRawMode(true);
969
+ this._keyHandler = (str, key) => {
970
+ try { this.handleKeypress(str, key); } catch (error) { this.fail(error); }
971
+ };
972
+ input.on('keypress', this._keyHandler);
973
+ this.render();
974
+ } catch (error) {
975
+ this.fail(error);
976
+ }
893
977
  }
894
978
 
895
979
  abortActiveRead(result = null) {
@@ -905,6 +989,7 @@ class InteractiveTuiPrompt {
905
989
  this.activeResolve = null;
906
990
  this.activeReject = null;
907
991
  resolve(result);
992
+ this._resumeParked();
908
993
  return true;
909
994
  }
910
995
 
@@ -916,6 +1001,7 @@ class InteractiveTuiPrompt {
916
1001
  this.activeResolve = null;
917
1002
  this.activeReject = null;
918
1003
  if (reject) reject(error);
1004
+ this._resumeParked();
919
1005
  }
920
1006
 
921
1007
  dispose() {
@@ -925,6 +1011,10 @@ class InteractiveTuiPrompt {
925
1011
  this.activeResolve = null;
926
1012
  this.activeReject = null;
927
1013
  this.isResolving = true;
1014
+ const parked = this.parkedReads.splice(0);
1015
+ for (const entry of parked) {
1016
+ try { entry.resolve(null); } catch { }
1017
+ }
928
1018
  }
929
1019
 
930
1020
  updateState() {
@@ -1712,6 +1802,10 @@ async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, sign
1712
1802
  console.log(formatQuestionsForCLI(questions, C));
1713
1803
  },
1714
1804
  onClarifyingAnswer: (queryId) => {
1805
+ if (globalTuiPrompt && typeof globalTuiPrompt.readLine === 'function') {
1806
+ return globalTuiPrompt.readLine({ prompt: `${C.gold}> ${C.reset}`, history: false, cancelValue: '' })
1807
+ .then(answer => String(answer ?? '').trim());
1808
+ }
1715
1809
  return new Promise((resolve) => {
1716
1810
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1717
1811
  rl.question(`${C.gold}> ${C.reset}`, (answer) => {
@@ -1926,7 +2020,7 @@ function vramZoneTag(zone) {
1926
2020
  * model's estimated footprint, so users immediately see whether the loaded
1927
2021
  * model is a heavy fit for their card. Informational only — never blocks.
1928
2022
  */
1929
- async function printResourceVerificationPanel(client) {
2023
+ async function printResourceVerificationPanel(client, { verbose = false } = {}) {
1930
2024
  if (!client || client.provider !== 'lmstudio') return;
1931
2025
  try {
1932
2026
  const [hw, runtime] = await Promise.all([
@@ -1950,12 +2044,44 @@ async function printResourceVerificationPanel(client) {
1950
2044
  contextLength: runtimeCtx,
1951
2045
  });
1952
2046
 
1953
- const lines = [];
1954
2047
  let livePct = null;
1955
2048
  if (hw.available && hw.freeBytes !== null && hw.freeBytes !== undefined) {
1956
2049
  const usedBytes = Math.max(0, hw.totalBytes - hw.freeBytes);
1957
2050
  const live = classifyAllocation({ totalBytes: usedBytes, availableBytes: hw.totalBytes });
1958
2051
  livePct = live.usagePct;
2052
+ }
2053
+
2054
+ let verdict = null;
2055
+ if (livePct !== null) {
2056
+ if (livePct <= 80) verdict = `${C.emerald}Resource load is comfortable (${livePct}% VRAM in use).${C.reset}`;
2057
+ 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}`;
2058
+ else verdict = `${C.roseGlow}VRAM is critically full (${livePct}%) — lower the context or switch to a smaller quant in LM Studio.${C.reset}`;
2059
+ } else if (estimate.totalBytes > 0 && hw.available) {
2060
+ const estCls = classifyAllocation({ totalBytes: estimate.totalBytes, availableBytes: hw.totalBytes });
2061
+ 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}`;
2062
+ else verdict = `${C.dim}Live usage unavailable — estimate says ${estCls.zone}.${C.reset}`;
2063
+ }
2064
+
2065
+ if (!verbose) {
2066
+ const gpuDesc = hw.available && hw.freeBytes !== null
2067
+ ? `${hw.name} (${livePct !== null ? `${livePct}% VRAM used` : 'active'}, ${formatGb(hw.freeBytes)} free)`
2068
+ : (hw.available ? hw.name : 'CPU / RAM');
2069
+ const cleanVerdict = verdict ? stripAnsi(verdict).replace(/\s*\([^)]*\)/, '') : 'Ready';
2070
+ printRichCard({
2071
+ title: 'Hardware & Inference Target',
2072
+ status: livePct && livePct > 95 ? 'warning' : 'connected',
2073
+ lines: [
2074
+ `• GPU: ${gpuDesc} • Model: ${client.model}`,
2075
+ `• Memory: ${cleanVerdict} (use /vram for full telemetry)`,
2076
+ ],
2077
+ });
2078
+ return;
2079
+ }
2080
+
2081
+ const lines = [];
2082
+ if (hw.available && hw.freeBytes !== null && hw.freeBytes !== undefined) {
2083
+ const usedBytes = Math.max(0, hw.totalBytes - hw.freeBytes);
2084
+ const live = classifyAllocation({ totalBytes: usedBytes, availableBytes: hw.totalBytes });
1959
2085
  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
2086
  lines.push(`${C.gold}Free VRAM:${C.reset} ${C.emerald}${formatGb(hw.freeBytes)}${C.reset}`);
1961
2087
  } else if (hw.available) {
@@ -1975,16 +2101,6 @@ async function printResourceVerificationPanel(client) {
1975
2101
  (estCls && estCls.zone !== 'UNKNOWN' ? ` ${vramZoneTag(estCls.zone)}` : ''));
1976
2102
  }
1977
2103
 
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
2104
  if (verdict) lines.push(`${C.gold}Verdict:${C.reset} ${verdict}`);
1989
2105
  lines.push(`${C.dim}Details: /vram live • /vram compare • /vram ctx${C.reset}`);
1990
2106
 
@@ -2072,7 +2188,7 @@ async function executeHeadlessQuery(opts, query) {
2072
2188
  let executionError = null;
2073
2189
 
2074
2190
  try {
2075
- const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
2191
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model, strictModel: Boolean(opts.model) });
2076
2192
  const modelResolution = await modelManager.resolve({ requestedModel: opts.model || client.model });
2077
2193
 
2078
2194
  if (!modelResolution.connected || !modelResolution.selectedModel) {
@@ -2322,8 +2438,13 @@ async function main() {
2322
2438
  apiKey: opts.apiKey,
2323
2439
  model: opts.model
2324
2440
  });
2325
- const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
2326
- const modelResolution = await modelManager.resolve({ requestedModel: opts.model || client.model });
2441
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model, strictModel: Boolean(opts.model) });
2442
+ const modelResolution = await modelManager.resolve({
2443
+ requestedModel: opts.model || client.model,
2444
+ autoWakeup: true,
2445
+ gpuOffload: opts.gpuOffload,
2446
+ cpuThreads: opts.cpuThreads,
2447
+ });
2327
2448
  if (!modelResolution.connected || !modelResolution.selectedModel) {
2328
2449
  console.error(modelResolution.error || `No available chat model found for ${client.providerName}`);
2329
2450
  process.exit(2);
@@ -2429,24 +2550,15 @@ async function main() {
2429
2550
  }
2430
2551
 
2431
2552
  if (!health.connected) {
2432
- printRichCard({
2433
- title: `${client.providerName} Offline`,
2434
- status: 'offline',
2435
- lines: [
2436
- `• Endpoint: ${baseUrl || 'local'} (${health.error || 'unreachable'})`,
2437
- ...(client.provider === 'lmstudio' ? ['• Action: Attempting auto-start of local LM Studio...'] : []),
2438
- ],
2439
- });
2440
-
2441
2553
  if (client.provider === 'lmstudio') {
2442
2554
  const autoHealth = await attemptAutoStartLMStudio(baseUrl, opts.model || client.model);
2443
2555
  if (autoHealth) {
2444
2556
  health = autoHealth;
2445
2557
  printRichCard({
2446
- title: 'LM Studio Started',
2447
- status: 'ok',
2558
+ title: 'LM Studio Auto-Started',
2559
+ status: 'connected',
2448
2560
  lines: [
2449
- 'Local LM Studio process started and responding.',
2561
+ '• Local LM Studio process started and responding.',
2450
2562
  ],
2451
2563
  });
2452
2564
  } else {
@@ -2456,9 +2568,9 @@ async function main() {
2456
2568
  title: 'LM Studio Offline • AI Chat Locked',
2457
2569
  status: 'offline',
2458
2570
  lines: [
2459
- 'LM Studio is not running. Free-form chat typing is locked to prevent errors.',
2460
- 'Open LM Studio and load a model (e.g. google/gemma-4-e4b) — it will auto-connect once loaded.',
2461
- 'Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit',
2571
+ `• Endpoint: ${baseUrl || 'http://127.0.0.1:1234/v1'} (unreachable)`,
2572
+ '• Open LM Studio and load google/gemma-4-e4b — auto-connects when loaded in VRAM.',
2573
+ '• Offline commands available: /model reconnect, /help, /provider, /doctor, /theme, /exit',
2462
2574
  ],
2463
2575
  });
2464
2576
  }
@@ -2478,10 +2590,15 @@ async function main() {
2478
2590
  }
2479
2591
  }
2480
2592
 
2481
- const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
2593
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model, strictModel: Boolean(opts.model) });
2482
2594
  const modelResolution = catalogOnlyMode
2483
2595
  ? { connected: false, state: 'catalog_only' }
2484
- : await modelManager.resolve({ requestedModel: opts.model || client.model });
2596
+ : await modelManager.resolve({
2597
+ requestedModel: opts.model || client.model,
2598
+ autoWakeup: true,
2599
+ gpuOffload: opts.gpuOffload,
2600
+ cpuThreads: opts.cpuThreads,
2601
+ });
2485
2602
  if (!catalogOnlyMode && (!modelResolution.connected || !modelResolution.selectedModel)) {
2486
2603
  printRichCard({
2487
2604
  title: 'Model Not Loaded • AI Chat Locked',
@@ -2505,13 +2622,14 @@ async function main() {
2505
2622
  ],
2506
2623
  });
2507
2624
  }
2508
- if (modelResolution.usedFallback) {
2625
+ const fallbackWarning = resolveModelFallbackWarning(modelResolution, opts);
2626
+ if (fallbackWarning) {
2509
2627
  printRichCard({
2510
2628
  title: 'Model Fallback',
2511
2629
  status: 'warning',
2512
2630
  lines: [
2513
- `Requested model ${opts.model} was unavailable.`,
2514
- `Fallback model ${modelResolution.selectedModel} selected.`,
2631
+ `Requested model ${fallbackWarning.requested} was unavailable.`,
2632
+ `Fallback model ${fallbackWarning.selected} selected.`,
2515
2633
  ],
2516
2634
  });
2517
2635
  }
@@ -2571,12 +2689,18 @@ async function main() {
2571
2689
  }
2572
2690
 
2573
2691
  if (!catalogOnlyMode) {
2574
- await printResourceVerificationPanel(client);
2692
+ await printResourceVerificationPanel(client, { verbose: opts.verbose });
2575
2693
  }
2576
2694
 
2577
2695
  if (agent.restoredMessageCount > 0) {
2578
2696
  const s = agent.session;
2579
- console.log(`\n ${C.sakura}${C.bold}[SESSION RESTORED]${C.reset} ${C.dim}${s.id} — ${s.messages.length} messages${C.reset}\n`);
2697
+ printRichCard({
2698
+ title: 'Active Session Restored',
2699
+ status: 'session',
2700
+ lines: [
2701
+ `• Session: ${s.id} — ${s.messages.length} message(s) loaded from history`,
2702
+ ],
2703
+ });
2580
2704
  }
2581
2705
 
2582
2706
  agent.stateMachine.on('enter:' + AgentStates.FETCHING_TOOL, ({ toolName }) => {
@@ -2631,7 +2755,7 @@ async function main() {
2631
2755
  ? await modelManager.resolve()
2632
2756
  : await client.checkConnection();
2633
2757
 
2634
- if (resolution?.connected && (resolution.selectedModel || resolution.activeLoadedModel)) {
2758
+ if (resolution?.connected && resolution.isModelLoaded !== false && (resolution.selectedModel || resolution.activeLoadedModel)) {
2635
2759
  const detectedModel = resolution.selectedModel || resolution.activeLoadedModel;
2636
2760
  catalogOnlyMode = false;
2637
2761
  tuiPrompt.isConnected = true;
@@ -2749,7 +2873,7 @@ async function main() {
2749
2873
  if (catalogOnlyMode && modelManager) {
2750
2874
  try {
2751
2875
  const autoProbe = await modelManager.resolve();
2752
- if (autoProbe?.connected && (autoProbe.selectedModel || autoProbe.activeLoadedModel)) {
2876
+ if (autoProbe?.connected && autoProbe.isModelLoaded !== false && (autoProbe.selectedModel || autoProbe.activeLoadedModel)) {
2753
2877
  cmdCtx.setCatalogOnlyMode(false);
2754
2878
  if (tuiPrompt) {
2755
2879
  tuiPrompt.modelName = autoProbe.selectedModel || autoProbe.activeLoadedModel;
@@ -2835,6 +2959,7 @@ async function main() {
2835
2959
  '/config', '/settings', '/conf',
2836
2960
  '/sources', '/src', '/srcs',
2837
2961
  '/queue',
2962
+ '/unload',
2838
2963
  '/ads', '/sponsor', '/ad',
2839
2964
  ]);
2840
2965
 
@@ -2853,7 +2978,7 @@ async function main() {
2853
2978
  if (base === '/session' && (!sub || ['info', 'list', 'summary'].includes(sub))) return true;
2854
2979
  if (base === '/lang' && !sub) return true;
2855
2980
  if (base === '/thinking' && !sub) return true;
2856
- if (base === '/model' && (!sub || sub === 'status' || sub === 'list' || sub === 'recommend')) return true;
2981
+ if (base === '/model' && (!sub || ['status', 'list', 'recommend', 'unload', 'engine', 'config', 'params', 'preset', 'presets', 'files', 'gguf', 'temp', 'temperature', 'topp', 'ctx', 'context', 'thinking'].includes(sub))) return true;
2857
2982
  if (base === '/provider' && (!sub || sub === 'list' || sub === 'status' || sub === 'info')) return true;
2858
2983
  if (base === '/mcp' && (!sub || sub === 'status' || sub === 'list' || sub === 'tools')) return true;
2859
2984
  if (base === '/env' && (!sub || sub === 'get' || sub === 'path')) return true;
@@ -3051,7 +3176,7 @@ async function main() {
3051
3176
  const autoProbe = modelManager
3052
3177
  ? await modelManager.resolve()
3053
3178
  : await client.checkConnection();
3054
- if (autoProbe?.connected && (autoProbe.selectedModel || autoProbe.activeLoadedModel)) {
3179
+ if (autoProbe?.connected && autoProbe.isModelLoaded !== false && (autoProbe.selectedModel || autoProbe.activeLoadedModel)) {
3055
3180
  catalogOnlyMode = false;
3056
3181
  tuiPrompt.isConnected = true;
3057
3182
  tuiPrompt.modelName = autoProbe.selectedModel || autoProbe.activeLoadedModel;
@@ -3108,6 +3233,7 @@ async function main() {
3108
3233
 
3109
3234
  lastQuery = trimmed;
3110
3235
  queryQueue.push({ id: 'q_' + Date.now(), number: queryNumber, prompt: trimmed, addedAt: Date.now() });
3236
+ queryCount++;
3111
3237
  triggerQueueProcessing();
3112
3238
  }
3113
3239
 
@@ -3116,7 +3242,10 @@ async function main() {
3116
3242
  globalTuiPrompt.dispose();
3117
3243
  globalTuiPrompt = null;
3118
3244
  }
3119
- exitAlternateScreen();
3245
+ exitAlternateScreen();
3246
+ if (queryCount === 0 && !process.stdin.isTTY) {
3247
+ console.log(`\n ${C.dim}Tip: Run 'kiku-agent' directly on its own line. Avoid chaining with ';' in PowerShell (e.g. 'cmd1 ; kiku-agent') as PowerShell pipes empty input into stdin.${C.reset}`);
3248
+ }
3120
3249
  process.stdout.write(`\n ${C.sakura}Goodbye • 再见 (鞠躬 ~ 谢谢!)${C.reset}\n\n`, () => {
3121
3250
  process.exit(0);
3122
3251
  });
@@ -0,0 +1,116 @@
1
+ export function toAnthropicMessages(messages = []) {
2
+ const system = [];
3
+ const converted = [];
4
+ for (const message of messages) {
5
+ const role = message?.role;
6
+ if (role === 'system') {
7
+ if (message.content) system.push(String(message.content));
8
+ continue;
9
+ }
10
+ if (role === 'tool') {
11
+ const toolUseId = message.tool_call_id || message.tool_use_id || 'unknown_tool_call';
12
+ const rawContent = String(message.content ?? '');
13
+ let isError = false;
14
+ try {
15
+ const parsed = JSON.parse(rawContent);
16
+ if (parsed && (parsed.status === 'error' || parsed.error || parsed.is_error)) {
17
+ isError = true;
18
+ }
19
+ } catch {}
20
+
21
+ const toolResultBlock = {
22
+ type: 'tool_result',
23
+ tool_use_id: toolUseId,
24
+ content: rawContent,
25
+ ...(isError ? { is_error: true } : {}),
26
+ };
27
+
28
+
29
+ const lastMsg = converted[converted.length - 1];
30
+ if (lastMsg && lastMsg.role === 'user' && Array.isArray(lastMsg.content) && lastMsg.content[0]?.type === 'tool_result') {
31
+ lastMsg.content.push(toolResultBlock);
32
+ } else {
33
+ converted.push({
34
+ role: 'user',
35
+ content: [toolResultBlock],
36
+ });
37
+ }
38
+ continue;
39
+ }
40
+ if (role === 'assistant') {
41
+ const blocks = [];
42
+
43
+ if (Array.isArray(message.thinking_blocks) && message.thinking_blocks.length > 0) {
44
+ blocks.push(...message.thinking_blocks);
45
+ }
46
+
47
+ const textContent = normalizeContent(message?.content);
48
+ if (textContent) {
49
+ blocks.push({ type: 'text', text: textContent });
50
+ }
51
+
52
+ if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
53
+ for (const call of message.tool_calls) {
54
+ blocks.push({
55
+ type: 'tool_use',
56
+ id: call.id || `call_${Date.now()}`,
57
+ name: call.function?.name || call.name || '',
58
+ input: parseJson(call.function?.arguments ?? call.arguments),
59
+ });
60
+ }
61
+ }
62
+ if (blocks.length > 0) {
63
+ converted.push({ role: 'assistant', content: blocks });
64
+ } else {
65
+ converted.push({ role: 'assistant', content: textContent || '' });
66
+ }
67
+ continue;
68
+ }
69
+ converted.push({ role: role === 'assistant' ? 'assistant' : 'user', content: normalizeContent(message?.content) });
70
+ }
71
+ return { system: system.join('\n\n'), messages: converted };
72
+ }
73
+
74
+ export function toAnthropicTools(tools = []) {
75
+ return (tools || []).map(tool => ({
76
+ name: tool.function?.name || tool.name,
77
+ description: tool.function?.description || tool.description || '',
78
+ input_schema: tool.function?.parameters || tool.parameters || { type: 'object', properties: {} },
79
+ })).filter(tool => tool.name);
80
+ }
81
+
82
+ export function normalizeAnthropicResponse(data = {}) {
83
+ const blocks = Array.isArray(data.content) ? data.content : [];
84
+ const text = blocks.filter(block => block.type === 'text').map(block => block.text || '').join('');
85
+ const reasoning = blocks.filter(block => block.type === 'thinking').map(block => block.thinking || '').join('');
86
+ const thinkingBlocks = blocks.filter(block => block.type === 'thinking' || block.type === 'redacted_thinking');
87
+ const toolCalls = blocks.filter(block => block.type === 'tool_use').map(block => ({
88
+ id: block.id || `call_${Date.now()}`,
89
+ type: 'function',
90
+ function: { name: block.name || '', arguments: JSON.stringify(block.input || {}) },
91
+ }));
92
+ return {
93
+ role: 'assistant',
94
+ content: text,
95
+ ...(reasoning ? { reasoning_content: reasoning } : {}),
96
+ ...(thinkingBlocks.length ? { thinking_blocks: thinkingBlocks } : {}),
97
+ ...(toolCalls.length ? { tool_calls: toolCalls } : {}),
98
+ usage: data.usage ? {
99
+ prompt_tokens: data.usage.input_tokens || 0,
100
+ completion_tokens: data.usage.output_tokens || 0,
101
+ reasoning_tokens: 0,
102
+ total_tokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
103
+ } : undefined,
104
+ };
105
+ }
106
+
107
+ function normalizeContent(content) {
108
+ if (Array.isArray(content)) return content.map(part => part?.text || '').join('');
109
+ return content == null ? '' : String(content);
110
+ }
111
+
112
+ function parseJson(value) {
113
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value;
114
+ try { return JSON.parse(String(value || '{}')); } catch { return {}; }
115
+ }
116
+