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
@@ -0,0 +1,317 @@
1
+ /**
2
+ * envManager — local .env lifecycle for kiku-agent.
3
+ *
4
+ * The npm package is consumed via a global install (`npm install -g kiku-agent`),
5
+ * so there is no project directory to host configuration. Environment files are
6
+ * resolved in this order:
7
+ *
8
+ * 1. package-root/.env — development checkouts (and legacy hand-made files)
9
+ * 2. <KIKU_DATA_DIR>/.env — user-owned location that survives npm upgrades
10
+ *
11
+ * Both files are loaded (package first, unset keys only). `/env` writes to the
12
+ * primary target: the package file when it exists, the user file otherwise.
13
+ *
14
+ * Only NECESSARY values are ever required; everything else is either
15
+ * auto-detected or left commented in the generated file.
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { KIKU_DATA_DIR } from './appPaths.js';
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ export const PACKAGE_ENV_PATH = path.resolve(__dirname, '..', '..', '.env');
27
+ export const USER_ENV_PATH = path.join(KIKU_DATA_DIR, '.env');
28
+
29
+ export function resolveEnvTargets({ packageEnvPath = PACKAGE_ENV_PATH, userEnvPath = USER_ENV_PATH } = {}) {
30
+ const packageExists = fs.existsSync(packageEnvPath);
31
+ return {
32
+ mode: packageExists ? 'repo' : 'user',
33
+ packageExists,
34
+ packageEnvPath,
35
+ userEnvPath,
36
+ primaryPath: packageExists ? packageEnvPath : userEnvPath,
37
+ };
38
+ }
39
+
40
+ export function parseEnvMap(text) {
41
+ const map = {};
42
+ for (const line of String(text || '').split(/\r?\n/)) {
43
+ const m = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
44
+ if (!m || line.trim().startsWith('#')) continue;
45
+ map[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
46
+ }
47
+ return map;
48
+ }
49
+
50
+ export function readEnvFile(filePath) {
51
+ try {
52
+ if (!fs.existsSync(filePath)) return null;
53
+ return parseEnvMap(fs.readFileSync(filePath, 'utf8'));
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function readMergedEnvMap({ packageEnvPath = PACKAGE_ENV_PATH, userEnvPath = USER_ENV_PATH } = {}) {
60
+ const map = {};
61
+ const sources = {};
62
+ const userMap = readEnvFile(userEnvPath) || {};
63
+ const packageMap = readEnvFile(packageEnvPath) || {};
64
+ for (const [key, value] of Object.entries(userMap)) {
65
+ map[key] = value;
66
+ sources[key] = userEnvPath;
67
+ }
68
+ for (const [key, value] of Object.entries(packageMap)) {
69
+ map[key] = value;
70
+ sources[key] = packageEnvPath;
71
+ }
72
+ return { map, sources, packageMap, userMap };
73
+ }
74
+
75
+ const SECRET_PATTERN = /(API_KEY|_KEYS|MASTER_KEY|SECRET|PEPPER|TOKEN|COOKIES|PASSWORD)/i;
76
+
77
+ export function isSecretKey(key) {
78
+ return SECRET_PATTERN.test(String(key || ''));
79
+ }
80
+
81
+ export function maskValue(key, value) {
82
+ const str = String(value ?? '');
83
+ if (!str) return '(not set)';
84
+ if (!isSecretKey(key)) return str;
85
+ if (str.length <= 8) return '****';
86
+ return `${str.slice(0, 4)}…${str.slice(-3)} (${str.length} chars)`;
87
+ }
88
+
89
+
90
+ export const ENV_VAR_CATALOG = Object.freeze([
91
+
92
+ { key: 'LMSTUDIO_BASE_URL', group: 'necessary', defaultValue: 'http://127.0.0.1:1234/v1', description: 'LM Studio (OpenAI-compatible) endpoint' },
93
+ { key: 'VLLM_BASE_URL', group: 'optional', defaultValue: 'http://127.0.0.1:8000/v1', description: 'vLLM OpenAI-compatible endpoint (WSL/Linux)' },
94
+
95
+
96
+ { key: 'KIKU_CONTEXT_LIMIT', group: 'recommended', auto: 'context', description: 'Context window for token telemetry (match LM Studio loaded_context_length)' },
97
+ { key: 'LMSTUDIO_MODELS_DIR', group: 'recommended', auto: 'modelsDir', description: 'Folder scanned for .gguf checkpoints (VRAM estimator)' },
98
+
99
+
100
+ { key: 'LLM_PROVIDER', group: 'optional', defaultValue: 'lmstudio', description: 'Default provider (lmstudio/vllm/openai/gemini/groq/deepseek/openrouter/custom)' },
101
+ { key: 'KIKU_VRAM_TOTAL_GB', group: 'optional', description: 'Pin the VRAM budget when GPU detection is unavailable' },
102
+ { key: 'KIKU_YT_COOKIES', group: 'optional', defaultValue: 'cookies.txt', description: 'Netscape cookies file for YouTube playback' },
103
+ { key: 'KIKU_YT_BROWSER', group: 'optional', description: 'Browser profile for cookie extraction (firefox/chrome/edge/brave)' },
104
+ { key: 'KIKU_YTDLP', group: 'optional', defaultValue: 'yt-dlp', description: 'yt-dlp binary path' },
105
+ { key: 'KIKU_YTDLP_CLIENT', group: 'optional', defaultValue: 'tv', description: 'YouTube player client (tv/ios/web)' },
106
+ { key: 'PORT_WEB_CHAT', group: 'optional', defaultValue: '3002', description: 'Web-chat server port' },
107
+ { key: 'KIKU_WEB_CHAT_HOST', group: 'optional', defaultValue: '127.0.0.1', description: 'Web-chat bind host (keep loopback unless behind a proxy)' },
108
+ { key: 'KIKU_THEME', group: 'optional', description: 'Terminal theme name' },
109
+ { key: 'KIKU_ENABLE_CN_WEB_TOOLS', group: 'optional', defaultValue: 'true', description: 'Enable Chinese web tools (search_chinese_web, search_bilibili)' },
110
+ { key: 'KIKU_MCP_ENABLE_WEB_TOOLS', group: 'optional', defaultValue: 'false', description: 'Expose web tools through the MCP server (KIKU_MCP_ENABLE_WEB_TOOLS=true)' },
111
+
112
+
113
+ { key: 'OPENAI_API_KEY', group: 'cloud', description: 'OpenAI' },
114
+ { key: 'GEMINI_API_KEY', group: 'cloud', description: 'Google Gemini' },
115
+ { key: 'GROQ_API_KEY', group: 'cloud', description: 'Groq' },
116
+ { key: 'DEEPSEEK_API_KEY', group: 'cloud', description: 'DeepSeek' },
117
+ { key: 'OPENROUTER_API_KEY', group: 'cloud', description: 'OpenRouter' },
118
+
119
+
120
+ { key: 'API_KEY_PEPPER', group: 'server', description: 'HMAC pepper for API-key hashing' },
121
+ { key: 'QUIZ_SECRET', group: 'server', description: 'Quiz challenge token secret' },
122
+ { key: 'KIKU_PROVIDER_KEY_ENCRYPTION_KEY', group: 'server', description: 'Encrypts provider keys at rest' },
123
+ { key: 'KIKU_MASTER_KEYS', group: 'server', description: 'Admin keys for /keys/generate' },
124
+ { key: 'KIKU_API_KEYS', group: 'server', description: 'Pre-seeded developer keys' },
125
+ { key: 'KIKU_VIP_KEYS', group: 'server', description: 'Pre-seeded VIP keys' },
126
+ { key: 'KIKU_AGENT_KEYS', group: 'server', description: 'Agent-execution keys' },
127
+ { key: 'MCP_API_KEY', group: 'server', description: 'MCP HTTP auth' },
128
+ ]);
129
+
130
+ const GROUP_LABELS = Object.freeze({
131
+ necessary: 'Necessary',
132
+ recommended: 'Recommended (auto-detected)',
133
+ optional: 'Optional',
134
+ cloud: 'Cloud provider keys',
135
+ server: 'Server deployment only',
136
+ });
137
+
138
+ export function describeGroup(group) {
139
+ return GROUP_LABELS[group] || group;
140
+ }
141
+
142
+
143
+ export async function detectAutoDefaults({ baseUrl, timeoutMs = 2500, signal, probe = true } = {}) {
144
+ const detected = {
145
+ lmStudioReachable: false,
146
+ loadedModel: null,
147
+ loadedModelType: null,
148
+ contextLength: null,
149
+ modelsDir: null,
150
+ };
151
+
152
+ if (probe !== false) {
153
+ const root = String(baseUrl || process.env.LMSTUDIO_BASE_URL || 'http://127.0.0.1:1234/v1')
154
+ .replace(/\/v1\/?$/, '')
155
+ .replace(/\/+$/, '');
156
+ try {
157
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
158
+ const fetchSignal = signal ? AbortSignal.any([timeoutSignal, signal]) : timeoutSignal;
159
+ const res = await fetch(`${root}/api/v0/models`, { signal: fetchSignal });
160
+ if (res.ok) {
161
+ const data = await res.json();
162
+ const models = Array.isArray(data?.data) ? data.data : [];
163
+ if (models.length > 0) detected.lmStudioReachable = true;
164
+ const loaded = models.find(m => m.state === 'loaded' && (m.type === 'llm' || m.type === 'vlm'))
165
+ || models.find(m => m.state === 'loaded');
166
+ if (loaded) {
167
+ detected.loadedModel = loaded.id || null;
168
+ detected.loadedModelType = loaded.type || null;
169
+ detected.contextLength = loaded.loaded_context_length ?? loaded.max_context_length ?? null;
170
+ }
171
+ }
172
+ } catch {
173
+
174
+ }
175
+ }
176
+
177
+ const candidates = [
178
+ process.env.LMSTUDIO_MODELS_DIR,
179
+ path.join(os.homedir(), '.lmstudio', 'models'),
180
+ process.platform === 'win32' && process.env.LOCALAPPDATA
181
+ ? path.join(process.env.LOCALAPPDATA, 'LM Studio', 'models')
182
+ : null,
183
+ ].filter(Boolean);
184
+ detected.modelsDir = candidates.find(p => {
185
+ try { return fs.existsSync(p); } catch { return false; }
186
+ }) || null;
187
+
188
+ return detected;
189
+ }
190
+
191
+
192
+ export function buildEnvTemplate({ values = {}, detected = null } = {}) {
193
+ const lines = [];
194
+ lines.push('# ==============================================================================');
195
+ lines.push('# Kiku Agent — local environment (.env)');
196
+ lines.push(`# Generated by \`/env init\` on ${new Date().toISOString().slice(0, 10)}.`);
197
+ lines.push('# Only necessary values are set; everything else stays commented until needed.');
198
+ lines.push('# ==============================================================================');
199
+ lines.push('');
200
+
201
+ const catalogByGroup = (group) => ENV_VAR_CATALOG.filter(entry => entry.group === group);
202
+
203
+ lines.push(`# --- ${describeGroup('necessary')} ----------------------------------------------------------`);
204
+ for (const entry of catalogByGroup('necessary')) {
205
+ lines.push(`# ${entry.description}`);
206
+ lines.push(`${entry.key}=${values[entry.key] ?? entry.defaultValue ?? ''}`);
207
+ }
208
+ lines.push('');
209
+
210
+ const recommended = catalogByGroup('recommended');
211
+ if (recommended.some(entry => values[entry.key])) {
212
+ lines.push(`# --- ${describeGroup('recommended')} ---------------------------------`);
213
+ for (const entry of recommended) {
214
+ lines.push(`# ${entry.description}`);
215
+ if (values[entry.key]) lines.push(`${entry.key}=${values[entry.key]}`);
216
+ else lines.push(`# ${entry.key}=`);
217
+ }
218
+ lines.push('');
219
+ }
220
+
221
+ lines.push(`# --- ${describeGroup('optional')} — uncomment what you need --`);
222
+ for (const entry of catalogByGroup('optional')) {
223
+ if (values[entry.key]) lines.push(`${entry.key}=${values[entry.key]}`);
224
+ else lines.push(`# ${entry.key}=${entry.defaultValue ?? ''} # ${entry.description}`);
225
+ }
226
+ lines.push('');
227
+
228
+ lines.push(`# --- ${describeGroup('cloud')} (only if you use cloud models) ---`);
229
+ for (const entry of catalogByGroup('cloud')) {
230
+ if (values[entry.key]) lines.push(`${entry.key}=${values[entry.key]}`);
231
+ else lines.push(`# ${entry.key}= # ${entry.description}`);
232
+ }
233
+ lines.push('');
234
+
235
+ lines.push(`# --- ${describeGroup('server')} — never set by fans/CLI users ---`);
236
+ lines.push('# These credentials (Upstash rate limiting, Supabase storage, API signing');
237
+ lines.push('# secrets) belong to the hosted REST API / web-chat deployment and are');
238
+ lines.push('# already configured by the platform developer on that environment.');
239
+ lines.push('# /env init will never prompt for them and they are not needed locally.');
240
+ const serverKeys = catalogByGroup('server').map(entry => entry.key).join(', ');
241
+ lines.push(`# ${serverKeys}`);
242
+ lines.push('');
243
+
244
+ if (detected?.loadedModel) {
245
+ lines.push(`# Detected LM Studio model at generation time: ${detected.loadedModel}` +
246
+ (detected.contextLength ? ` (loaded context ${detected.contextLength} tokens)` : ''));
247
+ }
248
+
249
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n') + '\n';
250
+ }
251
+
252
+ export function writeEnvFile(filePath, content, { allowOverwrite = false } = {}) {
253
+ if (fs.existsSync(filePath) && !allowOverwrite) {
254
+ throw new Error(`${filePath} already exists (use /env set to change individual keys)`);
255
+ }
256
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
257
+ fs.writeFileSync(filePath, content, 'utf8');
258
+ return filePath;
259
+ }
260
+
261
+ export function generateEnvFile({ targetPath, values = {}, detected = null, allowOverwrite = true } = {}) {
262
+ const content = buildEnvTemplate({ values, detected });
263
+ writeEnvFile(targetPath, content, { allowOverwrite });
264
+ return { path: targetPath, keys: Object.keys(values) };
265
+ }
266
+
267
+
268
+ export function upsertEnvKey(filePath, key, value) {
269
+ const cleanKey = String(key || '').trim();
270
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(cleanKey)) {
271
+ throw new Error(`Invalid environment variable name: ${key}`);
272
+ }
273
+ const cleanVal = String(value ?? '').replace(/[\r\n]+/g, '').trim();
274
+ const activeRe = new RegExp(`^${cleanKey}=`);
275
+ const commentedRe = new RegExp(`^#\\s*${cleanKey}=`);
276
+ const raw = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
277
+ const lines = raw ? raw.split(/\r?\n/) : [];
278
+
279
+ let replaced = false;
280
+ const next = [];
281
+ for (const line of lines) {
282
+ if (activeRe.test(line)) {
283
+ if (!replaced) {
284
+ next.push(`${cleanKey}=${cleanVal}`);
285
+ replaced = true;
286
+ }
287
+ continue;
288
+ }
289
+ if (commentedRe.test(line)) {
290
+ if (!replaced) {
291
+ next.push(`${cleanKey}=${cleanVal}`);
292
+ replaced = true;
293
+ }
294
+ continue;
295
+ }
296
+ next.push(line);
297
+ }
298
+ if (!replaced) next.push(`${cleanKey}=${cleanVal}`);
299
+
300
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
301
+ fs.writeFileSync(filePath, next.join('\n'), 'utf8');
302
+ return { path: filePath, key: cleanKey, value: cleanVal };
303
+ }
304
+
305
+ export function removeEnvKey(filePath, key) {
306
+ const cleanKey = String(key || '').trim();
307
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(cleanKey)) {
308
+ throw new Error(`Invalid environment variable name: ${key}`);
309
+ }
310
+ const activeRe = new RegExp(`^${cleanKey}=`);
311
+ const commentedRe = new RegExp(`^#\\s*${cleanKey}=`);
312
+ if (!fs.existsSync(filePath)) return { path: filePath, removed: false };
313
+ const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
314
+ const next = lines.filter(line => !activeRe.test(line) && !commentedRe.test(line));
315
+ fs.writeFileSync(filePath, next.join('\n'), 'utf8');
316
+ return { path: filePath, removed: next.length !== lines.length };
317
+ }
@@ -33,7 +33,7 @@ class EventBus {
33
33
  const exactSet = this._listeners.get(event);
34
34
  if (exactSet) {
35
35
  for (const fn of exactSet) {
36
- try { fn(data, event); } catch { /* swallow */ }
36
+ try { fn(data, event); } catch { }
37
37
  }
