kiku-agent 1.3.2 → 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 (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
@@ -0,0 +1,102 @@
1
+ DO $$
2
+ BEGIN
3
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'api_keys')
4
+ AND NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'jujingyi_kikuuu_api_keys') THEN
5
+ ALTER TABLE public.api_keys RENAME TO jujingyi_kikuuu_api_keys;
6
+ ELSIF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'jujingyi_kikuuu_api_keys') THEN
7
+ CREATE TABLE public.jujingyi_kikuuu_api_keys (
8
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
9
+ key_prefix VARCHAR(32) NOT NULL,
10
+ key_hash VARCHAR(64) UNIQUE NOT NULL,
11
+ tier VARCHAR(32) DEFAULT 'developer_vip',
12
+ rate_limit_per_min INT DEFAULT 1000,
13
+ is_active BOOLEAN DEFAULT TRUE,
14
+ created_at TIMESTAMPTZ DEFAULT NOW(),
15
+ last_used_at TIMESTAMPTZ,
16
+ expires_at TIMESTAMPTZ,
17
+ revoked_reason TEXT
18
+ );
19
+ END IF;
20
+ END $$;
21
+
22
+
23
+ DO $$
24
+ BEGIN
25
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'quiz_sessions')
26
+ AND NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'jujingyi_kikuuu_quiz_sessions') THEN
27
+ ALTER TABLE public.quiz_sessions RENAME TO jujingyi_kikuuu_quiz_sessions;
28
+ ELSIF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'jujingyi_kikuuu_quiz_sessions') THEN
29
+ CREATE TABLE public.jujingyi_kikuuu_quiz_sessions (
30
+ session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
31
+ challenge_token VARCHAR(255) UNIQUE NOT NULL,
32
+ is_completed BOOLEAN DEFAULT FALSE,
33
+ is_claimed BOOLEAN DEFAULT FALSE,
34
+ created_at TIMESTAMPTZ DEFAULT NOW(),
35
+ expires_at TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '15 minutes')
36
+ );
37
+ END IF;
38
+
39
+
40
+ IF EXISTS (
41
+ SELECT 1 FROM information_schema.columns
42
+ WHERE table_schema = 'public' AND table_name = 'jujingyi_kikuuu_quiz_sessions'
43
+ AND column_name = 'challenge_token' AND character_maximum_length < 255
44
+ ) THEN
45
+ ALTER TABLE public.jujingyi_kikuuu_quiz_sessions ALTER COLUMN challenge_token TYPE VARCHAR(255);
46
+ END IF;
47
+ END $$;
48
+
49
+
50
+ DO $$
51
+ BEGIN
52
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'api_request_logs')
53
+ AND NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'jujingyi_kikuuu_api_request_logs') THEN
54
+ ALTER TABLE public.api_request_logs RENAME TO jujingyi_kikuuu_api_request_logs;
55
+ ELSIF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'jujingyi_kikuuu_api_request_logs') THEN
56
+ CREATE TABLE public.jujingyi_kikuuu_api_request_logs (
57
+ id BIGSERIAL PRIMARY KEY,
58
+ ip_address VARCHAR(45) NOT NULL,
59
+ endpoint VARCHAR(255) NOT NULL,
60
+ method VARCHAR(10) NOT NULL,
61
+ status_code INT NOT NULL,
62
+ response_time_ms NUMERIC(8,2),
63
+ key_prefix VARCHAR(32),
64
+ created_at TIMESTAMPTZ DEFAULT NOW()
65
+ );
66
+ END IF;
67
+ END $$;
68
+
69
+
70
+ ALTER TABLE public.jujingyi_kikuuu_api_keys ENABLE ROW LEVEL SECURITY;
71
+ ALTER TABLE public.jujingyi_kikuuu_quiz_sessions ENABLE ROW LEVEL SECURITY;
72
+ ALTER TABLE public.jujingyi_kikuuu_api_request_logs ENABLE ROW LEVEL SECURITY;
73
+
74
+
75
+ DO $$
76
+ BEGIN
77
+
78
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'jujingyi_kikuuu_api_keys' AND policyname = 'Service Role Access on jujingyi_kikuuu_api_keys') THEN
79
+ CREATE POLICY "Service Role Access on jujingyi_kikuuu_api_keys"
80
+ ON public.jujingyi_kikuuu_api_keys FOR ALL USING (auth.role() = 'service_role');
81
+ END IF;
82
+
83
+
84
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'jujingyi_kikuuu_quiz_sessions' AND policyname = 'Service Role Access on jujingyi_kikuuu_quiz_sessions') THEN
85
+ CREATE POLICY "Service Role Access on jujingyi_kikuuu_quiz_sessions"
86
+ ON public.jujingyi_kikuuu_quiz_sessions FOR ALL USING (auth.role() = 'service_role');
87
+ END IF;
88
+
89
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'jujingyi_kikuuu_api_request_logs' AND policyname = 'Service Role Access on jujingyi_kikuuu_api_request_logs') THEN
90
+ CREATE POLICY "Service Role Access on jujingyi_kikuuu_api_request_logs"
91
+ ON public.jujingyi_kikuuu_api_request_logs FOR ALL USING (auth.role() = 'service_role');
92
+ END IF;
93
+ END $$;
94
+
95
+
96
+ ALTER INDEX IF EXISTS idx_api_keys_hash RENAME TO idx_jujingyi_kikuuu_api_keys_hash;
97
+ ALTER INDEX IF EXISTS idx_quiz_sessions_token RENAME TO idx_jujingyi_kikuuu_quiz_sessions_token;
98
+ ALTER INDEX IF EXISTS idx_api_logs_ip_time RENAME TO idx_jujingyi_kikuuu_api_logs_ip_time;
99
+
100
+ CREATE INDEX IF NOT EXISTS idx_jujingyi_kikuuu_api_keys_hash ON public.jujingyi_kikuuu_api_keys (key_hash) WHERE is_active = TRUE;
101
+ CREATE INDEX IF NOT EXISTS idx_jujingyi_kikuuu_quiz_sessions_token ON public.jujingyi_kikuuu_quiz_sessions (challenge_token) WHERE is_claimed = FALSE;
102
+ CREATE INDEX IF NOT EXISTS idx_jujingyi_kikuuu_api_logs_ip_time ON public.jujingyi_kikuuu_api_request_logs (ip_address, created_at DESC);
@@ -1,4 +1,4 @@
1
- CREATE TABLE IF NOT EXISTS public.api_keys (
1
+ CREATE TABLE IF NOT EXISTS public.jujingyi_kikuuu_api_keys (
2
2
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3
3
  key_prefix VARCHAR(32) NOT NULL,
4
4
  key_hash VARCHAR(64) UNIQUE NOT NULL,
@@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS public.api_keys (
12
12
  );
13
13
 
14
14
 
15
- CREATE TABLE IF NOT EXISTS public.quiz_sessions (
15
+ CREATE TABLE IF NOT EXISTS public.jujingyi_kikuuu_quiz_sessions (
16
16
  session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17
17
  challenge_token VARCHAR(64) UNIQUE NOT NULL,
18
18
  is_completed BOOLEAN DEFAULT FALSE,
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS public.quiz_sessions (
22
22
  );
23
23
 
24
24
 
25
- CREATE TABLE IF NOT EXISTS public.api_request_logs (
25
+ CREATE TABLE IF NOT EXISTS public.jujingyi_kikuuu_api_request_logs (
26
26
  id BIGSERIAL PRIMARY KEY,
27
27
  ip_address VARCHAR(45) NOT NULL,
28
28
  endpoint VARCHAR(255) NOT NULL,
@@ -34,30 +34,30 @@ CREATE TABLE IF NOT EXISTS public.api_request_logs (
34
34
  );
35
35
 
36
36
 
37
- ALTER TABLE public.api_keys ENABLE ROW LEVEL SECURITY;
38
- ALTER TABLE public.quiz_sessions ENABLE ROW LEVEL SECURITY;
39
- ALTER TABLE public.api_request_logs ENABLE ROW LEVEL SECURITY;
37
+ ALTER TABLE public.jujingyi_kikuuu_api_keys ENABLE ROW LEVEL SECURITY;
38
+ ALTER TABLE public.jujingyi_kikuuu_quiz_sessions ENABLE ROW LEVEL SECURITY;
39
+ ALTER TABLE public.jujingyi_kikuuu_api_request_logs ENABLE ROW LEVEL SECURITY;
40
40
 
41
41
 
42
42
  DO $$
43
43
  BEGIN
44
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Service Role Access on api_keys') THEN
45
- CREATE POLICY "Service Role Access on api_keys"
46
- ON public.api_keys FOR ALL USING (auth.role() = 'service_role');
44
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Service Role Access on jujingyi_kikuuu_api_keys') THEN
45
+ CREATE POLICY "Service Role Access on jujingyi_kikuuu_api_keys"
46
+ ON public.jujingyi_kikuuu_api_keys FOR ALL USING (auth.role() = 'service_role');
47
47
  END IF;
48
48
 
49
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Service Role Access on quiz_sessions') THEN
50
- CREATE POLICY "Service Role Access on quiz_sessions"
51
- ON public.quiz_sessions FOR ALL USING (auth.role() = 'service_role');
49
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Service Role Access on jujingyi_kikuuu_quiz_sessions') THEN
50
+ CREATE POLICY "Service Role Access on jujingyi_kikuuu_quiz_sessions"
51
+ ON public.jujingyi_kikuuu_quiz_sessions FOR ALL USING (auth.role() = 'service_role');
52
52
  END IF;
53
53
 
54
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Service Role Access on api_request_logs') THEN
55
- CREATE POLICY "Service Role Access on api_request_logs"
56
- ON public.api_request_logs FOR ALL USING (auth.role() = 'service_role');
54
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'Service Role Access on jujingyi_kikuuu_api_request_logs') THEN
55
+ CREATE POLICY "Service Role Access on jujingyi_kikuuu_api_request_logs"
56
+ ON public.jujingyi_kikuuu_api_request_logs FOR ALL USING (auth.role() = 'service_role');
57
57
  END IF;
58
58
  END $$;
59
59
 
60
60
 
61
- CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON public.api_keys (key_hash) WHERE is_active = TRUE;
62
- CREATE INDEX IF NOT EXISTS idx_quiz_sessions_token ON public.quiz_sessions (challenge_token) WHERE is_claimed = FALSE;
63
- CREATE INDEX IF NOT EXISTS idx_api_logs_ip_time ON public.api_request_logs (ip_address, created_at DESC);
61
+ CREATE INDEX IF NOT EXISTS idx_jujingyi_kikuuu_api_keys_hash ON public.jujingyi_kikuuu_api_keys (key_hash) WHERE is_active = TRUE;
62
+ CREATE INDEX IF NOT EXISTS idx_jujingyi_kikuuu_quiz_sessions_token ON public.jujingyi_kikuuu_quiz_sessions (challenge_token) WHERE is_claimed = FALSE;
63
+ CREATE INDEX IF NOT EXISTS idx_jujingyi_kikuuu_api_logs_ip_time ON public.jujingyi_kikuuu_api_request_logs (ip_address, created_at DESC);
@@ -1,218 +1,179 @@
1
- import fs from 'node:fs';
2
- import { createHash } from 'node:crypto';
3
- import { spawn } from 'node:child_process';
4
- import { LLMClient } from '../../agent/llmClient.js';
5
- import { KikuAgent } from '../../agent/agentCore.js';
6
- import { AgentMemory } from '../../agent/memory.js';
7
- import { ModelManager } from '../../agent/modelManager.js';
8
- import { config, getProviderBaseUrl, getPersistedActiveProvider } from '../../agent/config.js';
9
- import { PUBLIC_REMOTE_TOOL_NAMES } from '../../agent/publicTools.js';
10
-
11
- export const webAgentCache = new Map();
12
- export let sharedWebMemory = new AgentMemory({ persistence: false });
13
- export let currentAgent = null;
14
- const MAX_WEB_AGENT_SESSIONS = 24;
15
-
16
- export function setSharedWebMemory(mem) {
17
- if (mem) sharedWebMemory = mem;
18
- }
19
-
20
- export function setCurrentAgent(agent) {
21
- if (agent) {
22
- currentAgent = agent;
23
- if (agent.memory) sharedWebMemory = agent.memory;
24
- }
25
- }
26
-
27
- export function normalizeWebSessionId(value) {
28
- const normalized = String(value || '').trim().replace(/[^a-zA-Z0-9_-]+/g, '_').slice(0, 100);
29
- return normalized || null;
30
- }
31
-
32
- export function credentialFingerprint(value) {
33
- if (!value) return 'no-key';
34
- return createHash('sha256').update(String(value)).digest('hex').slice(0, 12);
35
- }
36
-
37
- export function computeSessionHash(messages) {
38
- if (!Array.isArray(messages) || messages.length === 0) return '';
39
- const payload = messages.map(m => (m.role || '') + ':' + (m.parts?.[0]?.content || m.text || '')).join('|');
40
- return createHash('sha256').update(payload).digest('hex').slice(0, 16);
41
- }
42
-
43
- export function getWebAgentCacheKey({ sessionId, provider, model, apiKey }) {
44
- if (!sessionId) return null;
45
- return [sessionId, provider || 'lmstudio', model || 'default', credentialFingerprint(apiKey)].join(':');
46
- }
47
-
48
- export async function prepareWebAgent(agent, options = {}) {
49
- await agent.ready();
50
- if (Array.isArray(options.history) && options.history.length > 0) {
51
- const agentMsgCount = agent.session?.messages?.length || 0;
52
- const frontendMsgCount = options.history.length;
53
- await agent.hydrateConversationHistory(options.history, {
54
- sessionId: options.sessionId,
55
- maxMessages: 24,
56
- force: frontendMsgCount > agentMsgCount,
57
- });
58
- }
59
- return agent;
60
- }
61
-
62
- export function cacheWebAgentConnection(cacheKey, connection) {
63
- if (!cacheKey) return;
64
- webAgentCache.delete(cacheKey);
65
- webAgentCache.set(cacheKey, connection);
66
- while (webAgentCache.size > MAX_WEB_AGENT_SESSIONS) {
67
- const oldestKey = webAgentCache.keys().next().value;
68
- const oldest = webAgentCache.get(oldestKey);
69
- oldest?.agent?.saveSession?.().catch(() => {});
70
- const msgCount = oldest?.agent?.session?.messages?.length || 0;
71
- if (msgCount === 0) {
72
- const storageFile = oldest?.agent?.session?.storageFile;
73
- if (storageFile) {
74
- fs.promises.unlink(storageFile).catch(() => {});
75
- }
76
- }
77
- webAgentCache.delete(oldestKey);
78
- }
79
- }
80
-
81
- export async function waitForLMStudio(baseUrl, timeoutMs = 8000) {
82
- const client = new LLMClient({ provider: 'lmstudio', baseUrl });
83
- const start = Date.now();
84
- while (Date.now() - start < timeoutMs) {
85
- try {
86
- const health = await client.checkConnection();
87
- if (health.connected) return health;
88
- } catch {}
89
- await new Promise(r => setTimeout(r, 600));
90
- }
91
- return null;
92
- }
93
-
94
- export async function attemptAutoStartLMStudio(baseUrl, model) {
95
- const isWin = process.platform === 'win32';
96
- const isMac = process.platform === 'darwin';
97
-
98
- const candidates = isWin
99
- ? [
100
- `${process.env.LOCALAPPDATA}\\LM Studio\\LM Studio.exe`,
101
- `${process.env.LOCALAPPDATA}\\Programs\\LM Studio\\LM Studio.exe`,
102
- 'C:\\Program Files\\LM Studio\\LM Studio.exe'
103
- ]
104
- : isMac
105
- ? ['/Applications/LM Studio.app/Contents/MacOS/LM Studio']
106
- : ['lmstudio', '/usr/local/bin/lmstudio', '/opt/lmstudio/bin/lmstudio'];
107
-
108
- const exe = candidates.find(p => {
109
- if (p.includes('/') || p.includes('\\')) {
110
- try { return fs.existsSync(p); } catch { return false; }
111
- }
112
- return true;
113
- });
114
-
115
- if (!exe) return null;
116
-
117
- try {
118
- let port = '1234';
119
- try { const parsedUrl = new URL(baseUrl); if (parsedUrl.port) port = parsedUrl.port; } catch {}
120
-
121
- const safeModel = (typeof model === 'string' && /^[a-zA-Z0-9_\-\.\/]+$/.test(model) && !model.startsWith('-'))
122
- ? model
123
- : 'google/gemma-4-e4b';
124
- const safePort = (/^\d{1,5}$/.test(port) && Number(port) > 0 && Number(port) <= 65535) ? port : '1234';
125
-
126
- const child = spawn(exe, isWin
127
- ? ['--model', safeModel, '--serve', '--port', safePort]
128
- : ['serve', '--model', safeModel, '--port', safePort],
129
- { detached: true, stdio: 'ignore', windowsHide: true }
130
- );
131
- child.unref();
132
-
133
- return await waitForLMStudio(baseUrl, 10000);
134
- } catch {
135
- return null;
136
- }
137
- }
138
-
139
- export async function resolveAgentConnection(customOptions = {}) {
140
- const isAuto = !customOptions.provider || customOptions.provider === 'auto';
141
- const serverProvider = currentAgent?.client?.provider || config.activeProvider || 'lmstudio';
142
- const provider = isAuto ? serverProvider : customOptions.provider;
143
-
144
- const serverModel = currentAgent?.client?.model || config.providers?.[provider]?.model || 'google/gemma-4-e4b';
145
- const requestedModel = (customOptions.model && !isAuto)
146
- ? customOptions.model
147
- : (provider === serverProvider ? serverModel : (config.providers?.[provider]?.model || 'google/gemma-4-e4b'));
148
-
149
- const isServerAgentMatch = Boolean(currentAgent?.client && provider === currentAgent.client.provider);
150
- const baseUrl = (typeof customOptions.baseUrl === 'string' && customOptions.baseUrl.trim()
151
- ? customOptions.baseUrl.trim().replace(/\/+$/, '')
152
- : null)
153
- || (isServerAgentMatch ? currentAgent.client.baseUrl : null)
154
- || getProviderBaseUrl(provider);
155
-
156
- const sessionId = normalizeWebSessionId(customOptions.sessionId);
157
-
158
- const apiKey = customOptions.apiKey
159
- || (isServerAgentMatch ? currentAgent.client.apiKey : '')
160
- || '';
161
-
162
- const client = new LLMClient({
163
- provider,
164
- baseUrl,
165
- model: requestedModel,
166
- apiKey
167
- });
168
-
169
- const modelManager = new ModelManager(client, { requestedModel });
170
- let resolution = await modelManager.resolve({ requestedModel });
171
-
172
- if (provider === 'lmstudio' && resolution.state === 'server_offline' && process.env.NODE_ENV !== 'test' && !process.execArgv.includes('--test')) {
173
- const started = await attemptAutoStartLMStudio(baseUrl, requestedModel);
174
- if (started) {
175
- resolution = await modelManager.resolve({ requestedModel });
176
- }
177
- }
178
-
179
- if (provider === 'vllm' && resolution.state === 'server_offline' && !resolution.error?.includes('WSL')) {
180
- const wslHint = 'vLLM runs on WSL/Linux port 8000 (Windows cannot host it natively). Start it with: wsl -d Ubuntu -- vllm serve <model> --port 8000';
181
- resolution.error = resolution.error ? `${resolution.error}\n Notice: ${wslHint}` : wslHint;
182
- }
183
-
184
- if (resolution.connected && resolution.selectedModel) {
185
- const agent = new KikuAgent({
186
- client,
187
- thinkingMode: customOptions.thinking || 'show',
188
- memory: new AgentMemory({ persistence: false }),
189
- sessionId,
190
- sessionPersistence: false,
191
- allowedToolNames: PUBLIC_REMOTE_TOOL_NAMES,
192
- });
193
- await prepareWebAgent(agent, { history: customOptions.history, sessionId });
194
- const connection = {
195
- ready: true,
196
- agent,
197
- client,
198
- provider,
199
- model: resolution.selectedModel,
200
- models: resolution.models || [],
201
- usedFallback: resolution.usedFallback || false,
202
- resolution,
203
- contentHash: computeSessionHash(agent.session?.messages || [])
204
- };
205
- return connection;
206
- }
207
-
208
- return {
209
- ready: false,
210
- client,
211
- provider,
212
- model: requestedModel,
213
- models: resolution.models || [],
214
- state: resolution.state,
215
- error: resolution.error || 'Server offline',
216
- resolution
217
- };
218
- }
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { LLMClient } from '../../agent/llmClient.js';
4
+ import { KikuAgent } from '../../agent/agentCore.js';
5
+ import { AgentMemory } from '../../agent/memory.js';
6
+ import { ModelManager } from '../../agent/modelManager.js';
7
+ import { config, getProviderBaseUrl, getPersistedActiveProvider } from '../../agent/config.js';
8
+ import { validateInferenceBaseUrl } from './urlGuard.js';
9
+ import { PUBLIC_REMOTE_TOOL_NAMES } from '../../agent/publicTools.js';
10
+ import { registerBackgroundProcess } from '../../agent/processManager.js';
11
+ import { wakeUpLmStudio } from '../../agent/lmStudioWakeup.js';
12
+
13
+ export let sharedWebMemory = new AgentMemory({ persistence: false });
14
+ export let currentAgent = null;
15
+
16
+ export function setSharedWebMemory(mem) {
17
+ if (mem) sharedWebMemory = mem;
18
+ }
19
+
20
+ export function setCurrentAgent(agent) {
21
+ if (agent) {
22
+ currentAgent = agent;
23
+ if (agent.memory) sharedWebMemory = agent.memory;
24
+ }
25
+ }
26
+
27
+ export function normalizeWebSessionId(value) {
28
+ const normalized = String(value || '').trim().replace(/[^a-zA-Z0-9_-]+/g, '_').slice(0, 100);
29
+ return normalized || null;
30
+ }
31
+
32
+ export function credentialFingerprint(value) {
33
+ if (!value) return 'no-key';
34
+ return createHash('sha256').update(String(value)).digest('hex').slice(0, 12);
35
+ }
36
+
37
+ export function computeSessionHash(messages) {
38
+ if (!Array.isArray(messages) || messages.length === 0) return '';
39
+ const payload = messages.map(m => (m.role || '') + ':' + (m.parts?.[0]?.content || m.text || '')).join('|');
40
+ return createHash('sha256').update(payload).digest('hex').slice(0, 16);
41
+ }
42
+
43
+
44
+ export async function prepareWebAgent(agent, options = {}) {
45
+ await agent.ready();
46
+ if (Array.isArray(options.history) && options.history.length > 0) {
47
+ const agentMsgCount = agent.session?.messages?.length || 0;
48
+ const frontendMsgCount = options.history.length;
49
+ await agent.hydrateConversationHistory(options.history, {
50
+ sessionId: options.sessionId,
51
+ maxMessages: 24,
52
+ force: frontendMsgCount > agentMsgCount,
53
+ });
54
+ }
55
+ return agent;
56
+ }
57
+
58
+
59
+ export async function waitForLMStudio(baseUrl, timeoutMs = 8000) {
60
+ const client = new LLMClient({ provider: 'lmstudio', baseUrl });
61
+ const start = Date.now();
62
+ while (Date.now() - start < timeoutMs) {
63
+ try {
64
+ const health = await client.checkConnection();
65
+ if (health.connected) return health;
66
+ } catch {}
67
+ await new Promise(r => setTimeout(r, 600));
68
+ }
69
+ return null;
70
+ }
71
+
72
+ let lmStudioAutoStartPromise = null;
73
+ export function attemptAutoStartLMStudio(baseUrl, model) {
74
+ if (lmStudioAutoStartPromise) return lmStudioAutoStartPromise;
75
+ lmStudioAutoStartPromise = attemptAutoStartLMStudioInner(baseUrl, model).finally(() => {
76
+ lmStudioAutoStartPromise = null;
77
+ });
78
+ return lmStudioAutoStartPromise;
79
+ }
80
+
81
+ async function attemptAutoStartLMStudioInner(baseUrl, model) {
82
+ try {
83
+ const res = await wakeUpLmStudio({
84
+ baseUrl,
85
+ model,
86
+ quiet: true,
87
+ maxWaitMs: 25000,
88
+ });
89
+ if (res.ok) {
90
+ return await waitForLMStudio(baseUrl, 5000);
91
+ }
92
+ } catch {}
93
+ return null;
94
+ }
95
+
96
+ export async function resolveAgentConnection(customOptions = {}) {
97
+ const isAuto = !customOptions.provider || customOptions.provider === 'auto';
98
+ const serverProvider = currentAgent?.client?.provider || config.activeProvider || 'lmstudio';
99
+ const provider = isAuto ? serverProvider : customOptions.provider;
100
+
101
+ const serverModel = currentAgent?.client?.model || config.providers?.[provider]?.model || 'google/gemma-4-e4b';
102
+ const requestedModel = (customOptions.model && !isAuto)
103
+ ? customOptions.model
104
+ : (provider === serverProvider ? serverModel : (config.providers?.[provider]?.model || 'google/gemma-4-e4b'));
105
+
106
+ const isServerAgentMatch = Boolean(currentAgent?.client && provider === currentAgent.client.provider);
107
+ const baseUrl = (typeof customOptions.baseUrl === 'string' && customOptions.baseUrl.trim()
108
+ ? customOptions.baseUrl.trim().replace(/\/+$/, '')
109
+ : null)
110
+ || (isServerAgentMatch ? currentAgent.client.baseUrl : null)
111
+ || getProviderBaseUrl(provider);
112
+
113
+ if (customOptions.callerIsTrusted !== undefined) {
114
+ const guard = await validateInferenceBaseUrl(baseUrl, { callerIsTrusted: customOptions.callerIsTrusted });
115
+ if (!guard.ok) { const error = new Error(guard.reason); error.statusCode = 400; throw error; }
116
+ }
117
+ const sessionId = normalizeWebSessionId(customOptions.sessionId);
118
+
119
+ const apiKey = customOptions.apiKey
120
+ || (isServerAgentMatch ? currentAgent.client.apiKey : '')
121
+ || '';
122
+
123
+ const client = new LLMClient({
124
+ provider,
125
+ baseUrl,
126
+ model: requestedModel,
127
+ apiKey
128
+ });
129
+
130
+ const modelManager = new ModelManager(client, { requestedModel });
131
+ let resolution = await modelManager.resolve({ requestedModel });
132
+
133
+ if (customOptions.allowAutoStart !== false && provider === 'lmstudio' && resolution.state === 'server_offline' && process.env.NODE_ENV !== 'test' && !process.execArgv.includes('--test')) {
134
+ const started = await attemptAutoStartLMStudio(baseUrl, requestedModel);
135
+ if (started) {
136
+ resolution = await modelManager.resolve({ requestedModel });
137
+ }
138
+ }
139
+
140
+ if (provider === 'vllm' && resolution.state === 'server_offline' && !resolution.error?.includes('WSL')) {
141
+ const wslHint = 'vLLM runs on WSL/Linux port 8000 (Windows cannot host it natively). Start it with: wsl -d Ubuntu -- vllm serve <model> --port 8000';
142
+ resolution.error = resolution.error ? `${resolution.error}\n Notice: ${wslHint}` : wslHint;
143
+ }
144
+
145
+ if (resolution.connected && resolution.selectedModel) {
146
+ const agent = new KikuAgent({
147
+ client,
148
+ thinkingMode: customOptions.thinking || 'show',
149
+ memory: new AgentMemory({ persistence: false }),
150
+ sessionId,
151
+ sessionPersistence: false,
152
+ allowedToolNames: PUBLIC_REMOTE_TOOL_NAMES,
153
+ });
154
+ await prepareWebAgent(agent, { history: customOptions.history, sessionId });
155
+ const connection = {
156
+ ready: true,
157
+ agent,
158
+ client,
159
+ provider,
160
+ model: resolution.selectedModel,
161
+ models: resolution.models || [],
162
+ usedFallback: resolution.usedFallback || false,
163
+ resolution,
164
+ contentHash: computeSessionHash(agent.session?.messages || [])
165
+ };
166
+ return connection;
167
+ }
168
+
169
+ return {
170
+ ready: false,
171
+ client,
172
+ provider,
173
+ model: requestedModel,
174
+ models: resolution.models || [],
175
+ state: resolution.state,
176
+ error: resolution.error || 'Server offline',
177
+ resolution
178
+ };
179
+ }
@@ -38,10 +38,17 @@ function cleanHistoryText(value) {
38
38
 
39
39
  export function detectConversationDomain(value) {
40
40
  const text = String(value || '');
41
+ let bestDomain = 'general';
42
+ let maxScore = 0;
41
43
  for (const [domain, pattern] of Object.entries(DOMAIN_PATTERNS)) {
42
- if (pattern.test(text)) return domain;
44
+ const matches = text.match(new RegExp(pattern.source, 'gi'));
45
+ const score = matches ? matches.length : 0;
46
+ if (score > maxScore) {
47
+ maxScore = score;
48
+ bestDomain = domain;
49
+ }
43
50
  }
44
- return 'general';
51
+ return bestDomain;
45
52
  }
46
53
 
47
54
  export function isContextDependentQuery(query) {