kiku-agent 1.3.1 → 1.3.3

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 (232) hide show
  1. package/README.md +69 -12
  2. package/package.json +5 -12
  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/AGENTS.md +228 -0
  18. package/server/agent/eval/analyze.py +684 -0
  19. package/server/agent/eval/gates.json +15 -0
  20. package/server/agent/eval/modules/analysis/compare.js +198 -0
  21. package/server/agent/eval/modules/analysis/gates.js +72 -0
  22. package/server/agent/eval/modules/analysis/judge.js +70 -0
  23. package/server/agent/eval/modules/core/cli.js +453 -0
  24. package/server/agent/eval/modules/core/config.js +60 -0
  25. package/server/agent/eval/modules/core/store.js +236 -0
  26. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  27. package/server/agent/eval/modules/evaluation/runner.js +288 -0
  28. package/server/agent/eval/modules/evaluation/scoring.js +363 -0
  29. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  30. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  31. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  32. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  33. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  34. package/server/agent/eval/modules/ui/evalUi.js +4799 -0
  35. package/server/agent/eval/modules/utils/smokes.js +119 -0
  36. package/server/agent/eventBus.js +1 -0
  37. package/server/agent/ink/main.jsx +196 -191
  38. package/server/agent/jobManager.js +11 -1
  39. package/server/agent/lanDiscovery.js +721 -23
  40. package/server/agent/llmClient.js +317 -71
  41. package/server/agent/lmStudioWakeup.js +517 -65
  42. package/server/agent/locationService.js +797 -0
  43. package/server/agent/mcpQuizData.js +509 -0
  44. package/server/agent/memory/contextAllocator.js +19 -7
  45. package/server/agent/memory.js +3 -1
  46. package/server/agent/modelCompatibility.js +352 -0
  47. package/server/agent/modelDefaults.js +1 -0
  48. package/server/agent/modelManager.js +46 -2
  49. package/server/agent/newsWatch.js +9 -3
  50. package/server/agent/processManager.js +229 -0
  51. package/server/agent/prompts.js +174 -8
  52. package/server/agent/routingInterceptor.js +9 -1
  53. package/server/agent/sanitizer.js +41 -6
  54. package/server/agent/searchQueryPlanner.js +202 -0
  55. package/server/agent/session.js +59 -4
  56. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  57. package/server/agent/tests/agentFollowUps.test.js +253 -0
  58. package/server/agent/tests/agentMusicControl.test.js +245 -0
  59. package/server/agent/tests/apiRouter.test.js +108 -0
  60. package/server/agent/tests/architectureFixes.test.js +81 -0
  61. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  62. package/server/agent/tests/autoContext.test.js +176 -0
  63. package/server/agent/tests/autoResearch.test.js +64 -0
  64. package/server/agent/tests/bilibiliEmbed.test.js +296 -0
  65. package/server/agent/tests/catalogSync.test.js +130 -0
  66. package/server/agent/tests/chatHardening.test.js +121 -0
  67. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  68. package/server/agent/tests/citations.test.js +180 -0
  69. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  70. package/server/agent/tests/clarifyingQuestions.test.js +627 -0
  71. package/server/agent/tests/cliAutomation.test.js +158 -0
  72. package/server/agent/tests/cloudProviders.test.js +322 -0
  73. package/server/agent/tests/commandInput.test.js +24 -0
  74. package/server/agent/tests/commands.test.js +506 -0
  75. package/server/agent/tests/contextIntelligence.test.js +209 -0
  76. package/server/agent/tests/conversationSafety.test.js +53 -0
  77. package/server/agent/tests/cpuMetrics.test.js +101 -0
  78. package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
  79. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  80. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  81. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  82. package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
  83. package/server/agent/tests/engineService.test.js +436 -0
  84. package/server/agent/tests/engineUnload.test.js +94 -0
  85. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  86. package/server/agent/tests/envBootstrap.test.js +87 -0
  87. package/server/agent/tests/envCommand.test.js +284 -0
  88. package/server/agent/tests/errorHandling.test.js +80 -0
  89. package/server/agent/tests/errors.test.js +13 -0
  90. package/server/agent/tests/evalAnalyze.test.js +54 -0
  91. package/server/agent/tests/evalCommand.test.js +54 -0
  92. package/server/agent/tests/evalCorpus.test.js +126 -0
  93. package/server/agent/tests/evalHarness.test.js +62 -0
  94. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  95. package/server/agent/tests/evalMetrics.test.js +320 -0
  96. package/server/agent/tests/evalStore.test.js +190 -0
  97. package/server/agent/tests/evalStudio.test.js +179 -0
  98. package/server/agent/tests/feedbackLearning.test.js +186 -0
  99. package/server/agent/tests/fetchApi.test.js +99 -0
  100. package/server/agent/tests/firstRun.test.js +44 -0
  101. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  102. package/server/agent/tests/hardwareTiers.test.js +206 -0
  103. package/server/agent/tests/hybridWebSearch.test.js +194 -0
  104. package/server/agent/tests/judgeService.test.js +251 -0
  105. package/server/agent/tests/lanDiscovery.test.js +324 -0
  106. package/server/agent/tests/lanInterfaces.test.js +510 -0
  107. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  108. package/server/agent/tests/locationTool.test.js +278 -0
  109. package/server/agent/tests/mcpClient.test.js +201 -0
  110. package/server/agent/tests/memoryRoute.test.js +173 -0
  111. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  112. package/server/agent/tests/modelCompatibility.test.js +148 -0
  113. package/server/agent/tests/modelManager.test.js +141 -0
  114. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  115. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  116. package/server/agent/tests/newCommands.test.js +347 -0
  117. package/server/agent/tests/newCommandsTui.test.js +141 -0
  118. package/server/agent/tests/newTools.test.js +113 -0
  119. package/server/agent/tests/newsIntelligence.test.js +178 -0
  120. package/server/agent/tests/nextGenArchitecture.test.js +101 -0
  121. package/server/agent/tests/offlineInput.test.js +217 -0
  122. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  123. package/server/agent/tests/openDomainSearch.test.js +121 -0
  124. package/server/agent/tests/packagingGuard.test.js +57 -0
  125. package/server/agent/tests/presentation.test.js +61 -0
  126. package/server/agent/tests/processManager.test.js +144 -0
  127. package/server/agent/tests/providerPersistence.test.js +44 -0
  128. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  129. package/server/agent/tests/queryDetails.test.js +126 -0
  130. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  131. package/server/agent/tests/realWorldValidation.test.js +391 -0
  132. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  133. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  134. package/server/agent/tests/sanitizer.test.js +37 -0
  135. package/server/agent/tests/scenariosData.js +378 -0
  136. package/server/agent/tests/schemaRouting.test.js +54 -0
  137. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  138. package/server/agent/tests/securityAuditRemediation.test.js +221 -0
  139. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  140. package/server/agent/tests/securityHardening.test.js +146 -0
  141. package/server/agent/tests/securitySanitizer.test.js +38 -0
  142. package/server/agent/tests/sessionAndCopy.test.js +267 -0
  143. package/server/agent/tests/sidebarControls.test.js +135 -0
  144. package/server/agent/tests/statefulMemory.test.js +277 -0
  145. package/server/agent/tests/subagent.test.js +216 -0
  146. package/server/agent/tests/subagentProgress.test.js +109 -0
  147. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  148. package/server/agent/tests/suggestions.test.js +199 -0
  149. package/server/agent/tests/suggestionsTui.test.js +251 -0
  150. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  151. package/server/agent/tests/terminalImage.test.js +80 -0
  152. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  153. package/server/agent/tests/terminalText.test.js +133 -0
  154. package/server/agent/tests/textProtocolLoop.test.js +197 -0
  155. package/server/agent/tests/textToolParsing.test.js +187 -0
  156. package/server/agent/tests/theme.test.js +26 -0
  157. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  158. package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
  159. package/server/agent/tests/tools.test.js +259 -0
  160. package/server/agent/tests/unifiedEndpoints.test.js +159 -0
  161. package/server/agent/tests/v110Features.test.js +262 -0
  162. package/server/agent/tests/versionBump.test.js +69 -0
  163. package/server/agent/tests/vramEstimator.test.js +595 -0
  164. package/server/agent/tests/webChatDocs.test.js +199 -0
  165. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  166. package/server/agent/tests/webChatInspection.test.js +551 -0
  167. package/server/agent/tests/webChatMcp.test.js +94 -0
  168. package/server/agent/tests/webChatMemory.test.js +112 -0
  169. package/server/agent/tests/webChatSecurity.test.js +169 -0
  170. package/server/agent/tests/webExtract.test.js +284 -0
  171. package/server/agent/tests/webSources.test.js +281 -0
  172. package/server/agent/textToolProtocol.js +173 -46
  173. package/server/agent/toolRouter.js +318 -44
  174. package/server/agent/tools.js +877 -96
  175. package/server/agent/ui.js +39 -34
  176. package/server/agent/vramService.js +35 -54
  177. package/server/agent/webSearch.js +441 -162
  178. package/server/agent/websearch/orchestrator.js +15 -5
  179. package/server/agent/websearch/resilience/cache.js +9 -2
  180. package/server/agent/websearch/security/ssrf.js +41 -10
  181. package/server/agent/youtubeAuth.js +5 -20
  182. package/server/apiRouter.js +5558 -4422
  183. package/server/eval/judgeRubrics.js +110 -0
  184. package/server/eval/judgeService.js +290 -0
  185. package/server/eval/runBenchmark.js +182 -0
  186. package/server/mcp/data/discography.js +2 -2
  187. package/server/mcp/examples/antigravity.example.json +17 -0
  188. package/server/mcp/examples/librechat.example.yaml +16 -0
  189. package/server/mcp/handler.js +8 -4
  190. package/server/mcp/security.js +220 -21
  191. package/server/mcp/server.js +2 -1
  192. package/server/mcp/tests/connection.test.js +162 -0
  193. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  194. package/server/mcp/tools.js +7 -1
  195. package/server/supabase_migration_prefix.sql +102 -0
  196. package/server/supabase_schema.sql +63 -0
  197. package/server/web-chat/core/connection.js +179 -218
  198. package/server/web-chat/core/conversationContext.js +9 -2
  199. package/server/web-chat/core/directCatalogEngine.js +52 -28
  200. package/server/web-chat/core/dramaKnowledge.js +33 -22
  201. package/server/web-chat/core/followUpEngine.js +735 -81
  202. package/server/web-chat/core/futureEventGuard.js +5 -1
  203. package/server/web-chat/core/musicControlAction.js +70 -0
  204. package/server/web-chat/core/security.js +32 -21
  205. package/server/web-chat/core/toolInspector.js +6 -6
  206. package/server/web-chat/core/urlGuard.js +45 -0
  207. package/server/web-chat/router.js +204 -157
  208. package/server/web-chat/routes/chatRoute.js +885 -488
  209. package/server/web-chat/routes/engineRoute.js +270 -0
  210. package/server/web-chat/routes/exportRoute.js +1 -1
  211. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  212. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  213. package/server/web-chat/routes/mcpRoute.js +386 -0
  214. package/server/web-chat/routes/memoryRoute.js +253 -0
  215. package/server/web-chat/routes/providerRoute.js +49 -16
  216. package/server/web-chat/routes/staticRoute.js +306 -17
  217. package/server/web-chat/routes/statusRoute.js +194 -73
  218. package/server/web-chat/routes/suggestRoute.js +11 -3
  219. package/server/web-chat/routes/verifyRoute.js +26 -4
  220. package/server/web-chat/server.js +7 -6
  221. package/server/web-chat/ui/docsBuilder.js +3558 -0
  222. package/server/web-chat/ui/htmlBuilder.js +12 -0
  223. package/server/web-chat/ui/scripts/client.js +3401 -1302
  224. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  225. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  226. package/server/web-chat/ui/styles/lan-card.css +165 -0
  227. package/server/web-chat/ui/styles/main.css +4289 -447
  228. package/server/web-chat/ui/sw.js +64 -0
  229. package/server/web-chat/ui/templates/body.html +407 -281
  230. package/src/data/awardsData.js +146 -146
  231. package/src/data/musicVideoData.js +6 -4
  232. package/src/lib/vram/estimator.js +195 -19