38
38
  }
39
39
 
@@ -42,14 +42,14 @@ class EventBus {
42
42
  const wildcardSet = this._listeners.get(wildcardEvent);
43
43
  if (wildcardSet) {
44
44
  for (const fn of wildcardSet) {
45
- try { fn(data, event); } catch { /* swallow */ }
45
+ try { fn(data, event); } catch { }
46
46
  }
47
47
  }
48
48
 
49
49
  const globalSet = this._listeners.get('*');
50
50
  if (globalSet) {
51
51
  for (const fn of globalSet) {
52
- try { fn(data, event); } catch { /* swallow */ }
52
+ try { fn(data, event); } catch { }
53
53
  }
54
54
  }
55
55
  }
@@ -1,11 +1,8 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
 
4
- // Minimum feedback samples on a feature before it becomes a learned directive.
5
4
  export const FEEDBACK_MIN_SAMPLES = 5;
6
- // Like-rate at/above this promotes a "prefer" directive.
7
5
  export const FEEDBACK_LIKE_THRESHOLD = 0.7;
8
- // Like-rate at/below this promotes an "avoid" directive.
9
6
  export const FEEDBACK_DISLIKE_THRESHOLD = 0.35;
10
7
  export const FEEDBACK_MAX_DIRECTIVES = 6;
