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
@@ -1,6 +1,15 @@
1
- import { config, CLOUD_PROVIDERS, normalizeProviderModel, matchModelIdentifier, extractModelQuantization, savePersistedProviderKey, savePersistedActiveProvider, savePersistedProviderModel, getPersistedActiveProvider } from './config.js';
1
+ import { config, CLOUD_PROVIDERS, normalizeProviderModel, matchModelIdentifier, extractModelQuantization, savePersistedProviderKey, savePersistedActiveProvider, savePersistedProviderModel, getPersistedActiveProvider, getProviderBaseUrl, savePersistedProviderBaseUrl, setActiveProvider } from './config.js';
2
2
  import { parseApiErrorText } from './errors.js';
3
3
  import { estimateTokens } from './telemetry.js';
4
+ import { estimateTokenCount } from './contextCompactor.js';
5
+
6
+ function computeRetryDelay(attempt, headers) {
7
+ const retryAfter = Number(headers?.get?.('retry-after'));
8
+ if (Number.isFinite(retryAfter) && retryAfter > 0) {
9
+ return Math.min(retryAfter * 1000, 15000) + Math.floor(Math.random() * 400);
10
+ }
11
+ return Math.min(8000, 1200 * 2 ** attempt) + Math.floor(Math.random() * 400);
12
+ }
4
13
 
