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
@@ -39,6 +39,7 @@ export async function handleClarifyAnalyticsRoute(req, res, options = {}) {
39
39
  }
40
40
 
41
41
  export async function handleClarifyConfigRoute(req, res, options = {}, action = 'get') {
42
+ if (!isWebChatAdmin(req)) return sendError(res, 401, 'Admin API key required');
42
43
  try {
43
44
  const config = getConfig();
44
45
 
@@ -48,8 +49,6 @@ export async function handleClarifyConfigRoute(req, res, options = {}, action =
48
49
  config: config.getConfig(),
49
50
  });
50
51
  }
51
-
52
- if (!isWebChatAdmin(req)) return sendError(res, 401, 'Admin API key required');
53
52
 
54
53
  if (action === 'reset') {
55
54
  const newConfig = config.reset();
@@ -1,5 +1,6 @@
1
1
  import { normalizeWebSessionId } from '../core/connection.js';
2
2
  import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
3
+ import { PACKAGE_VERSION } from '../../agent/version.js';
3
4
 
4
5
  export async function handleExportRoute(req, res) {
5
6
  let body;
@@ -19,7 +20,7 @@ export async function handleExportRoute(req, res) {
19
20
  exportedAt: new Date().toISOString(),
20
21
  sessionId: normSessionId,
21
22
  messages: history,
22
- metadata: { app: '鞠婧祎', version: '1.0.0' }
23
+ metadata: { app: '鞠婧祎', version: PACKAGE_VERSION }
23
24
  };
24
25
  res.writeHead(200, {
25
26
  'Content-Type': 'application/json; charset=utf-8',
@@ -0,0 +1,90 @@
1
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
2
+ import { scanLan, mapServerTypeToProvider } from '../../agent/lanDiscovery.js';
3
+ import { isPrivateIPv4 } from '../../agent/sanitizer.js';
4
+ import { resolveClientIp, isWebChatAdmin } from '../core/security.js';
5
+ import {
6
+ getProvider,
7
+ getUnifiedProviderCatalog,
8
+ savePersistedProviderBaseUrl,
9
+ setActiveProvider,
10
+ } from '../../agent/config.js';
11
+ import { currentAgent, setCurrentAgent } from '../core/connection.js';
12
+
13
+ function lanDiscoveryAllowed(req) {
14
+ if (String(process.env.KIKU_ALLOW_REMOTE_LAN_DISCOVERY || '').toLowerCase() === 'true') return true;
15
+ if (isWebChatAdmin(req)) return true;
16
+ const ip = resolveClientIp(req);
17
+ return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
18
+ }
19
+
20
+ export async function handleLanDiscoveryRoute(req, res) {
21
+ if (!lanDiscoveryAllowed(req)) {
22
+ return sendError(res, 403, 'LAN discovery is restricted to the local machine. Set KIKU_ALLOW_REMOTE_LAN_DISCOVERY=true to allow other devices.');
23
+ }
24
+ try {
25
+ const urlObj = new URL(req.url, 'http://localhost');
26
+ const subnetsParam = urlObj.searchParams.get('subnets');
27
+ const portsParam = urlObj.searchParams.get('ports');
28
+ const options = {};
29
+ if (subnetsParam && subnetsParam.trim()) {
30
+ options.subnets = subnetsParam.split(',').map(s => s.trim()).filter(Boolean);
31
+ }
32
+ if (portsParam && portsParam.trim()) {
33
+ options.ports = portsParam.split(',').map(Number).filter(p => Number.isInteger(p) && p >= 1 && p <= 65535);
34
+ }
35
+ const result = await scanLan(options);
36
+ sendJson(res, 200, { status: 'ok', ...result });
37
+ } catch (err) {
38
+ sendError(res, 400, err.message);
39
+ }
40
+ }
41
+
42
+ export async function handleLanApplyRoute(req, res) {
43
+ if (!lanDiscoveryAllowed(req)) {
44
+ return sendError(res, 403, 'LAN discovery is restricted to the local machine. Set KIKU_ALLOW_REMOTE_LAN_DISCOVERY=true to allow other devices.');
45
+ }
46
+
47
+ let body;
48
+ try {
49
+ body = await parseJsonBody(req, 16 * 1024);
50
+ } catch (err) {
51
+ return sendError(res, 400, `Invalid JSON body: ${err.message}`);
52
+ }
53
+
54
+ const host = String(body.host || '').trim();
55
+ const port = Number(body.port);
56
+ const model = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : null;
57
+ const serverType = String(body.serverType || 'openai-compatible').toLowerCase();
58
+
59
+ if (!/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(host) || !isPrivateIPv4(host)) {
60
+ return sendError(res, 400, 'host must be a private IPv4 address discovered by the LAN scan');
61
+ }
62
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
63
+ return sendError(res, 400, 'port must be an integer between 1 and 65535');
64
+ }
65
+
66
+ const provider = mapServerTypeToProvider(serverType);
67
+ if (!getProvider(provider)) {
68
+ return sendError(res, 400, `Unknown provider "${provider}"`);
69
+ }
70
+
71
+ const baseUrl = `http://${host}:${port}/v1`;
72
+ savePersistedProviderBaseUrl(provider, baseUrl);
73
+ setActiveProvider(provider, { model, baseUrl });
74
+
75
+ if (currentAgent?.client) {
76
+ try {
77
+ currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl });
78
+ setCurrentAgent(currentAgent);
79
+ } catch {}
80
+ }
81
+
82
+ const catalog = getUnifiedProviderCatalog();
83
+ sendJson(res, 200, {
84
+ status: 'ok',
85
+ applied: { provider, baseUrl, model },
86
+ activeProvider: catalog.activeProvider,
87
+ activeBaseUrl: catalog.activeBaseUrl,
88
+ activeModel: catalog.activeModel,
89
+ });
90
+ }
@@ -1,5 +1,32 @@
1
1
  import { socialUpdatesData } from '../../../src/data/socialUpdatesData.js';
2
2
  import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
3
+ import { searchNews } from '../../agent/webSearch.js';
4
+ import { rankNewsItems } from '../../agent/newsIntelligence.js';
5
+
6
+ const LIVE_TIMEOUT_MS = 6000;
7
+
8
+ async function fetchLiveItems(query, days) {
9
+ const controller = new AbortController();
10
+ const timer = setTimeout(() => controller.abort(), LIVE_TIMEOUT_MS);
11
+ try {
12
+ const result = await searchNews(query, 12);
13
+ if (result?.status !== 'success') return [];
14
+ const cutoff = days > 0 ? new Date(Date.now() - days * 86400000) : null;
15
+ return rankNewsItems(result.results || [], { cutoff })
16
+ .filter(item => !cutoff || !item.date || item.date.getTime() >= cutoff.getTime())
17
+ .map(item => ({
18
+ title: item.title,
19
+ summary: item.snippet || '',
20
+ date: item.date ? item.date.toISOString().slice(0, 10) : null,
21
+ platform: item.tierLabel || 'news',
22
+ url: item.url,
23
+ }));
24
+ } catch {
25
+ return [];
26
+ } finally {
27
+ clearTimeout(timer);
28
+ }
29
+ }
3
30
 
4
31
  export async function handleNewsRoute(req, res) {
5
32
  let body = {};
@@ -8,12 +35,13 @@ export async function handleNewsRoute(req, res) {
8
35
  } catch {}
9
36
 
10
37
  const { query = 'จวีจิ้งอี Ju Jingyi', days = 30 } = body;
11
- let newsItems = [];
12
- const source = 'social';
13
38
 
39
+ const liveItems = await fetchLiveItems(String(query || 'จวีจิ้งอี Ju Jingyi'), Number(days) || 30);
40
+
41
+ let socialItems = [];
14
42
  try {
15
43
  const cards = socialUpdatesData?.douyin?.cards || socialUpdatesData?.cards || [];
16
- newsItems = cards.map(c => ({
44
+ socialItems = cards.map(c => ({
17
45
  title: c.title || c.titleCn,
18
46
  summary: c.captionTh || c.captionCn || c.content,
19
47
  date: c.date,
@@ -22,12 +50,14 @@ export async function handleNewsRoute(req, res) {
22
50
  }));
23
51
  } catch {}
24
52
 
53
+ const items = [...liveItems, ...socialItems.slice(0, 6)];
25
54
  sendJson(res, 200, {
26
55
  status: 'ok',
27
56
  query,
28
57
  days,
29
- items: newsItems.slice(0, 10),
30
- source,
31
- count: newsItems.length
58
+ items: items.slice(0, 12),
59
+ liveCount: liveItems.length,
60
+ source: liveItems.length > 0 ? 'live+social' : 'social',
61
+ count: items.length
32
62
  });
33
63
  }
@@ -0,0 +1,87 @@
1
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
2
+ import {
3
+ getUnifiedProviderCatalog,
4
+ getProviderBaseUrl,
5
+ setActiveProvider,
6
+ savePersistedProviderBaseUrl,
7
+ getProvider,
8
+ } from '../../agent/config.js';
9
+ import { currentAgent, setCurrentAgent } from '../core/connection.js';
10
+ import { LLMClient } from '../../agent/llmClient.js';
11
+
12
+ export async function handleProviderGetRoute(req, res) {
13
+ try {
14
+ const catalog = getUnifiedProviderCatalog();
15
+ sendJson(res, 200, {
16
+ status: 'ok',
17
+ activeProvider: catalog.activeProvider,
18
+ activeBaseUrl: catalog.activeBaseUrl,
19
+ activeModel: catalog.activeModel,
20
+ localEndpoints: catalog.localEndpoints,
21
+ providers: catalog.providers,
22
+ });
23
+ } catch (err) {
24
+ sendError(res, 500, err.message);
25
+ }
26
+ }
27
+
28
+ export async function handleProviderUpdateRoute(req, res) {
29
+ let body;
30
+ try {
31
+ body = await parseJsonBody(req);
32
+ } catch (err) {
33
+ return sendError(res, 400, `Invalid JSON body: ${err.message}`);
34
+ }
35
+ try {
36
+ const provider = String(body.provider || body.activeProvider || '').toLowerCase().trim() || null;
37
+ const model = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : null;
38
+ const baseUrl = typeof body.baseUrl === 'string' && body.baseUrl.trim() ? body.baseUrl.trim().replace(/\/+$/, '') : null;
39
+
40
+ if (baseUrl && !provider) {
41
+ return sendError(res, 400, 'Provider is required when setting a baseUrl');
42
+ }
43
+
44
+ if (baseUrl && provider) {
45
+ const pDef = getProvider(provider);
46
+ if (!pDef) return sendError(res, 400, `Unknown provider "${provider}"`);
47
+ try {
48
+ new URL(baseUrl);
49
+ } catch {
50
+ return sendError(res, 400, `Invalid baseUrl "${baseUrl}"`);
51
+ }
52
+ savePersistedProviderBaseUrl(provider, baseUrl);
53
+ }
54
+
55
+ let catalog;
56
+ if (provider) {
57
+ const pDef = getProvider(provider);
58
+ if (!pDef) return sendError(res, 400, `Unknown provider "${provider}"`);
59
+ setActiveProvider(provider, { model, baseUrl });
60
+ if (currentAgent?.client) {
61
+ try {
62
+ currentAgent.client.switchProvider(provider, { model: model || undefined, baseUrl: baseUrl || undefined });
63
+ setCurrentAgent(currentAgent);
64
+ } catch {}
65
+ } else if (baseUrl || model) {
66
+ try {
67
+ const client = new LLMClient({ provider, model: model || undefined, baseUrl: baseUrl || getProviderBaseUrl(provider) });
68
+ void client;
69
+ } catch {}
70
+ }
71
+ catalog = getUnifiedProviderCatalog();
72
+ } else {
73
+ catalog = getUnifiedProviderCatalog();
74
+ }
75
+
76
+ sendJson(res, 200, {
77
+ status: 'ok',
78
+ activeProvider: catalog.activeProvider,
79
+ activeBaseUrl: catalog.activeBaseUrl,
80
+ activeModel: catalog.activeModel,
81
+ localEndpoints: catalog.localEndpoints,
82
+ providers: catalog.providers,
83
+ });
84
+ } catch (err) {
85
+ sendError(res, 500, err.message);
86
+ }
87
+ }
@@ -26,16 +26,48 @@ const MIME_TYPES = {
26
26
  };
27
27
 
28
28
  export function handleStaticRoute(req, res, pathname, port) {
29
- if (pathname === '/favicon.ico' || pathname === '/favicon.svg') {
30
- const svgIcon = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌸</text></svg>`;
31
- res.writeHead(200, {
32
- 'Content-Type': 'image/svg+xml',
33
- 'Cache-Control': 'public, max-age=86400, immutable'
34
- });
35
- res.end(svgIcon);
29
+ const rawUrl = req?.url || '';
30
+ let currentPath = pathname;
31
+ if (!currentPath && rawUrl) {
32
+ try {
33
+ currentPath = new URL(rawUrl, 'http://localhost').pathname;
34
+ } catch {
35
+ currentPath = rawUrl;
36
+ }
37
+ }
38
+ currentPath = currentPath || '/';
39
+
40
+ const rawContainsTraversal = rawUrl.includes('..') || /%2e/i.test(rawUrl);
41
+ let decodedPath;
42
+ try {
43
+ decodedPath = decodeURIComponent(currentPath);
44
+ } catch {
45
+ decodedPath = currentPath;
46
+ }
47
+ const decodedContainsTraversal = decodedPath.includes('..') || decodedPath.includes('\0');
48
+
49
+ if (rawContainsTraversal || decodedContainsTraversal) {
50
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
51
+ res.end('403 Forbidden: Access denied');
36
52
  return true;
37
53
  }
38
54
 
55
+ if (currentPath === '/favicon.ico') {
56
+ let icoPath = path.join(PUBLIC_DIR, 'favicon.ico');
57
+ if (!fs.existsSync(icoPath)) {
58
+ const cwdPath = path.join(process.cwd(), 'public', 'favicon.ico');
59
+ if (fs.existsSync(cwdPath)) icoPath = cwdPath;
60
+ }
61
+ if (fs.existsSync(icoPath)) {
62
+ res.writeHead(200, {
63
+ 'Content-Type': 'image/x-icon',
64
+ 'Cache-Control': 'public, max-age=86400, immutable'
65
+ });
66
+ res.end(fs.readFileSync(icoPath));
67
+ return true;
68
+ }
69
+ }
70
+
39
71
  if (pathname === '/sw.js' || pathname === '/service-worker.js') {
40
72
  let swPath = path.join(PUBLIC_DIR, 'sw.js');
41
73
  if (!fs.existsSync(swPath)) {
@@ -72,13 +104,29 @@ self.addEventListener('fetch', (event) => {
72
104
  return true;
73
105
  }
74
106
 
75
- if (pathname.startsWith('/fonts/') || pathname.startsWith('/images/') || pathname === '/manifest.json' || pathname === '/robots.txt') {
76
- const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, '');
77
- let filePath = path.join(PUBLIC_DIR, safePath);
107
+ if (currentPath.startsWith('/fonts/') || currentPath.startsWith('/images/') || currentPath === '/manifest.json' || currentPath === '/robots.txt') {
108
+ const targetDir = fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : path.join(process.cwd(), 'public');
109
+ const targetDirCanonical = path.resolve(targetDir);
110
+ const targetDirPrefix = targetDirCanonical + path.sep;
111
+
112
+ const relativePath = currentPath.replace(/^\/+/, '');
113
+ const resolvedPath = path.resolve(targetDirCanonical, relativePath);
114
+
115
+ if (!resolvedPath.startsWith(targetDirPrefix) && resolvedPath !== targetDirCanonical) {
116
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
117
+ res.end('403 Forbidden: Access denied');
118
+ return true;
119
+ }
120
+
121
+ let filePath = resolvedPath;
78
122
  if (!fs.existsSync(filePath)) {
79
- const cwdPath = path.join(process.cwd(), 'public', safePath);
80
- if (fs.existsSync(cwdPath)) filePath = cwdPath;
123
+ const cwdPublic = path.resolve(process.cwd(), 'public');
124
+ const cwdPath = path.resolve(cwdPublic, relativePath);
125
+ if (cwdPath.startsWith(cwdPublic + path.sep) && fs.existsSync(cwdPath)) {
126
+ filePath = cwdPath;
127
+ }
81
128
  }
129
+
82
130
  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
83
131
  const ext = path.extname(filePath).toLowerCase();
84
132
  res.writeHead(200, {
@@ -90,7 +138,7 @@ self.addEventListener('fetch', (event) => {
90
138
  }
91
139
  }
92
140
 
93
- if (pathname === '/' || pathname === '/chat' || pathname.startsWith('/chat/') || pathname === '/index.html') {
141
+ if (currentPath === '/' || currentPath === '/chat' || currentPath.startsWith('/chat/') || currentPath === '/index.html') {
94
142
  res.writeHead(200, {
95
143
  'Content-Type': 'text/html; charset=utf-8',
96
144
  'Cache-Control': 'no-cache, no-store, must-revalidate'
@@ -1,5 +1,6 @@
1
1
  import { toolsSchema } from '../../agent/tools.js';
2
- import { resolveAgentConnection } from '../core/connection.js';
2
+ import { resolveAgentConnection, currentAgent } from '../core/connection.js';
3
+ import { config, CLOUD_PROVIDERS, getProviderBaseUrl, getUnifiedProviderCatalog } from '../../agent/config.js';
3
4
  import { getAgentMemorySnapshot } from '../core/memorySnapshot.js';
4
5
  import { sendJson, sendError } from '../core/errorHandler.js';
5
6
  import { extractApiKey } from '../core/security.js';
@@ -8,17 +9,30 @@ export async function handleStatusRoute(req, res, urlObj, port) {
8
9
  try {
9
10
  const queryProvider = urlObj.searchParams.get('provider');
10
11
  const queryModel = urlObj.searchParams.get('model');
11
- const queryApiKey = extractApiKey(req);
12
+ const queryApiKey = extractApiKey(req);
12
13
  const querySessionId = urlObj.searchParams.get('sessionId');
14
+ const queryBaseUrl = urlObj.searchParams.get('baseUrl');
13
15
 
14
16
  const conn = await resolveAgentConnection({
15
17
  provider: queryProvider,
16
18
  model: queryModel,
17
19
  apiKey: queryApiKey,
18
20
  sessionId: querySessionId,
21
+ baseUrl: queryBaseUrl || undefined,
19
22
  });
20
23
 
21
24
  const totalTools = conn.agent?.getAllToolsSchema?.().length || toolsSchema.length;
25
+ const serverDefaultProvider = currentAgent?.client?.provider || config.activeProvider || 'lmstudio';
26
+ const serverDefaultModel = currentAgent?.client?.model || config.providers?.[serverDefaultProvider]?.model || 'google/gemma-4-e4b';
27
+ const serverDefaultProviderName = currentAgent?.client?.providerName || (CLOUD_PROVIDERS?.[serverDefaultProvider]?.name) || serverDefaultProvider;
28
+ const serverDefaultBaseUrl = currentAgent?.client?.baseUrl || getProviderBaseUrl(serverDefaultProvider);
29
+ let localEndpoints = null;
30
+ let providerCatalog = null;
31
+ try {
32
+ const catalog = getUnifiedProviderCatalog();
33
+ localEndpoints = catalog.localEndpoints;
34
+ providerCatalog = catalog.providers;
35
+ } catch {}
22
36
 
23
37
  sendJson(res, 200, {
24
38
  status: 'ok',
@@ -32,6 +46,13 @@ export async function handleStatusRoute(req, res, urlObj, port) {
32
46
  activeLoadedModel: conn.resolution?.activeLoadedModel || conn.model,
33
47
  loadedModels: conn.models || [],
34
48
  usedFallback: conn.usedFallback || false,
49
+ serverDefaultProvider,
50
+ serverDefaultModel,
51
+ serverDefaultProviderName,
52
+ serverDefaultBaseUrl,
53
+ localEndpoints,
54
+ providers: providerCatalog,
55
+ hasServerAgent: Boolean(currentAgent),
35
56
  error: conn.error || null,
36
57
  capabilities: {
37
58
  reactAgent: Boolean(conn.agent),
@@ -0,0 +1,22 @@
1
+ import { parseJsonBody, sendJson, sendError } from '../core/errorHandler.js';
2
+ import { buildHardwareReport, computeVramEstimate, parseEstimateRequest } from '../../agent/vramService.js';
3
+
4
+ export async function handleSystemHardwareRoute(req, res) {
5
+ try {
6
+ const report = await buildHardwareReport();
7
+ return sendJson(res, 200, { status: 'success', data: report });
8
+ } catch (err) {
9
+ return sendError(res, 500, err.message || 'Hardware probe failed');
10
+ }
11
+ }
12
+
13
+ export async function handleEstimateVramRoute(req, res, urlObj) {
14
+ try {
15
+ const body = req.method === 'POST' ? await parseJsonBody(req) : {};
16
+ const request = parseEstimateRequest(body, urlObj?.searchParams || null);
17
+ const estimate = await computeVramEstimate(request);
18
+ return sendJson(res, 200, { status: 'success', data: estimate });
19
+ } catch (err) {
20
+ return sendError(res, 500, err.message || 'VRAM estimation failed');
21
+ }
22
+ }