11
8
  const MAX_EVENTS = 500;
@@ -46,10 +43,6 @@ export function extractAnswerFeatures(answer = '', toolsUsed = []) {
46
43
  };
47
44
  }
48
45
 
49
- // Each definition maps one observable answer trait to an optional "prefer"
50
- // directive (high like-rate) and/or an "avoid" directive (low like-rate).
51
- // Traits whose dislike-side is ambiguous (e.g. disliking a short answer does
52
- // not imply verbosity is wanted) only define the like side.
53
46
  const FEATURE_DEFS = [
54
47
  {
55
48
  key: 'len:short',
@@ -183,7 +176,6 @@ export class FeedbackLearning {
183
176
  out.push({ id: `avoid:${def.key}`, kind: 'avoid', text: def.dislike, support: samples, likeRate: Number(likeRate.toFixed(2)), updatedAt: new Date().toISOString() });
184
177
  }
185
178
  }
186
- // Prefer stronger evidence first; cap so the prompt stays lean.
187
179
  out.sort((a, b) => b.support - a.support || (b.kind === 'avoid' ? 1 : 0) - (a.kind === 'avoid' ? 1 : 0));
188
180
  this.state.directives = out.slice(0, FEEDBACK_MAX_DIRECTIVES);
189
181
  }
@@ -29,11 +29,97 @@ function runProcessCapture(cmd, args, timeoutMs = 2500) {
29
29
  });