5
14
  export class LLMClient {
6
15
  constructor(options = {}) {
@@ -12,7 +21,7 @@ export class LLMClient {
12
21
 
13
22
  this.provider = providerId;
14
23
  this.providerName = providerDef.name || providerId;
15
- this.baseUrl = (options.baseUrl || providerDef.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
24
+ this.baseUrl = (options.baseUrl || getProviderBaseUrl(providerId) || providerDef.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
16
25
  this.apiKey = options.apiKey !== undefined
17
26
  ? options.apiKey
18
27
  : (providerDef.apiKey !== undefined ? providerDef.apiKey : (providerDef.isLocal ? (config.lmStudio.apiKey || 'lm-studio') : ''));
@@ -33,12 +42,21 @@ export class LLMClient {
33
42
 
34
43
  this.provider = pId;
35
44
  this.providerName = providerDef.name || pId;
36
- this.baseUrl = (options.baseUrl || providerDef.baseUrl).replace(/\/+$/, '');
45
+ this.baseUrl = (options.baseUrl || getProviderBaseUrl(pId) || providerDef.baseUrl).replace(/\/+$/, '');
37
46
  this.apiKey = options.apiKey !== undefined ? options.apiKey : (providerDef.apiKey || '');
38
47
  this.model = normalizeProviderModel(pId, options.model || providerDef.defaultModel);
39
48
  this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
40
49
 
41
- savePersistedActiveProvider(pId, this.model);
50
+ setActiveProvider(pId, { model: this.model, baseUrl: options.baseUrl || null });
51
+ return this.getProviderInfo();
52
+ }
53
+
54
+ setBaseUrl(baseUrl, providerId = null) {
55
+ const targetProvider = String(providerId || this.provider).toLowerCase().trim();
56
+ const cleanUrl = savePersistedProviderBaseUrl(targetProvider, baseUrl);
57
+ if (targetProvider === this.provider) {
58
+ this.baseUrl = cleanUrl;
59
+ }
42
60
  return this.getProviderInfo();
43
61
  }
44
62
 
@@ -91,6 +109,8 @@ export class LLMClient {
91
109
  }
92
110
  } else if (this.provider === 'lmstudio') {
93
111
  headers['Authorization'] = 'Bearer lm-studio';
112
+ } else if (this.provider === 'vllm') {
113
+ headers['Authorization'] = 'Bearer token-kiku';
94
114
  }
95
115
 
96
116
  return headers;
@@ -141,6 +161,9 @@ export class LLMClient {
141
161
  } else if (res.status === 429) {
142
162
  formattedError = `429 Rate Limit / Quota Exceeded: ${errSnippet}`;
143
163
  }
164
+ if (def.platformNotice && process.platform === 'win32') {
165
+ formattedError += `\n Notice: ${def.platformNotice}`;
166
+ }
144
167
 
145
168
  return {
146
169
  connected: false,
@@ -168,16 +191,19 @@ export class LLMClient {
168
191
  const exactMatch = models.find(m => m === this.model);
169
192
  const fuzzyMatch = models.find(m => matchModelIdentifier(this.model, m));
170
193
 
171
-
172
- const matchedGemma = (activeLoaded && /gemma/i.test(activeLoaded))
173
- ? activeLoaded
174
- : (fuzzyMatch || models.find(m =>
175
- m === 'google/gemma-4-e4b' ||
176
- m === 'gemma-4-e4b' ||
177
- m.toLowerCase().includes('gemma-4-e4b') ||
178
- (m.toLowerCase().includes('gemma') && m.toLowerCase().includes('e4b')) ||
179
- /gemma/i.test(m)
180
- ));
194
+
195
+ const modelIsGemmaOrUnset = !this.model || /gemma/i.test(this.model);
196
+ const matchedGemma = modelIsGemmaOrUnset
197
+ ? ((activeLoaded && /gemma/i.test(activeLoaded))
198
+ ? activeLoaded
199
+ : (fuzzyMatch || models.find(m =>
200
+ m === 'google/gemma-4-e4b' ||
201
+ m === 'gemma-4-e4b' ||
202
+ m.toLowerCase().includes('gemma-4-e4b') ||
203
+ (m.toLowerCase().includes('gemma') && m.toLowerCase().includes('e4b')) ||
204
+ /gemma/i.test(m)
205
+ )))
206
+ : null;
181
207
 
182
208
  const resolvedActive = exactMatch || (activeLoaded ? activeLoaded : (fuzzyMatch || matchedGemma || this.model));
183
209
 
@@ -214,32 +240,66 @@ export class LLMClient {
214
240
  };
215
241
  } catch (err) {
216
242
  const latencyMs = Date.now() - startTime;
243
+ let errMsg = err.name === 'AbortError'
244
+ ? `Connection timed out at ${this.baseUrl} (>8s)`
245
+ : `Cannot reach ${this.providerName} at ${this.baseUrl} (${err.message})`;
246
+ if (def.platformNotice && process.platform === 'win32') {
247
+ errMsg += `\n Notice: ${def.platformNotice}`;
248
+ }
217
249
  return {
218
250
  connected: false,
219
251
  provider: this.provider,
220
252
  providerName: this.providerName,
221
- error: err.name === 'AbortError'
222
- ? `Connection timed out at ${this.baseUrl} (>8s)`
223
- : `Cannot reach ${this.providerName} at ${this.baseUrl} (${err.message})`,
253
+ error: errMsg,
224
254
  models: [],
225
255
  latencyMs
226
256
  };
227
257
  }
228
258
  }
229
259
 
230
- async chat({
231
- messages,
232
- tools = null,
233
- temperature = 0.3,
234
- maxTokens = 8192,
235
- stream = true,
236
- signal = null,
237
- onToken = null,
238
- onThought = null
239
- }) {
240
- const isLegacyReasoningModel = /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
260
+ async healthCheck() {
261
+ return this.checkConnection();
262
+ }
263
+
264
+ _isLegacyReasoningModel() {
265
+ return /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
266
+ }
267
+
268
+
269
+ toolsWillBeOmitted() {
270
+ return this._isLegacyReasoningModel();
271
+ }
272
+
273
+ async chat(firstArg, secondArg = {}) {
274
+ let messages, tools, temperature, maxTokens, stream, signal, onToken, onThought;
275
+ if (Array.isArray(firstArg)) {
276
+ messages = firstArg;
277
+ ({
278
+ tools = null,
279
+ temperature = 0.3,
280
+ maxTokens = 8192,
281
+ stream = true,
282
+ signal = null,
283
+ onToken = null,
284
+ onThought = null
285
+ } = secondArg || {});
286
+ } else {
287
+ ({
288
+ messages,
289
+ tools = null,
290
+ temperature = 0.3,
291
+ maxTokens = 8192,
292
+ stream = true,
293
+ signal = null,
294
+ onToken = null,
295
+ onThought = null
296
+ } = firstArg || {});
297
+ }
298
+ const isLegacyReasoningModel = this._isLegacyReasoningModel();
241
299
  const usesCompletionTokenLimit = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
242
300
  const rejectsTemperature = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
301
+
302
+ this.toolsDisabledReason = null;
243
303
 
244
304
  const buildPayload = (includeTools = true, includeTemp = true) => {
245
305
  const p = {
@@ -258,9 +318,13 @@ export class LLMClient {
258
318
  p.max_tokens = maxTokens;
259
319
  }
260
320
 
261
- if (includeTools && tools && Array.isArray(tools) && tools.length > 0 && !isLegacyReasoningModel) {
262
- p.tools = tools;
263
- p.tool_choice = "auto";
321
+ if (includeTools && tools && Array.isArray(tools) && tools.length > 0) {
322
+ if (isLegacyReasoningModel) {
323
+ this.toolsDisabledReason = 'reasoning_model';
324
+ } else {
325
+ p.tools = tools;
326
+ p.tool_choice = "auto";
327
+ }
264
328
  }
265
329
 
266
330
  if (stream) {
@@ -278,7 +342,11 @@ export class LLMClient {
278
342
  const abortHandler = () => controller.abort();
279
343
  if (signal) signal.addEventListener('abort', abortHandler, { once: true });
280
344
 
345
+ let lastTimeoutReset = 0;
281
346
  const resetTimeout = () => {
347
+ const now = Date.now();
348
+ if (now - lastTimeoutReset < 500) return;
349
+ lastTimeoutReset = now;
282
350
  clearTimeout(timeoutId);
283
351
  timeoutId = setTimeout(() => controller.abort(), this.timeout);
284
352
  };
@@ -308,6 +376,7 @@ export class LLMClient {
308
376
 
309
377
  if ((lowerErr.includes('tool') || lowerErr.includes('function') || lowerErr.includes('schema')) && payload.tools) {
310
378
  payload = buildPayload(false, true);
379
+ this.toolsDisabledReason = 'provider_rejected_schema';
311
380
  retryNeeded = true;
312
381
  } else if (lowerErr.includes('temperature') && payload.temperature !== undefined) {
313
382
  payload = buildPayload(Boolean(payload.tools), false);
@@ -345,8 +414,7 @@ export class LLMClient {
345
414
  }
346
415
 
347
416
  if ([429, 500, 502, 503, 504].includes(res.status) && attempt < maxRetries && !controller.signal.aborted) {
348
- const backoffMs = (attempt + 1) * 1200;
349
- await new Promise(r => setTimeout(r, backoffMs));
417
+ await new Promise(r => setTimeout(r, computeRetryDelay(attempt, res.headers)));
350
418
  continue;
351
419
  }
352
420
 
@@ -362,8 +430,7 @@ export class LLMClient {
362
430
  if (controller.signal.aborted || attempt >= maxRetries || fetchErr.status) {
363
431
  throw fetchErr;
364
432
  }
365
- const backoffMs = (attempt + 1) * 1200;
366
- await new Promise(r => setTimeout(r, backoffMs));
433
+ await new Promise(r => setTimeout(r, computeRetryDelay(attempt, null)));
367
434
  }
368
435
  }
369
436
 
@@ -388,8 +455,7 @@ export class LLMClient {
388
455
  total_tokens: data.usage.total_tokens || ((data.usage.prompt_tokens || 0) + (data.usage.completion_tokens || 0))
389
456
  };
390
457
  } else {
391
- const inputPrompt = messages.map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''))).join('\n');
392
- const pTokens = estimateTokens(inputPrompt);
458
+ const pTokens = estimateTokenCount(messages);
393
459
  const cTokens = estimateTokens(msg.content || '');
394
460
  msg.usage = {
395
461
  prompt_tokens: pTokens,
@@ -409,6 +475,7 @@ export class LLMClient {
409
475
  let toolCalls = [];
410
476
  let buffer = '';
411
477
  let reportedUsage = null;
478
+ let malformedChunks = 0;
412
479
 
413
480
  while (true) {
414
481
  const { done, value } = await reader.read();
@@ -460,19 +527,24 @@ export class LLMClient {
460
527
  }
461
528
  }
462
529
  } catch {
530
+ malformedChunks++;
463
531
  }
464
532
  }
465
533
  }
466
534
 
467
535
  clearTimeout(timeoutId);
468
536
 
537
+ if (malformedChunks > 0) {
538
+
539
+ console.warn(`[${this.providerName}] Dropped ${malformedChunks} malformed SSE chunk(s) during streaming.`);
540
+ }
541
+
469
542
  let promptTokens = reportedUsage?.prompt_tokens ?? 0;
470
543
  let completionTokens = reportedUsage?.completion_tokens ?? 0;
471
544
  let reasoningTokens = reportedUsage?.completion_tokens_details?.reasoning_tokens ?? 0;
472
545
 
473
546
  if (!promptTokens) {
474
- const inputPrompt = messages.map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''))).join('\n');
475
- promptTokens = estimateTokens(inputPrompt);
547
+ promptTokens = estimateTokenCount(messages);
476
548
  }
477
549
  if (!completionTokens) {
478
550
  completionTokens = estimateTokens(fullContent + (fullReasoning || ''));
@@ -0,0 +1,144 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import fs from 'node:fs';
5
+
6
+ export async function isLmStudioResponding(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
7
+ try {
8
+ const root = baseUrl.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
9
+ const res = await fetch(`${root}/v1/models`, { signal: AbortSignal.timeout(timeoutMs) });
10
+ return res.ok;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ export async function getLoadedModels(baseUrl = 'http://127.0.0.1:1234/v1', timeoutMs = 2500) {
17
+ 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) : [];
23
+ } catch {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ export function resolveLmsCli() {
29
+ const userHome = os.homedir();
30
+ const isWin = process.platform === 'win32';
31
+ const candidates = [
32
+ path.join(userHome, '.lmstudio', 'bin', isWin ? 'lms.exe' : 'lms'),
33
+ isWin ? 'lms.exe' : 'lms'
34
+ ];
35
+ return candidates.find(c => {
36
+ try { return fs.existsSync(c); } catch { return false; }
37
+ }) || (isWin ? 'lms.exe' : 'lms');
38
+ }
39
+
40
+ export async function loadLmStudioModel(model, options = {}) {
41
+ 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
+ });
49
+ let output = '';
50
+ child.stdout?.on('data', d => { output += d.toString(); });
51
+ child.stderr?.on('data', d => { output += d.toString(); });
52
+ child.on('close', code => {
53
+ resolve({ ok: code === 0, output });
54
+ });
55
+ child.on('error', err => {
56
+ resolve({ ok: false, error: err.message });
57
+ });
58
+ });
59
+ }
60
+
61
+ export async function wakeUpLmStudio(options = {}) {
62
+ const {
63
+ baseUrl = 'http://127.0.0.1:1234/v1',
64
+ model = 'google/gemma-4-e4b',
65
+ maxWaitMs = 35000
66
+ } = options;
67
+
68
+ const isLocal = baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost');
69
+ if (!isLocal) {
70
+ return { ok: false, reason: 'Non-local endpoint, automatic wakeup skipped' };
71
+ }
72
+
73
+
74
+ let serverUp = await isLmStudioResponding(baseUrl, 2000);
75
+ 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, [], {
87
+ detached: true,
88
+ stdio: 'ignore',
89
+ windowsHide: false
90
+ });
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
+ });
103
+ lmsChild.unref();
104
+ } catch {}
105
+
106
+ const start = Date.now();
107
+ while (Date.now() - start < maxWaitMs) {
108
+ await new Promise(r => setTimeout(r, 1500));
109
+ if (await isLmStudioResponding(baseUrl, 2000)) {
110
+ serverUp = true;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+
116
+ if (!serverUp) {
117
+ return { ok: false, reason: 'Timed out waiting for LM Studio server' };
118
+ }
119
+
120
+
121
+ 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);
128
+
129
+ const start = Date.now();
130
+ 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 };
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ return { ok: true, alreadyRunning: true };
144
+ }
@@ -185,7 +185,6 @@ export class McpClientManager {
185
185
  let resources = serverConfig.resources || [];
186
186
  let prompts = serverConfig.prompts || [];
187
187
 
188
- // If tools not pre-provided, fetch once upon connection
189
188
  if (!tools || tools.length === 0) {
190
189
  try {
191
190
  if (!this._isRateLimited(name)) {
@@ -570,7 +569,6 @@ export class McpClientManager {
570
569
  if (cfg.enabled === false || cfg.disabled === true) continue;
571
570
  if (this.servers.has(name)) continue;
572
571
 
573
- // If tools are cached or it's a known Kiku remote MCP server, register lazily without network call on startup
574
572
  let tools = cfg.tools;
575
573
  if (!tools || tools.length === 0) {
576
574
  if (name === 'kiku-mcp' || /jujingyi-kikuuu|vercel\.app/i.test(cfg.url || '')) {
@@ -641,3 +639,5 @@ export class McpClientManager {
641
639
  return results;
642
640
  }
643
641
  }
642
+
643
+ export const mcpClientManager = new McpClientManager();