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
@@ -12,13 +12,47 @@ import { DebateOrchestrator } from './debate.js';
12
12
  import { timelineData } from '../../src/data/timelineData.js';
13
13
  import { COMMAND_NAMES, getCommandDetails, findCommandDefinition } from './commandRegistry.js';
14
14
  import { THEMES, THEME_NAMES, ansiBackground, ansiForeground, getThemeName, setTheme } from './theme.js';
15
- import { getGpuMetrics, formatProgressBar } from './gpuMetrics.js';
15
+ import { getGpuMetrics, getSharedMemoryMetrics, formatProgressBar } from './gpuMetrics.js';
16
+ import { computeVramEstimate, getHardwareTarget, probeLmStudioRuntime, resolveSystemHardwareTier } from './vramService.js';
17
+ import { formatGb, bytesToGb, QUANT_BPW, classifyAllocation, QUANT_LADDER, parseBpw } from '../../src/lib/vram/estimator.js';
18
+ import { matchModelIdentifier } from './config.js';
19
+ import { MODEL_REGISTRY } from '../../src/lib/vram/modelRegistry.js';
20
+ import { computePercentile } from './tokenTracker.js';
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import http from 'node:http';
16
24
  import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
17
25
  import { QUIZ_QUESTIONS, QUIZ_I18N, LETTER_LABELS, shuffleQuizQuestions } from './quizData.js';
18
26
  import crypto from 'node:crypto';
19
27
  import { isAbortError } from './errors.js';
20
28
  import { featureCommandHandlers, aiNarration, languageHint } from './commandFeatures.js';
21
29
  import { startWebChatServer, stopWebChatServer, getWebChatServerStatus, openBrowser } from '../webChatServer.js';
30
+ import readline from 'node:readline';
31
+ import {
32
+ resolveEnvTargets,
33
+ readEnvFile,
34
+ readMergedEnvMap,
35
+ upsertEnvKey,
36
+ removeEnvKey,
37
+ maskValue,
38
+ isSecretKey,
39
+ ENV_VAR_CATALOG,
40
+ describeGroup,
41
+ detectAutoDefaults,
42
+ generateEnvFile,
43
+ } from './envManager.js';
44
+ import { cleanUrl } from './research.js';
45
+
46
+ function askEnvQuestion(question, defaultValue = '') {
47
+ return new Promise((resolve) => {
48
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
49
+ const suffix = defaultValue ? ` ${C.dim}[${defaultValue}]${C.reset}` : '';
50
+ rl.question(`${C.cyberCyan}>${C.reset} ${question}${suffix}: `, (answer) => {
51
+ rl.close();
52
+ resolve(String(answer ?? '').trim());
53
+ });
54
+ });
55
+ }
22
56
 