30
30
  }
31
31
 
32
- export async function getGpuMetrics() {
32
+ function detectGpuVendor(name) {
33
+ return name.includes('NVIDIA')
34
+ ? 'NVIDIA'
35
+ : name.includes('AMD') || name.includes('Radeon')
36
+ ? 'AMD'
37
+ : name.includes('Intel')
38
+ ? 'Intel'
39
+ : 'Generic';
40
+ }
41
+
42
+ let cachedSharedMemory = null;
43
+ const SHARED_MEMORY_TTL_MS = 10_000;
44
+
45
+ export async function getSharedMemoryMetrics(processFilter = '') {
46
+ if (process.platform !== 'win32') {
47
+ return { available: false, sharedMemoryUsedMb: 0, hasSpillover: false };
48
+ }
49
+ if (cachedSharedMemory && (Date.now() - cachedSharedMemory.timestamp < SHARED_MEMORY_TTL_MS)) {
50
+ return cachedSharedMemory.data;
51
+ }
52
+ try {
53
+ const script = processFilter
54
+ ? `$p = @(Get-Process '*${processFilter}*' -ErrorAction SilentlyContinue).Id; if ($p -and $p.Count -gt 0) { $paths = $p | ForEach-Object { "\\GPU Process Memory(pid_" + $_ + "_*)\\Shared Usage" }; $s = (Get-Counter $paths -ErrorAction SilentlyContinue).CounterSamples; if ($s) { ($s | Measure-Object -Property CookedValue -Sum).Sum } else { 0 } } else { (Get-Counter '\\GPU Process Memory(*)\\Shared Usage' -ErrorAction SilentlyContinue).CounterSamples | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum }`
55
+ : `(Get-Counter '\\GPU Process Memory(*)\\Shared Usage' -ErrorAction SilentlyContinue).CounterSamples | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum`;
56
+ const res = await runProcessCapture('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], 2500);
57
+ if (res.ok && res.stdout) {
58
+ const bytes = parseFloat(res.stdout.trim()) || 0;
59
+ const mb = Number((bytes / (1024 * 1024)).toFixed(1));
60
+ const result = {
61
+ available: true,
62
+ sharedMemoryBytes: bytes,
63
+ sharedMemoryUsedMb: mb,
64
+ hasSpillover: mb > 5.0,
65
+ };
66
+ cachedSharedMemory = { data: result, timestamp: Date.now() };
67
+ return result;
68
+ }
69
+ } catch {}
70
+ return { available: false, sharedMemoryUsedMb: 0, hasSpillover: false };
71
+ }
72
+
73
+ let cachedGpuMetrics = null;
74
+ let lastGpuMetricsFetch = 0;
75
+ const GPU_METRICS_TTL_MS = 5_000;
76
+
77
+ let gpuMetricsInFlight = null;
78
+ let windowsFallbackCache = null;
79
+ let windowsFallbackInFlight = null;
80
+ const WINDOWS_FALLBACK_TTL_MS = 300_000;
81
+
82
+ export async function getGpuMetrics(options = {}) {
83
+ const hasCache = cachedGpuMetrics !== null;
84
+ const isFresh = hasCache && (Date.now() - lastGpuMetricsFetch < GPU_METRICS_TTL_MS);
85
+
86
+ if (isFresh && !options.noCache) {
87
+ return cachedGpuMetrics;
88
+ }
89
+
90
+ if (hasCache && !options.noCache) {
91
+ if (!gpuMetricsInFlight) {
92
+ gpuMetricsInFlight = computeGpuMetrics(options)
93
+ .catch(() => cachedGpuMetrics)
94
+ .finally(() => { gpuMetricsInFlight = null; });
95
+ }
96
+ return cachedGpuMetrics;
97
+ }
98
+
99
+ if (gpuMetricsInFlight) return gpuMetricsInFlight;
100
+ gpuMetricsInFlight = computeGpuMetrics(options);
101
+ try {
102
+ return await gpuMetricsInFlight;
103
+ } finally {
104
+ gpuMetricsInFlight = null;
105
+ }
106
+ }
33
107
 
34
- const nv = await runProcessCapture('nvidia-smi', [
35
- '--query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,temperature.gpu,driver_version',
36
- '--format=csv,noheader,nounits'
108
+ async function computeGpuMetrics(options = {}) {
109
+ const result = await computeGpuMetricsUncached(options);
110
+ cachedGpuMetrics = result;
111
+ lastGpuMetricsFetch = Date.now();
112
+ return result;
113
+ }
114
+
115
+ async function computeGpuMetricsUncached(options = {}) {
116
+ const includeShared = options.includeShared !== false;
117
+ const [nv, shared] = await Promise.all([
118
+ runProcessCapture('nvidia-smi', [
119
+ '--query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,temperature.gpu,driver_version',
120
+ '--format=csv,noheader,nounits'
121
+ ]),
122
+ includeShared ? getSharedMemoryMetrics('lm') : Promise.resolve(cachedSharedMemory?.data || { available: false, sharedMemoryUsedMb: 0, hasSpillover: false })
37
123
  ]);
38
124
 
39
125
  if (nv.ok && nv.stdout) {
@@ -54,6 +140,8 @@ export async function getGpuMetrics() {
54
140
  totalMb,
55
141
  usedMb,
56
142
  freeMb,
143
+ source: 'nvidia-smi (NVML)',
144
+ approximate: false,
57
145
  totalGb: (totalMb / 1024).toFixed(1),
58
146
  usedGb: (usedMb / 1024).toFixed(1),
59
147
  freeGb: (freeMb / 1024).toFixed(1),
@@ -61,6 +149,8 @@ export async function getGpuMetrics() {
61
149
  utilizationGpu: utilPercent,
62
150
  temperatureC: tempC,
63
151
  driverVersion: driver || 'N/A',
152
+ sharedMemoryUsedMb: shared.sharedMemoryUsedMb || 0,
153
+ hasSpillover: Boolean(shared.hasSpillover),
64
154
  };
65
155
  });
66
156
 
@@ -71,62 +161,26 @@ export async function getGpuMetrics() {
71
161
  count: gpus.length,
72
162
  primary: gpus[0],
73
163
  gpus,
164
+ sharedMemory: shared,
74
165
  };
75
166
  }
76
167
  }
77
168
 
78
-
79
- if (process.platform === 'win32') {
80
- const ps = await runProcessCapture('powershell', [
81
- '-NoProfile',
82
- '-NonInteractive',
83
- '-Command',
84
- 'Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM | ConvertTo-Json'
85
- ]);
86
-
87
- if (ps.ok && ps.stdout) {
88
- try {
89
- let data = JSON.parse(ps.stdout);
90
- if (!Array.isArray(data)) data = [data];
91
- const gpus = data.filter(item => item?.Name).map((item, idx) => {
92
- const ramBytes = item.AdapterRAM || 0;
93
- const totalMb = Math.round(ramBytes / (1024 * 1024));
94
- const name = String(item.Name);
95
- const vendor = name.includes('NVIDIA')
96
- ? 'NVIDIA'
97
- : name.includes('AMD') || name.includes('Radeon')
98
- ? 'AMD'
99
- : name.includes('Intel')
100
- ? 'Intel'
101
- : 'Generic';
102
169
 
103
- return {
104
- index: idx,
105
- name,
106
- vendor,
107
- totalMb,
108
- usedMb: null,
109
- freeMb: null,
110
- totalGb: (totalMb / 1024).toFixed(1),
111
- usedGb: null,
112
- freeGb: null,
113
- usedPercent: null,
114
- utilizationGpu: null,
115
- temperatureC: null,
116
- driverVersion: 'N/A',
117
- };
118
- });
119
-
120
- if (gpus.length > 0) {
121
- return {
122
- available: true,
123
- vendor: gpus[0].vendor,
124
- count: gpus.length,
125
- primary: gpus[0],
126
- gpus,
127
- };
128
- }
129
- } catch {}
170
+ if (process.platform === 'win32') {
171
+ if (windowsFallbackCache && Date.now() < windowsFallbackCache.expires) {
172
+ return windowsFallbackCache.data;
173
+ }
174
+ if (!windowsFallbackInFlight) windowsFallbackInFlight = probeWindowsGpuFallback();
175
+ let fallback = null;
176
+ try {
177
+ fallback = await windowsFallbackInFlight;
178
+ } finally {
179
+ windowsFallbackInFlight = null;
180
+ }
181
+ if (fallback) {
182
+ windowsFallbackCache = { expires: Date.now() + WINDOWS_FALLBACK_TTL_MS, data: fallback };
183
+ return fallback;
130
184
  }
131
185
  }
132
186
 
@@ -175,6 +229,85 @@ export async function getGpuMetrics() {
175
229
  };
176
230
  }
177
231
 
232
+ async function probeWindowsGpuFallback() {
233
+ const REGISTRY_SNIPPET = "$b='HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}'; Get-ChildItem $b -ErrorAction SilentlyContinue | ForEach-Object { $p = Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue; if ($p.DriverDesc -and $p.'HardwareInformation.qwMemorySize') { $raw = $p.'HardwareInformation.qwMemorySize'; $n = [UInt64]0; if ($raw -is [byte[]]) { $n = [BitConverter]::ToUInt64([byte[]]$raw, 0) } elseif ($raw -is [long] -or $raw -is [int]) { $n = [UInt64]$raw }; if ($n -gt 0) { [PSCustomObject]@{ Name = $p.DriverDesc; Bytes = $n } } } } | ConvertTo-Json";
234
+
235
+ let gpus = [];
236
+
237
+ const ps = await runProcessCapture('powershell', [
238
+ '-NoProfile',
239
+ '-NonInteractive',
240
+ '-Command',
241
+ REGISTRY_SNIPPET,
242
+ ]);
243
+ if (ps.stdout) {
244
+ try {
245
+ let data = JSON.parse(ps.stdout);
246
+ if (!Array.isArray(data)) data = [data];
247
+ gpus = data.filter(item => item?.Name && Number(item.Bytes) > 0).map((item, idx) => {
248
+ const name = String(item.Name);
249
+ return {
250
+ index: idx,
251
+ name,
252
+ vendor: detectGpuVendor(name),
253
+ totalMb: Math.round(Number(item.Bytes) / (1024 * 1024)),
254
+ usedMb: null,
255
+ freeMb: null,
256
+ source: 'windows-registry',
257
+ approximate: false,
258
+ };
259
+ });
260
+ } catch {}
261
+ }
262
+
263
+ if (gpus.length === 0) {
264
+ const wmi = await runProcessCapture('powershell', [
265
+ '-NoProfile',
266
+ '-NonInteractive',
267
+ '-Command',
268
+ 'Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM | ConvertTo-Json'
269
+ ]);
270
+ if (wmi.stdout) {
271
+ try {
272
+ let data = JSON.parse(wmi.stdout);
273
+ if (!Array.isArray(data)) data = [data];
274
+ gpus = data.filter(item => item?.Name).map((item, idx) => ({
275
+ index: idx,
276
+ name: String(item.Name),
277
+ vendor: detectGpuVendor(String(item.Name)),
278
+ totalMb: Math.round((item.AdapterRAM || 0) / (1024 * 1024)),
279
+ usedMb: null,
280
+ freeMb: null,
281
+ source: 'wmi-adapterram',
282
+ approximate: true,
283
+ }));
284
+ } catch {}
285
+ }
286
+ }
287
+
288
+ if (gpus.length === 0) return null;
289
+
290
+ gpus.sort((a, b) => (b.totalMb || 0) - (a.totalMb || 0));
291
+ gpus.forEach((g, idx) => { g.index = idx; });
292
+ const primary = gpus[0];
293
+ return {
294
+ available: true,
295
+ vendor: primary.vendor,
296
+ count: gpus.length,
297
+ primary: {
298
+ ...primary,
299
+ totalGb: (primary.totalMb / 1024).toFixed(1),
300
+ usedGb: null,
301
+ freeGb: null,
302
+ usedPercent: null,
303
+ utilizationGpu: null,
304
+ temperatureC: null,
305
+ driverVersion: 'N/A',
306
+ },
307
+ gpus,
308
+ };
309
+ }
310
+
178
311
  export function formatProgressBar(percent = 0, width = 16) {
179
312
  const safePct = Math.max(0, Math.min(100, Number(percent) || 0));
180
313
  const filled = Math.round((safePct / 100) * width);