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.
- package/README.md +459 -407
- package/package.json +27 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +629 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +528 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +383 -179
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
package/server/agent/commands.js
CHANGED
|
@@ -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)) ||
|
|
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 :
|
|
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
|
-
|
|
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} ${
|
|
720
|
+
`${C.bold}${C.slate}Sources Found:${C.reset} ${cleanSources.length}`,
|
|
390
721
|
'',
|
|
391
722
|
`${C.bold}${C.roseGlow}Synthesized Answer:${C.reset}`,
|
|
392
|
-
...
|
|
723
|
+
...formatMarkdownLines(latest.answer || 'No findings recorded.').map(l => ` ${l}`),
|
|
393
724
|
'',
|
|
394
725
|
`${C.bold}${C.lavender}Sources Cited:${C.reset}`,
|
|
395
|
-
...(
|
|
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:
|
|
409
|
-
lines:
|
|
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 (${
|
|
483
|
-
...(
|
|
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 • ${
|
|
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
|
-
|
|
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] |
|
|
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
|
|
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
|
-
'/
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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,
|
|
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 =
|
|
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': '/
|
|
4351
|
+
'/ver': '/about', '/v': '/about', '/version': '/about',
|
|
3041
4352
|
'/sys': '/system', '/os': '/system',
|
|
3042
|
-
'/
|
|
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) {
|