@@ -2,12 +2,119 @@ import { spawn } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import fs from 'node:fs';
5
+ import { resolveDynamicWindowsAppPaths } from './appPaths.js';
6
+ import { registerBackgroundProcess } from './processManager.js';
7
+
8
+ const LMSTUDIO_PROBE_TTL_MS = 15_000;
9
+ const lmStudioProbeCache = new Map();
10
+
11
+
12
+ export function clearLmStudioProbeCache() {
13
+ lmStudioProbeCache.clear();
14
+ }
15
+
16
+
17
+ export async function probeLmStudioRuntime(baseUrl = 'http://127.0.0.1:1234/v1', options = {}) {
18
+ const { timeoutMs = 2500, bypassCache = false } = options;
19
+ const key = String(baseUrl || 'http://127.0.0.1:1234/v1');
20
+
21
+ if (!bypassCache) {
22
+ const cached = lmStudioProbeCache.get(key);
23
+ if (cached) {
24
+ if (cached.inFlight) return cached.inFlight;
25
+ if (Date.now() - cached.fetchedAt < LMSTUDIO_PROBE_TTL_MS) return cached.data;
26
+ }
27
+ }
28
+
29
+ const inFlightPromise = probeLmStudioRuntimeUncached(baseUrl, timeoutMs);
30
+ const entry = { fetchedAt: Date.now(), data: null, inFlight: inFlightPromise };
31
+ lmStudioProbeCache.set(key, entry);
5
32
 
6
- export async function isLmStudioResponding(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
7
33
  try {
8
- const root = baseUrl.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
34
+ const result = await inFlightPromise;
35
+ entry.data = result;
36
+ entry.fetchedAt = Date.now();
37
+ return result;
38
+ } catch (err) {
39
+ lmStudioProbeCache.delete(key);
40
+ throw err;
41
+ } finally {
42
+ entry.inFlight = null;
43
+ }
44
+ }
45
+
46
+ async function probeLmStudioRuntimeUncached(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
47
+ const result = {
48
+ reachable: false,
49
+ loadedModel: null,
50
+ loadedModels: [],
51
+ loadedModelType: null,
52
+ loadedContextLength: null,
53
+ modelMaxContextLength: null,
54
+ quantization: null,
55
+ pendingModel: null,
56
+ models: []
57
+ };
58
+
59
+ const root = String(baseUrl || 'http://127.0.0.1:1234/v1').replace(/\/v1\/?$/, '').replace(/\/+$/, '');
60
+
61
+
62
+ try {
63
+ const res = await fetch(`${root}/api/v0/models`, { signal: AbortSignal.timeout(timeoutMs) });
64
+ if (res.ok) {
65
+ const data = await res.json();
66
+ const models = Array.isArray(data?.data) ? data.data : [];
67
+ if (models.length > 0) {
68
+ result.reachable = true;
69
+ result.models = models.map(m => m.id).filter(Boolean);
70
+
71
+ const loadedList = models.filter(m => m.state === 'loaded');
72
+ result.loadedModels = loadedList.map(m => m.id).filter(Boolean);
73
+
74
+ const activeLoaded = loadedList.find(m => m.type === 'llm' || m.type === 'vlm') || loadedList[0];
75
+ if (activeLoaded) {
76
+ result.loadedModel = activeLoaded.id || null;
77
+ result.loadedModelType = activeLoaded.type || null;
78
+ result.loadedContextLength = activeLoaded.loaded_context_length ?? activeLoaded.max_context_length ?? null;
79
+ result.modelMaxContextLength = activeLoaded.max_context_length ?? activeLoaded.loaded_context_length ?? null;
80
+ result.quantization = activeLoaded.quantization || null;
81
+ return result;
82
+ }
83
+
84
+ const pending = models.find(m => (m.state === 'loading') && (m.type === 'llm' || m.type === 'vlm'))
85
+ || models.find(m => m.state === 'loading');
86
+ if (pending) result.pendingModel = pending.id || null;
87
+ return result;
88
+ }
89
+ }
90
+ } catch {
91
+
92
+ }
93
+
94
+
95
+ try {
9
96
  const res = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(timeoutMs) });
10
- return res.ok;
97
+ if (res.ok) {
98
+ const data = await res.json();
99
+ const models = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : []);
100
+ result.reachable = true;
101
+ result.models = models.map(m => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
102
+ result.loadedModels = [...result.models];
103
+ result.loadedModel = result.models[0] || null;
104
+ result.loadedModelType = 'llm';
105
+ return result;
106
+ }
107
+ } catch {
108
+
109
+ }
110
+
111
+ return result;
112
+ }
113
+
114
+ export async function isLmStudioResponding(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
115
+ try {
116
+ const probe = await probeLmStudioRuntime(baseUrl, { timeoutMs, bypassCache: true });
117
+ return Boolean(probe?.reachable);
11
118
  } catch {
12
119
  return false;
13
120
  }
@@ -15,11 +122,8 @@ export async function isLmStudioResponding(baseUrl = 'http://127.0.0.1:1234/v1',
15
122
 
16
123
  export async function getLoadedModels(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
17
124
  try {
18
- const root = baseUrl.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
19
- const res = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(timeoutMs) });
20
- if (!res.ok) return [];
21
- const data = await res.json();
22
- return Array.isArray(data.data) ? data.data.map(m => m.id) : [];
125
+ const probe = await probeLmStudioRuntime(baseUrl, { timeoutMs, bypassCache: true });
126
+ return probe?.loadedModels || [];
23
127
  } catch {
24
128
  return [];
25
129
  }
@@ -30,6 +134,7 @@ export function resolveLmsCli() {
30
134
  const isWin = process.platform === 'win32';
31
135
  const candidates = [
32
136
  path.join(userHome, '.lmstudio', 'bin', isWin ? 'lms.exe' : 'lms'),
137
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'LM Studio', 'resources', 'app', 'bin', isWin ? 'lms.exe' : 'lms'),
33
138
  isWin ? 'lms.exe' : 'lms'
34
139
  ];
35
140
  return candidates.find(c => {
@@ -37,76 +142,260 @@ export function resolveLmsCli() {
37
142
  }) || (isWin ? 'lms.exe' : 'lms');
38
143
  }
39
144
 
145
+ export function normalizeLmStudioModelKey(modelInput) {
146
+ if (!modelInput || typeof modelInput !== 'string') return 'google/gemma-4-e4b';
147
+ const trimmed = modelInput.trim();
148
+ if (!trimmed) return 'google/gemma-4-e4b';
149
+
150
+ if (trimmed === 'google/gemma-4-e4b' || trimmed.startsWith('google/gemma-4-e4b@')) {
151
+ return trimmed;
152
+ }
153
+
154
+ const clean = trimmed.toLowerCase().replace(/\\/g, '/');
155
+ const baseName = clean.split('/').pop() || clean;
156
+
157
+ if (/(?=.*gemma)(?=.*e4b)/i.test(baseName)) {
158
+ if (/(?:q6|6bit)/i.test(baseName)) return 'google/gemma-4-e4b@q6_k';
159
+ if (/(?:q8|8bit)/i.test(baseName)) return 'google/gemma-4-e4b@q8_0';
160
+ return 'google/gemma-4-e4b';
161
+ }
162
+
163
+ if (baseName.endsWith('.gguf')) {
164
+ return baseName.slice(0, -5);
165
+ }
166
+
167
+ return trimmed;
168
+ }
169
+
170
+ export function resolveParallelSlotBudget(requested, availableCpus = os.cpus()?.length, options = {}) {
171
+ const detected = Number(availableCpus);
172
+ const logicalCpus = Number.isFinite(detected) && detected > 0 ? Math.floor(detected) : 4;
173
+ const physicalCores = Number(options?.physicalCores) || (options?.hyperThreading ? Math.max(1, Math.floor(logicalCpus / 2)) : logicalCpus);
174
+ const htMode = String(options?.htMode || 'auto').toLowerCase();
175
+
176
+ let effectiveMax = logicalCpus;
177
+ if (htMode === 'physical' || options?.hyperThreading === false) {
178
+ effectiveMax = physicalCores;
179
+ }
180
+
181
+ const ceiling = Math.max(1, Math.min(256, effectiveMax));
182
+ const num = Number(requested);
183
+ if (!Number.isFinite(num) || num < 1 || num > 256) {
184
+ return { value: undefined, requested: num, ceiling, capped: false, htMode, physicalCores, logicalCpus };
185
+ }
186
+ const floored = Math.floor(num);
187
+ const value = Math.min(floored, ceiling);
188
+ return { value, requested: floored, ceiling, capped: value < floored, htMode, physicalCores, logicalCpus };
189
+ }
190
+
191
+ export function buildLmsLoadArgs(model, options = {}) {
192
+ const args = ['load', model, '-y', '--identifier', model];
193
+ const isGemma4E4b = /(?:^|[\\/])gemma[-_. ]?4[-_. ]?e4b(?:@|$)|^google\/gemma-4-e4b/i.test(String(model || ''));
194
+ const gpu = options.gpuOffload ?? options.nGpuLayers ?? options.gpu;
195
+ if (gpu !== undefined && gpu !== null && gpu !== 'auto' && gpu !== '') {
196
+ const gpuStr = String(gpu).toLowerCase().trim();
197
+ if (gpuStr === 'off' || gpuStr === '0') {
198
+ args.push('--gpu', 'off');
199
+ } else if (gpuStr === 'max' || gpuStr === 'all') {
200
+ args.push('--gpu', 'max');
201
+ } else {
202
+ const num = Number(gpuStr);
203
+ if (Number.isFinite(num)) {
204
+ if (num >= 0 && num <= 1) {
205
+ args.push('--gpu', String(num));
206
+ } else if (num >= 90) {
207
+ args.push('--gpu', 'max');
208
+ } else if (num > 1) {
209
+ const ratio = Math.min(1, Math.round((num / 33) * 100) / 100);
210
+ args.push('--gpu', String(ratio));
211
+ }
212
+ }
213
+ }
214
+ }
215
+ const ctx = Number(options.contextLength ?? (isGemma4E4b ? 32768 : undefined));
216
+ if (Number.isFinite(ctx) && ctx >= 512 && ctx <= 131072) {
217
+ args.push('--context-length', String(Math.floor(ctx)));
218
+ }
219
+ const parallel = options.parallel !== undefined
220
+ ? Number(options.parallel)
221
+ : (options.parallelSlots !== undefined
222
+ ? Number(options.parallelSlots)
223
+ : (options.cpuThreads !== undefined ? Number(options.cpuThreads) : (options.threads !== undefined ? Number(options.threads) : (isGemma4E4b ? 1 : undefined))));
224
+ const budget = resolveParallelSlotBudget(parallel, options.availableCpus, {
225
+ htMode: options.htMode || options.hyperThreadingMode,
226
+ physicalCores: options.physicalCores,
227
+ hyperThreading: options.hyperThreading,
228
+ });
229
+ if (budget.value !== undefined) {
230
+ args.push('--parallel', String(budget.value));
231
+ }
232
+
233
+ if (options.ttl && Number.isFinite(Number(options.ttl)) && Number(options.ttl) > 0) {
234
+ args.push('--ttl', String(Math.floor(Number(options.ttl))));
235
+ }
236
+ return args;
237
+ }
238
+
40
239
  export async function loadLmStudioModel(model, options = {}) {
41
240
  const lmsCmd = resolveLmsCli();
42
- return new Promise((resolve) => {
43
- const args = ['load', model, '-y', '--identifier', model];
44
- const child = spawn(lmsCmd, args, {
45
- windowsHide: true,
46
- stdio: 'pipe',
47
- shell: process.platform === 'win32'
48
- });
241
+ const normalized = normalizeLmStudioModelKey(model);
242
+
243
+ const executeLoad = (targetModel) => new Promise((resolve) => {
244
+ const args = buildLmsLoadArgs(targetModel, options);
245
+ const isCmdOrBat = process.platform === 'win32' && (lmsCmd.toLowerCase().endsWith('.cmd') || lmsCmd.toLowerCase().endsWith('.bat'));
246
+ let child;
247
+ if (isCmdOrBat) {
248
+ const cmdString = `"${lmsCmd}" ${args.map(a => `"${a}"`).join(' ')}`;
249
+ child = spawn(cmdString, {
250
+ windowsHide: true,
251
+ stdio: 'pipe',
252
+ shell: true
253
+ });
254
+ } else {
255
+ child = spawn(lmsCmd, args, {
256
+ windowsHide: true,
257
+ stdio: 'pipe',
258
+ shell: false
259
+ });
260
+ }
49
261
  let output = '';
50
262
  child.stdout?.on('data', d => { output += d.toString(); });
51
263
  child.stderr?.on('data', d => { output += d.toString(); });
52
264
  child.on('close', code => {
53
- resolve({ ok: code === 0, output });
265
+ resolve({ ok: code === 0, output, args, exitCode: code });
54
266
  });
55
267
  child.on('error', err => {
56
- resolve({ ok: false, error: err.message });
268
+ resolve({ ok: false, error: err.message, args });
57
269
  });
58
270
  });
271
+
272
+ const primaryResult = await executeLoad(normalized);
273
+ if (primaryResult.ok || primaryResult.exitCode === 0) {
274
+ return primaryResult;
275
+ }
276
+
277
+ if (primaryResult.output && /Model not found|No model found/i.test(primaryResult.output)) {
278
+ try {
279
+ const isCmdOrBat = process.platform === 'win32' && (lmsCmd.toLowerCase().endsWith('.cmd') || lmsCmd.toLowerCase().endsWith('.bat'));
280
+ const lsOutput = await new Promise((res) => {
281
+ const lsChild = isCmdOrBat
282
+ ? spawn(`"${lmsCmd}" ls --json`, { windowsHide: true, stdio: 'pipe', shell: true })
283
+ : spawn(lmsCmd, ['ls', '--json'], { windowsHide: true, stdio: 'pipe', shell: false });
284
+ let out = '';
285
+ lsChild.stdout?.on('data', d => { out += d.toString(); });
286
+ lsChild.stderr?.on('data', d => { out += d.toString(); });
287
+ lsChild.on('close', () => res(out));
288
+ lsChild.on('error', () => res(''));
289
+ });
290
+
291
+ if (lsOutput.trim()) {
292
+ const models = JSON.parse(lsOutput.trim());
293
+ if (Array.isArray(models) && models.length > 0) {
294
+ const rawSearch = String(model || '').toLowerCase();
295
+ const cleanSearch = rawSearch.replace(/\\/g, '/').split('/').pop().replace(/\.gguf$/i, '');
296
+ const match = models.find(m => {
297
+ const key = String(m.modelKey || '').toLowerCase();
298
+ const pathStr = String(m.path || '').toLowerCase();
299
+ const name = String(m.displayName || '').toLowerCase();
300
+ return key === cleanSearch || pathStr.includes(cleanSearch) || name.includes(cleanSearch) ||
301
+ (cleanSearch.includes('gemma') && (key.includes('gemma') || name.includes('gemma')));
302
+ });
303
+ if (match?.modelKey && match.modelKey !== normalized) {
304
+ const retryRes = await executeLoad(match.modelKey);
305
+ if (retryRes.ok || retryRes.exitCode === 0) {
306
+ return retryRes;
307
+ }
308
+ }
309
+ }
310
+ }
311
+ } catch {}
312
+ }
313
+
314
+ return primaryResult;
59
315
  }
60
316
 
61
317
  export async function wakeUpLmStudio(options = {}) {
62
318
  const {
63
319
  baseUrl = 'http://127.0.0.1:1234/v1',
64
- model = 'google/gemma-4-e4b',
65
- maxWaitMs = 35000
320
+ model: rawModel = 'google/gemma-4-e4b',
321
+ maxWaitMs = 60000,
322
+ quiet = false,
323
+ gpuOffload,
324
+ cpuThreads,
325
+ contextLength,
326
+ parallel,
327
+ splitMode,
328
+ tensorSplit,
329
+ mainGpu,
330
+ flashAttn,
331
+ batchSize,
332
+ ubatchSize,
333
+ htMode,
334
+ hyperThreadingMode,
335
+ physicalCores,
336
+ hyperThreading,
66
337
  } = options;
67
338
 
68
- const isLocal = baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost');
339
+ const model = normalizeLmStudioModelKey(rawModel);
340
+
341
+ const isLocal = baseUrl.includes('127.0.0.1:1234') || baseUrl.includes('localhost:1234') || baseUrl.includes(':1234');
69
342
  if (!isLocal) {
70
- return { ok: false, reason: 'Non-local endpoint, automatic wakeup skipped' };
343
+ return { ok: false, reason: 'Non-local or non-standard LM Studio endpoint, automatic wakeup skipped' };
71
344
  }
72
345
 
346
+ let probe = await probeLmStudioRuntime(baseUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
347
+ let serverUp = Boolean(probe?.reachable);
73
348
 
74
- let serverUp = await isLmStudioResponding(baseUrl, 2000);
75
349
  if (!serverUp) {
76
- const userHome = os.homedir();
77
- const possiblePaths = [
78
- path.join(userHome, 'AppData', 'Local', 'Programs', 'LM Studio', 'LM Studio.exe'),
79
- path.join(process.env.LOCALAPPDATA || '', 'Programs', 'LM Studio', 'LM Studio.exe'),
80
- 'C:\\Program Files\\LM Studio\\LM Studio.exe'
81
- ];
82
-
83
- const appExe = possiblePaths.find(p => p && fs.existsSync(p));
84
- if (appExe) {
85
- try {
86
- const child = spawn(appExe, [], {
350
+
351
+ try {
352
+ const lmsCmd = resolveLmsCli();
353
+ const portMatch = String(baseUrl).match(/:([0-9]+)/);
354
+ const targetPort = portMatch ? portMatch[1] : '1234';
355
+ const isCmdOrBat = process.platform === 'win32' && (lmsCmd.toLowerCase().endsWith('.cmd') || lmsCmd.toLowerCase().endsWith('.bat'));
356
+ let lmsChild;
357
+ if (isCmdOrBat) {
358
+ const cmdString = `"${lmsCmd}" server start --port ${targetPort} --cors`;
359
+ lmsChild = spawn(cmdString, {
87
360
  detached: true,
88
361
  stdio: 'ignore',
89
- windowsHide: false
362
+ windowsHide: true,
363
+ shell: true
90
364
  });
91
- child.unref();
92
- } catch {}
93
- }
94
-
95
- try {
96
- const lmsCmd = resolveLmsCli();
97
- const lmsChild = spawn(lmsCmd, ['server', 'start'], {
98
- detached: true,
99
- stdio: 'ignore',
100
- windowsHide: true,
101
- shell: process.platform === 'win32'
102
- });
365
+ } else {
366
+ lmsChild = spawn(lmsCmd, ['server', 'start', '--port', targetPort, '--cors'], {
367
+ detached: true,
368
+ stdio: 'ignore',
369
+ windowsHide: true,
370
+ shell: false
371
+ });
372
+ }
373
+ if (lmsChild.pid) {
374
+ registerBackgroundProcess('lmstudio-server', lmsChild, {
375
+ command: lmsCmd,
376
+ args: ['server', 'start', '--port', targetPort, '--cors'],
377
+ metadata: { type: 'headless-task', port: targetPort, baseUrl, persistOnExit: true },
378
+ persistOnExit: true,
379
+ });
380
+ registerBackgroundProcess('lmstudio', lmsChild, {
381
+ command: lmsCmd,
382
+ args: ['server', 'start', '--port', targetPort, '--cors'],
383
+ metadata: { type: 'headless-task', port: targetPort, baseUrl, persistOnExit: true },
384
+ persistOnExit: true,
385
+ });
386
+ }
103
387
  lmsChild.unref();
104
- } catch {}
388
+ } catch (err) {
389
+ if (!quiet) {
390
+ try { process.stderr.write(`[Kiku Auto-Wakeup] Failed to launch headless LM Studio server: ${err.message}\n`); } catch {}
391
+ }
392
+ }
105
393
 
106
394
  const start = Date.now();
107
395
  while (Date.now() - start < maxWaitMs) {
108
- await new Promise(r => setTimeout(r, 1500));
109
- if (await isLmStudioResponding(baseUrl, 2000)) {
396
+ await new Promise(r => setTimeout(r, 1200));
397
+ probe = await probeLmStudioRuntime(baseUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
398
+ if (probe?.reachable) {
110
399
  serverUp = true;
111
400
  break;
112
401
  }
@@ -114,31 +403,194 @@ export async function wakeUpLmStudio(options = {}) {
114
403
  }
115
404
 
116
405
  if (!serverUp) {
117
- return { ok: false, reason: 'Timed out waiting for LM Studio server' };
406
+ return { ok: false, reason: 'Timed out waiting for LM Studio server on port 1234' };
118
407
  }
119
408
 
120
-
121
409
  if (model) {
122
- const loaded = await getLoadedModels(baseUrl);
123
- if (!loaded.includes(model)) {
124
- try {
125
- process.stdout.write(`\n🌸 [Kiku Auto-Wakeup] LM Studio server active. Auto-loading model "${model}" into VRAM...\n`);
126
- } catch {}
127
- await loadLmStudioModel(model);
410
+ const isModelLoaded = (probe?.loadedModels && probe.loadedModels.includes(model)) ||
411
+ (probe?.loadedModel && (probe.loadedModel === model || probe.loadedModel.includes(model)));
412
+ if (!isModelLoaded) {
413
+ if (!quiet) {
414
+ try {
415
+ process.stdout.write(`\n🌸 [Kiku Auto-Wakeup] LM Studio server active. Auto-loading model "${model}" into VRAM...\n`);
416
+ } catch {}
417
+ }
418
+ const loadRes = await loadLmStudioModel(model, {
419
+ gpuOffload,
420
+ cpuThreads,
421
+ contextLength,
422
+ parallel,
423
+ splitMode,
424
+ tensorSplit,
425
+ mainGpu,
426
+ flashAttn,
427
+ batchSize,
428
+ ubatchSize,
429
+ htMode: htMode || hyperThreadingMode,
430
+ physicalCores,
431
+ hyperThreading,
432
+ });
433
+ if (!loadRes.ok && loadRes.exitCode !== 0) {
434
+ const immediateProbe = await probeLmStudioRuntime(baseUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
435
+ const loaded = (immediateProbe?.loadedModels && immediateProbe.loadedModels.includes(model)) ||
436
+ (immediateProbe?.loadedModel && (immediateProbe.loadedModel === model || immediateProbe.loadedModel.includes(model)));
437
+ if (!loaded) {
438
+ return {
439
+ ok: false,
440
+ modelLoaded: false,
441
+ reason: `LM Studio CLI failed to load model "${model}" (exit code ${loadRes.exitCode ?? 'error'}). ${loadRes.output || loadRes.error || ''}`.trim()
442
+ };
443
+ }
444
+ }
128
445
 
129
446
  const start = Date.now();
447
+ let ready = false;
130
448
  while (Date.now() - start < maxWaitMs) {
131
- await new Promise(r => setTimeout(r, 1500));
132
- const currentModels = await getLoadedModels(baseUrl);
133
- if (currentModels.includes(model)) {
134
- try {
135
- process.stdout.write(`🌸 [Kiku Auto-Wakeup] Model "${model}" loaded and ready!\n\n`);
136
- } catch {}
137
- return { ok: true, awakened: true, modelLoaded: true };
449
+ await new Promise(r => setTimeout(r, 1200));
450
+ probe = await probeLmStudioRuntime(baseUrl, { timeoutMs: 2000, bypassCache: true }).catch(() => null);
451
+ ready = (probe?.loadedModels && probe.loadedModels.includes(model)) ||
452
+ (probe?.loadedModel && (probe.loadedModel === model || probe.loadedModel.includes(model)));
453
+ if (ready) {
454
+ if (!quiet) {
455
+ try {
456
+ process.stdout.write(`🌸 [Kiku Auto-Wakeup] Model "${model}" loaded and ready!\n\n`);
457
+ } catch {}
458
+ }
459
+ return { ok: true, awakened: true, modelLoaded: true, loadedModel: model };
138
460
  }
139
461
  }
462
+
463
+ if (!ready) {
464
+ return {
465
+ ok: false,
466
+ modelLoaded: false,
467
+ reason: `Timed out waiting for model "${model}" to load into VRAM (${Math.round(maxWaitMs / 1000)}s)`
468
+ };
469
+ }
470
+ }
471
+ }
472
+
473
+ return { ok: true, alreadyRunning: true, modelLoaded: true, loadedModel: probe?.loadedModel || model };
474
+ }
475
+
476
+ export async function unloadLmStudioModel(model = null, options = {}) {
477
+ const baseUrl = options.baseUrl || 'http://127.0.0.1:1234/v1';
478
+ const root = baseUrl.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
479
+ const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 2500;
480
+ const isDefaultPort = !options.baseUrl || options.baseUrl.includes(':1234');
481
+ let restSuccess = false;
482
+ let restMessage = '';
483
+
484
+ let loadedModels = [];
485
+ try {
486
+ const probeRes = await fetch(`${root}/api/v0/models`, { signal: AbortSignal.timeout(timeoutMs) });
487
+ if (probeRes.ok) {
488
+ const probeData = await probeRes.json().catch(() => null);
489
+ const items = Array.isArray(probeData?.data) ? probeData.data : [];
490
+ loadedModels = items.filter(m => m.state === 'loaded').map(m => m.id).filter(Boolean);
491
+ }
492
+ } catch {}
493
+
494
+ if (loadedModels.length === 0) {
495
+ try {
496
+ const probeRes = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(timeoutMs) });
497
+ if (probeRes.ok) {
498
+ const probeData = await probeRes.json().catch(() => null);
499
+ const items = Array.isArray(probeData?.data) ? probeData.data : (Array.isArray(probeData) ? probeData : []);
500
+ loadedModels = items.map(m => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
501
+ }
502
+ } catch {}
503
+ }
504
+
505
+ const rawModel = model ? String(model).trim() : '';
506
+ const normalizedKey = rawModel ? normalizeLmStudioModelKey(rawModel) : '';
507
+
508
+ let targetIds = [];
509
+ if (rawModel && rawModel !== 'all') {
510
+ const exact = loadedModels.find(id => id.toLowerCase() === rawModel.toLowerCase());
511
+ const norm = loadedModels.find(id => id.toLowerCase() === normalizedKey.toLowerCase());
512
+ const partial = loadedModels.find(id => {
513
+ const lid = id.toLowerCase();
514
+ const lraw = rawModel.toLowerCase();
515
+ const lnorm = normalizedKey.toLowerCase();
516
+ return lid.includes(lraw) || lraw.includes(lid) || (lnorm && (lid.includes(lnorm) || lnorm.includes(lid)));
517
+ });
518
+
519
+ if (exact) targetIds = [exact];
520
+ else if (norm) targetIds = [norm];
521
+ else if (partial) targetIds = [partial];
522
+ else if (loadedModels.length === 1) targetIds = [loadedModels[0]];
523
+ else targetIds = [normalizedKey || rawModel];
524
+ } else {
525
+ targetIds = loadedModels.length > 0 ? [...loadedModels] : [];
526
+ }
527
+
528
+ let unloadedCount = 0;
529
+ if (targetIds.length > 0) {
530
+ for (const tid of targetIds) {
531
+ try {
532
+ const res = await fetch(`${root}/api/v1/models/unload`, {
533
+ method: 'POST',
534
+ headers: { 'Content-Type': 'application/json' },
535
+ body: JSON.stringify({ instance_id: tid }),
536
+ signal: AbortSignal.timeout(timeoutMs)
537
+ });
538
+ if (res.ok) {
539
+ const data = await res.json().catch(() => null);
540
+ if (data && (data.instance_id || !data.error)) {
541
+ unloadedCount++;
542
+ }
543
+ }
544
+ } catch {}
140
545
  }
141
546
  }
142
547
 
143
- return { ok: true, alreadyRunning: true };
548
+ if (unloadedCount > 0) {
549
+ restSuccess = true;
550
+ restMessage = 'Model successfully unloaded from LM Studio and GPU VRAM released';
551
+ }
552
+
553
+ let cliSuccess = false;
554
+ let cliOutput = '';
555
+ if (!restSuccess && isDefaultPort) {
556
+ try {
557
+ const lmsCmd = resolveLmsCli();
558
+ const runCli = (args) => {
559
+ const isCmdOrBat = process.platform === 'win32' && (lmsCmd.toLowerCase().endsWith('.cmd') || lmsCmd.toLowerCase().endsWith('.bat'));
560
+ return new Promise((resolve) => {
561
+ const child = isCmdOrBat
562
+ ? spawn(`"${lmsCmd}" ${args.join(' ')}`, { windowsHide: true, stdio: 'pipe', shell: true })
563
+ : spawn(lmsCmd, args, { windowsHide: true, stdio: 'pipe', shell: false });
564
+ let out = '';
565
+ child.stdout?.on('data', d => { out += d.toString(); });
566
+ child.stderr?.on('data', d => { out += d.toString(); });
567
+ child.on('close', code => {
568
+ const combined = out.trim();
569
+ const hasError = !combined || /(?:model not found|cannot find a model|error|failed)/i.test(combined);
570
+ resolve({ ok: code === 0 && !hasError, code, out: combined });
571
+ });
572
+ child.on('error', err => resolve({ ok: false, code: -1, out: err.message }));
573
+ });
574
+ };
575
+
576
+ const primaryTarget = targetIds[0] || (rawModel !== 'all' ? (normalizedKey || rawModel) : null);
577
+ let cliResult = await runCli(primaryTarget ? ['unload', primaryTarget] : ['unload', '--all']);
578
+ if (!cliResult.ok) {
579
+ cliResult = await runCli(['unload', '--all']);
580
+ }
581
+ cliSuccess = cliResult.ok || /(?:unloaded|released|success|no models to unload)/i.test(cliResult.out);
582
+ cliOutput = cliResult.out;
583
+ } catch {}
584
+ }
585
+
586
+ clearLmStudioProbeCache();
587
+
588
+ const success = restSuccess || cliSuccess || (loadedModels.length === 0 && Boolean(cliOutput && /no models to unload/i.test(cliOutput)));
589
+ return {
590
+ ok: success,
591
+ unloaded: success,
592
+ engine: 'lms',
593
+ model: targetIds[0] || model || 'all',
594
+ message: success ? (restMessage || cliOutput || 'Model successfully unloaded from VRAM') : (cliOutput || 'Failed to unload model from LM Studio')
595
+ };
144
596
  }