23
57
  function levenshteinDistance(a, b) {
24
58
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -92,6 +126,228 @@ export function fuzzyMatchTools(query) {
92
126
  .sort((a, b) => b.score - a.score);
93
127
  }
94
128
 
129
+ function parseVramArgs(tokens = []) {
130
+ const overrides = {};
131
+ const positional = [];
132
+ let force = false;
133
+ const num = (value) => {
134
+ const parsed = Number(value);
135
+ return Number.isFinite(parsed) ? parsed : undefined;
136
+ };
137
+ for (let i = 0; i < tokens.length; i++) {
138
+ const token = tokens[i];
139
+ const nextVal = () => tokens[++i];
140
+ if (token === '--ctx') overrides.contextLength = num(nextVal());
141
+ else if (token === '--quant') overrides.quant = nextVal();
142
+ else if (token === '--params') overrides.paramsB = num(nextVal());
143
+ else if (token === '--layers') overrides.layers = num(nextVal());
144
+ else if (token === '--kv-heads') overrides.kvHeads = num(nextVal());
145
+ else if (token === '--head-dim') overrides.headDim = num(nextVal());
146
+ else if (token === '--cache-bits') overrides.cacheBits = num(nextVal());
147
+ else if (token === '--batch') overrides.batchSize = num(nextVal());
148
+ else if (token === '--force') force = true;
149
+ else positional.push(token);
150
+ }
151
+ return { overrides, positional, force };
152
+ }
153
+
154
+ function zoneBadge(zone) {
155
+ const color = zone === 'GREEN' ? C.emerald : zone === 'YELLOW' ? C.gold : zone === 'RED' ? C.roseGlow : C.slate;
156
+ return `${color}[${zone}]${C.reset}`;
157
+ }
158
+
159
+ function resolveVramModelTarget(ctx, positional = []) {
160
+ const target = positional.join(' ');
161
+ if (!target) return { modelName: ctx.agent?.client?.model || null, modelPath: null };
162
+ if (/\.gguf$/i.test(target)) return { modelName: null, modelPath: target };
163
+ return { modelName: target, modelPath: null };
164
+ }
165
+
166
+ /** `/vram live` — actual VRAM usage right now vs. the model's footprint. */
167
+ async function printVramLivePanel(ctx, { overrides, positional }) {
168
+ const client = ctx.agent?.client || null;
169
+ const { modelName, modelPath } = resolveVramModelTarget(ctx, positional);
170
+
171
+ const [hw, runtime, shared] = await Promise.all([
172
+ getHardwareTarget(),
173
+ client?.baseUrl ? probeLmStudioRuntime(client.baseUrl) : Promise.resolve({ reachable: false }),
174
+ getSharedMemoryMetrics('lm'),
175
+ ]);
176
+
177
+ const result = await computeVramEstimate({ model: modelName, modelPath, ...overrides, includeHardware: false });
178
+ const est = result.estimate;
179
+
180
+ const lines = [];
181
+ let livePct = null;
182
+ let freeBytes = null;
183
+ if (hw.available && hw.freeBytes !== null && hw.freeBytes !== undefined) {
184
+ const usedBytes = Math.max(0, hw.totalBytes - hw.freeBytes);
185
+ freeBytes = hw.freeBytes;
186
+ const live = classifyAllocation({ totalBytes: usedBytes, availableBytes: hw.totalBytes });
187
+ livePct = live.usagePct;
188
+ const filled = Math.round((Math.min(100, live.usagePct) / 100) * 24);
189
+ const bar = `${C.gold}${'█'.repeat(filled)}${C.dim}${'░'.repeat(24 - filled)}${C.reset}`;
190
+ lines.push(`${C.gold}GPU (live):${C.reset} ${C.bold}${C.white}${hw.name}${C.reset} ${C.dim}via ${hw.source}${C.reset}`);
191
+ lines.push(` ${bar} ${C.bold}${live.usagePct}%${C.reset} ${C.dim}(${formatGb(usedBytes)} / ${formatGb(hw.totalBytes)} used) ${zoneBadge(live.zone)}${C.reset}`);
192
+ lines.push(`${C.gold}Free VRAM:${C.reset} ${C.emerald}${formatGb(freeBytes)}${C.reset}`);
193
+ if (shared?.available) {
194
+ if (shared.hasSpillover) {
195
+ lines.push(`${C.roseGlow}Shared GPU:${C.reset} ${C.bold}${C.roseGlow}${shared.sharedMemoryUsedMb} MB (SPILLOVER DETECTED — PCIe fallback degrades decoding speed)${C.reset}`);
196
+ } else {
197
+ lines.push(`${C.gold}Shared GPU:${C.reset} ${C.emerald}${shared.sharedMemoryUsedMb} MB (0% spillover — 100% Dedicated VRAM)${C.reset}`);
198
+ }
199
+ }
200
+ } else if (hw.available) {
201
+ lines.push(`${C.gold}GPU:${C.reset} ${C.white}${hw.name}${C.reset} ${C.dim}(total ${formatGb(hw.totalBytes)} — live usage unavailable)${C.reset}`);
202
+ } else {
203
+ lines.push(`${C.gold}GPU:${C.reset} ${C.dim}not detected — ${hw.reason || 'no queryable VRAM'}${C.reset}`);
204
+ }
205
+
206
+ lines.push(`${C.gold}Model:${C.reset} ${C.bold}${C.white}${result.architecture.name || modelName || modelPath || 'unknown'}${C.reset}${result.approximate ? C.dim + ' (approx)' : ''}${C.reset} ${C.dim}[${result.configSource}]${C.reset}`);
207
+ if (runtime?.reachable) {
208
+ lines.push(`${C.gold}LM Studio:${C.reset} loaded ${C.white}${runtime.loadedModel || '?'}${C.reset}${runtime.loadedContextLength ? `${C.dim} @ ${runtime.loadedContextLength.toLocaleString('en-US')} ctx${runtime.quantization ? ` • ${runtime.quantization}` : ''}${C.reset}` : ''}`);
209
+ } else {
210
+ lines.push(`${C.gold}LM Studio:${C.reset} ${C.dim}not reachable — using checkpoint/registry context${C.reset}`);
211
+ }
212
+
213
+ const runtimeCtx = Number(overrides.contextLength) || runtime?.loadedContextLength || est.contextLength;
214
+ const estCls = hw.available ? classifyAllocation({ totalBytes: est.totalBytes, availableBytes: hw.totalBytes }) : null;
215
+ lines.push(`${C.gold}Estimate:${C.reset} ${C.bold}${C.white}${formatGb(est.totalBytes)}${C.reset} @ ${C.white}${runtimeCtx ? runtimeCtx.toLocaleString('en-US') : '?'} ctx${C.reset}` +
216
+ ` ${C.dim}(weights ${formatGb(est.weightsBytes)} + KV ${formatGb(est.kvCacheBytes)} + fixed ${formatGb((est.activationBytes || 0) + (est.overheadBytes || 0))})${C.reset}` +
217
+ (estCls && estCls.zone !== 'UNKNOWN' ? ` ${zoneBadge(estCls.zone)}` : ''));
218
+
219
+ const loadedMatch = Boolean(runtime?.loadedModel && client?.model && matchModelIdentifier(client.model, runtime.loadedModel));
220
+ if (loadedMatch && livePct !== null) {
221
+ lines.push('',
222
+ livePct <= 80
223
+ ? `${C.emerald}${C.bold}Model already loaded & running at ${livePct}% VRAM — plenty of headroom.${C.reset}`
224
+ : `${C.gold}${C.bold}Model already loaded & running at ${livePct}% VRAM — it fits, but the card is tight.${C.reset}`,
225
+ `${C.dim}The naive full-attention estimate overstates sliding-window models (Gemma/Qwen); the live reading above is authoritative.${C.reset}`);
226
+ } else if (freeBytes !== null && est.totalBytes > 0) {
227
+ if (est.totalBytes <= freeBytes) {
228
+ lines.push('', `${C.emerald}${C.bold}Fits right now:${C.reset} ${C.emerald}free VRAM ${formatGb(freeBytes)} covers the ${formatGb(est.totalBytes)} estimate.${C.reset}`);
229
+ } else {
230
+ lines.push('', `${C.roseGlow}Does not fit right now:${C.reset} estimate ${formatGb(est.totalBytes)} vs ${formatGb(freeBytes)} free.`,
231
+ `${C.dim}Close other VRAM users, lower the context, or pick a lighter quant: /vram compare${C.reset}`);
232
+ }
233
+ } else if (estCls && estCls.zone !== 'UNKNOWN') {
234
+ lines.push('', `${C.dim}Live usage unavailable — estimate-only feasibility: ${estCls.zone}${C.reset}`);
235
+ }
236
+
237
+ printSolidPanel({
238
+ title: 'VRAM Live Status',
239
+ lines,
240
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
241
+ });
242
+ return 'HANDLED';
243
+ }
244
+
245
+ /** `/vram compare` — every quantization for one model/context against the card. */
246
+ async function printVramQuantCompare(ctx, { overrides, positional }) {
247
+ const client = ctx.agent?.client || null;
248
+ const { modelName, modelPath } = resolveVramModelTarget(ctx, positional);
249
+
250
+ const base = await computeVramEstimate({ model: modelName, modelPath, ...overrides, includeHardware: false });
251
+ const est = base.estimate;
252
+ if (!est.paramCount || !est.layers || !est.kvHeads || !est.headDim) {
253
+ console.log(`\n ${C.amber}Insufficient architecture data for ${base.architecture.name || modelName || 'model'}.${C.reset}`);
254
+ console.log(` ${C.dim}Pass explicit geometry: --params <B> --layers <n> --kv-heads <n> --head-dim <n> (or check /vram list).${C.reset}\n`);
255
+ return 'HANDLED';
256
+ }
257
+
258
+ const ctxLen = Number(overrides.contextLength) || est.contextLength || 16384;
259
+ const hw = await getHardwareTarget();
260
+ const fixed = (est.activationBytes || 0) + (est.overheadBytes || 0);
261
+ const visionBytes = est.visionEncoderBytes || 0;
262
+ const kvPerToken = est.kvBytesPerToken;
263
+
264
+ const rows = QUANT_LADDER.map((quant) => {
265
+ const bpw = parseBpw(quant);
266
+ const weights = Math.round(est.paramCount * bpw / 8) + visionBytes;
267
+ const kv = kvPerToken ? Math.round(kvPerToken * ctxLen) : 0;
268
+ const total = weights + kv + fixed;
269
+ const cls = hw.available ? classifyAllocation({ totalBytes: total, availableBytes: hw.totalBytes }) : null;
270
+ return { quant, weights, total, cls };
271
+ });
272
+
273
+ const lines = [
274
+ `${C.gold}Model:${C.reset} ${C.bold}${C.white}${base.architecture.name || modelName || 'model'}${C.reset} ${C.dim}(${(est.paramCount / 1e9).toFixed(1)}B • ${est.layers}L • GQA ${est.queryHeads}/${est.kvHeads} • d=${est.headDim}${visionBytes ? ' • VLM' : ''})${C.reset}`,
275
+ `${C.gold}Context:${C.reset} ${C.white}${ctxLen.toLocaleString('en-US')} tokens${C.reset}${hw.available ? ` ${C.dim}vs ${formatGb(hw.totalBytes)} card${C.reset}` : ` ${C.dim}(no GPU detected — zones unavailable)${C.reset}`}`,
276
+ '',
277
+ `${C.bold}${C.lavender}${'Quantization'.padEnd(13)}${'Weights'.padStart(10)} ${'Total'.padStart(10)} ${'vs Card'.padStart(9)} Zone${C.reset}`,
278
+ ];
279
+ for (const row of rows) {
280
+ const marker = row.quant === est.quant ? `${C.cyberCyan}*${C.reset}` : ' ';
281
+ const zone = row.cls ? zoneBadge(row.cls.zone) : `${C.dim}—${C.reset}`;
282
+ const pct = row.cls ? `${row.cls.usagePct}%` : '—';
283
+ lines.push(`${marker} ${String(row.quant.toUpperCase()).padEnd(12)}${formatGb(row.weights).padStart(10)} ${formatGb(row.total).padStart(10)} ${String(pct).padStart(9)} ${zone}`);
284
+ }
285
+ const firstGreen = rows.find(r => r.cls?.zone === 'GREEN');
286
+ if (firstGreen) {
287
+ lines.push('', `${C.dim}First GREEN at this context: ${C.reset}${C.emerald}${firstGreen.quant.toUpperCase()} → ${formatGb(firstGreen.total)}${C.reset}`);
288
+ }
289
+ lines.push(`${C.dim}Estimates assume full-attention KV caching; sliding-window models (Gemma, Qwen SWA layers) cache less.${C.reset}`);
290
+ printSolidPanel({
291
+ title: 'VRAM Quantization Compare',
292
+ lines,
293
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
294
+ });
295
+ return 'HANDLED';
296
+ }
297
+
298
+ /** `/vram ctx` — how far the context ladder fits at the chosen quantization. */
299
+ async function printVramContextLadder(ctx, { overrides, positional }) {
300
+ const client = ctx.agent?.client || null;
301
+ const { modelName, modelPath } = resolveVramModelTarget(ctx, positional);
302
+
303
+ const base = await computeVramEstimate({ model: modelName, modelPath, ...overrides, includeHardware: false });
304
+ const est = base.estimate;
305
+ if (!est.paramCount || !est.kvBytesPerToken) {
306
+ console.log(`\n ${C.amber}Insufficient architecture data for ${base.architecture.name || modelName || 'model'} — pass --params/--layers/--kv-heads/--head-dim.${C.reset}\n`);
307
+ return 'HANDLED';
308
+ }
309
+
310
+ const quant = String(overrides.quant || est.quant || 'q4_k_m');
311
+ const bpw = parseBpw(quant) ?? 4.5;
312
+ const weights = Math.round(est.paramCount * bpw / 8) + (est.visionEncoderBytes || 0);
313
+ const fixed = (est.activationBytes || 0) + (est.overheadBytes || 0);
314
+ const kvPerToken = est.kvBytesPerToken;
315
+ const hw = await getHardwareTarget();
316
+ const ladder = [2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144];
317
+
318
+ const rows = ladder.map((ctxTokens) => {
319
+ const total = weights + Math.round(kvPerToken * ctxTokens) + fixed;
320
+ const cls = hw.available ? classifyAllocation({ totalBytes: total, availableBytes: hw.totalBytes }) : null;
321
+ return { ctxTokens, total, cls };
322
+ });
323
+
324
+ const lines = [
325
+ `${C.gold}Model:${C.reset} ${C.bold}${C.white}${base.architecture.name || modelName || 'model'}${C.reset} ${C.dim}@ ${quant.toUpperCase()} (${(est.paramCount / 1e9).toFixed(1)}B • ${formatGb(weights)} weights + ${formatGb(fixed)} fixed)${C.reset}`,
326
+ '',
327
+ `${C.bold}${C.lavender}${'Context'.padStart(10)} ${'KV Cache'.padStart(10)} ${'Total'.padStart(10)} ${'vs Card'.padStart(9)} Zone${C.reset}`,
328
+ ];
329
+ for (const row of rows) {
330
+ const zone = row.cls ? zoneBadge(row.cls.zone) : `${C.dim}—${C.reset}`;
331
+ const pct = row.cls ? `${row.cls.usagePct}%` : '—';
332
+ lines.push(`${String(row.ctxTokens.toLocaleString('en-US')).padStart(10)} ${formatGb(Math.round(kvPerToken * row.ctxTokens)).padStart(10)} ${formatGb(row.total).padStart(10)} ${String(pct).padStart(9)} ${zone}`);
333
+ }
334
+ const maxGreen = [...rows].reverse().find(r => r.cls?.zone === 'GREEN');
335
+ if (maxGreen) {
336
+ lines.push('', `${C.dim}Largest GREEN context: ${C.reset}${C.emerald}${maxGreen.ctxTokens.toLocaleString('en-US')} tokens${C.reset}`);
337
+ } else {
338
+ lines.push('', `${C.amber}No ladder step fits this card — try a lighter quant: /vram compare${C.reset}`);
339
+ }
340
+ lines.push(`${C.dim}Estimates assume full-attention KV caching; sliding-window models (Gemma, Qwen SWA layers) cache less.${C.reset}`);
341
+ printSolidPanel({
342
+ title: 'VRAM Context Ladder',
343
+ lines,
344
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
345
+ });
346
+ return 'HANDLED';
347
+ }
348
+
349
+ let lastLanScan = null;
350
+
95
351
  export const handlers = {
96
352
  ...featureCommandHandlers,
97
353
  '/exit': async (ctx) => {
@@ -100,6 +356,70 @@ export const handlers = {
100
356
  return 'EXIT';
101
357
  },
102
358
 
359
+ '/ingest': async (ctx) => {
360
+ const sub = (ctx.sub || '').toLowerCase().trim();
361
+ const rest = (ctx.extra || '').trim();
362
+ const { getDynamicCatalogStats, clearDynamicCatalog } = await import('./dynamicCatalog.js');
363
+ const { runIngestionScan, ingestRawEvent } = await import('./ingestionWorker.js');
364
+
365
+ if (!sub || sub === 'status') {
366
+ const stats = getDynamicCatalogStats();
367
+ console.log(`\n ${C.bold}${C.lavender}Autonomous Dynamic Catalog Overlay Status:${C.reset}`);
368
+ console.log(` ${C.dim}─────────────────────────────────────────────────${C.reset}`);
369
+ console.log(` ${C.white}Total Ingested Entries:${C.reset} ${C.bold}${C.emerald}${stats.totalEntries}${C.reset}`);
370
+ console.log(` ${C.dim}├─${C.reset} ${C.gold}Brands:${C.reset} ${stats.brandsCount}`);
371
+ console.log(` ${C.dim}├─${C.reset} ${C.cyberCyan}Timeline:${C.reset} ${stats.timelineCount}`);
372
+ console.log(` ${C.dim}├─${C.reset} ${C.sakura}Gallery:${C.reset} ${stats.galleryCount}`);
373
+ console.log(` ${C.dim}├─${C.reset} ${C.lavender}Works:${C.reset} ${stats.worksCount}`);
374
+ console.log(` ${C.dim}└─${C.reset} ${C.roseGlow}Events:${C.reset} ${stats.eventsCount}`);
375
+ console.log(` ${C.dim}Last Updated:${C.reset} ${stats.updatedAt}\n`);
376
+ return 'HANDLED';
377
+ }
378
+
379
+ if (sub === 'clear') {
380
+ clearDynamicCatalog();
381
+ console.log(`\n ${C.emerald}✓ Dynamic catalog overlay cleared successfully.${C.reset}\n`);
382
+ return 'HANDLED';
383
+ }
384
+
385
+ if (sub === 'add') {
386
+ if (!rest) {
387
+ console.log(`\n ${C.amber}Usage: /ingest add <event or live text>${C.reset}\n`);
388
+ return 'HANDLED';
389
+ }
390
+ console.log(`\n ${C.cyberCyan}Ingesting event into dynamic catalog...${C.reset}`);
391
+ const client = ctx?.agent?.client || null;
392
+ const res = await ingestRawEvent(rest, { client, source: 'cli_manual', trustTier: 1 });
393
+ if (res.status === 'success') {
394
+ console.log(` ${C.emerald}✓ Ingested successfully into category "${res.entry.category}":${C.reset}`);
395
+ console.log(` ${C.white}Title/Brand:${C.reset} ${res.entry.title || res.entry.brand}`);
396
+ console.log(` ${C.white}Year/Date:${C.reset} ${res.entry.year} (${res.entry.date || 'N/A'})`);
397
+ console.log(` ${C.white}Highlight:${C.reset} ${res.entry.highlight || res.entry.summary}\n`);
398
+ } else if (res.status === 'duplicate') {
399
+ console.log(` ${C.amber}[!] Skipped duplicate: ${res.message}${C.reset}\n`);
400
+ } else {
401
+ console.log(` ${C.red}✗ Failed: ${res.reason || res.message}${C.reset}\n`);
402
+ }
403
+ return 'HANDLED';
404
+ }
405
+
406
+ if (sub === 'run') {
407
+ const topic = rest || 'Ju Jingyi 鞠婧祎';
408
+ console.log(`\n ${C.cyberCyan}Scanning source-backed news & events for "${topic}"...${C.reset}`);
409
+ const client = ctx?.agent?.client || null;
410
+ const summary = await runIngestionScan({ topic, client });
411
+ console.log(` ${C.emerald}✓ Scan complete:${C.reset}`);
412
+ console.log(` Scanned: ${summary.totalScanned}`);
413
+ console.log(` Ingested: ${C.bold}${C.emerald}${summary.ingestedCount}${C.reset}`);
414
+ console.log(` Duplicates skipped: ${summary.skippedDuplicates}`);
415
+ console.log(` Untrusted skipped: ${summary.skippedUntrusted}\n`);
416
+ return 'HANDLED';
417
+ }
418
+
419
+ console.log(`\n ${C.amber}Unknown /ingest subcommand: "${sub}". Use status, run, add, or clear.${C.reset}\n`);
420
+ return 'HANDLED';
421
+ },
422
+
103
423
  '/help': async (ctx) => {
104
424
  const sub = (ctx.sub || ctx.extra || '').trim();
105
425
  if (typeof ctx.printHelp === 'function') {
@@ -145,7 +465,7 @@ export const handlers = {
145
465
 
146
466
  '/web': async (ctx) => {
147
467
  const rawAction = (ctx.sub || 'start').toLowerCase();
148
- const extraPort = Number(ctx.extra || (ctx.sub && /^\d+$/.test(ctx.sub) ? ctx.sub : null)) || 3300;
468
+ const extraPort = Number(ctx.extra || (ctx.sub && /^\d+$/.test(ctx.sub) ? ctx.sub : null)) || 3002;
149
469
 
150
470
  if (rawAction === 'stop' || rawAction === 'close') {
151
471
  const stopped = await stopWebChatServer();
@@ -162,6 +482,7 @@ export const handlers = {
162
482
  const lines = [
163
483
  `• Status: ${status.running ? `${C.emerald}Running${C.reset}` : `${C.slate}Stopped${C.reset}`}`,
164
484
  `• URL: ${status.running ? `${C.cyberCyan}${status.url}${C.reset}` : `${C.dim}${status.url}${C.reset}`}`,
485
+ `• Tip: /portal chat also opens the chat URL on the dev server (port 3000)`,
165
486
  `• Port: ${status.port}`,
166
487
  `• Provider: ${status.provider || 'lmstudio'}`,
167
488
  `• Model: ${status.model || 'google/gemma-4-e4b'}`,
@@ -178,7 +499,7 @@ export const handlers = {
178
499
  await stopWebChatServer();
179
500
  }
180
501
 
181
- const port = extraPort >= 1000 ? extraPort : 3300;
502
+ const port = extraPort >= 1000 ? extraPort : 3002;
182
503
  const res = await startWebChatServer({ port, agent: ctx.agent });
183
504
  openBrowser(res.url);
184
505
 
@@ -198,8 +519,6 @@ export const handlers = {
198
519
  return 'HANDLED';
199
520
  },
200
521
 
201
- '/web-chat': async (ctx) => handlers['/web'](ctx),
202
-
203
522
  '/details': async (ctx) => {
204
523
  if (typeof ctx.printQueryDetails === 'function') {
205
524
  ctx.printQueryDetails([ctx.sub, ctx.extra].filter(Boolean).join(' '));
@@ -359,6 +678,14 @@ export const handlers = {
359
678
  const action = (ctx.sub || '').toLowerCase();
360
679
  const latest = ctx.agent.getResearchStatus().lastResult;
361
680
 
681
+ if (action === 'auto' || action === 'autoresearch') {
682
+ const autoParts = ['/autoresearch', ...(ctx.parts?.slice(2) || [])];
683
+ return handlers['/autoresearch']({ ...ctx, sub: autoParts[1] || 'status', parts: autoParts });
684
+ }
685
+ if (action === 'on' || action === 'off') {
686
+ return handlers['/autoresearch']({ ...ctx, sub: action, parts: ['/autoresearch', action, ...(ctx.parts?.slice(3) || [])] });
687
+ }
688
+
362
689
  if (action === 'status' || action === 'workers') {
363
690
  const status = ctx.agent.getResearchStatus();
364
691
  const lines = status.state === 'running'
@@ -376,23 +703,27 @@ export const handlers = {
376
703
  }
377
704
 
378
705
  if (action === 'cancel') {
379
- console.log(ctx.agent.cancelResearch()
706
+ const cancelled = Boolean(ctx.agent?.cancelResearch?.() || ctx.cancelActiveQuery?.());
707
+ console.log(cancelled
380
708
  ? `\n ${C.emerald}[OK] Research cancellation requested.${C.reset}\n`
381
709
  : `\n ${C.amber}No research job is running.${C.reset}\n`);
382
710
  return 'HANDLED';
383
711
  }
384
712
 
385
713
  if (action === 'last') {
714
+ const cleanSources = (latest?.sources || [])
715
+ .map(s => cleanUrl(String(s)))
716
+ .filter(s => /^https?:\/\//i.test(s) && !s.includes('...') && !s.includes('…'));
386
717
  const lines = latest
387
718
  ? [
388
719
  `${C.bold}${C.cyberCyan}Research ID:${C.reset} ${latest.id}`,
389
- `${C.bold}${C.slate}Sources Found:${C.reset} ${latest.sources?.length || 0}`,
720
+ `${C.bold}${C.slate}Sources Found:${C.reset} ${cleanSources.length}`,
390
721
  '',
391
722
  `${C.bold}${C.roseGlow}Synthesized Answer:${C.reset}`,
392
- ...String(latest.answer).split('\n').map(l => ` ${l}`),
723
+ ...formatMarkdownLines(latest.answer || 'No findings recorded.').map(l => ` ${l}`),
393
724
  '',
394
725
  `${C.bold}${C.lavender}Sources Cited:${C.reset}`,
395
- ...(latest.sources?.length ? latest.sources.map((s, i) => ` [${i + 1}] ${s}`) : [` ${C.dim}None recorded.${C.reset}`])
726
+ ...(cleanSources.length ? cleanSources.map((s, i) => ` [${i + 1}] ${C.cyberCyan}${s}${C.reset}`) : [` ${C.dim}None recorded.${C.reset}`])
396
727
  ]
397
728
  : [`${C.dim}No completed research yet. Run /research <question> to start.${C.reset}`];
398
729
 
@@ -404,9 +735,12 @@ export const handlers = {
404
735
  }
405
736
 
406
737
  if (action === 'sources') {
738
+ const cleanSources = (latest?.sources || [])
739
+ .map(s => cleanUrl(String(s)))
740
+ .filter(s => /^https?:\/\//i.test(s) && !s.includes('...') && !s.includes('…'));
407
741
  printSolidPanel({
408
- title: 'Research Sources',
409
- lines: latest?.sources?.length ? latest.sources.map((url, index) => `${C.gold}[${index + 1}]${C.reset} ${C.cyberCyan}${url}${C.reset}`) : [`${C.dim}No research sources recorded.${C.reset}`],
742
+ title: `Research Sources (${cleanSources.length})`,
743
+ lines: cleanSources.length ? cleanSources.map((url, index) => `${C.gold}[${index + 1}]${C.reset} ${C.cyberCyan}${url}${C.reset}`) : [`${C.dim}No research sources recorded.${C.reset}`],
410
744
  });
411
745
  return 'HANDLED';
412
746
  }
@@ -473,18 +807,21 @@ export const handlers = {
473
807
  durationMs: Date.now() - startTime,
474
808
  });
475
809
 
810
+ const cleanSources = (result.sources || [])
811
+ .map(s => cleanUrl(String(s)))
812
+ .filter(s => /^https?:\/\//i.test(s) && !s.includes('...') && !s.includes('…'));
476
813
  const lines = [
477
814
  `${C.bold}${C.emerald}[DONE] Research synthesis completed in ${elapsed}s across 3 workers.${C.reset}`,
478
815
  '',
479
816
  `${C.bold}${C.roseGlow}Comprehensive Findings:${C.reset}`,
480
817
  ...formatMarkdownLines(result.answer || 'No findings returned.').map(l => ` ${l}`),
481
818
  '',
482
- `${C.bold}${C.lavender}Validated Sources (${result.sources.length}):${C.reset}`,
483
- ...(result.sources.length > 0 ? result.sources.map((s, i) => ` [${i + 1}] ${C.cyberCyan}${s}${C.reset}`) : [` ${C.dim}Catalog sources${C.reset}`]),
819
+ `${C.bold}${C.lavender}Validated Sources (${cleanSources.length}):${C.reset}`,
820
+ ...(cleanSources.length > 0 ? cleanSources.map((s, i) => ` [${i + 1}] ${C.cyberCyan}${s}${C.reset}`) : [` ${C.dim}Catalog sources${C.reset}`]),
484
821
  ];
485
822
 
486
823
  printSolidPanel({
487
- title: `Deep Research Report • ${result.sources.length} sources • ${elapsed}s`,
824
+ title: `Deep Research Report • ${cleanSources.length} sources • ${elapsed}s`,
488
825
  lines
489
826
  });
490
827
  } catch (error) {
@@ -737,6 +1074,42 @@ export const handlers = {
737
1074
  const rawAction = (ctx.sub || 'list').toLowerCase();
738
1075
  const extra = (ctx.extra || '').trim();
739
1076
 
1077
+ if (rawAction === 'news') {
1078
+ const executor = ctx.executeTool || executeTool;
1079
+ ctx.setIsProcessing?.(true, 'Scanning recent news for research ideas…');
1080
+ try {
1081
+ const result = await executor('search_news', { query: 'จวีจิ้งอี Ju Jingyi latest news when:7d', maxResults: 10 });
1082
+ const items = (result?.results || []).slice(0, 10);
1083
+ if (!items.length) {
1084
+ printRichCard({
1085
+ title: 'News-Driven Suggestions',
1086
+ status: 'info',
1087
+ lines: ['No recent news found in the last 7 days to seed suggestions.'],
1088
+ });
1089
+ return 'HANDLED';
1090
+ }
1091
+ const insight = await aiNarration(ctx, {
1092
+ label: 'Generating news-driven suggestions…',
1093
+ system: 'You are the Kiku research planner. From these news items propose exactly 3 specific research questions a Ju Jingyi fan would want answered. Output only the 3 numbered questions, nothing else.',
1094
+ user: JSON.stringify({ items: items.map(i => ({ title: i.title, snippet: (i.snippet || '').slice(0, 200) })) }),
1095
+ limit: 500,
1096
+ });
1097
+ printRichCard({
1098
+ title: 'News-Driven Research Suggestions',
1099
+ status: insight ? 'ok' : 'info',
1100
+ lines: insight
1101
+ ? insight.split('\n').filter(Boolean).map(l => `• ${l}`)
1102
+ : items.slice(0, 3).map(i => `• ${i.title}`),
1103
+ });
1104
+ console.log(` ${C.dim}Run /research "<question>" or /suggestion run all to investigate.${C.reset}\n`);
1105
+ } catch (err) {
1106
+ printRichCard({ title: 'News Suggestions Error', status: 'error', lines: [err.message || String(err)] });
1107
+ } finally {
1108
+ ctx.setIsProcessing?.(false);
1109
+ }
1110
+ return 'HANDLED';
1111
+ }
1112
+
740
1113
  if (rawAction === 'run' || rawAction === 'exec' || rawAction === 'start') {
741
1114
  const targets = extra || 'all';
742
1115
 
@@ -994,31 +1367,7 @@ export const handlers = {
994
1367
  }
995
1368
 
996
1369
  if (sub === 'summary' || sub === 'summarize') {
997
- const messages = (agent?.session?.messages || []).map(m => {
998
- const textPart = m.parts?.find(p => p.type === 'text');
999
- return { role: m.role, text: textPart ? textPart.content : '' };
1000
- }).filter(m => m.text);
1001
- if (messages.length === 0) {
1002
- printSolidPanel({
1003
- title: 'History Summary',
1004
- lines: [`${C.dim}No conversation history to summarize.${C.reset}`],
1005
- headerBg: '\x1b[48;2;48;54;70m',
1006
- bg: '\x1b[48;2;36;40;52m',
1007
- });
1008
- return 'HANDLED';
1009
- }
1010
- const summary = await aiNarration(ctx, {
1011
- system: `You are the Kiku session archivist. In 2-3 concise sentences, summarize the key topics, queries, and findings discussed across this conversation history. ${languageHint(ctx)} Do not invent facts. No markdown.`,
1012
- user: JSON.stringify({ messageCount: messages.length, messages: messages.slice(-15) }),
1013
- limit: 450,
1014
- });
1015
- printSolidPanel({
1016
- title: `History Summary • ${messages.length} message${messages.length === 1 ? '' : 's'}`,
1017
- lines: summary ? [` ${C.white}${summary}${C.reset}`] : [`${C.dim}Could not generate history summary (requires active chat model).${C.reset}`],
1018
- headerBg: '\x1b[48;2;48;54;70m',
1019
- bg: '\x1b[48;2;36;40;52m',
1020
- });
1021
- return 'HANDLED';
1370
+ return handlers['/session']({ ...ctx, sub: 'summary' });
1022
1371
  }
1023
1372
 
1024
1373
  if (sub === 'search' && extra) {
@@ -1139,7 +1488,9 @@ export const handlers = {
1139
1488
  });
1140
1489
  return 'HANDLED';
1141
1490
  }
1491
+ console.log(`\n ${C.rose}🌸 Kiku Analyst${C.reset} ${C.dim}is generating session summary with AI model...${C.reset}\n`);
1142
1492
  const summary = await aiNarration(ctx, {
1493
+ label: 'Summarizing session…',
1143
1494
  system: `You are the Kiku session analyst. In 2-3 concise sentences, summarize what this session achieved, what tools were used, and key takeaways for the user. ${languageHint(ctx)} Do not invent facts. No markdown.`,
1144
1495
  user: JSON.stringify({
1145
1496
  sessionId: sess.id,
@@ -1286,6 +1637,31 @@ export const handlers = {
1286
1637
  return 'HANDLED';
1287
1638
  },
1288
1639
 
1640
+ '/autoresearch': async (ctx) => {
1641
+ const { sub } = ctx;
1642
+ const { config } = await import('./config.js');
1643
+ const action = (sub || '').toLowerCase();
1644
+ if (action === 'on' || action === 'off' || action === 'auto') {
1645
+ config.agent.autoResearch = action;
1646
+ }
1647
+ const mode = String(config.agent.autoResearch || 'auto').toLowerCase();
1648
+ const descriptions = {
1649
+ on: 'Aggressive — every query with a research signal is routed into deep research',
1650
+ off: 'Disabled — the model decides on its own (system prompt rule 8)',
1651
+ auto: 'Smart — deep research is triggered automatically only when the question is long or multi-part (default)',
1652
+ };
1653
+ printSolidPanel({
1654
+ title: 'Auto Deep Research',
1655
+ lines: [
1656
+ `${C.gold}Mode:${C.reset} ${C.bold}${C.white}${mode.toUpperCase()}${C.reset}`,
1657
+ `${C.dim}${descriptions[mode] || descriptions.auto}${C.reset}`,
1658
+ `${C.dim}Change: /autoresearch [on|off|auto]${C.reset} ${C.dim}(env: KIKU_AUTO_RESEARCH)${C.reset}`,
1659
+ ],
1660
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1661
+ });
1662
+ return 'HANDLED';
1663
+ },
1664
+
1289
1665
  '/lang': async (ctx) => {
1290
1666
  const { agent, sub } = ctx;
1291
1667
  const langArg = (sub || '').toLowerCase();
@@ -1402,6 +1778,7 @@ export const handlers = {
1402
1778
  parsedArgs = { [firstProp]: rawArgs };
1403
1779
  }
1404
1780
 
1781
+ ctx.setIsProcessing?.(true, `Tool: ${actualToolName}`);
1405
1782
  const runner = createProgressRunner({
1406
1783
  badge: 'RUN',
1407
1784
  title: `Executing ${actualToolName}`,
@@ -1517,6 +1894,7 @@ export const handlers = {
1517
1894
  return 'HANDLED';
1518
1895
  }
1519
1896
  const summary = await aiNarration(ctx, {
1897
+ label: 'Summarizing memory…',
1520
1898
  system: `You are the Kiku persona & memory archivist. Synthesize a concise profile (2-3 sentences) of what this user knows, likes, or tracks about Ju Jingyi based on their saved facts and topics. ${languageHint(ctx)} Do not invent facts. No markdown.`,
1521
1899
  user: JSON.stringify({
1522
1900
  storedFactsCount: facts.length,
@@ -1572,6 +1950,7 @@ export const handlers = {
1572
1950
  if (!songName) {
1573
1951
  console.log(`\n ${C.amber}Usage: /play <song name>${C.reset}\n`);
1574
1952
  } else {
1953
+ ctx.setIsProcessing?.(true, `Searching YouTube: ${songName.slice(0, 24)}…`);
1575
1954
  await audioPlayer.play(songName);
1576
1955
  console.log(`\n ${C.emerald}[OK] Playing: ${C.white}${songName}${C.reset}\n`);
1577
1956
  }
@@ -1615,6 +1994,7 @@ export const handlers = {
1615
1994
  const q = ctx.extra || '';
1616
1995
  if (!q) { console.log(`\n ${C.amber}Usage: /search <query>${C.reset}\n`); return 'HANDLED'; }
1617
1996
  try {
1997
+ ctx.setIsProcessing?.(true, `Searching YouTube: ${q.slice(0, 24)}…`);
1618
1998
  const results = await audioPlayer.searchYouTube(q);
1619
1999
  if (results.length === 0) { console.log(`\n ${C.amber}No results for "${q}"${C.reset}\n`); }
1620
2000
  else {
@@ -1663,7 +2043,9 @@ export const handlers = {
1663
2043
  ctx.tuiPrompt.render();
1664
2044
  } else {
1665
2045
  console.clear();
1666
- ctx.printWelcome();
2046
+ if (typeof ctx.printWelcome === 'function') {
2047
+ ctx.printWelcome();
2048
+ }
1667
2049
  console.log(` ${C.emerald}[OK] Fresh start.${C.reset}\n`);
1668
2050
  }
1669
2051
  return 'HANDLED';
@@ -1674,11 +2056,198 @@ export const handlers = {
1674
2056
  return handlers['/session']({ ...ctx, sub: 'export', extra: fmt });
1675
2057
  },
1676
2058
 
2059
+ '/lan': async (ctx) => {
2060
+ const { scanLan, mapServerTypeToProvider } = await import('./lanDiscovery.js');
2061
+ const { savePersistedProviderBaseUrl } = await import('./config.js');
2062
+ const action = (ctx.sub || 'scan').toLowerCase();
2063
+ const parts = ctx.parts || [];
2064
+
2065
+ if (action === 'use') {
2066
+ const arg = (parts[2] || ctx.extra || '').trim();
2067
+ const endpoints = lastLanScan?.endpoints || [];
2068
+ if (!arg) {
2069
+ console.log(`\n ${C.amber}Usage: /lan use <n> (run /lan first to discover endpoints)${C.reset}\n`);
2070
+ return 'HANDLED';
2071
+ }
2072
+ const endpoint = /^\d+$/.test(arg)
2073
+ ? endpoints[Number(arg) - 1]
2074
+ : endpoints.find(e => e.url === arg.replace(/\/+$/, '') || `${e.host}:${e.port}` === arg);
2075
+ if (!endpoint) {
2076
+ console.log(`\n ${C.red}[ERROR] Endpoint "${arg}" not found in the last scan (${endpoints.length} found).${C.reset}`);
2077
+ console.log(` ${C.dim}Run /lan to refresh the list.${C.reset}\n`);
2078
+ return 'HANDLED';
2079
+ }
2080
+
2081
+ const provider = mapServerTypeToProvider(endpoint.serverType);
2082
+ const model = endpoint.models[0] || null;
2083
+ const cleanUrl = savePersistedProviderBaseUrl(provider, endpoint.url);
2084
+ const options = { baseUrl: cleanUrl, ...(model ? { model } : {}) };
2085
+ try {
2086
+ const info = ctx.client.switchProvider(provider, options);
2087
+ if (ctx.agent?.client) ctx.agent.client.switchProvider(provider, options);
2088
+ if (ctx.modelManager) {
2089
+ ctx.modelManager.client = ctx.client;
2090
+ ctx.modelManager.requestedModel = info.model;
2091
+ const resolution = await ctx.modelManager.resolve({ requestedModel: info.model });
2092
+ if (resolution.connected && resolution.selectedModel) {
2093
+ ctx.setCatalogOnlyMode?.(false);
2094
+ if (ctx.tuiPrompt) {
2095
+ ctx.tuiPrompt.isConnected = true;
2096
+ ctx.tuiPrompt.modelName = resolution.selectedModel;
2097
+ ctx.tuiPrompt.provider = info.providerName;
2098
+ ctx.tuiPrompt.render();
2099
+ }
2100
+ } else if (!resolution.connected) {
2101
+ ctx.setCatalogOnlyMode?.(true);
2102
+ console.log(` ${C.amber}• Endpoint did not answer model resolution: ${resolution.error}${C.reset}`);
2103
+ }
2104
+ }
2105
+ ctx.baseUrl = info.baseUrl;
2106
+ console.log(`\n ${C.emerald}[OK] Connected to LAN endpoint ${C.bold}${endpoint.host}:${endpoint.port}${C.reset}`);
2107
+ console.log(` ${C.slate}• Server:${C.reset} ${C.white}${endpoint.serverName}${C.reset}`);
2108
+ console.log(` ${C.slate}• Provider:${C.reset} ${C.cyberCyan}${info.provider}${C.reset} → ${C.cyberCyan}${info.baseUrl}${C.reset}`);
2109
+ console.log(` ${C.slate}• Model:${C.reset} ${C.white}${info.model}${C.reset}`);
2110
+ if (endpoint.models.length > 1) {
2111
+ console.log(` ${C.dim}• ${endpoint.models.length} models available — switch with /model use <id>${C.reset}`);
2112
+ }
2113
+ console.log('');
2114
+ } catch (err) {
2115
+ console.log(`\n ${C.red}[LAN ERROR] ${err.message}${C.reset}\n`);
2116
+ }
2117
+ return 'HANDLED';
2118
+ }
2119
+
2120
+ console.log(`\n ${C.cyberCyan}Scanning local network for LM servers (LM Studio :1234, vLLM :8000, Ollama :11434, :8080)...${C.reset}`);
2121
+ const restTokens = parts.slice(2).map(t => String(t || '').trim()).filter(Boolean);
2122
+ if (action !== 'scan' && action !== 'json' && action !== 'use') {
2123
+ restTokens.unshift(action);
2124
+ }
2125
+ let scanTargets = null;
2126
+ let scanPorts = null;
2127
+ const portsFlagIdx = restTokens.indexOf('--ports');
2128
+ if (portsFlagIdx >= 0) {
2129
+ const rawPorts = String(restTokens[portsFlagIdx + 1] || '').split(',').map(Number).filter(p => Number.isInteger(p) && p >= 1 && p <= 65535);
2130
+ if (rawPorts.length === 0) {
2131
+ console.log(`\n ${C.amber}Usage: /lan scan <targets> --ports <p1,p2,...>${C.reset}`);
2132
+ console.log(` ${C.dim}Example: /lan scan 192.168.1.0/24 --ports 1234,8000${C.reset}\n`);
2133
+ return 'HANDLED';
2134
+ }
2135
+ scanPorts = rawPorts;
2136
+ restTokens.splice(portsFlagIdx, 2);
2137
+ }
2138
+ if (restTokens.length > 0) {
2139
+ scanTargets = restTokens.join(',').split(',').map(s => s.trim()).filter(Boolean);
2140
+ }
2141
+
2142
+ let result;
2143
+ try {
2144
+ result = await scanLan({
2145
+ ...(scanTargets ? { subnets: scanTargets } : {}),
2146
+ ...(scanPorts ? { ports: scanPorts } : {}),
2147
+ signal: ctx.signal,
2148
+ });
2149
+ } catch (err) {
2150
+ if (err?.name === 'AbortError' || ctx.signal?.aborted) {
2151
+ printRichCard({
2152
+ title: 'LAN Scan Cancelled',
2153
+ status: 'info',
2154
+ lines: ['• Scan aborted by user before completion.'],
2155
+ });
2156
+ } else {
2157
+ console.log(` ${C.red}[LAN ERROR] ${err.message}${C.reset}\n`);
2158
+ }
2159
+ return 'HANDLED';
2160
+ }
2161
+ lastLanScan = result;
2162
+
2163
+ if (action === 'json') {
2164
+ console.log(JSON.stringify(result, null, 2));
2165
+ return 'HANDLED';
2166
+ }
2167
+
2168
+ const endpoints = result.endpoints || [];
2169
+ const lines = [
2170
+ `${C.dim}Subnets: ${result.subnets.join(', ')} • ${result.targetsScanned} probes in ${(result.durationMs / 1000).toFixed(1)}s${C.reset}`,
2171
+ '',
2172
+ ];
2173
+ if (endpoints.length === 0) {
2174
+ lines.push(` ${C.amber}No LM servers found on the local network.${C.reset}`);
2175
+ lines.push('');
2176
+ lines.push(`${C.dim}Checklist: is the LM Studio server started ("lms server start" / Developer > Server)?${C.reset}`);
2177
+ lines.push(`${C.dim}vLLM running in WSL must bind 0.0.0.0 to be reachable ("vllm serve <model> --host 0.0.0.0").${C.reset}`);
2178
+ lines.push(`${C.dim}Non-standard ports can be reached via /provider url <provider> <url>.${C.reset}`);
2179
+ } else {
2180
+ endpoints.forEach((e, i) => {
2181
+ const authBadge = e.authRequired ? ` ${C.amber}[API key required]${C.reset}` : '';
2182
+ lines.push(` ${C.bold}${C.emerald}[${i + 1}]${C.reset} ${C.bold}${C.white}${e.serverName}${C.reset} ${C.dim}—${C.reset} ${C.cyberCyan}http://${e.host}:${e.port}${C.reset} ${C.dim}(${e.latencyMs}ms)${C.reset}${authBadge}`);
2183
+ if (e.models.length > 0) {
2184
+ for (const m of e.models.slice(0, 5)) {
2185
+ lines.push(` ${C.slate}• ${m}${C.reset}`);
2186
+ }
2187
+ if (e.models.length > 5) {
2188
+ lines.push(` ${C.dim}... and ${e.models.length - 5} more${C.reset}`);
2189
+ }
2190
+ } else {
2191
+ lines.push(` ${C.dim}no models reported${C.reset}`);
2192
+ }
2193
+ });
2194
+ lines.push('');
2195
+ lines.push(`${C.dim}Connect: /lan use <n> (persists the Base URL for this provider)${C.reset}`);
2196
+ }
2197
+ printSolidPanel({ title: `LAN LM Server Discovery (${endpoints.length} found)`, lines });
2198
+ return 'HANDLED';
2199
+ },
2200
+
1677
2201
  '/provider': async (ctx) => {
1678
- const { CLOUD_PROVIDERS, getProvider, getRecommendedModels, isProviderKeyPersistenceEnabled } = await import('./config.js');
2202
+ const { CLOUD_PROVIDERS, getProvider, getRecommendedModels, isProviderKeyPersistenceEnabled, getUnifiedProviderCatalog, savePersistedProviderBaseUrl } = await import('./config.js');
1679
2203
  const action = (ctx.sub || 'status').toLowerCase();
1680
2204
  const parts = ctx.parts || [];
1681
2205
 
2206
+ if (action === 'url' || action === 'endpoint' || action === 'baseurl') {
2207
+ const arg1 = parts[2] || '';
2208
+ const arg2 = parts[3] || '';
2209
+ let targetProvider = ctx.client.provider;
2210
+ let newUrl = arg1;
2211
+
2212
+ if (arg2 && getProvider(arg1)) {
2213
+ targetProvider = arg1.toLowerCase();
2214
+ newUrl = arg2;
2215
+ }
2216
+
2217
+ if (!newUrl) {
2218
+ const catalog = getUnifiedProviderCatalog();
2219
+ const lines = [];
2220
+ lines.push(`${C.gold}Active Engine:${C.reset} ${C.bold}${C.white}${catalog.activeProvider}${C.reset} (${catalog.activeBaseUrl})`);
2221
+ lines.push('');
2222
+ lines.push(`${C.cyberCyan}Local Endpoints:${C.reset}`);
2223
+ lines.push(` • ${C.white}LM Studio (port 1234):${C.reset} ${C.slate}${catalog.localEndpoints.lmstudio.baseUrl}${C.reset} ${catalog.localEndpoints.lmstudio.isActive ? `${C.emerald}[ACTIVE]` : ''}`);
2224
+ lines.push(` • ${C.white}vLLM (port 8000 / WSL):${C.reset} ${C.slate}${catalog.localEndpoints.vllm.baseUrl}${C.reset} ${catalog.localEndpoints.vllm.isActive ? `${C.emerald}[ACTIVE]` : ''}`);
2225
+ lines.push('');
2226
+ lines.push(`${C.dim}Set custom URL: /provider url <provider> <url> (e.g. /provider url vllm http://127.0.0.1:8000/v1)${C.reset}`);
2227
+ printSolidPanel({ title: 'API Base URL Manager', lines });
2228
+ return 'HANDLED';
2229
+ }
2230
+
2231
+ const pDef = getProvider(targetProvider);
2232
+ if (!pDef) {
2233
+ console.log(`\n ${C.red}[ERROR] Unknown provider "${targetProvider}".${C.reset}\n`);
2234
+ return 'HANDLED';
2235
+ }
2236
+
2237
+ const cleanUrl = savePersistedProviderBaseUrl(targetProvider, newUrl);
2238
+ if (ctx.client?.setBaseUrl) {
2239
+ ctx.client.setBaseUrl(cleanUrl, targetProvider);
2240
+ }
2241
+ if (ctx.agent?.client?.setBaseUrl) {
2242
+ ctx.agent.client.setBaseUrl(cleanUrl, targetProvider);
2243
+ }
2244
+ ctx.baseUrl = cleanUrl;
2245
+
2246
+ console.log(`\n ${C.emerald}[OK] Base URL updated and saved for ${targetProvider}${C.reset}`);
2247
+ console.log(` ${C.slate}• New Base URL:${C.reset} ${C.cyberCyan}${cleanUrl}${C.reset}\n`);
2248
+ return 'HANDLED';
2249
+ }
2250
+
1682
2251
  if (action === 'list') {
1683
2252
  const active = ctx.client.provider || 'lmstudio';
1684
2253
  const lines = Object.values(CLOUD_PROVIDERS).map(p => {
@@ -1694,7 +2263,7 @@ export const handlers = {
1694
2263
  lines: [
1695
2264
  ...lines,
1696
2265
  '',
1697
- `${C.dim}Switch: /provider use <name> [model] | Models: /provider models <name> | Docs: /provider info <name>${C.reset}`
2266
+ `${C.dim}Switch: /provider use <name> [model] | Base URL: /provider url | Models: /provider models <name>${C.reset}`
1698
2267
  ],
1699
2268
  });
1700
2269
  return 'HANDLED';
@@ -1748,6 +2317,9 @@ export const handlers = {
1748
2317
  if (!info.hasApiKey && !info.isLocal) {
1749
2318
  console.log(` ${C.dim}Tip: Set ${info.envKey} in .env or run /provider setkey <key>${C.reset}`);
1750
2319
  }
2320
+ if (info.platformNotice && process.platform === 'win32') {
2321
+ console.log(` ${C.amber}[NOTICE] ${info.platformNotice}${C.reset}`);
2322
+ }
1751
2323
  console.log('');
1752
2324
  } catch (err) {
1753
2325
  console.log(`\n ${C.red}[PROVIDER ERROR] ${err.message}${C.reset}\n`);
@@ -1756,6 +2328,7 @@ export const handlers = {
1756
2328
  }
1757
2329
 
1758
2330
  if (action === 'models' || action === 'model' || action === 'catalog') {
2331
+ ctx.setIsProcessing?.(true, 'Fetching model catalog…');
1759
2332
  const targetProvider = (ctx.extra || ctx.client.provider || 'lmstudio').toLowerCase().trim();
1760
2333
  const providerDef = getProvider(targetProvider);
1761
2334
  if (!providerDef) {
@@ -1815,6 +2388,7 @@ export const handlers = {
1815
2388
  `${C.gold}Features:${C.reset} ${C.slate}${(p.features || []).join(', ')}${C.reset}`,
1816
2389
  `${C.gold}Docs:${C.reset} ${C.cyberCyan}${p.docUrl || 'N/A'}${C.reset}`,
1817
2390
  `${C.gold}API Ref:${C.reset} ${C.cyberCyan}${p.apiRefUrl || 'N/A'}${C.reset}`,
2391
+ ...(p.platformNotice ? [`${C.gold}Platform:${C.reset} ${C.amber}${p.platformNotice}${C.reset}`] : []),
1818
2392
  '',
1819
2393
  `${C.dim}Switch: /provider use ${p.id} | Models: /provider models ${p.id}${C.reset}`
1820
2394
  ],
@@ -1825,6 +2399,7 @@ export const handlers = {
1825
2399
  if (action === 'test' || action === 'ping' || action === 'check') {
1826
2400
  const targetProvider = (ctx.extra || ctx.client.provider || 'lmstudio').toLowerCase().trim();
1827
2401
  console.log(`\n ${C.cyberCyan}[TESTING] Testing connection to ${targetProvider}...${C.reset}`);
2402
+ ctx.setIsProcessing?.(true, `Testing ${targetProvider}…`);
1828
2403
  let testClient = ctx.client;
1829
2404
  if (targetProvider !== ctx.client.provider) {
1830
2405
  const { LLMClient } = await import('./llmClient.js');
@@ -1832,6 +2407,16 @@ export const handlers = {
1832
2407
  }
1833
2408
  const check = await testClient.checkConnection();
1834
2409
  if (check.connected) {
2410
+ if (targetProvider === ctx.client.provider) {
2411
+ ctx.setCatalogOnlyMode?.(false);
2412
+ if (ctx.tuiPrompt) {
2413
+ ctx.tuiPrompt.isConnected = true;
2414
+ if (check.activeLoadedModel || check.model) {
2415
+ ctx.tuiPrompt.modelName = check.activeLoadedModel || check.model;
2416
+ }
2417
+ ctx.tuiPrompt.render();
2418
+ }
2419
+ }
1835
2420
  console.log(` ${C.emerald}[OK] Connected to ${check.providerName}!${C.reset}`);
1836
2421
  console.log(` ${C.slate}• Latency:${C.reset} ${C.emerald}${check.latencyMs || 0}ms${C.reset}`);
1837
2422
  console.log(` ${C.slate}• Active Model:${C.reset} ${C.white}${check.activeModel || check.model || 'N/A'}${C.reset}`);
@@ -1871,10 +2456,10 @@ export const handlers = {
1871
2456
  const masked = newKey.length > 8
1872
2457
  ? `${newKey.slice(0, 4)}...${newKey.slice(-4)}`
1873
2458
  : '****';
1874
- const persistenceLabel = isProviderKeyPersistenceEnabled()
1875
- ? 'saved with encrypted persistence'
1876
- : 'saved in memory only (set KIKU_PROVIDER_KEY_ENCRYPTION_KEY to persist)';
1877
- console.log(`\n ${C.emerald}[OK] API key ${persistenceLabel} for ${targetProvider}${C.reset} (${masked})\n`);
2459
+ const persistenceLabel = isProviderKeyPersistenceEnabled()
2460
+ ? 'saved with encrypted persistence'
2461
+ : 'saved in memory only (set KIKU_PROVIDER_KEY_ENCRYPTION_KEY to persist)';
2462
+ console.log(`\n ${C.emerald}[OK] API key ${persistenceLabel} for ${targetProvider}${C.reset} (${masked})\n`);
1878
2463
  return 'HANDLED';
1879
2464
  }
1880
2465
 
@@ -1896,24 +2481,43 @@ export const handlers = {
1896
2481
  `${C.gold}Pricing:${C.reset} ${C.slate}${pInfo.pricingSummary || 'N/A'}${C.reset}`,
1897
2482
  `${C.gold}Docs:${C.reset} ${C.cyberCyan}${pInfo.docUrl || 'N/A'}${C.reset}`,
1898
2483
  '',
1899
- `${C.dim}Commands: /provider list | /provider use <name> [model] | /provider models | /provider test${C.reset}`,
2484
+ `${C.dim}Commands: /provider list | /provider use <name> [model] | /provider url | /provider test${C.reset}`,
1900
2485
  ],
1901
2486
  headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1902
2487
  });
1903
2488
  return 'HANDLED';
1904
2489
  },
1905
2490
 
2491
+ '/url': async (ctx) => handlers['/provider']({ ...ctx, sub: 'url', parts: ['/provider', 'url', ...(ctx.parts?.slice(1) || [])] }),
2492
+ '/endpoint': async (ctx) => handlers['/provider']({ ...ctx, sub: 'url', parts: ['/provider', 'url', ...(ctx.parts?.slice(1) || [])] }),
2493
+
1906
2494
  '/model': async (ctx) => {
1907
2495
  const rawSub = (ctx.sub || '').trim();
1908
2496
  const action = (rawSub || 'status').toLowerCase();
1909
2497
  const providerName = ctx.client.providerName || 'Model';
1910
2498
  const { getRecommendedModels, CLOUD_PROVIDERS } = await import('./config.js');
1911
2499
 
2500
+ if (action === 'ping') {
2501
+ try {
2502
+ const start = Date.now();
2503
+ ctx.setIsProcessing?.(true, 'Pinging model server…');
2504
+ const check = await ctx.client.checkConnection();
2505
+ const ms = Date.now() - start;
2506
+ console.log(check.connected
2507
+ ? `\n ${C.emerald}● Connected${C.reset} ${C.dim}(${ms}ms)${C.reset} — ${C.white}${check.activeModel || check.model || 'Ready'}${C.reset}\n`
2508
+ : `\n ${C.red}● Disconnected${C.reset} ${C.dim}(${ms}ms)${C.reset} — ${C.amber}${check.error || 'Server unreachable'}${C.reset}\n`);
2509
+ } catch (err) {
2510
+ console.log(`\n ${C.red}[PING ERROR] ${err.message || String(err)}${C.reset}\n`);
2511
+ }
2512
+ return 'HANDLED';
2513
+ }
2514
+
1912
2515
  if (action === 'reconnect') {
1913
2516
  if (!ctx.modelManager) {
1914
2517
  console.log(`\n ${C.red}[MODEL ERROR] Runtime model manager is unavailable.${C.reset}\n`);
1915
2518
  return 'HANDLED';
1916
2519
  }
2520
+ ctx.setIsProcessing?.(true, 'Reconnecting model server…');
1917
2521
  const resolution = await ctx.modelManager.resolve();
1918
2522
  if (resolution.connected && resolution.selectedModel) {
1919
2523
  ctx.setCatalogOnlyMode?.(false);
@@ -1948,10 +2552,27 @@ export const handlers = {
1948
2552
  return 'HANDLED';
1949
2553
  }
1950
2554
 
2555
+ ctx.setIsProcessing?.(true, 'Probing model server…');
1951
2556
  const liveCheck = ctx.modelManager
1952
2557
  ? await ctx.modelManager.inspect()
1953
2558
  : await ctx.client.checkConnection();
1954
2559
 
2560
+ if (liveCheck.connected && (liveCheck.activeLoadedModel || (liveCheck.models && liveCheck.models.length > 0))) {
2561
+ let resolvedModel = liveCheck.activeLoadedModel;
2562
+ if (ctx.modelManager) {
2563
+ const resolution = await ctx.modelManager.resolve();
2564
+ if (resolution.connected && resolution.selectedModel) {
2565
+ resolvedModel = resolution.selectedModel;
2566
+ }
2567
+ }
2568
+ ctx.setCatalogOnlyMode?.(false);
2569
+ if (ctx.tuiPrompt) {
2570
+ ctx.tuiPrompt.isConnected = true;
2571
+ if (resolvedModel) ctx.tuiPrompt.modelName = resolvedModel;
2572
+ ctx.tuiPrompt.render();
2573
+ }
2574
+ }
2575
+
1955
2576
  if (action === 'list') {
1956
2577
  const activeLoaded = liveCheck.activeLoadedModel;
1957
2578
  const serverModels = (liveCheck.models || []).slice(0, 40).map(id => {
@@ -2100,6 +2721,7 @@ export const handlers = {
2100
2721
  }
2101
2722
 
2102
2723
  if (action === 'connect' || action === 'add') {
2724
+ ctx.setIsProcessing?.(true, 'Connecting MCP server…');
2103
2725
  const parts = (ctx.parts || []).slice(2);
2104
2726
  const name = parts[0];
2105
2727
  const targetUrlOrType = parts[1];
@@ -2183,6 +2805,7 @@ export const handlers = {
2183
2805
  }
2184
2806
 
2185
2807
  if (action === 'reload' || action === 'reconnect') {
2808
+ ctx.setIsProcessing?.(true, 'Reloading MCP servers…');
2186
2809
  const target = (ctx.extra || '').trim();
2187
2810
  printRichCard({
2188
2811
  title: 'Reloading MCP Configuration',
@@ -2310,21 +2933,7 @@ export const handlers = {
2310
2933
  return 'HANDLED';
2311
2934
  },
2312
2935
 
2313
- '/ping': async (ctx) => {
2314
- try {
2315
- const start = Date.now();
2316
- const check = await ctx.client.checkConnection();
2317
- const ms = Date.now() - start;
2318
- if (check.connected) {
2319
- console.log(`\n ${C.emerald}● Connected${C.reset} ${C.dim}(${ms}ms)${C.reset} — ${C.white}${check.activeModel || check.model || 'Ready'}${C.reset}\n`);
2320
- } else {
2321
- console.log(`\n ${C.red}● Disconnected${C.reset} ${C.dim}(${ms}ms)${C.reset} — ${C.amber}${check.error || 'Server unreachable'}${C.reset}\n`);
2322
- }
2323
- } catch (err) {
2324
- console.log(`\n ${C.red}[PING ERROR] ${err.message || String(err)}${C.reset}\n`);
2325
- }
2326
- return 'HANDLED';
2327
- },
2936
+ '/ping': async (ctx) => handlers['/model']({ ...ctx, sub: 'ping', parts: ['/model', 'ping'] }),
2328
2937
 
2329
2938
  '/profile': async (ctx) => {
2330
2939
  const { listProfiles, getProfile, setActiveProfile, createOrUpdateProfile, deleteProfile, getActiveProfile } = await import('./profiles.js');
@@ -2451,6 +3060,7 @@ export const handlers = {
2451
3060
  '/doctor': async (ctx) => {
2452
3061
  try {
2453
3062
  const { runDoctor } = await import('./doctor.js');
3063
+ ctx.setIsProcessing?.(true, 'Running diagnostics…');
2454
3064
  const report = await runDoctor({ client: ctx.client });
2455
3065
  printSolidPanel({
2456
3066
  title: `Kiku Doctor • ${report.ok ? 'READY' : 'ACTION REQUIRED'}`,
@@ -2465,22 +3075,12 @@ export const handlers = {
2465
3075
  return 'HANDLED';
2466
3076
  },
2467
3077
 
2468
- '/version': async () => {
2469
- printSolidPanel({
2470
- title: 'Version',
2471
- lines: [
2472
- `${C.gold}Package:${C.reset} ${C.white}${PACKAGE_NAME}${C.reset}`,
2473
- `${C.gold}Version:${C.reset} ${C.white}${PACKAGE_VERSION}${C.reset}`,
2474
- `${C.gold}Node:${C.reset} ${C.white}${process.version}${C.reset}`,
2475
- `${C.gold}Platform:${C.reset} ${C.white}${process.platform} ${process.arch}${C.reset}`,
2476
- ],
2477
- });
2478
- return 'HANDLED';
2479
- },
3078
+ '/version': async () => handlers['/about'](),
2480
3079
 
2481
- '/system': async () => {
3080
+ '/system': async (ctx) => {
2482
3081
  const os = await import('node:os');
2483
3082
  const mem = process.memoryUsage();
3083
+ ctx.setIsProcessing?.(true, 'Probing hardware…');
2484
3084
  const gpuData = await getGpuMetrics().catch(() => ({ available: false }));
2485
3085
 
2486
3086
  const lines = [
@@ -2507,7 +3107,8 @@ export const handlers = {
2507
3107
  return 'HANDLED';
2508
3108
  },
2509
3109
 
2510
- '/gpu': async () => {
3110
+ '/gpu': async (ctx) => {
3111
+ ctx.setIsProcessing?.(true, 'Probing hardware…');
2511
3112
  const gpuData = await getGpuMetrics().catch(() => ({ available: false }));
2512
3113
 
2513
3114
  if (!gpuData.available || !gpuData.primary) {
@@ -2558,17 +3159,189 @@ export const handlers = {
2558
3159
  return 'HANDLED';
2559
3160
  },
2560
3161
 
2561
- '/about': async () => {
2562
- printSolidPanel({
2563
- title: 'About Kiku AI Agent',
2564
- lines: [
2565
- `${C.white}Intelligent assistant for Ju Jingyi (鞠婧祎)${C.reset}`,
2566
- `${C.gold}Engine:${C.reset} ${C.white}LM Studio + Gemma 4${C.reset}`,
2567
- `${C.gold}Tools:${C.reset} ${C.white}${toolsSchema.length} registered${C.reset}`,
2568
- `${C.gold}Languages:${C.reset} ${C.white}TH, ZH, EN (auto)${C.reset}`,
2569
- ],
2570
- });
2571
- return 'HANDLED';
3162
+ '/vram': async (ctx) => {
3163
+ const { sub, extra } = ctx;
3164
+ try {
3165
+ if (sub === 'list') {
3166
+ const capabilityTags = (entry) => (entry.capabilities || [])
3167
+ .map(cap => `[${cap.toUpperCase()}]`).join(' ') || '[BASE]';
3168
+ const lines = MODEL_REGISTRY.map(entry => {
3169
+ const flags = [
3170
+ `P=${entry.paramsB}B`,
3171
+ `L=${entry.layers}`,
3172
+ `GQA ${entry.queryHeads}Q/${entry.kvHeads}KV`,
3173
+ `d=${entry.headDim}`,
3174
+ entry.contextLength ? `ctx≤${entry.contextLength.toLocaleString('en-US')}` : null,
3175
+ entry.isMoE ? 'MoE (total params)' : null,
3176
+ ].filter(Boolean).join(' • ');
3177
+ return `${C.gold}${capabilityTags(entry)} ${entry.label}${C.reset}\n ${C.dim}${flags}${C.reset}`;
3178
+ });
3179
+ printSolidPanel({
3180
+ title: 'Built-in Model Registry (approximate architectures)',
3181
+ lines: [...lines, '', `${C.dim}Exact GGUF/HuggingFace metadata always takes precedence over these entries.${C.reset}`],
3182
+ });
3183
+ return 'HANDLED';
3184
+ }
3185
+
3186
+ if (sub === 'live') {
3187
+ return await printVramLivePanel(ctx, parseVramArgs(String(extra || '').trim().split(/\s+/).filter(Boolean)));
3188
+ }
3189
+ if (sub === 'compare') {
3190
+ return await printVramQuantCompare(ctx, parseVramArgs(String(extra || '').trim().split(/\s+/).filter(Boolean)));
3191
+ }
3192
+ if (sub === 'ctx' || sub === 'contexts') {
3193
+ return await printVramContextLadder(ctx, parseVramArgs(String(extra || '').trim().split(/\s+/).filter(Boolean)));
3194
+ }
3195
+ if (sub === 'tier' || sub === 'hardware' || sub === 'bus') {
3196
+ const resolved = await resolveSystemHardwareTier();
3197
+ const hw = resolved.hardware;
3198
+ const profile = resolved.profile;
3199
+ const spec = profile?.spec || profile;
3200
+ const tier = resolved.tier;
3201
+ const budget = resolved.budget;
3202
+ const lines = [
3203
+ `${C.gold}GPU Device:${C.reset} ${C.bold}${C.white}${spec?.name || hw.name || 'Unknown GPU'}${C.reset}`,
3204
+ `${C.gold}Architecture:${C.reset} ${C.white}${spec?.arch || tier.architecture.family || 'Unknown'}${C.reset} ${C.dim}(${spec?.pcie || 'PCIe'})${C.reset}`,
3205
+ `${C.gold}Memory Bus:${C.reset} ${C.white}${spec?.busBits ? `${spec.busBits}-bit ${spec.memoryType || 'GDDR'}` : 'Unknown'}${C.reset} • ${C.cyberCyan}${spec?.bandwidthGbs ? `${spec.bandwidthGbs.toFixed(1)} GB/s` : 'N/A'}${C.reset}`,
3206
+ `${C.gold}VRAM Capacity:${C.reset} ${C.white}${hw.totalBytes ? formatGb(hw.totalBytes) : `${spec?.vramGb || 0} GB`}${C.reset}` + (hw.freeBytes ? ` ${C.dim}(${formatGb(hw.freeBytes)} free right now)${C.reset}` : ''),
3207
+ '',
3208
+ `${C.bold}${C.lavender}Hardware Classification & AI Budget:${C.reset}`,
3209
+ ` • AI Tier: ${C.bold}${C.emerald}${tier.name}${C.reset}`,
3210
+ ` • Max Context: ${C.white}${tier.contextLimit.toLocaleString('en-US')} tokens${C.reset}`,
3211
+ ` • Rec. KV Quant: ${C.bold}${C.gold}${tier.effectiveKvQuant}${C.reset}`,
3212
+ ` • Dyn. KV Budget: ${C.white}${budget?.kvBudgetMb ? `${budget.kvBudgetMb} MB` : 'N/A'}${C.reset}`,
3213
+ ` • Constrained Card:${tier.isConstrained ? ` ${C.amber}Yes (KV quantization & compact context applied)${C.reset}` : ` ${C.emerald}No (high headroom)${C.reset}`}`,
3214
+ '',
3215
+ `${C.dim}Telemetry: Disambiguated against TechPowerUp offline catalog & local NVML hardware probe.${C.reset}`,
3216
+ ];
3217
+ printSolidPanel({
3218
+ title: `GPU Hardware Classification • ${tier.name}`,
3219
+ lines,
3220
+ headerBg: '\x1b[48;2;48;54;70m',
3221
+ bg: '\x1b[48;2;36;40;52m'
3222
+ });
3223
+ return 'HANDLED';
3224
+ }
3225
+
3226
+ const argTokens = String(sub === 'check' ? (extra || '') : [sub, extra].filter(Boolean).join(' '))
3227
+ .trim().split(/\s+/).filter(Boolean);
3228
+ const { overrides, positional, force } = parseVramArgs(argTokens);
3229
+ const target = positional.join(' ');
3230
+ let modelName = null;
3231
+ let modelPath = null;
3232
+ if (target) {
3233
+ if (/\.gguf$/i.test(target)) modelPath = target;
3234
+ else modelName = target;
3235
+ } else {
3236
+ modelName = ctx.agent?.client?.model || null;
3237
+ }
3238
+
3239
+ const result = await computeVramEstimate({ model: modelName, modelPath, ...overrides });
3240
+ const est = result.estimate;
3241
+ const cls = result.classification || { zone: 'UNKNOWN', usagePct: null, headroomBytes: null, message: '' };
3242
+ const hw = result.hardware;
3243
+ const zoneColor = cls.zone === 'GREEN' ? C.emerald : cls.zone === 'YELLOW' ? C.gold : cls.zone === 'RED' ? C.roseGlow : C.slate;
3244
+ const barWidth = 18;
3245
+ const usagePct = cls.usagePct !== null && cls.usagePct !== undefined ? cls.usagePct : 0;
3246
+ const bigFilled = Math.round((Math.min(100, usagePct) / 100) * 46);
3247
+ const bigBar = `${zoneColor}${'█'.repeat(bigFilled)}${C.dim}${'░'.repeat(46 - bigFilled)}${C.reset}`;
3248
+
3249
+ const row = (label, bytes) => {
3250
+ const share = est.totalBytes > 0 && bytes !== null && bytes !== undefined ? bytes / est.totalBytes : 0;
3251
+ const filled = Math.round(share * barWidth);
3252
+ const miniBar = `${zoneColor}${'█'.repeat(filled)}${C.dim}${'░'.repeat(barWidth - filled)}${C.reset}`;
3253
+ return `${C.white}${String(label).padEnd(34)}${C.reset}${C.cyberCyan}${formatGb(bytes).padStart(10)}${C.reset} ${miniBar}`;
3254
+ };
3255
+
3256
+ const quantLabel = String(est.quant || 'q4_k_m').toUpperCase();
3257
+ const ctxLabel = est.contextLength ? `${est.contextLength.toLocaleString('en-US')} tokens` : 'unknown';
3258
+ const kvLabelParts = [
3259
+ est.layers ? `${est.layers}L` : null,
3260
+ est.kvHeads && est.queryHeads ? `GQA ${est.queryHeads}/${est.kvHeads}` : null,
3261
+ est.headDim ? `d=${est.headDim}` : null,
3262
+ ].filter(Boolean).join(', ');
3263
+
3264
+ const isMeasured = cls.basis === 'measured';
3265
+ const predCls = result.predictiveClassification || cls;
3266
+ const predPct = predCls.usagePct !== null && predCls.usagePct !== undefined ? predCls.usagePct : 0;
3267
+
3268
+ const vramStatusRows = isMeasured
3269
+ ? [
3270
+ `${C.gold}Estimated Peak VRAM:${C.reset} ${C.bold}${C.white}${formatGb(est.totalBytes)}${C.reset}` +
3271
+ (hw ? `${C.dim} vs ${(hw.totalBytes / 1024 ** 3).toFixed(2)} GB card (predicted: ${predPct}%)${C.reset}` : ''),
3272
+ `${C.gold}Live GPU Usage:${C.reset} ${C.bold}${C.white}${formatGb(cls.usedBytes)}${C.reset}` +
3273
+ (hw ? `${C.dim} / ${(hw.totalBytes / 1024 ** 3).toFixed(2)} GB${C.reset} ${C.bold}${zoneColor}(${usagePct}% · measured)${C.reset}` : ''),
3274
+ '',
3275
+ `${C.bold}[STATUS: ${cls.zone === 'GREEN' ? 'FEASIBLE' : cls.zone === 'YELLOW' ? 'MARGINAL / WARNING' : cls.zone === 'RED' ? 'CRITICAL / OOM' : 'UNVERIFIED'} · LIVE MEASURED]${C.reset}`,
3276
+ `[${bigBar}] ${zoneColor}${usagePct}% (${cls.zone} ZONE · measured)${C.reset}`,
3277
+ ]
3278
+ : [
3279
+ `${C.gold}Total Estimated VRAM:${C.reset} ${C.bold}${C.white}${formatGb(est.totalBytes)}${C.reset}` +
3280
+ (hw ? `${C.dim} / ${(hw.totalBytes / 1024 ** 3).toFixed(2)} GB${C.reset} ${C.dim}(${usagePct}%)${C.reset}` : ''),
3281
+ '',
3282
+ `${C.bold}[STATUS: ${cls.zone === 'GREEN' ? 'FEASIBLE' : cls.zone === 'YELLOW' ? 'MARGINAL / WARNING' : cls.zone === 'RED' ? 'CRITICAL / OOM' : 'UNVERIFIED'}]${C.reset}`,
3283
+ `[${bigBar}] ${zoneColor}${usagePct}% (${cls.zone} ZONE)${C.reset}`,
3284
+ ];
3285
+
3286
+ const lines = [
3287
+ `${C.gold}Target Model:${C.reset} ${C.bold}${C.white}${result.architecture.name || modelName || modelPath || 'unknown'}${C.reset}${result.approximate ? C.dim + ' (approximate)' : ''}${C.reset}`,
3288
+ `${C.gold}Config Source:${C.reset} ${C.white}${result.configSource}${C.reset}${result.configError ? C.dim + ` (${result.configError})` : ''}${C.reset}`,
3289
+ `${C.gold}Context Length:${C.reset} ${C.white}${ctxLabel}${C.reset}${C.dim} | Batch Size: ${est.batchSize} | Cache: ${est.cacheBits}-bit${C.reset}`,
3290
+ `${C.gold}Detected Device:${C.reset} ${hw ? C.white + `${hw.name} (${(hw.totalBytes / 1024 ** 3).toFixed(2)} GB VRAM)` + C.reset : C.dim + 'No queryable VRAM device'}${C.reset}`,
3291
+ '',
3292
+ `${C.bold}${C.lavender}Component${C.reset}${' '.repeat(25)}${C.bold}${C.lavender}Calculated Memory${C.reset}`,
3293
+ ...[
3294
+ row(`Weights (P=${est.paramCount ? (est.paramCount / 1e9).toFixed(1) : '?' }B, ${quantLabel})`, est.weightsBytes),
3295
+ row(`KV Cache (${kvLabelParts || 'geometry unknown'}, ${est.effectiveContextLength ? est.effectiveContextLength.toLocaleString('en-US') : '0'} tok)`, est.kvCacheBytes),
3296
+ row('Activation Workspace', est.activationBytes),
3297
+ row('CUDA / Runtime Overhead', est.overheadBytes),
3298
+ ],
3299
+ '',
3300
+ ...vramStatusRows,
3301
+ ];
3302
+
3303
+ if (cls.headroomBytes !== null && cls.headroomBytes !== undefined) {
3304
+ lines.push('', `${C.gold}Headroom:${C.reset} ${cls.headroomBytes >= 0 ? C.emerald + formatGb(cls.headroomBytes) : C.roseGlow + `over budget by ${formatGb(-cls.headroomBytes)}`}${C.reset}`);
3305
+ }
3306
+ for (const note of result.notes || []) {
3307
+ lines.push(`${C.dim}[INFO] ${note}${C.reset}`);
3308
+ }
3309
+ for (const warning of result.estimate.warnings) {
3310
+ lines.push(`${C.dim}[WARN] ${warning}${C.reset}`);
3311
+ }
3312
+ for (const suggestion of result.remediation || []) {
3313
+ lines.push(`${C.amber}Recommendation: ${suggestion.description}${C.reset}`);
3314
+ }
3315
+ if (cls.zone === 'RED') {
3316
+ lines.push(force
3317
+ ? `${C.roseGlow}[FORCED] Override active — execution may fail with CUDA OOM or heavy CPU offloading.${C.reset}`
3318
+ : `${C.roseGlow}ABORT: Allocation exceeds safe limits. Re-run with --force to override.${C.reset}`);
3319
+ }
3320
+
3321
+ printSolidPanel({
3322
+ title: sub === 'check' ? 'KIKU Resource Verification • Pre-Flight' : 'KIKU Resource Verification',
3323
+ lines,
3324
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
3325
+ });
3326
+ } catch (err) {
3327
+ console.log(`\n ${C.red}[VRAM ERROR] ${err.message || String(err)}${C.reset}\n`);
3328
+ }
3329
+ return 'HANDLED';
3330
+ },
3331
+
3332
+ '/about': async () => {
3333
+ printSolidPanel({
3334
+ title: 'About Kiku AI Agent',
3335
+ lines: [
3336
+ `${C.white}Intelligent assistant for Ju Jingyi (鞠婧祎)${C.reset}`,
3337
+ `${C.gold}Engine:${C.reset} ${C.white}LM Studio + Gemma 4${C.reset}`,
3338
+ `${C.gold}Tools:${C.reset} ${C.white}${toolsSchema.length} registered${C.reset}`,
3339
+ `${C.gold}Languages:${C.reset} ${C.white}TH, ZH, EN (auto)${C.reset}`,
3340
+ `${C.gold}Package:${C.reset} ${C.white}${PACKAGE_NAME} v${PACKAGE_VERSION}${C.reset}`,
3341
+ `${C.gold}Runtime:${C.reset} ${C.white}Node ${process.version} • ${process.platform}/${process.arch}${C.reset}`,
3342
+ ],
3343
+ });
3344
+ return 'HANDLED';
2572
3345
  },
2573
3346
 
2574
3347
  '/login': async (ctx) => {
@@ -2609,7 +3382,7 @@ export const handlers = {
2609
3382
  if (audioPlayer.isPlaying || audioPlayer.queue.length > 0) {
2610
3383
  console.log(`\n ${C.emerald}${await audioPlayer.clearQueue()}${C.reset}\n`);
2611
3384
  } else {
2612
- ctx.clearQueryQueue();
3385
+ ctx.clearQueryQueue?.();
2613
3386
  console.log(`\n ${C.emerald}[OK] Execution queue cleared.${C.reset}\n`);
2614
3387
  }
2615
3388
  } else if (audioPlayer.isPlaying || audioPlayer.queue.length > 0) {
@@ -2620,17 +3393,75 @@ export const handlers = {
2620
3393
  headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
2621
3394
  });
2622
3395
  } else {
2623
- const qLen = ctx.getQueryQueueLength();
3396
+ const qLen = typeof ctx.getQueryQueueLength === 'function' ? ctx.getQueryQueueLength() : 0;
2624
3397
  console.log(`\n ${C.slate}Execution queue: ${qLen} items${C.reset}\n`);
2625
3398
  }
2626
3399
  return 'HANDLED';
2627
3400
  },
2628
3401
 
2629
3402
  '/stats': async (ctx) => {
3403
+ const view = String(ctx.sub || '').toLowerCase();
3404
+ if (view === 'tokens' || view === 'session') {
3405
+ const agent = ctx.agent;
3406
+ const history = Array.isArray(agent?.queryMetricsHistory) ? agent.queryMetricsHistory : [];
3407
+ if (history.length === 0) {
3408
+ printSolidPanel({
3409
+ title: 'Session Token Telemetry',
3410
+ lines: [
3411
+ `${C.dim}No token telemetry recorded yet — run a query first.${C.reset}`,
3412
+ `${C.dim}Set KIKU_CONTEXT_LIMIT to enable context-budget tracking.${C.reset}`,
3413
+ ],
3414
+ });
3415
+ return 'HANDLED';
3416
+ }
3417
+
3418
+ const sums = history.reduce((acc, m) => {
3419
+ acc.prompt += Number(m?.counts?.prompt_tokens) || 0;
3420
+ acc.completion += Number(m?.counts?.completion_tokens) || 0;
3421
+ acc.total += Number(m?.counts?.total_tokens) || 0;
3422
+ acc.decodeTps.push(Number(m?.rates?.decode_tps) || 0);
3423
+ acc.ttft.push(Number(m?.timing_ms?.ttft) || 0);
3424
+ acc.itlMean.push(Number(m?.rates?.itl_mean_ms) || 0);
3425
+ acc.itlP95.push(Number(m?.rates?.itl_p95_ms) || 0);
3426
+ acc.e2e.push(Number(m?.rates?.e2e_tps) || 0);
3427
+ return acc;
3428
+ }, { prompt: 0, completion: 0, total: 0, decodeTps: [], ttft: [], itlMean: [], itlP95: [], e2e: [] });
3429
+ const avg = arr => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
3430
+ const last = history[history.length - 1];
3431
+
3432
+ const lines = [
3433
+ `${C.gold}Queries:${C.reset} ${C.cyberCyan}${history.length}${C.reset}${C.dim} • latest model: ${last?.model_id || 'unknown'}${C.reset}`,
3434
+ `${C.gold}Tokens:${C.reset} ${C.white}${sums.prompt.toLocaleString('en-US')} in / ${sums.completion.toLocaleString('en-US')} out${C.reset} ${C.dim}(${sums.total.toLocaleString('en-US')} total)${C.reset}`,
3435
+ `${C.gold}Decode:${C.reset} ${C.emerald}${avg(sums.decodeTps).toFixed(1)} tok/s avg${C.reset} ${C.dim}• best ${Math.max(...sums.decodeTps).toFixed(1)} • last ${(Number(last?.rates?.decode_tps) || 0).toFixed(1)}${C.reset}`,
3436
+ `${C.gold}TTFT:${C.reset} ${C.white}avg ${avg(sums.ttft).toFixed(0)} ms • p95 ${computePercentile(sums.ttft, 0.95)?.toFixed(0) || 0} ms${C.reset}`,
3437
+ `${C.gold}ITL:${C.reset} ${C.white}avg ${avg(sums.itlMean).toFixed(1)} ms • p95 ${avg(sums.itlP95).toFixed(1)} ms${C.reset}`,
3438
+ `${C.gold}E2E:${C.reset} ${C.white}${avg(sums.e2e).toFixed(1)} tok/s avg (prompt + completion / wall time)${C.reset}`,
3439
+ ];
3440
+
3441
+ if (last?.counts?.context_limit) {
3442
+ lines.push(
3443
+ `${C.gold}Context:${C.reset} ${C.white}${(last.counts.total_tokens / 1000).toFixed(1)}k / ${(last.counts.context_limit / 1000).toFixed(1)}k${C.reset} ${C.dim}(${last.context_usage_pct}% • ${last.counts.remaining_tokens.toLocaleString('en-US')} remaining)${C.reset}`
3444
+ );
3445
+ } else {
3446
+ lines.push(`${C.gold}Context:${C.reset} ${C.dim}limit unknown — set KIKU_CONTEXT_LIMIT${C.reset}`);
3447
+ }
3448
+ if (ctx.agent?.lastTelemetry?.costUsd) {
3449
+ lines.push(`${C.gold}Session cost:${C.reset} ${C.white}$${Number(ctx.agent.lastTelemetry.costUsd).toFixed(4)}${C.reset} ${C.dim}(last query)${C.reset}`);
3450
+ }
3451
+
3452
+ printSolidPanel({
3453
+ title: 'Session Token Telemetry',
3454
+ lines,
3455
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
3456
+ });
3457
+ return 'HANDLED';
3458
+ }
3459
+
2630
3460
  try {
2631
3461
  const result = await executeTool('get_system_stats', {});
2632
3462
  const s = result.stats || {};
2633
3463
  const insight = await aiNarration(ctx, {
3464
+ label: 'Narrating catalog stats…',
2634
3465
  system: `You are the Kiku data narrator. In ONE short sentence, point out the most interesting thing about this catalog snapshot. ${languageHint(ctx)} Mention only the numbers provided. Never invent numbers. No markdown.`,
2635
3466
  user: JSON.stringify({ totalWorks: s.totalWorks || 0, totalSongs: s.totalUniqueSongs || 0, brands: s.brandEndorsements || 0, awards: s.totalAwards || 0, timeline: s.timelineMilestones || 0 }),
2636
3467
  limit: 240,
@@ -2690,7 +3521,7 @@ export const handlers = {
2690
3521
  ctx.copyQueryDetails(reference);
2691
3522
  return 'HANDLED';
2692
3523
  }
2693
- const { getCopyableContent, copyToClipboard, parseDetailsRequest } = await import('./queryDetails.js');
3524
+ const { getCopyableContent, copyToClipboardAsync, parseDetailsRequest } = await import('./queryDetails.js');
2694
3525
  const reference = [ctx.sub, ctx.extra].filter(Boolean).join(' ');
2695
3526
  const request = parseDetailsRequest(reference);
2696
3527
  const details = ctx.agent?.lastQueryDetails || ctx.agent?._lastQueryDetails || null;
@@ -2703,7 +3534,7 @@ export const handlers = {
2703
3534
  });
2704
3535
  return 'HANDLED';
2705
3536
  }
2706
- const copied = copyToClipboard(copyable.text);
3537
+ const copied = await copyToClipboardAsync(copyable.text);
2707
3538
  printRichCard({
2708
3539
  title: copied ? 'Copied to Clipboard' : 'Copy Content Ready',
2709
3540
  status: copied ? 'ok' : 'info',
@@ -2717,7 +3548,11 @@ export const handlers = {
2717
3548
  },
2718
3549
 
2719
3550
  '/ads': async (ctx) => {
2720
- ctx.printAd();
3551
+ if (typeof ctx.printAd === 'function') {
3552
+ ctx.printAd();
3553
+ } else {
3554
+ console.log(`\n ${C.amber}No sponsor card available in this view.${C.reset}\n`);
3555
+ }
2721
3556
  return 'HANDLED';
2722
3557
  },
2723
3558
 
@@ -2751,6 +3586,7 @@ export const handlers = {
2751
3586
  return 'HANDLED';
2752
3587
  }
2753
3588
 
3589
+ ctx.setIsProcessing?.(true, 'Rendering gallery image…');
2754
3590
  const displayed = typeof ctx.showTerminalImage === 'function' && ctx.showTerminalImage(renderOptions);
2755
3591
  if (!displayed) {
2756
3592
  printRichCard({
@@ -2818,6 +3654,8 @@ export const handlers = {
2818
3654
  if (stdin.isTTY) {
2819
3655
  try { stdin.setRawMode(false); } catch {}
2820
3656
  }
3657
+
3658
+ ctx.setIsProcessing?.(false);
2821
3659
  rl = createInterface({ input: stdin, output: process.stdout });
2822
3660
  }
2823
3661
 
@@ -3005,6 +3843,478 @@ export const handlers = {
3005
3843
  return 'HANDLED';
3006
3844
  },
3007
3845
 
3846
+ '/env': async (ctx) => {
3847
+ const sub = String(ctx.sub || '').toLowerCase().trim();
3848
+
3849
+ const targets = resolveEnvTargets(ctx.envTargets || {});
3850
+ const overridePath = typeof ctx.envOverridePath === 'string' && ctx.envOverridePath
3851
+ ? ctx.envOverridePath
3852
+ : null;
3853
+ const targetPath = overridePath || targets.primaryPath;
3854
+ const interactive = process.stdin.isTTY === true;
3855
+ const merged = readMergedEnvMap({ packageEnvPath: targets.packageEnvPath, userEnvPath: targets.userEnvPath });
3856
+ const effectiveMap = overridePath ? (readEnvFile(targetPath) || {}) : merged.map;
3857
+ const effectiveBaseUrl = process.env.LMSTUDIO_BASE_URL
3858
+ || merged.map.LMSTUDIO_BASE_URL
3859
+ || (overridePath ? (readEnvFile(targetPath) || {}).LMSTUDIO_BASE_URL : null)
3860
+ || 'http://127.0.0.1:1234/v1';
3861
+ const probeAllowed = ctx.envProbe !== false && !ctx.signal?.aborted;
3862
+
3863
+ if (sub === 'path') {
3864
+ printSolidPanel({
3865
+ title: 'Environment File Location',
3866
+ lines: [
3867
+ `${C.gold}Mode:${C.reset} ${targets.mode === 'repo' ? 'repository .env (development checkout)' : 'user .env (global install)'}`,
3868
+ `${C.gold}Active:${C.reset} ${C.white}${targetPath}${C.reset}`,
3869
+ `${C.gold}Also scanned:${C.reset} ${C.dim}${targets.userEnvPath}${C.reset}`,
3870
+ `${C.dim}Package .env wins when both exist; unset keys fall through to the user file.${C.reset}`,
3871
+ ],
3872
+ });
3873
+ return 'HANDLED';
3874
+ }
3875
+
3876
+ if (sub === 'set') {
3877
+
3878
+ const key = String(ctx.parts?.[2] || '').trim().toUpperCase();
3879
+ const value = String(ctx.parts?.slice(3).join(' ') || '').trim();
3880
+ if (!key || !value) {
3881
+ console.log(`\n ${C.amber}Usage: /env set <KEY> <VALUE>${C.reset}\n`);
3882
+ return 'HANDLED';
3883
+ }
3884
+ try {
3885
+ upsertEnvKey(targetPath, key, value);
3886
+ process.env[key] = value;
3887
+ printRichCard({
3888
+ title: 'Environment Updated',
3889
+ status: 'ok',
3890
+ lines: [
3891
+ `${key}=${isSecretKey(key) ? maskValue(key, value) : value}`,
3892
+ `${C.dim}file: ${targetPath}${C.reset}`,
3893
+ `${C.dim}Applied to this session's process.env. Provider endpoint snapshots refresh on next launch —${C.reset}`,
3894
+ `${C.dim}use /provider url <provider> <url> to switch the live endpoint right away.${C.reset}`,
3895
+ ],
3896
+ });
3897
+ } catch (err) {
3898
+ console.log(`\n ${C.red}[ENV ERROR] ${err.message}${C.reset}\n`);
3899
+ }
3900
+ return 'HANDLED';
3901
+ }
3902
+
3903
+ if (sub === 'get') {
3904
+ const key = String(ctx.parts?.[2] || '').trim().toUpperCase();
3905
+ if (!key) {
3906
+ console.log(`\n ${C.amber}Usage: /env get <KEY>${C.reset}\n`);
3907
+ return 'HANDLED';
3908
+ }
3909
+ const sourcePath = overridePath
3910
+ ? targetPath
3911
+ : (merged.sources[key] || null);
3912
+ if (!(key in effectiveMap)) {
3913
+ const runtimeNote = process.env[key] !== undefined && process.env[key] !== ''
3914
+ ? ` ${C.dim}(present in the running process environment only)${C.reset}`
3915
+ : '';
3916
+ console.log(`\n ${C.amber}${key} is not set in ${targetPath}${C.reset}${runtimeNote}\n`);
3917
+ return 'HANDLED';
3918
+ }
3919
+ const sourceLabel = overridePath ? '' : ` ${C.dim}(from ${sourcePath})${C.reset}`;
3920
+ console.log(`\n ${C.gold}${key}${C.reset}=${C.white}${maskValue(key, effectiveMap[key])}${C.reset}${sourceLabel}\n`);
3921
+ return 'HANDLED';
3922
+ }
3923
+
3924
+ if (sub === 'unset') {
3925
+ const key = String(ctx.parts?.[2] || '').trim().toUpperCase();
3926
+ if (!key) {
3927
+ console.log(`\n ${C.amber}Usage: /env unset <KEY>${C.reset}\n`);
3928
+ return 'HANDLED';
3929
+ }
3930
+ try {
3931
+ const results = overridePath
3932
+ ? [removeEnvKey(targetPath, key)]
3933
+ : [removeEnvKey(targets.packageEnvPath, key), removeEnvKey(targets.userEnvPath, key)];
3934
+ delete process.env[key];
3935
+ const touched = results.filter(r => r.removed).map(r => r.path);
3936
+ console.log(touched.length > 0
3937
+ ? `\n ${C.emerald}[OK] Removed ${key} from:\n${touched.map(p => ` • ${p}`).join('\n')}${C.reset}\n`
3938
+ : `\n ${C.amber}${key} was not present in ${targetPath}${C.reset}\n`);
3939
+ } catch (err) {
3940
+ console.log(`\n ${C.red}[ENV ERROR] ${err.message}${C.reset}\n`);
3941
+ }
3942
+ return 'HANDLED';
3943
+ }
3944
+
3945
+ if (sub === 'init') {
3946
+ const exists = fs.existsSync(targetPath);
3947
+ if (exists && !overridePath) {
3948
+ if (!interactive) {
3949
+ printRichCard({
3950
+ title: '.env Already Exists',
3951
+ status: 'info',
3952
+ lines: [
3953
+ `${C.dim}${targetPath}${C.reset}`,
3954
+ 'Use /env set <KEY> <VALUE> to change individual keys,',
3955
+ 'or run /env init again from an interactive terminal to regenerate it.',
3956
+ ],
3957
+ });
3958
+ return 'HANDLED';
3959
+ }
3960
+ const answer = await askEnvQuestion(`.env already exists at ${targetPath}. Overwrite? (y/N)`, 'N');
3961
+ if (!/^y(es)?$/i.test(answer)) {
3962
+ console.log(`\n ${C.dim}Aborted — file left untouched.${C.reset}\n`);
3963
+ return 'HANDLED';
3964
+ }
3965
+ }
3966
+
3967
+ const detected = await detectAutoDefaults({ baseUrl: effectiveBaseUrl, probe: probeAllowed, signal: ctx.signal });
3968
+ let values;
3969
+ if (interactive) {
3970
+
3971
+ ctx.setIsProcessing?.(false);
3972
+ console.log(`\n ${C.bold}${C.cyberCyan}Kiku environment setup${C.reset} ${C.dim}— Enter accepts the default; everything not asked stays optional.${C.reset}\n`);
3973
+ values = {};
3974
+ const baseUrl = await askEnvQuestion('LM Studio endpoint URL', 'http://127.0.0.1:1234/v1');
3975
+ values.LMSTUDIO_BASE_URL = baseUrl || 'http://127.0.0.1:1234/v1';
3976
+ if (detected.loadedModel) {
3977
+ console.log(` ${C.dim}Detected loaded model: ${detected.loadedModel}${C.reset}`);
3978
+ }
3979
+ const contextAnswer = await askEnvQuestion('Context window for token telemetry (tokens)', detected.contextLength ? String(detected.contextLength) : '');
3980
+ if (contextAnswer && Number(contextAnswer) > 0) values.KIKU_CONTEXT_LIMIT = String(Math.trunc(Number(contextAnswer)));
3981
+ const dirAnswer = await askEnvQuestion('LM Studio models directory', detected.modelsDir || '');
3982
+ if (dirAnswer) values.LMSTUDIO_MODELS_DIR = dirAnswer;
3983
+ const wantCloud = await askEnvQuestion('Configure a cloud provider API key now? (y/N)', 'N');
3984
+ if (/^y(es)?$/i.test(wantCloud)) {
3985
+ const provider = (await askEnvQuestion('Provider (openai/gemini/groq/deepseek/openrouter)', 'openai')).toUpperCase();
3986
+ const keyMap = {
3987
+ OPENAI: 'OPENAI_API_KEY',
3988
+ GEMINI: 'GEMINI_API_KEY',
3989
+ GROQ: 'GROQ_API_KEY',
3990
+ DEEPSEEK: 'DEEPSEEK_API_KEY',
3991
+ OPENROUTER: 'OPENROUTER_API_KEY',
3992
+ };
3993
+ const keyName = keyMap[provider];
3994
+ if (keyName) {
3995
+ const apiKey = await askEnvQuestion(`${provider} API key`, '');
3996
+ if (apiKey) values[keyName] = apiKey;
3997
+ }
3998
+ }
3999
+ } else {
4000
+ values = { LMSTUDIO_BASE_URL: 'http://127.0.0.1:1234/v1' };
4001
+ if (detected.contextLength) values.KIKU_CONTEXT_LIMIT = String(detected.contextLength);
4002
+ if (detected.modelsDir) values.LMSTUDIO_MODELS_DIR = detected.modelsDir;
4003
+ }
4004
+
4005
+ try {
4006
+ const result = generateEnvFile({ targetPath, values, detected, allowOverwrite: true });
4007
+ for (const [k, v] of Object.entries(values)) {
4008
+ if (process.env[k] === undefined) process.env[k] = String(v);
4009
+ }
4010
+ printRichCard({
4011
+ title: '.env Created',
4012
+ status: 'ok',
4013
+ lines: [
4014
+ `${C.dim}${result.path}${C.reset}`,
4015
+ `Set: ${result.keys.join(', ')}`,
4016
+ detected.loadedModel
4017
+ ? `${C.dim}LM Studio: ${detected.loadedModel}${detected.contextLength ? ` • ${detected.contextLength} ctx` : ''}${C.reset}`
4018
+ : `${C.dim}LM Studio was offline — defaults written; run /env init again later to auto-detect.${C.reset}`,
4019
+ 'Everything else is intentionally left commented (not necessary for CLI usage).',
4020
+ `${C.dim}Verify with /ping or /model status.${C.reset}`,
4021
+ ],
4022
+ });
4023
+ } catch (err) {
4024
+ console.log(`\n ${C.red}[ENV ERROR] ${err.message}${C.reset}\n`);
4025
+ }
4026
+ return 'HANDLED';
4027
+ }
4028
+
4029
+ if (sub && !['path', 'set', 'get', 'unset', 'init'].includes(sub)) {
4030
+ console.log(`\n ${C.amber}Unknown /env subcommand: ${sub}${C.reset} ${C.dim}(use init|set|get|unset|path)${C.reset}\n`);
4031
+ }
4032
+
4033
+
4034
+ const map = overridePath ? (readEnvFile(targetPath) || {}) : merged.map;
4035
+ const detected = await detectAutoDefaults({ baseUrl: effectiveBaseUrl, probe: probeAllowed, timeoutMs: 800, signal: ctx.signal });
4036
+ const catalogKeys = new Set(ENV_VAR_CATALOG.map(e => e.key));
4037
+ const otherKeys = Object.keys(map).filter(k => !catalogKeys.has(k) && String(map[k] || ''));
4038
+ const lines = [
4039
+ `${C.gold}Mode:${C.reset} ${targets.mode === 'repo' ? 'repository .env (development)' : 'user .env (global install)'}`,
4040
+ `${C.gold}File:${C.reset} ${C.white}${targetPath}${C.reset}`,
4041
+ `${C.gold}Effective endpoint:${C.reset} ${C.cyberCyan}${effectiveBaseUrl}${C.reset}`,
4042
+ `${C.gold}LM Studio:${C.reset} ${probeAllowed
4043
+ ? (detected.lmStudioReachable
4044
+ ? `${C.emerald}reachable${C.reset}${detected.loadedModel ? ` ${C.dim}• ${detected.loadedModel}${detected.contextLength ? ` @ ${detected.contextLength} ctx` : ''}${C.reset}` : ''}`
4045
+ : `${C.dim}not reachable (start LM Studio server, or set LMSTUDIO_BASE_URL)${C.reset}`)
4046
+ : `${C.dim}probe skipped this run${C.reset}`}`,
4047
+ '',
4048
+ ];
4049
+
4050
+ for (const group of ['necessary', 'recommended']) {
4051
+ lines.push(`${C.bold}${C.lavender}${describeGroup(group)}:${C.reset}`);
4052
+ for (const entry of ENV_VAR_CATALOG.filter(e => e.group === group)) {
4053
+ const isSet = entry.key in map;
4054
+ const shown = isSet ? maskValue(entry.key, map[entry.key]) : (entry.defaultValue ? `default: ${entry.defaultValue}` : '—');
4055
+ lines.push(` ${isSet
4056
+ ? `${C.emerald}set ${C.reset}${C.white}${entry.key.padEnd(22)}${C.reset}${C.dim}${shown}${C.reset}`
4057
+ : `${C.dim}unset ${entry.key.padEnd(22)}${shown}${C.reset}`}`);
4058
+ }
4059
+ lines.push('');
4060
+ }
4061
+
4062
+ for (const group of ['optional', 'cloud', 'server']) {
4063
+ const entries = ENV_VAR_CATALOG.filter(e => e.group === group);
4064
+ if (group === 'server' && targets.mode !== 'repo') {
4065
+
4066
+ lines.push(`${C.bold}${C.lavender}Server deployment credentials:${C.reset} ${C.dim}owned by the platform developer (hosted API) — never needed or prompted for the CLI${C.reset}`);
4067
+ continue;
4068
+ }
4069
+ const setKeys = entries.filter(e => e.key in map);
4070
+ const summary = setKeys.length > 0
4071
+ ? `${setKeys.length}/${entries.length} set ${C.dim}(${setKeys.map(e => e.key).join(', ')})${C.reset}`
4072
+ : `${C.dim}none set — defaults apply; not required for CLI usage${C.reset}`;
4073
+ lines.push(`${C.bold}${C.lavender}${describeGroup(group)}:${C.reset} ${summary}`);
4074
+ }
4075
+
4076
+ if (otherKeys.length > 0) {
4077
+ lines.push('');
4078
+ lines.push(`${C.bold}${C.lavender}Other keys detected in your env files (not part of the standard catalog):${C.reset}`);
4079
+ for (const k of otherKeys) {
4080
+ lines.push(` ${C.cyberCyan}• ${k.padEnd(26)}${C.reset}${C.dim}${maskValue(k, map[k])}${C.reset}`);
4081
+ }
4082
+ }
4083
+
4084
+ lines.push('');
4085
+ lines.push(`${C.dim}/env init • /env set <KEY> <VALUE> • /env get <KEY> • /env unset <KEY> • /env path${C.reset}`);
4086
+
4087
+ printSolidPanel({
4088
+ title: 'Local Environment (.env)',
4089
+ lines,
4090
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
4091
+ });
4092
+ return 'HANDLED';
4093
+ },
4094
+
4095
+ '/eval': async (ctx) => {
4096
+ const sub = (ctx.sub || 'ui').toLowerCase();
4097
+ const extra = (ctx.extra || '').trim();
4098
+
4099
+ if (sub === 'ui' || sub === 'studio') {
4100
+ const port = Number(extra) || 3000;
4101
+ const url = `http://localhost:${port}/eval`;
4102
+ openBrowser(url);
4103
+ printRichCard({
4104
+ title: 'Interactive Eval Studio Opened',
4105
+ status: 'ok',
4106
+ lines: [
4107
+ `• Eval Studio Dashboard: ${C.bold}${C.cyberCyan}${url}${C.reset}`,
4108
+ `• Live benchmark test runner & performance visualization.`,
4109
+ `• Web UI launched in default browser.`,
4110
+ ],
4111
+ });
4112
+ return 'HANDLED';
4113
+ }
4114
+
4115
+ if (sub === 'status') {
4116
+ try {
4117
+ const { DEFAULTS, REPO_ROOT } = await import('./eval/modules/core/config.js');
4118
+ const raw = await fs.promises.readFile(path.join(REPO_ROOT, DEFAULTS.out), 'utf8');
4119
+ const report = JSON.parse(raw);
4120
+ const rows = Array.isArray(report.rows) ? report.rows : [];
4121
+ const completed = rows.filter(r => r.status === 'completed').length;
4122
+ const errors = rows.length - completed.length;
4123
+ const passRate = report.summary?.passAt1 ?? report.summary?.lexicalCoverageRate;
4124
+ const model = report.metadata?.model || 'unknown';
4125
+ const finishedAt = report.metadata?.finishedAt || report.metadata?.startedAt || 'unknown';
4126
+ const partialBadge = report.metadata?.archivedPartial === true ? ` ${C.amber}[partial]${C.reset}` : '';
4127
+ printSolidPanel({
4128
+ title: 'Eval Status • Live Report',
4129
+ lines: [
4130
+ `• Model: ${C.bold}${C.white}${model}${C.reset}${partialBadge}`,
4131
+ `• Rows: ${C.bold}${C.white}${rows.length}${C.reset} (${completed} completed, ${errors > 0 ? `${C.amber}${errors} errors${C.reset}` : '0 errors'})`,
4132
+ `• Pass@1: ${passRate != null ? `${C.emerald}${(Number(passRate) * 100).toFixed(1)}%${C.reset}` : `${C.dim}n/a${C.reset}`}`,
4133
+ `• Last activity: ${C.dim}${finishedAt}${C.reset}`,
4134
+ `${C.dim}Next: /eval ui (interactive studio) • /eval quick (3-case smoke) • /eval run --limit 3${C.reset}`,
4135
+ ],
4136
+ });
4137
+ } catch {
4138
+ printRichCard({
4139
+ title: 'Eval Status',
4140
+ status: 'info',
4141
+ lines: [
4142
+ 'No evaluation report found yet.',
4143
+ `• ${C.bold}/eval quick${C.reset} — 3-case smoke run against the active model`,
4144
+ `• ${C.bold}/eval ui${C.reset} — interactive Eval Studio dashboard`,
4145
+ `• ${C.bold}/eval validate${C.reset} — verify the 500-case frozen corpus (no LLM)`,
4146
+ ],
4147
+ });
4148
+ }
4149
+ return 'HANDLED';
4150
+ }
4151
+
4152
+ if (sub === 'validate') {
4153
+ try {
4154
+ const { cases, securityCases, allCases, validateCases } = await import('./eval/modules/evaluation/cases.js');
4155
+ validateCases();
4156
+ const categories = [...new Set(allCases.map(c => c.category))];
4157
+ printSolidPanel({
4158
+ title: 'Evaluation Corpus Validation • Frozen Ground Truth',
4159
+ lines: [
4160
+ `${C.bold}${C.emerald}✓ Corpus Valid: ${allCases.length} cases verified${C.reset}`,
4161
+ `• Total Cases: ${C.bold}${C.white}${allCases.length}${C.reset} (${cases.length} catalog + ${securityCases.length} security)`,
4162
+ `• Categories: ${C.dim}${categories.join(', ')}${C.reset}`,
4163
+ `• Integrity: Reference answers frozen — gold standards intact`,
4164
+ ],
4165
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
4166
+ });
4167
+ } catch (err) {
4168
+ console.log(` ${C.amber}Validate error: ${err.message}${C.reset}`);
4169
+ }
4170
+ return 'HANDLED';
4171
+ }
4172
+
4173
+ if (sub === 'dashboard' || sub === 'quick' || ['run', 'judge', 'compare', 'gate', 'analyze', 'dashboard-png'].includes(sub)) {
4174
+ const { spawn } = await import('node:child_process');
4175
+ let args;
4176
+ if (sub === 'quick') {
4177
+ args = ['server/agent/eval/modules/core/cli.js', 'run', '--limit', '3'];
4178
+ } else {
4179
+ args = ['server/agent/eval/modules/core/cli.js', sub];
4180
+ if (extra) args.push(...extra.split(/\s+/).filter(Boolean));
4181
+ }
4182
+
4183
+ const child = spawn(process.execPath, args, {
4184
+ stdio: ['ignore', 'pipe', 'pipe'],
4185
+ cwd: process.cwd(),
4186
+ windowsHide: true,
4187
+ });
4188
+
4189
+ const forward = chunk => {
4190
+ for (const line of chunk.split(/\r?\n/)) {
4191
+ if (line.trim()) console.log(line);
4192
+ }
4193
+ };
4194
+ child.stdout?.on('data', b => forward(b.toString()));
4195
+ child.stderr?.on('data', b => forward(b.toString()));
4196
+ child.on('error', err => {
4197
+ console.log(` ${C.amber}Eval error: ${err.message}${C.reset}`);
4198
+ });
4199
+ child.on('close', code => {
4200
+ if (ctx.signal?.aborted) {
4201
+ console.log(` ${C.amber}/eval ${sub} cancelled.${C.reset}`);
4202
+ } else if (code !== 0 && code !== null) {
4203
+ console.log(` ${C.amber}/eval ${sub} exited with code ${code}${C.reset}`);
4204
+ }
4205
+ });
4206
+ ctx.signal?.addEventListener('abort', () => {
4207
+ try { child.kill(); } catch {}
4208
+ }, { once: true });
4209
+ return 'HANDLED';
4210
+ }
4211
+
4212
+ printRichCard({
4213
+ title: 'Kiku Eval Suite & Benchmark Harness',
4214
+ status: 'info',
4215
+ lines: [
4216
+ `• Subcommands: ${C.bold}${C.cyberCyan}ui, status, validate, quick, dashboard, run, judge, compare, gate, analyze${C.reset}`,
4217
+ `• /eval status - Show live report summary (rows, pass rate, errors)`,
4218
+ `• /eval ui - Open interactive Eval Studio web UI (/eval)`,
4219
+ `• /eval dashboard - Generate & open mobile-responsive HTML evaluation report`,
4220
+ `• /eval validate - Check 500 frozen ground truth cases without LLM`,
4221
+ `• /eval quick - Run 3-case live smoke evaluation against active model`,
4222
+ `• /eval run|judge|compare|gate|analyze [flags] - forwarded to the eval CLI`,
4223
+ ],
4224
+ });
4225
+ return 'HANDLED';
4226
+ },
4227
+
4228
+ '/hub': async (ctx) => {
4229
+ const filter = (ctx.sub || ctx.extra || '').trim();
4230
+ try {
4231
+ const { renderKikuHub } = await import('../../scripts/kikuHub.js');
4232
+ await renderKikuHub(filter);
4233
+ } catch (err) {
4234
+ console.log(`\n ${C.amber}[HUB ERROR] ${err.message}${C.reset}\n`);
4235
+ }
4236
+ return 'HANDLED';
4237
+ },
4238
+
4239
+ '/portal': async (ctx) => {
4240
+ const rawSub = (ctx.sub || 'open').toLowerCase();
4241
+ const extraPort = Number(ctx.extra || (/^\d+$/.test(ctx.sub) ? ctx.sub : null)) || 3000;
4242
+ const baseUrl = `http://localhost:${extraPort}`;
4243
+
4244
+ if (rawSub === 'api' || rawSub === 'docs') {
4245
+ const url = `${baseUrl}/api-docs`;
4246
+ openBrowser(url);
4247
+ printRichCard({
4248
+ title: 'REST API v1 Documentation Opened',
4249
+ status: 'ok',
4250
+ lines: [
4251
+ `• Interactive API Docs: ${C.bold}${C.cyberCyan}${url}${C.reset}`,
4252
+ `• OpenAPI 3.1 Spec: ${C.dim}${baseUrl}/openapi.json${C.reset}`,
4253
+ `• Endpoints live at ${C.white}${baseUrl}/api/v1/*${C.reset}`,
4254
+ ],
4255
+ });
4256
+ return 'HANDLED';
4257
+ }
4258
+
4259
+ if (rawSub === 'chat') {
4260
+ const url = `${baseUrl}/chat`;
4261
+ openBrowser(url);
4262
+ printRichCard({
4263
+ title: 'Luxury Web Chat Opened',
4264
+ status: 'ok',
4265
+ lines: [
4266
+ `• Web Chat interface: ${C.bold}${C.cyberCyan}${url}${C.reset}`,
4267
+ `• Direct AI model streaming on Port ${extraPort}.`,
4268
+ ],
4269
+ });
4270
+ return 'HANDLED';
4271
+ }
4272
+
4273
+ if (rawSub === 'eval' || rawSub === 'studio') {
4274
+ return handlers['/eval']({ ...ctx, sub: 'ui', extra: String(extraPort), parts: ['/eval', 'ui'] });
4275
+ }
4276
+
4277
+ if (rawSub === 'status' || rawSub === 'info') {
4278
+ const isLive = await new Promise((resolve) => {
4279
+ const req = http.get(`${baseUrl}/api/v1/profile`, { timeout: 1500 }, (res) => {
4280
+ resolve(res.statusCode >= 200 && res.statusCode < 400);
4281
+ res.resume();
4282
+ });
4283
+ req.on('timeout', () => { req.destroy(); resolve(false); });
4284
+ req.on('error', () => resolve(false));
4285
+ });
4286
+
4287
+ const lines = [
4288
+ `• Dev Server: ${isLive ? `${C.emerald}Active (Port ${extraPort})${C.reset}` : `${C.amber}Offline or starting (Port ${extraPort})${C.reset}`}`,
4289
+ `• Fan Portal SPA: ${C.cyberCyan}${baseUrl}/${C.reset}`,
4290
+ `• REST API v1: ${C.cyberCyan}${baseUrl}/api-docs${C.reset}`,
4291
+ `• Luxury Web Chat: ${C.cyberCyan}${baseUrl}/chat${C.reset}`,
4292
+ `• Eval Studio: ${C.cyberCyan}${baseUrl}/eval${C.reset}`,
4293
+ '',
4294
+ `${C.dim}Tip: Run "npm run dev" to launch all services in a single in-process dev server.${C.reset}`,
4295
+ ];
4296
+
4297
+ printRichCard({
4298
+ title: 'Kiku Web Portal & API Status',
4299
+ status: isLive ? 'ok' : 'info',
4300
+ lines,
4301
+ });
4302
+ return 'HANDLED';
4303
+ }
4304
+
4305
+ openBrowser(baseUrl);
4306
+ printRichCard({
4307
+ title: 'Kiku Fan Portal Opened',
4308
+ status: 'ok',
4309
+ lines: [
4310
+ `• Fan Portal SPA: ${C.bold}${C.cyberCyan}${baseUrl}/${C.reset}`,
4311
+ `• In-process unified Vite dev server (Port ${extraPort}).`,
4312
+ `• Browser opened automatically.`,
4313
+ ],
4314
+ });
4315
+ return 'HANDLED';
4316
+ },
4317
+
3008
4318
  '/exit': async (ctx) => {
3009
4319
  try { await ctx.agent?.endSession(); } catch {}
3010
4320
  return 'EXIT';
@@ -3013,6 +4323,7 @@ export const handlers = {
3013
4323
 
3014
4324
  const ALIASES = {
3015
4325
  '/quit': '/exit', '/q': '/exit',
4326
+ '/web-chat': '/web',
3016
4327
  '/abort': '/cancel',
3017
4328
  '/cp': '/copy', '/clip': '/copy', '/clipboard': '/copy',
3018
4329
  '/?': '/help', '/commands': '/help', '/menu': '/help',
@@ -3037,9 +4348,13 @@ const ALIASES = {
3037
4348
  '/health': '/ping',
3038
4349
  '/settings': '/config', '/conf': '/config',
3039
4350
  '/persona': '/prompt', '/sysprompt': '/prompt',
3040
- '/ver': '/version', '/v': '/version',
4351
+ '/ver': '/about', '/v': '/about', '/version': '/about',
3041
4352
  '/sys': '/system', '/os': '/system',
3042
- '/vram': '/gpu', '/gpus': '/gpu',
4353
+ '/gpus': '/gpu',
4354
+ '/resource': '/vram',
4355
+ '/benchmark': '/eval', '/evaluation': '/eval',
4356
+ '/center': '/hub',
4357
+ '/app': '/portal', '/webportal': '/portal',
3043
4358
  '/info': '/about',
3044
4359
  '/sponsor': '/ads', '/ad': '/ads',
3045
4360
  '/image': '/gallery', '/photo': '/gallery',
@@ -3050,10 +4365,10 @@ const ALIASES = {
3050
4365
  '/kvip': '/key',
3051
4366
  '/switch': '/provider',
3052
4367
  '/providers': '/provider',
3053
- '/fallback': '/provider',
3054
4368
  '/profiles': '/profile',
3055
4369
  '/preset': '/profile',
3056
4370
  '/presets': '/profile',
4371
+ '/ingestion': '/ingest',
3057
4372
  };
3058
4373
 
3059
4374
  export async function routeCommand(rawInput, ctx) {