kiku-agent 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +293 -122
  2. package/package.json +60 -61
  3. package/server/agent/agentCore.js +892 -782
  4. package/server/agent/appPaths.js +21 -0
  5. package/server/agent/audioPlayer.js +739 -673
  6. package/server/agent/cli.js +1414 -1111
  7. package/server/agent/commandRegistry.js +74 -58
  8. package/server/agent/commands.js +1694 -788
  9. package/server/agent/config.js +264 -90
  10. package/server/agent/contextCompactor.js +79 -0
  11. package/server/agent/doctor.js +69 -0
  12. package/server/agent/eventBus.js +107 -103
  13. package/server/agent/gpuMetrics.js +189 -0
  14. package/server/agent/llmClient.js +369 -200
  15. package/server/agent/mcpClient.js +364 -0
  16. package/server/agent/memory.js +670 -566
  17. package/server/agent/modelManager.js +77 -0
  18. package/server/agent/research.js +202 -0
  19. package/server/agent/session.js +244 -239
  20. package/server/agent/subagent.js +116 -0
  21. package/server/agent/theme.js +125 -124
  22. package/server/agent/toolRouter.js +201 -0
  23. package/server/agent/tools.js +1679 -1528
  24. package/server/agent/ui.js +760 -469
  25. package/server/mcp/claude-desktop-config.example.json +1 -1
  26. package/server/mcp/data/discography.js +36 -36
  27. package/server/mcp/dataAdapter.js +102 -0
  28. package/server/mcp/examples/claude-code.mcp.example.json +19 -0
  29. package/server/mcp/examples/claude-desktop.example.json +11 -0
  30. package/server/mcp/examples/cline.example.json +13 -0
  31. package/server/mcp/examples/cursor.example.json +14 -0
  32. package/server/mcp/examples/gemini-cli.example.json +17 -0
  33. package/server/mcp/examples/opencode.example.json +21 -0
  34. package/server/mcp/examples/vscode.example.json +19 -0
  35. package/server/mcp/examples/windsurf.example.json +14 -0
  36. package/server/mcp/handler.js +85 -159
  37. package/server/mcp/security.js +140 -138
  38. package/server/mcp/server.js +129 -100
  39. package/server/mcp/stdio.js +4 -2
@@ -1,788 +1,1694 @@
1
- import { toolsSchema, executeTool } from './tools.js';
2
- import { audioPlayer } from './audioPlayer.js';
3
- import { ensureYouTubeCookies, resetCookieCheck, getCookieStatus } from './youtubeAuth.js';
4
- import { AgentMemory } from './memory.js';
5
- import { C, UI_ICONS, printSolidPanel, randomFanName } from './ui.js';
6
- import { COMMAND_NAMES } from './commandRegistry.js';
7
- import { THEMES, THEME_NAMES, ansiBackground, ansiForeground, getThemeName, setTheme } from './theme.js';
8
-
9
- function levenshteinDistance(a, b) {
10
- const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
11
- for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
12
- for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
13
- for (let i = 1; i <= a.length; i++) {
14
- for (let j = 1; j <= b.length; j++) {
15
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
16
- matrix[i][j] = Math.min(
17
- matrix[i - 1][j] + 1,
18
- matrix[i][j - 1] + 1,
19
- matrix[i - 1][j - 1] + cost
20
- );
21
- }
22
- }
23
- return matrix[a.length][b.length];
24
- }
25
-
26
- const VALID_COMMANDS = [...COMMAND_NAMES, '/export'];
27
-
28
- export function findClosestCommand(cmd) {
29
- let closest = null;
30
- let minDistance = Infinity;
31
- for (const vc of VALID_COMMANDS) {
32
- const dist = levenshteinDistance(cmd.toLowerCase(), vc);
33
- if (dist < minDistance) {
34
- minDistance = dist;
35
- closest = vc;
36
- }
37
- }
38
- return minDistance <= 3 ? closest : null;
39
- }
40
-
41
- export function fuzzyMatchTools(query) {
42
- if (!query) return toolsSchema.map(t => ({ tool: t, score: 10 }));
43
- const q = query.toLowerCase();
44
- const qWords = q.split(/\s+/).filter(Boolean);
45
-
46
- const scored = toolsSchema.map(t => {
47
- const name = t.function.name.toLowerCase();
48
- const desc = (t.function.description || '').toLowerCase();
49
- let score = 0;
50
-
51
- if (name === q) score = 200;
52
- else if (name.startsWith(q)) score = 150;
53
- else if (name.includes(q)) score = 100;
54
- else if (qWords.every(w => name.includes(w))) score = 80;
55
- else if (qWords.some(w => name.includes(w))) score = 65;
56
- else if (qWords.every(w => desc.includes(w))) score = 50;
57
- else if (qWords.some(w => desc.includes(w))) score = 40;
58
- else {
59
- const dist = levenshteinDistance(q, name);
60
- const threshold = Math.floor(Math.min(q.length, name.length) * 0.4);
61
- if (dist <= threshold && dist <= 6) score = 30 - dist * 3;
62
- if (score === 0) {
63
- for (const word of name.split('_')) {
64
- const wd = levenshteinDistance(q, word);
65
- const wThreshold = Math.floor(Math.min(q.length, word.length) * 0.4);
66
- if (wd <= wThreshold && wd <= 4) {
67
- score = Math.max(score, 35 - wd * 5);
68
- }
69
- }
70
- }
71
- }
72
-
73
- return { tool: t, score };
74
- });
75
-
76
- return scored
77
- .filter(s => s.score > 0)
78
- .sort((a, b) => b.score - a.score);
79
- }
80
-
81
- export const handlers = {
82
- '/exit': async (ctx) => {
83
- try { await ctx.agent.endSession(); } catch {}
84
- console.log(`\n ${C.sakura}Session archived. Thank you for supporting Ju Jingyi. Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
85
- return 'EXIT';
86
- },
87
-
88
- '/help': async (ctx) => {
89
- const sub = ctx.extra || '';
90
- ctx.printHelp(sub);
91
- return 'HANDLED';
92
- },
93
-
94
- '/details': async (ctx) => {
95
- ctx.printQueryDetails(ctx.sub || '');
96
- return 'HANDLED';
97
- },
98
-
99
- '/history': async (ctx) => {
100
- const { agent, sub, extra } = ctx;
101
- if (sub === 'clear') {
102
- agent.session.messages = [];
103
- agent.conversationHistory = [agent.conversationHistory[0]];
104
- await agent.session.clearDisk().catch(() => {});
105
- console.log(`\n ${C.emerald}[OK] Chat history cleared.${C.reset}\n`);
106
- return 'HANDLED';
107
- }
108
-
109
- if (sub === 'search' && extra) {
110
- const q = extra.toLowerCase().trim();
111
- const matches = [];
112
- for (const msg of agent.session.messages) {
113
- const textPart = msg.parts.find(p => p.type === 'text');
114
- if (textPart && textPart.content.toLowerCase().includes(q)) {
115
- matches.push({ role: msg.role, text: textPart.content, createdAt: msg.createdAt });
116
- }
117
- }
118
- printSolidPanel({
119
- title: `History Search Results for "${extra}" (${matches.length} matches)`,
120
- lines: matches.length === 0
121
- ? [`${C.dim}No messages found matching "${extra}".${C.reset}`]
122
- : matches.map(m => {
123
- const prefix = m.role === 'user' ? `${C.roseGlow}User` : `${C.cyberCyan}Kiku`;
124
- return `${prefix}:${C.reset} ${C.white}${m.text.slice(0, 80)}${m.text.length > 80 ? '...' : ''}${C.reset}`;
125
- }),
126
- headerBg: '\x1b[48;2;48;54;70m',
127
- bg: '\x1b[48;2;36;40;52m'
128
- });
129
- return 'HANDLED';
130
- }
131
-
132
- let count = 10;
133
- if (sub && !isNaN(parseInt(sub))) count = Math.min(parseInt(sub), 50);
134
-
135
- const recentMessages = agent.session.messages.slice(-count);
136
- if (recentMessages.length === 0) {
137
- console.log(`\n ${C.dim}No conversation history yet.${C.reset}\n`);
138
- return 'HANDLED';
139
- }
140
-
141
- const lines = [];
142
- for (const msg of recentMessages) {
143
- if (msg.role === 'user') {
144
- const textPart = msg.parts.find(p => p.type === 'text');
145
- if (textPart) {
146
- lines.push(`${C.roseGlow}${C.bold}${randomFanName()}:${C.reset} ${C.white}${textPart.content}${C.reset}`);
147
- }
148
- } else if (msg.role === 'assistant') {
149
- const textPart = msg.parts.find(p => p.type === 'text');
150
- if (textPart) {
151
- const preview = textPart.content.length > 120 ? textPart.content.slice(0, 120) + '…' : textPart.content;
152
- lines.push(`${C.cyberCyan}${C.bold}Kiku:${C.reset} ${C.white}${preview.replace(/\n/g, ' ')}${C.reset}`);
153
- }
154
- }
155
- }
156
- printSolidPanel({
157
- title: `Conversation History (last ${recentMessages.length} messages)`,
158
- lines,
159
- headerBg: '\x1b[48;2;48;54;70m',
160
- bg: '\x1b[48;2;36;40;52m'
161
- });
162
- return 'HANDLED';
163
- },
164
-
165
- '/session': async (ctx) => {
166
- const { agent, sub } = ctx;
167
- const sess = agent.session;
168
- const action = (sub || 'info').toLowerCase();
169
-
170
- if (action === 'info' || !action) {
171
- const msgCount = sess.messages.length;
172
- const userMsgs = sess.messages.filter(m => m.role === 'user').length;
173
- const assistantMsgs = sess.messages.filter(m => m.role === 'assistant').length;
174
- const toolCalls = sess.stats.totalToolCalls || 0;
175
- const totalDur = ((sess.stats.totalDurationMs || 0) / 1000).toFixed(1);
176
- const createdAt = sess.createdAt ? sess.createdAt.toLocaleString() : 'unknown';
177
-
178
- printSolidPanel({
179
- title: `Active Session Details`,
180
- lines: [
181
- `${C.gold}Session ID:${C.reset} ${C.white}${sess.id}${C.reset}`,
182
- `${C.gold}Created At:${C.reset} ${C.white}${createdAt}${C.reset}`,
183
- `${C.gold}Total Messages:${C.reset} ${C.cyberCyan}${msgCount}${C.reset} total ${C.dim}(${userMsgs} user, ${assistantMsgs} assistant)${C.reset}`,
184
- `${C.gold}Total Queries:${C.reset} ${C.cyberCyan}${sess.stats.totalQueries}${C.reset}`,
185
- `${C.gold}Tool Calls:${C.reset} ${C.cyberCyan}${toolCalls}${C.reset}`,
186
- `${C.gold}Total Duration:${C.reset} ${C.cyberCyan}${totalDur}s${C.reset}`,
187
- `${C.gold}Thinking Mode:${C.reset} ${C.bold}${C.gold}${sess.thinkingMode.toUpperCase()}${C.reset}`
188
- ],
189
- headerBg: '\x1b[48;2;48;54;70m',
190
- bg: '\x1b[48;2;36;40;52m'
191
- });
192
- return 'HANDLED';
193
- }
194
-
195
- if (action === 'list') {
196
- const sessions = agent.memory.getArchivedSessions(15);
197
- printSolidPanel({
198
- title: `Archived Sessions (Total: ${sessions.length})`,
199
- lines: sessions.length === 0
200
- ? [`${C.dim}No archived sessions found.${C.reset}`]
201
- : sessions.map(s => {
202
- const date = new Date(s.createdAt).toLocaleString();
203
- const dur = ((s.stats?.totalDurationMs || 0) / 1000).toFixed(1);
204
- return `${C.gold}${s.id}${C.reset} ${C.dim}(${date})${C.reset} — ${s.messageCount} msgs, ${dur}s`;
205
- }),
206
- headerBg: '\x1b[48;2;48;54;70m',
207
- bg: '\x1b[48;2;36;40;52m'
208
- });
209
- return 'HANDLED';
210
- }
211
-
212
- if (action === 'export') {
213
- const exported = await agent.session.exportToMarkdown();
214
- console.log(`\n ${C.emerald}[OK] Session transcript exported.${C.reset}`);
215
- console.log(` ${C.slate}• File:${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
216
- return 'HANDLED';
217
- }
218
-
219
- if (action === 'clear' || action === 'new' || action === 'reset') {
220
- await agent.endSession();
221
- console.log(`\n ${C.emerald}[OK] Previous session archived. Started fresh.${C.reset}\n`);
222
- return 'HANDLED';
223
- }
224
-
225
- console.log(`\n ${C.amber}Unknown session action: "${sub}". Use /session [info|list|export|clear]${C.reset}\n`);
226
- return 'HANDLED';
227
- },
228
-
229
- '/thinking': async (ctx) => {
230
- const { agent, sub } = ctx;
231
- const targetMode = (sub || '').toLowerCase();
232
- if (targetMode === 'show' || targetMode === 'hide') {
233
- agent.session.setThinkingMode(targetMode);
234
- console.log(`\n ${C.emerald}[OK] Thinking mode set to ${C.bold}${targetMode.toUpperCase()}${C.reset}\n`);
235
- } else {
236
- const current = agent.session.toggleThinkingMode();
237
- console.log(`\n ${C.emerald}[OK] Thinking mode toggled to ${C.bold}${current.toUpperCase()}${C.reset}\n`);
238
- }
239
- return 'HANDLED';
240
- },
241
-
242
- '/lang': async (ctx) => {
243
- const { agent, sub } = ctx;
244
- const langArg = (sub || '').toLowerCase();
245
- if (langArg === 'th' || langArg === 'zh' || langArg === 'en' || langArg === 'auto') {
246
- agent.memory.setPreference('language', langArg === 'auto' ? null : langArg);
247
- const langNames = { th: 'Thai', zh: 'Chinese', en: 'English', auto: 'Auto-detect' };
248
- console.log(`\n ${C.emerald}[OK] Language updated to:${C.reset} ${C.bold}${C.white}${langNames[langArg]}${C.reset}\n`);
249
- } else {
250
- const current = agent.memory.preferences.language || 'auto';
251
- printSolidPanel({
252
- title: `Language Preference`,
253
- lines: [`${C.gold}Current:${C.reset} ${C.bold}${C.white}${current.toUpperCase()}${C.reset}`, `${C.dim}Usage: /lang [th|zh|en|auto]${C.reset}`],
254
- headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
255
- });
256
- }
257
- return 'HANDLED';
258
- },
259
-
260
- '/theme': async (ctx) => {
261
- const action = (ctx.sub || 'current').toLowerCase();
262
- const current = getThemeName();
263
-
264
- if (action === 'list') {
265
- printSolidPanel({
266
- title: `Themes (${THEME_NAMES.length})`,
267
- lines: THEME_NAMES.map(name => {
268
- const theme = THEMES[name];
269
- const marker = name === current ? '[x]' : '[ ]';
270
- const sample = ansiForeground(theme.colors.primary);
271
- const preview = `${ansiBackground(theme.colors.panel)}${ansiForeground(theme.colors.text)}${C.bold} Aa ${C.reset}`;
272
- return `${marker} ${preview} ${sample}${C.bold}${name.padEnd(12)}${C.reset} ${C.dim}${theme.description}${C.reset}`;
273
- }),
274
- });
275
- return 'HANDLED';
276
- }
277
-
278
- if (action === 'current') {
279
- const theme = THEMES[current];
280
- printSolidPanel({
281
- title: 'Theme',
282
- lines: [
283
- `${C.gold}Current:${C.reset} ${C.bold}${C.white}${theme.label} (${current})${C.reset}`,
284
- `${C.dim}${theme.description}${C.reset}`,
285
- `${C.sakura}Primary${C.reset} ${C.roseGlow}Accent${C.reset} ${C.cyberCyan}Info${C.reset} ${C.emerald}Success${C.reset} ${C.amber}Warning${C.reset} ${C.red}Error${C.reset}`,
286
- `${C.dim}Usage: /theme list or /theme <name>${C.reset}`,
287
- ],
288
- });
289
- return 'HANDLED';
290
- }
291
-
292
- const requested = action === 'reset' ? 'sakura' : action;
293
- const theme = setTheme(requested);
294
- if (!theme) {
295
- console.log(`\n ${C.amber}Unknown theme: "${action}".${C.reset}`);
296
- console.log(` ${C.dim}Available: ${THEME_NAMES.join(', ')}${C.reset}\n`);
297
- return 'HANDLED';
298
- }
299
-
300
- printSolidPanel({
301
- title: 'Theme Updated',
302
- lines: [
303
- `${C.emerald}[OK] Active theme: ${C.bold}${theme.label} (${requested})${C.reset}`,
304
- `${C.dim}${theme.description}${C.reset}`,
305
- `${C.sakura}Primary${C.reset} ${C.roseGlow}Accent${C.reset} ${C.cyberCyan}Info${C.reset} ${C.emerald}Success${C.reset} ${C.amber}Warning${C.reset} ${C.red}Error${C.reset}`,
306
- `${C.dim}Saved for future Kiku Agent sessions.${C.reset}`,
307
- ],
308
- });
309
- return 'HANDLED';
310
- },
311
-
312
- '/tool': async (ctx) => {
313
- const { sub, extra } = ctx;
314
- const toolName = sub;
315
- const rawArgs = extra || '';
316
-
317
- if (!toolName) {
318
- console.log(`\n ${C.amber}Usage: /tool <tool_name> [args]${C.reset}\n`);
319
- return 'HANDLED';
320
- }
321
-
322
- const schema = toolsSchema.find(t => t.function.name.toLowerCase() === toolName.toLowerCase());
323
- if (!schema) {
324
- console.log(`\n ${C.red}Unknown tool: "${toolName}". Use /tools to view all.${C.reset}\n`);
325
- return 'HANDLED';
326
- }
327
-
328
- const actualToolName = schema.function.name;
329
-
330
- if (!rawArgs || !rawArgs.trim()) {
331
- const props = schema.function.parameters?.properties || {};
332
- const required = schema.function.parameters?.required || [];
333
- console.log(`\n ${C.gold}Tool:${C.reset} ${C.bold}${C.cyberCyan}${actualToolName}${C.reset}`);
334
- console.log(` ${C.gold}Description:${C.reset} ${C.white}${schema.function.description}${C.reset}`);
335
- for (const [pName, pMeta] of Object.entries(props)) {
336
- const reqStr = required.includes(pName) ? `${C.red}[required]${C.reset}` : `${C.dim}[optional]${C.reset}`;
337
- console.log(` • ${C.bold}${pName}${C.reset} (${pMeta.type || 'any'}) ${reqStr}: ${pMeta.description || ''}`);
338
- }
339
- console.log(`\n`);
340
- return 'HANDLED';
341
- }
342
-
343
- let parsedArgs = {};
344
- if (rawArgs.startsWith('{') && rawArgs.endsWith('}')) {
345
- try { parsedArgs = JSON.parse(rawArgs); } catch { parsedArgs = { query: rawArgs }; }
346
- } else {
347
- const props = schema.function.parameters?.properties || {};
348
- const firstProp = Object.keys(props)[0] || 'query';
349
- parsedArgs = { [firstProp]: rawArgs };
350
- }
351
-
352
- console.log(`\n ${C.cyberCyan}[RUNNING] ${C.bold}${actualToolName}${C.reset}...`);
353
- const start = Date.now();
354
- try {
355
- const result = await executeTool(actualToolName, parsedArgs);
356
- const dur = ((Date.now() - start) / 1000).toFixed(2);
357
- console.log(` ${C.emerald}[OK] Completed in ${dur}s${C.reset}`);
358
- console.log(JSON.stringify(result, null, 2));
359
- console.log('');
360
- } catch (err) {
361
- console.log(` ${C.red}[ERROR] ${err.message}${C.reset}\n`);
362
- }
363
- return 'HANDLED';
364
- },
365
-
366
- '/tools': async (ctx) => {
367
- const filter = (ctx.sub || '').toLowerCase().trim();
368
- const filtered = filter
369
- ? toolsSchema.filter(t => t.function.name.toLowerCase().includes(filter) || t.function.description.toLowerCase().includes(filter))
370
- : toolsSchema;
371
-
372
- console.log(`\n ${C.bold}${C.sakura}Registered Tools (${filtered.length}/${toolsSchema.length})${C.reset}\n`);
373
- for (const [idx, t] of filtered.entries()) {
374
- const f = t.function;
375
- const params = Object.keys(f.parameters?.properties || {}).join(', ');
376
- console.log(` ${C.gold}${String(idx + 1).padStart(2)}. ${C.bold}${C.cyberCyan}${f.name}${C.reset}${C.slate}(${params})${C.reset}`);
377
- console.log(` ${C.dim}${C.roseGlow}${f.description}${C.reset}`);
378
- }
379
- console.log(`\n`);
380
- return 'HANDLED';
381
- },
382
-
383
- '/mem': async (ctx) => {
384
- const { agent, sub, extra, parts } = ctx;
385
- const memAction = (sub || '').toLowerCase();
386
-
387
- if (memAction === 'add') {
388
- const factText = (parts || []).slice(2).join(' ').trim();
389
- if (!factText) {
390
- console.log(`\n ${C.amber}Usage: /mem add <fact>${C.reset}\n`);
391
- } else {
392
- const entry = agent.memory.addFact(factText, { source: 'user' });
393
- console.log(`\n ${C.emerald}[OK] Saved:${C.reset} ${C.white}${factText.slice(0, 80)}${C.reset} (ID: ${entry.id})\n`);
394
- }
395
- return 'HANDLED';
396
- }
397
-
398
- if (memAction === 'del' || memAction === 'rm') {
399
- const factId = (parts || [])[2]?.trim();
400
- if (!factId) {
401
- console.log(`\n ${C.amber}Usage: /mem del <fact_id>${C.reset}\n`);
402
- } else if (agent.memory.removeFact(factId)) {
403
- console.log(`\n ${C.emerald}[OK] Removed [${factId}]${C.reset}\n`);
404
- } else {
405
- console.log(`\n ${C.red}Not found [${factId}]${C.reset}\n`);
406
- }
407
- return 'HANDLED';
408
- }
409
-
410
- if (memAction === 'search') {
411
- const query = (parts || []).slice(2).join(' ').trim();
412
- if (!query) {
413
- console.log(`\n ${C.amber}Usage: /mem search <keyword>${C.reset}\n`);
414
- } else {
415
- const results = agent.memory.searchFacts(query);
416
- printSolidPanel({
417
- title: `Memory Search: "${query}" (${results.length})`,
418
- lines: results.map(fact => `${C.white}${fact.content.slice(0, 65)}${C.reset} ${C.dim}(${fact.id})${C.reset}`),
419
- });
420
- }
421
- return 'HANDLED';
422
- }
423
-
424
- if (memAction === 'list') {
425
- const grouped = agent.memory.getFactsByCategory();
426
- const factLines = [];
427
- for (const [cat, facts] of Object.entries(grouped)) {
428
- factLines.push(`${C.bold}${C.cyberCyan}${cat.toUpperCase()}${C.reset} (${facts.length})`);
429
- for (const fact of facts.slice(0, 6)) {
430
- factLines.push(` • ${C.white}${fact.content.slice(0, 65)}${C.reset}`);
431
- }
432
- }
433
- printSolidPanel({ title: 'Stored Facts', lines: factLines });
434
- return 'HANDLED';
435
- }
436
-
437
- if (memAction === 'topics') {
438
- const topics = agent.memory.getTopTopics(10);
439
- printSolidPanel({
440
- title: 'Topic Profile',
441
- lines: topics.map(t => {
442
- const bar = '█'.repeat(Math.min(t.count, 15));
443
- return `${C.gold}${t.topic.padEnd(14)}${C.reset} ${C.cyberCyan}${bar}${C.reset} ${C.dim}(${t.count}x)${C.reset}`;
444
- }),
445
- });
446
- return 'HANDLED';
447
- }
448
-
449
- if (memAction === 'export') {
450
- const exported = await agent.memory.exportToJSON();
451
- console.log(`\n ${C.emerald}[OK] Memory exported.${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
452
- return 'HANDLED';
453
- }
454
-
455
- if (memAction === 'import') {
456
- const filePath = (parts || []).slice(2).join(' ').trim();
457
- if (!filePath) {
458
- console.log(`\n ${C.amber}Usage: /mem import <path>${C.reset}\n`);
459
- } else {
460
- const result = await agent.memory.importFromJSON(filePath);
461
- if (result.success) {
462
- console.log(`\n ${C.emerald}[OK] Imported ${result.factsImported} facts.${C.reset}\n`);
463
- } else {
464
- console.log(`\n ${C.red}[ERROR] ${result.message}${C.reset}\n`);
465
- }
466
- }
467
- return 'HANDLED';
468
- }
469
-
470
- if (memAction === 'clear') {
471
- await agent.memory.clearAll();
472
- console.log(`\n ${C.emerald}[OK] Memory cleared.${C.reset}\n`);
473
- return 'HANDLED';
474
- }
475
-
476
- const memStatus = agent.memory.getStatus();
477
- printSolidPanel({
478
- title: 'Memory Status',
479
- lines: [`${C.gold}Facts:${C.reset} ${C.cyberCyan}${memStatus.factsCount}${C.reset} ${C.gold}Sessions:${C.reset} ${C.cyberCyan}${memStatus.sessionsCount}${C.reset}`],
480
- });
481
- return 'HANDLED';
482
- },
483
-
484
- '/play': async (ctx) => {
485
- const songName = ctx.extra || '';
486
- if (!songName) {
487
- console.log(`\n ${C.amber}Usage: /play <song name>${C.reset}\n`);
488
- } else {
489
- await audioPlayer.play(songName);
490
- console.log(`\n ${C.emerald}[OK] Playing: ${C.white}${songName}${C.reset}\n`);
491
- }
492
- return 'HANDLED';
493
- },
494
-
495
- '/stop': async () => { await audioPlayer.stop(); console.log(`\n ${C.emerald}[OK] Stopped${C.reset}\n`); return 'HANDLED'; },
496
- '/pause': async () => { const r = await audioPlayer.pause(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
497
- '/resume': async () => { const r = await audioPlayer.resume(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
498
- '/next': async () => { const r = await audioPlayer.next(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
499
- '/prev': async () => { const r = await audioPlayer.prev(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
500
- '/mute': async () => { const r = await audioPlayer.mute(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
501
- '/unmute': async () => { const r = await audioPlayer.unmute(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
502
-
503
- '/add': async (ctx) => {
504
- const song = ctx.extra || '';
505
- if (!song) { console.log(`\n ${C.amber}Usage: /add <song>${C.reset}\n`); }
506
- else { const r = await audioPlayer.addToQueue(song); console.log(`\n ${C.emerald}${r}${C.reset}\n`); }
507
- return 'HANDLED';
508
- },
509
-
510
- '/remove': async (ctx) => {
511
- const index = parseInt(ctx.sub, 10);
512
- if (isNaN(index)) { console.log(`\n ${C.amber}Usage: /remove <index>${C.reset}\n`); }
513
- else { const r = await audioPlayer.removeFromQueue(index); console.log(`\n ${C.emerald}${r}${C.reset}\n`); }
514
- return 'HANDLED';
515
- },
516
-
517
- '/clearqueue': async () => { const r = await audioPlayer.clearQueue(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
518
-
519
- '/search': async (ctx) => {
520
- const q = ctx.extra || '';
521
- if (!q) { console.log(`\n ${C.amber}Usage: /search <query>${C.reset}\n`); return 'HANDLED'; }
522
- try {
523
- const results = await audioPlayer.searchYouTube(q);
524
- if (results.length === 0) { console.log(`\n ${C.amber}No results for "${q}"${C.reset}\n`); }
525
- else {
526
- printSolidPanel({
527
- title: `YouTube: "${q}" (${results.length})`,
528
- lines: results.map((r, i) => `${C.gold}${i + 1}.${C.reset} ${C.white}${r.title}${C.reset} ${C.dim}(${r.duration})${C.reset}`),
529
- headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
530
- });
531
- }
532
- } catch (err) { console.log(`\n ${C.red}${err.message}${C.reset}\n`); }
533
- return 'HANDLED';
534
- },
535
-
536
- '/current': async () => {
537
- const state = audioPlayer.getFullState();
538
- if (!state.isPlaying && !state.isPaused) {
539
- console.log(`\n ${C.slate}No music playing.${C.reset}\n`);
540
- } else {
541
- printSolidPanel({
542
- title: `${UI_ICONS.music} Current Track`,
543
- lines: [
544
- `${C.gold}Track:${C.reset} ${C.white}${state.currentTrack}${C.reset}`,
545
- `${C.gold}Status:${C.reset} ${state.isPaused ? C.amber + 'Paused' : C.emerald + 'Playing'}${C.reset}`,
546
- `${C.gold}Queue:${C.reset} ${C.cyberCyan}${state.queueLength} songs${C.reset}`
547
- ],
548
- headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
549
- });
550
- }
551
- return 'HANDLED';
552
- },
553
-
554
- '/retry': async (ctx) => {
555
- if (!ctx.lastQuery) {
556
- console.log(`\n ${C.amber}No previous query.${C.reset}\n`);
557
- return 'HANDLED';
558
- }
559
- return 'RETRY';
560
- },
561
-
562
- '/clear': async (ctx) => {
563
- try { await ctx.agent.endSession(); } catch {}
564
- console.clear();
565
- ctx.printWelcome();
566
- console.log(` ${C.emerald}[OK] Fresh start.${C.reset}\n`);
567
- return 'HANDLED';
568
- },
569
-
570
- '/save': async (ctx) => {
571
- const exported = await ctx.agent.session.exportToMarkdown();
572
- console.log(`\n ${C.emerald}[OK] Exported.${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
573
- return 'HANDLED';
574
- },
575
-
576
- '/model': async (ctx) => {
577
- const liveCheck = await ctx.client.checkConnection();
578
- printSolidPanel({
579
- title: `LM Studio Diagnostics`,
580
- lines: [
581
- `${C.gold}Status:${C.reset} ${liveCheck.connected ? C.emerald + '● Online' : C.red + '○ Offline'}${C.reset}`,
582
- `${C.gold}Model:${C.reset} ${C.bold}${C.white}${liveCheck.activeModel || 'None'}${C.reset}`,
583
- `${C.gold}URL:${C.reset} ${C.cyberCyan}${ctx.baseUrl}${C.reset}`
584
- ],
585
- headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
586
- });
587
- return 'HANDLED';
588
- },
589
-
590
- '/ping': async (ctx) => {
591
- const start = Date.now();
592
- const check = await ctx.client.checkConnection();
593
- const ms = Date.now() - start;
594
- if (check.connected) {
595
- console.log(`\n ${C.emerald}● Connected${C.reset} ${C.dim}(${ms}ms)${C.reset} — ${C.white}${check.activeModel}${C.reset}\n`);
596
- } else {
597
- console.log(`\n ${C.red}● Disconnected${C.reset} ${C.dim}(${ms}ms)${C.reset}\n`);
598
- }
599
- return 'HANDLED';
600
- },
601
-
602
- '/config': async (ctx) => {
603
- const { config } = await import('./config.js');
604
- printSolidPanel({
605
- title: `Configuration`,
606
- lines: [
607
- `${C.gold}URL:${C.reset} ${C.cyberCyan}${ctx.baseUrl}${C.reset}`,
608
- `${C.gold}Model:${C.reset} ${C.white}${config.lmStudio.model}${C.reset}`,
609
- `${C.gold}Theme:${C.reset} ${C.white}${getThemeName()}${C.reset}`,
610
- `${C.gold}Max Iters:${C.reset} ${C.white}${ctx.agent.maxIterations}${C.reset}`,
611
- `${C.gold}Window:${C.reset} ${C.white}${ctx.agent.slidingWindowSize}${C.reset}`,
612
- `${C.gold}Tools:${C.reset} ${C.cyberCyan}${toolsSchema.length}${C.reset}`
613
- ],
614
- headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
615
- });
616
- return 'HANDLED';
617
- },
618
-
619
- '/version': async () => {
620
- printSolidPanel({
621
- title: 'Version',
622
- lines: [
623
- `${C.gold}Version:${C.reset} ${C.white}1.0.0${C.reset}`,
624
- `${C.gold}Node:${C.reset} ${C.white}${process.version}${C.reset}`,
625
- `${C.gold}Platform:${C.reset} ${C.white}${process.platform} ${process.arch}${C.reset}`,
626
- ],
627
- });
628
- return 'HANDLED';
629
- },
630
-
631
- '/system': async () => {
632
- const os = await import('node:os');
633
- const mem = process.memoryUsage();
634
- printSolidPanel({
635
- title: 'System',
636
- lines: [
637
- `${C.gold}OS:${C.reset} ${C.white}${os.default.platform()} ${os.default.release()}${C.reset}`,
638
- `${C.gold}CPU:${C.reset} ${C.white}${os.default.cpus().length} cores${C.reset}`,
639
- `${C.gold}RAM:${C.reset} ${C.white}${(os.default.totalmem() / 1073741824).toFixed(1)} GB${C.reset}`,
640
- `${C.gold}Heap:${C.reset} ${C.white}${(mem.heapUsed / 1048576).toFixed(1)} MB${C.reset}`,
641
- ],
642
- });
643
- return 'HANDLED';
644
- },
645
-
646
- '/about': async () => {
647
- printSolidPanel({
648
- title: 'About Kiku AI Agent',
649
- lines: [
650
- `${C.white}Intelligent assistant for Ju Jingyi (鞠婧祎)${C.reset}`,
651
- `${C.gold}Engine:${C.reset} ${C.white}LM Studio + Gemma 4${C.reset}`,
652
- `${C.gold}Tools:${C.reset} ${C.white}${toolsSchema.length} registered${C.reset}`,
653
- `${C.gold}Languages:${C.reset} ${C.white}TH, ZH, EN (auto)${C.reset}`,
654
- ],
655
- });
656
- return 'HANDLED';
657
- },
658
-
659
- '/login': async (ctx) => {
660
- console.log(`\n ${C.cyberCyan}YouTube Login${C.reset}\n`);
661
- resetCookieCheck();
662
- const result = await ensureYouTubeCookies({ timeout: 180000 });
663
- if (result.ok) {
664
- console.log(` ${C.emerald}[OK] Session active (${result.source}).${C.reset}\n`);
665
- } else {
666
- console.log(` ${C.red}[ERROR] Not available: ${result.reason}${C.reset}\n`);
667
- }
668
- return 'HANDLED';
669
- },
670
-
671
- '/ytstatus': async () => {
672
- const status = getCookieStatus();
673
- const ensured = status.ensured;
674
- printSolidPanel({
675
- title: 'YouTube Status',
676
- lines: [
677
- `${C.gold}Browser:${C.reset} ${status.envBrowser ? C.emerald + status.envBrowser : C.dim + 'n/a'}${C.reset}`,
678
- `${C.gold}Cookies:${C.reset} ${status.envCookies ? C.emerald + status.envCookies : C.dim + 'n/a'}${C.reset}`,
679
- `${C.gold}Status:${C.reset} ${ensured ? (ensured.ok ? C.emerald + '[OK]' : C.red + '[ERROR] Failed') : C.dim + 'Unchecked'}${C.reset}`
680
- ],
681
- headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
682
- });
683
- return 'HANDLED';
684
- },
685
-
686
- '/queue': async (ctx) => {
687
- const { sub } = ctx;
688
- if (sub === 'clear') {
689
- if (audioPlayer.isPlaying || audioPlayer.queue.length > 0) {
690
- console.log(`\n ${C.emerald}${await audioPlayer.clearQueue()}${C.reset}\n`);
691
- } else {
692
- ctx.clearQueryQueue();
693
- console.log(`\n ${C.emerald}[OK] Execution queue cleared.${C.reset}\n`);
694
- }
695
- } else if (audioPlayer.isPlaying || audioPlayer.queue.length > 0) {
696
- const queue = audioPlayer.getQueue();
697
- printSolidPanel({
698
- title: `${UI_ICONS.music} Queue (${queue.length})`,
699
- lines: queue.map((item, idx) => `${item.isCurrent ? C.gold + '[PLAYING]' : C.dim + (idx + 1)} ${C.white}${item.title}${C.reset}`),
700
- headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
701
- });
702
- } else {
703
- const qLen = ctx.getQueryQueueLength();
704
- console.log(`\n ${C.slate}Execution queue: ${qLen} items${C.reset}\n`);
705
- }
706
- return 'HANDLED';
707
- },
708
-
709
- '/stats': async () => {
710
- const result = await executeTool('get_system_stats', {});
711
- const s = result.stats;
712
- printSolidPanel({
713
- title: `Database Overview`,
714
- lines: [
715
- `${C.gold}Works:${C.reset} ${C.cyberCyan}${s.totalWorks}${C.reset}`,
716
- `${C.gold}Songs:${C.reset} ${C.cyberCyan}${s.totalUniqueSongs}${C.reset}`,
717
- `${C.gold}Brands:${C.reset} ${C.cyberCyan}${s.brandEndorsements}${C.reset}`,
718
- `${C.gold}Awards:${C.reset} ${C.cyberCyan}${s.totalAwards}${C.reset}`,
719
- `${C.gold}Timeline:${C.reset} ${C.cyberCyan}${s.timelineMilestones}${C.reset}`
720
- ],
721
- headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
722
- });
723
- return 'HANDLED';
724
- },
725
-
726
- '/prompt': async (ctx) => {
727
- const { config } = await import('./config.js');
728
- const lines = config.agent.systemPersona.split('\n').slice(0, 20).map(l => `${C.dim}${C.white}${l}${C.reset}`);
729
- printSolidPanel({ title: `System Prompt`, lines, headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m' });
730
- return 'HANDLED';
731
- },
732
-
733
- '/ads': async (ctx) => {
734
- ctx.printAd();
735
- return 'HANDLED';
736
- },
737
- };
738
-
739
- const ALIASES = {
740
- '/quit': '/exit', '/q': '/exit',
741
- '/?': '/help', '/commands': '/help', '/menu': '/help',
742
- '/hist': '/history', '/h': '/history',
743
- '/sess': '/session', '/sessions': '/session',
744
- '/think': '/thinking', '/th': '/thinking',
745
- '/detail': '/details', '/d': '/details',
746
- '/language': '/lang',
747
- '/themes': '/theme',
748
- '/exec': '/tool', '/call': '/tool',
749
- '/toollist': '/tools',
750
- '/metrics': '/stats', '/catalog': '/stats',
751
- '/llm': '/model', '/engine': '/model',
752
- '/health': '/ping',
753
- '/settings': '/config', '/conf': '/config',
754
- '/persona': '/prompt', '/sysprompt': '/prompt',
755
- '/ver': '/version', '/v': '/version',
756
- '/sys': '/system', '/os': '/system',
757
- '/info': '/about',
758
- '/sponsor': '/ads', '/ad': '/ads',
759
- '/cls': '/clear', '/reset': '/clear',
760
- '/export': '/save',
761
- '/r': '/retry',
762
- };
763
-
764
- export async function routeCommand(rawInput, ctx) {
765
- const trimmed = rawInput.trim();
766
- if (!trimmed.startsWith('/')) return 'UNKNOWN';
767
-
768
- const parts = trimmed.split(/\s+/);
769
- let rawCmd = parts[0].toLowerCase();
770
- const sub = parts[1] || '';
771
- const extra = parts.slice(2).join(' ');
772
-
773
- if (ALIASES[rawCmd]) rawCmd = ALIASES[rawCmd];
774
-
775
- const handler = handlers[rawCmd];
776
- if (!handler) {
777
- const suggestion = findClosestCommand(rawCmd);
778
- console.log(`\n ${C.amber}Unknown command: "${rawCmd}"${C.reset}`);
779
- if (suggestion) {
780
- console.log(` ${C.dim}Did you mean ${C.bold}${C.cyberCyan}${suggestion}${C.reset}${C.dim}?${C.reset}\n`);
781
- } else {
782
- console.log(` ${C.dim}Type ${C.emerald}/help${C.reset}${C.dim} for available commands.${C.reset}\n`);
783
- }
784
- return 'UNKNOWN';
785
- }
786
-
787
- return await handler({ ...ctx, sub, extra, parts });
788
- }
1
+ import { toolsSchema, executeTool } from './tools.js';
2
+ import { audioPlayer } from './audioPlayer.js';
3
+ import { ensureYouTubeCookies, resetCookieCheck, getCookieStatus } from './youtubeAuth.js';
4
+ import { AgentMemory } from './memory.js';
5
+ import { C, UI_ICONS, printSolidPanel, randomFanName, createProgressRunner, wrapText, getFullWidth, getWelcomeHeroLines } from './ui.js';
6
+ import { COMMAND_NAMES } from './commandRegistry.js';
7
+ import { THEMES, THEME_NAMES, ansiBackground, ansiForeground, getThemeName, setTheme } from './theme.js';
8
+ import { getGpuMetrics, formatProgressBar } from './gpuMetrics.js';
9
+
10
+ function levenshteinDistance(a, b) {
11
+ const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
12
+ for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
13
+ for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
14
+ for (let i = 1; i <= a.length; i++) {
15
+ for (let j = 1; j <= b.length; j++) {
16
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
17
+ matrix[i][j] = Math.min(
18
+ matrix[i - 1][j] + 1,
19
+ matrix[i][j - 1] + 1,
20
+ matrix[i - 1][j - 1] + cost
21
+ );
22
+ }
23
+ }
24
+ return matrix[a.length][b.length];
25
+ }
26
+
27
+ const VALID_COMMANDS = [...COMMAND_NAMES, '/export'];
28
+
29
+ export function findClosestCommand(cmd) {
30
+ let closest = null;
31
+ let minDistance = Infinity;
32
+ for (const vc of VALID_COMMANDS) {
33
+ const dist = levenshteinDistance(cmd.toLowerCase(), vc);
34
+ if (dist < minDistance) {
35
+ minDistance = dist;
36
+ closest = vc;
37
+ }
38
+ }
39
+ return minDistance <= 3 ? closest : null;
40
+ }
41
+
42
+ export function fuzzyMatchTools(query) {
43
+ if (!query) return toolsSchema.map(t => ({ tool: t, score: 10 }));
44
+ const q = query.toLowerCase();
45
+ const qWords = q.split(/\s+/).filter(Boolean);
46
+
47
+ const scored = toolsSchema.map(t => {
48
+ const name = t.function.name.toLowerCase();
49
+ const desc = (t.function.description || '').toLowerCase();
50
+ let score = 0;
51
+
52
+ if (name === q) score = 200;
53
+ else if (name.startsWith(q)) score = 150;
54
+ else if (name.includes(q)) score = 100;
55
+ else if (qWords.every(w => name.includes(w))) score = 80;
56
+ else if (qWords.some(w => name.includes(w))) score = 65;
57
+ else if (qWords.every(w => desc.includes(w))) score = 50;
58
+ else if (qWords.some(w => desc.includes(w))) score = 40;
59
+ else {
60
+ const dist = levenshteinDistance(q, name);
61
+ const threshold = Math.floor(Math.min(q.length, name.length) * 0.4);
62
+ if (dist <= threshold && dist <= 6) score = 30 - dist * 3;
63
+ if (score === 0) {
64
+ for (const word of name.split('_')) {
65
+ const wd = levenshteinDistance(q, word);
66
+ const wThreshold = Math.floor(Math.min(q.length, word.length) * 0.4);
67
+ if (wd <= wThreshold && wd <= 4) {
68
+ score = Math.max(score, 35 - wd * 5);
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ return { tool: t, score };
75
+ });
76
+
77
+ return scored
78
+ .filter(s => s.score > 0)
79
+ .sort((a, b) => b.score - a.score);
80
+ }
81
+
82
+ export const handlers = {
83
+ '/exit': async (ctx) => {
84
+ try { await ctx.agent.endSession(); } catch {}
85
+ console.log(`\n ${C.sakura}Session archived. Thank you for supporting Ju Jingyi. Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
86
+ return 'EXIT';
87
+ },
88
+
89
+ '/help': async (ctx) => {
90
+ const sub = ctx.sub || ctx.extra || '';
91
+ if (typeof ctx.printHelp === 'function') {
92
+ ctx.printHelp(sub);
93
+ }
94
+ return 'HANDLED';
95
+ },
96
+
97
+ '/details': async (ctx) => {
98
+ ctx.printQueryDetails(ctx.sub || '');
99
+ return 'HANDLED';
100
+ },
101
+
102
+ '/subagent': async (ctx) => {
103
+ const rawSub = (ctx.sub || '').toLowerCase();
104
+ const latest = ctx.agent._lastSubagentResult || null;
105
+
106
+ if (rawSub === 'status' || rawSub === 'last' || rawSub === 'tools') {
107
+ if (!latest) {
108
+ printSolidPanel({
109
+ title: 'Subagent Status',
110
+ lines: [`${C.dim}No subagent has been executed yet. Run /subagent <task> to start one.${C.reset}`]
111
+ });
112
+ return 'HANDLED';
113
+ }
114
+
115
+ const lines = [
116
+ `${C.bold}${C.cyberCyan}Task:${C.reset} ${latest.task}`,
117
+ `${C.bold}${C.slate}Iterations:${C.reset} ${latest.iterations || 1} • ${C.bold}Tools Used:${C.reset} ${latest.toolCalls?.length || 0}`,
118
+ ''
119
+ ];
120
+
121
+ if (latest.toolCalls && latest.toolCalls.length > 0) {
122
+ lines.push(`${C.bold}${C.lavender}Tool Execution History:${C.reset}`);
123
+ for (const [idx, tc] of latest.toolCalls.entries()) {
124
+ const statusBadge = tc.status === 'error' ? `${C.red}[ERROR]` : `${C.emerald}[OK]`;
125
+ lines.push(` ${C.gold}[${idx + 1}]${C.reset} ${C.bold}${tc.name}${C.reset} ${statusBadge}`);
126
+ if (tc.args && Object.keys(tc.args).length > 0) {
127
+ lines.push(` ${C.dim}args: ${JSON.stringify(tc.args)}${C.reset}`);
128
+ }
129
+ }
130
+ lines.push('');
131
+ }
132
+
133
+ lines.push(`${C.bold}${C.roseGlow}Findings Report:${C.reset}`);
134
+ for (const fLine of String(latest.answer || 'No report generated.').split('\n')) {
135
+ lines.push(` ${fLine}`);
136
+ }
137
+
138
+ printSolidPanel({
139
+ title: `Subagent Details • ${latest.toolCalls?.length || 0} Tools`,
140
+ lines
141
+ });
142
+ return 'HANDLED';
143
+ }
144
+
145
+ const task = [ctx.sub, ctx.extra].filter(Boolean).join(' ').trim();
146
+ if (!task) {
147
+ printSolidPanel({
148
+ title: 'Subagent Command Help',
149
+ lines: [
150
+ `${C.bold}${C.white}Usage:${C.reset} ${C.cyberCyan}/subagent <task>${C.reset}`,
151
+ `${C.dim}Examples:${C.reset}`,
152
+ ` • ${C.amber}/subagent สรุปผลงานซีรีส์ปี 2026 พร้อมตรวจสอบค่ายเพลง${C.reset}`,
153
+ ` • ${C.amber}/subagent ค้นหาประวัติรางวัล Golden Eagle ของจวีจิ้งอี${C.reset}`,
154
+ ` • ${C.amber}/subagent status${C.reset} ${C.dim}(ดูรายงานและ Tool calls ของ Subagent ล่าสุด)${C.reset}`,
155
+ ]
156
+ });
157
+ return 'HANDLED';
158
+ }
159
+
160
+ if (ctx.catalogOnlyMode) {
161
+ console.log(`\n ${C.amber}Subagents require a loaded LM Studio chat model. Use /model reconnect after starting LM Studio.${C.reset}\n`);
162
+ return 'HANDLED';
163
+ }
164
+
165
+ printSolidPanel({
166
+ title: 'Subagent Spawned • Autonomous Worker',
167
+ lines: [
168
+ `${C.bold}${C.cyberCyan}Objective:${C.reset} ${task}`,
169
+ `${C.dim}Mode: Bounded Iterative Search (Catalog + Web) | Capabilities: 14 tools${C.reset}`
170
+ ],
171
+ bg: '\x1b[48;2;32;36;48m'
172
+ });
173
+
174
+ const startTime = Date.now();
175
+ try {
176
+ const result = await ctx.agent.runSubagent(task, {
177
+ onProgress: event => {
178
+ if (event.type === 'iteration') {
179
+ console.log(` ${C.dim}[ITERATION ${event.iteration}/${event.maxIterations}]...${C.reset}`);
180
+ } else if (event.type === 'tool') {
181
+ console.log(` ${C.cyberCyan}▸ [TOOL]${C.reset} Executing ${C.bold}${event.name}${C.reset}...`);
182
+ }
183
+ },
184
+ });
185
+
186
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
187
+ ctx.agent._lastSubagentResult = result;
188
+
189
+ const lines = [
190
+ `${C.bold}${C.emerald}[DONE] Subagent completed in ${elapsed}s with ${result.toolCalls.length} tool calls.${C.reset}`,
191
+ ''
192
+ ];
193
+
194
+ if (result.toolCalls.length > 0) {
195
+ lines.push(`${C.bold}${C.lavender}Tools Invoked:${C.reset}`);
196
+ for (const [i, tc] of result.toolCalls.entries()) {
197
+ lines.push(` ${C.gold}[${i + 1}]${C.reset} ${C.cyberCyan}${tc.name}${C.reset} ${C.dim}(${JSON.stringify(tc.args || {})})${C.reset}`);
198
+ }
199
+ lines.push('');
200
+ }
201
+
202
+ lines.push(`${C.bold}${C.roseGlow}Synthesized Evidence:${C.reset}`);
203
+ for (const line of String(result.answer || 'No findings returned.').split('\n')) {
204
+ lines.push(` ${line}`);
205
+ }
206
+
207
+ lines.push('');
208
+ lines.push(`${C.dim}Tip: Type /subagent status or press F2 (/details) to inspect tool parameters and results.${C.reset}`);
209
+
210
+ printSolidPanel({
211
+ title: `Subagent Report • ${result.toolCalls.length} tool calls • ${elapsed}s`,
212
+ lines,
213
+ });
214
+ } catch (error) {
215
+ console.log(`\n ${C.red}[SUBAGENT ERROR] ${error.message}${C.reset}\n`);
216
+ }
217
+ return 'HANDLED';
218
+ },
219
+
220
+ '/research': async (ctx) => {
221
+ const action = (ctx.sub || '').toLowerCase();
222
+ const latest = ctx.agent.getResearchStatus().lastResult;
223
+
224
+ if (action === 'status' || action === 'workers') {
225
+ const status = ctx.agent.getResearchStatus();
226
+ const lines = status.state === 'running'
227
+ ? [
228
+ `${C.bold}${C.cyberCyan}Question:${C.reset} ${status.question}`,
229
+ `${C.bold}${C.lavender}Active Multi-Workers (3 parallel subagents):${C.reset}`,
230
+ ...status.workers.map((worker, i) => ` ${C.gold}[Worker ${i + 1}]${C.reset} ${C.bold}${worker.label}${C.reset}: ${worker.state === 'running' ? C.amber + 'RUNNING' : C.emerald + 'COMPLETE'}${C.reset} (${worker.toolCalls} tool calls)`),
231
+ ]
232
+ : [
233
+ `${C.dim}No research job is currently running.${C.reset}`,
234
+ ...(latest ? [`${C.bold}Latest Research ID:${C.reset} ${latest.id}`, `${C.dim}Run /research last to view the full report.${C.reset}`] : [])
235
+ ];
236
+ printSolidPanel({ title: `Research Status • ${status.state.toUpperCase()}`, lines });
237
+ return 'HANDLED';
238
+ }
239
+
240
+ if (action === 'cancel') {
241
+ console.log(ctx.agent.cancelResearch()
242
+ ? `\n ${C.emerald}[OK] Research cancellation requested.${C.reset}\n`
243
+ : `\n ${C.amber}No research job is running.${C.reset}\n`);
244
+ return 'HANDLED';
245
+ }
246
+
247
+ if (action === 'last') {
248
+ const lines = latest
249
+ ? [
250
+ `${C.bold}${C.cyberCyan}Research ID:${C.reset} ${latest.id}`,
251
+ `${C.bold}${C.slate}Sources Found:${C.reset} ${latest.sources?.length || 0}`,
252
+ '',
253
+ `${C.bold}${C.roseGlow}Synthesized Answer:${C.reset}`,
254
+ ...String(latest.answer).split('\n').map(l => ` ${l}`),
255
+ '',
256
+ `${C.bold}${C.lavender}Sources Cited:${C.reset}`,
257
+ ...(latest.sources?.length ? latest.sources.map((s, i) => ` [${i + 1}] ${s}`) : [` ${C.dim}None recorded.${C.reset}`])
258
+ ]
259
+ : [`${C.dim}No completed research yet. Run /research <question> to start.${C.reset}`];
260
+
261
+ printSolidPanel({
262
+ title: latest ? `Research Findings • ${latest.id}` : 'Research',
263
+ lines,
264
+ });
265
+ return 'HANDLED';
266
+ }
267
+
268
+ if (action === 'sources') {
269
+ printSolidPanel({
270
+ title: 'Research Sources',
271
+ lines: latest?.sources?.length ? latest.sources.map((url, index) => `${C.gold}[${index + 1}]${C.reset} ${C.cyberCyan}${url}${C.reset}`) : [`${C.dim}No research sources recorded.${C.reset}`],
272
+ });
273
+ return 'HANDLED';
274
+ }
275
+
276
+ const question = [ctx.sub, ctx.extra].filter(Boolean).join(' ').trim();
277
+ if (!question) {
278
+ printSolidPanel({
279
+ title: 'Deep Research Command Help',
280
+ lines: [
281
+ `${C.bold}${C.white}Usage:${C.reset} ${C.cyberCyan}/research <question>${C.reset}`,
282
+ `${C.dim}Subcommands:${C.reset}`,
283
+ ` • ${C.amber}/research status${C.reset} ${C.dim}(ดูสถานะ Worker ทั้ง 3 ตัว)${C.reset}`,
284
+ ` • ${C.amber}/research last${C.reset} ${C.dim}(ดูรายงานวิจัยล่าสุด)${C.reset}`,
285
+ ` • ${C.amber}/research sources${C.reset} ${C.dim}(ดูรายการแหล่งอ้างอิง URL ทั้งหมด)${C.reset}`,
286
+ ` • ${C.amber}/research cancel${C.reset} ${C.dim}(ยกเลิกงานวิจัยที่กำลังทำ)${C.reset}`,
287
+ ]
288
+ });
289
+ return 'HANDLED';
290
+ }
291
+
292
+ if (ctx.catalogOnlyMode) {
293
+ console.log(`\n ${C.amber}Coordinated research requires a loaded chat model. Local catalog queries remain available.${C.reset}\n`);
294
+ return 'HANDLED';
295
+ }
296
+
297
+ printSolidPanel({
298
+ title: 'Deep Research Initiated • 3 Coordinated Workers',
299
+ lines: [
300
+ `${C.bold}${C.cyberCyan}Research Question:${C.reset} ${question}`,
301
+ ` ${C.gold}[Worker 1]${C.reset} Local Ju Jingyi Catalog & Work Detail Explorer`,
302
+ ` ${C.gold}[Worker 2]${C.reset} Web News, Endorsements & Discography Explorer`,
303
+ ` ${C.gold}[Worker 3]${C.reset} Fact Verification & Contradiction Resolver`,
304
+ ],
305
+ bg: '\x1b[48;2;32;36;48m'
306
+ });
307
+
308
+ const startTime = Date.now();
309
+ try {
310
+ const result = await ctx.agent.runResearch(question, {
311
+ onProgress: event => {
312
+ if (event.type === 'worker-start') console.log(` ${C.dim}▸ Worker started: ${event.label}${C.reset}`);
313
+ if (event.type === 'worker-complete') console.log(` ${C.emerald}[OK] Worker finished: ${event.label}${C.reset}`);
314
+ },
315
+ });
316
+
317
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
318
+ const lines = [
319
+ `${C.bold}${C.emerald}[DONE] Research synthesis completed in ${elapsed}s across 3 workers.${C.reset}`,
320
+ '',
321
+ `${C.bold}${C.roseGlow}Comprehensive Findings:${C.reset}`,
322
+ ...String(result.answer).split('\n').map(l => ` ${l}`),
323
+ '',
324
+ `${C.bold}${C.lavender}Validated Sources (${result.sources.length}):${C.reset}`,
325
+ ...(result.sources.length > 0 ? result.sources.map((s, i) => ` [${i + 1}] ${C.cyberCyan}${s}${C.reset}`) : [` ${C.dim}Catalog sources${C.reset}`]),
326
+ ];
327
+
328
+ printSolidPanel({
329
+ title: `Deep Research Report • ${result.sources.length} sources • ${elapsed}s`,
330
+ lines
331
+ });
332
+ } catch (error) {
333
+ console.log(`\n ${C.red}[RESEARCH ERROR] ${error.message}${C.reset}\n`);
334
+ }
335
+ return 'HANDLED';
336
+ },
337
+
338
+ '/verify': async (ctx) => {
339
+ const claim = [ctx.sub, ctx.extra].filter(Boolean).join(' ').trim();
340
+ if (!claim) {
341
+ printSolidPanel({
342
+ title: 'Claim Verification Help',
343
+ lines: [
344
+ `${C.bold}${C.white}Usage:${C.reset} ${C.cyberCyan}/verify <claim about Ju Jingyi>${C.reset}`,
345
+ `${C.dim}Checks facts across Catalog, Official Media, and Contradiction Resolver.${C.reset}`,
346
+ `Example: ${C.amber}/verify จวีจิ้งอีเป็น Global Ambassador ของ Maybelline ในปี 2026${C.reset}`
347
+ ]
348
+ });
349
+ return 'HANDLED';
350
+ }
351
+
352
+ if (ctx.catalogOnlyMode) {
353
+ console.log(`\n ${C.amber}Verification requires a loaded chat model. Use /model reconnect after starting LM Studio.${C.reset}\n`);
354
+ return 'HANDLED';
355
+ }
356
+
357
+ printSolidPanel({
358
+ title: 'Fact-Check & Verification Mode',
359
+ lines: [
360
+ `${C.bold}${C.cyberCyan}Claim:${C.reset} "${claim}"`,
361
+ ` ${C.dim}1. Catalog Verification -> 2. Web Corroboration -> 3. Contradiction Analysis${C.reset}`
362
+ ],
363
+ bg: '\x1b[48;2;36;40;52m'
364
+ });
365
+
366
+ const startTime = Date.now();
367
+ try {
368
+ const result = await ctx.agent.runResearch(claim, { mode: 'verify' });
369
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
370
+ const answerStr = String(result.answer || '');
371
+
372
+ let verdictBadge = `${C.amber}[UNVERIFIED / AMBIGUOUS]`;
373
+ if (/VERIFIED|จริง|ถูกต้อง|ยืนยันแล้ว|CONFIRMED/i.test(answerStr) && !/UNVERIFIED|FALSE|เท็จ|ไม่จริง/i.test(answerStr.slice(0, 80))) {
374
+ verdictBadge = `${C.emerald}${C.bold}[VERIFIED / FACTUAL]${C.reset}`;
375
+ } else if (/FALSE|เท็จ|ไม่จริง|ข่าวลือ|CONTRADICTED|REFUTED/i.test(answerStr)) {
376
+ verdictBadge = `${C.red}${C.bold}[FALSE / REFUTED]${C.reset}`;
377
+ } else {
378
+ verdictBadge = `${C.amber}${C.bold}[UNVERIFIED / INSUFFICIENT EVIDENCE]${C.reset}`;
379
+ }
380
+
381
+ const lines = [
382
+ `${C.bold}Verdict:${C.reset} ${verdictBadge} ${C.dim}(Checked in ${elapsed}s)${C.reset}`,
383
+ '',
384
+ `${C.bold}${C.roseGlow}Verification Breakdown:${C.reset}`,
385
+ ...answerStr.split('\n').map(l => ` ${l}`),
386
+ '',
387
+ `${C.bold}${C.lavender}Evidence & Sources (${result.sources.length}):${C.reset}`,
388
+ ...(result.sources.length > 0 ? result.sources.map((s, i) => ` [${i + 1}] ${C.cyberCyan}${s}${C.reset}`) : [` ${C.dim}Catalog data verification${C.reset}`])
389
+ ];
390
+
391
+ printSolidPanel({
392
+ title: `Verification Verdict • ${result.sources.length} sources`,
393
+ lines
394
+ });
395
+ } catch (error) {
396
+ console.log(`\n ${C.red}[VERIFY ERROR] ${error.message}${C.reset}\n`);
397
+ }
398
+ return 'HANDLED';
399
+ },
400
+
401
+ '/sources': async (ctx) => {
402
+ const latest = ctx.agent.getResearchStatus().lastResult;
403
+ printSolidPanel({
404
+ title: 'Latest Research Sources',
405
+ lines: latest?.sources?.length ? latest.sources.map((url, index) => `${C.gold}[${index + 1}]${C.reset} ${C.cyberCyan}${url}${C.reset}`) : [`${C.dim}No research sources recorded.${C.reset}`],
406
+ });
407
+ return 'HANDLED';
408
+ },
409
+
410
+ '/skill': async (ctx) => {
411
+ const action = (ctx.sub || 'list').toLowerCase();
412
+ const memory = ctx.agent.memory;
413
+
414
+ if (action === 'learn' || action === 'add') {
415
+ if (!ctx.extra) {
416
+ printSolidPanel({
417
+ title: 'Skill Learn Help',
418
+ lines: [
419
+ `${C.bold}${C.white}Usage:${C.reset} ${C.cyberCyan}/skill learn <instruction on how Kiku should answer>${C.reset}`,
420
+ `Example: ${C.amber}/skill learn ตอบสั้นกระชับและลงท้ายด้วย '蜜橘' เสมอ${C.reset}`
421
+ ]
422
+ });
423
+ } else {
424
+ const skill = memory.addUserSkill(ctx.extra);
425
+ printSolidPanel({
426
+ title: 'Learned Response Skill Added',
427
+ lines: [
428
+ `${C.emerald}[OK] Skill successfully saved to long-term agent memory!${C.reset}`,
429
+ `${C.bold}ID:${C.reset} ${skill.id}`,
430
+ `${C.bold}Rule:${C.reset} ${skill.instruction}`,
431
+ `${C.dim}Kiku will now apply this persona rule across all future answers.${C.reset}`
432
+ ]
433
+ });
434
+ }
435
+ return 'HANDLED';
436
+ }
437
+
438
+ if (action === 'del' || action === 'remove') {
439
+ const deleted = memory.removeUserSkill(ctx.extra);
440
+ printSolidPanel({
441
+ title: 'Remove Skill',
442
+ lines: [
443
+ deleted
444
+ ? `${C.emerald}[OK] Response skill ${ctx.extra} removed.${C.reset}`
445
+ : `${C.amber}Skill not found. Use /skill list to see active skill IDs.${C.reset}`
446
+ ]
447
+ });
448
+ return 'HANDLED';
449
+ }
450
+
451
+ if (action === 'clear') {
452
+ memory.clearUserSkills();
453
+ printSolidPanel({
454
+ title: 'Skills Cleared',
455
+ lines: [`${C.emerald}[OK] All learned response skills have been cleared.${C.reset}`]
456
+ });
457
+ return 'HANDLED';
458
+ }
459
+
460
+ if (action === 'on' || action === 'off') {
461
+ const isEnabled = action === 'on';
462
+ memory.setPreference('skillsEnabled', isEnabled);
463
+ printSolidPanel({
464
+ title: 'Skill Toggle',
465
+ lines: [`${C.emerald}[OK] Learned response skills are now ${isEnabled ? `${C.bold}ENABLED (ON)` : `${C.dim}DISABLED (OFF)`}${C.reset}.`]
466
+ });
467
+ return 'HANDLED';
468
+ }
469
+
470
+ const skills = memory.preferences.userSkills || [];
471
+ const isEnabled = memory.preferences.skillsEnabled !== false;
472
+ const lines = [
473
+ `${C.bold}Status:${C.reset} ${isEnabled ? `${C.emerald}ACTIVE (ON)${C.reset}` : `${C.amber}INACTIVE (OFF)${C.reset}`} • ${skills.length} skills learned`,
474
+ ''
475
+ ];
476
+
477
+ if (skills.length > 0) {
478
+ lines.push(`${C.bold}${C.lavender}Learned Rules:${C.reset}`);
479
+ for (const s of skills) {
480
+ lines.push(` ${C.gold}[${s.id}]${C.reset} ${C.white}${s.instruction}${C.reset}`);
481
+ }
482
+ } else {
483
+ lines.push(`${C.dim}No custom skills learned yet.${C.reset}`);
484
+ lines.push(`Teach Kiku rules with: ${C.cyberCyan}/skill learn <instruction>${C.reset}`);
485
+ }
486
+
487
+ printSolidPanel({
488
+ title: `Learned Response Skills • ${isEnabled ? 'ON' : 'OFF'}`,
489
+ lines
490
+ });
491
+ return 'HANDLED';
492
+ },
493
+
494
+ '/history': async (ctx) => {
495
+ const { agent, sub, extra } = ctx;
496
+ if (sub === 'clear') {
497
+ agent.session.messages = [];
498
+ agent.conversationHistory = [agent.conversationHistory[0]];
499
+ await agent.session.clearDisk().catch(() => {});
500
+ console.log(`\n ${C.emerald}[OK] Chat history cleared.${C.reset}\n`);
501
+ return 'HANDLED';
502
+ }
503
+
504
+ if (sub === 'search' && extra) {
505
+ const q = extra.toLowerCase().trim();
506
+ const matches = [];
507
+ for (const msg of agent.session.messages) {
508
+ const textPart = msg.parts.find(p => p.type === 'text');
509
+ if (textPart && textPart.content.toLowerCase().includes(q)) {
510
+ matches.push({ role: msg.role, text: textPart.content, createdAt: msg.createdAt });
511
+ }
512
+ }
513
+ printSolidPanel({
514
+ title: `History Search Results for "${extra}" (${matches.length} matches)`,
515
+ lines: matches.length === 0
516
+ ? [`${C.dim}No messages found matching "${extra}".${C.reset}`]
517
+ : matches.map(m => {
518
+ const prefix = m.role === 'user' ? `${C.roseGlow}User` : `${C.cyberCyan}Kiku`;
519
+ return `${prefix}:${C.reset} ${C.white}${m.text.slice(0, 80)}${m.text.length > 80 ? '...' : ''}${C.reset}`;
520
+ }),
521
+ headerBg: '\x1b[48;2;48;54;70m',
522
+ bg: '\x1b[48;2;36;40;52m'
523
+ });
524
+ return 'HANDLED';
525
+ }
526
+
527
+ let count = 10;
528
+ if (sub && !isNaN(parseInt(sub))) count = Math.min(parseInt(sub), 50);
529
+
530
+ const recentMessages = agent.session.messages.slice(-count);
531
+ if (recentMessages.length === 0) {
532
+ console.log(`\n ${C.dim}No conversation history yet.${C.reset}\n`);
533
+ return 'HANDLED';
534
+ }
535
+
536
+ const lines = [];
537
+ for (const msg of recentMessages) {
538
+ if (msg.role === 'user') {
539
+ const textPart = msg.parts.find(p => p.type === 'text');
540
+ if (textPart) {
541
+ lines.push(`${C.roseGlow}${C.bold}${randomFanName()}:${C.reset} ${C.white}${textPart.content}${C.reset}`);
542
+ }
543
+ } else if (msg.role === 'assistant') {
544
+ const textPart = msg.parts.find(p => p.type === 'text');
545
+ if (textPart) {
546
+ const preview = textPart.content.length > 120 ? textPart.content.slice(0, 120) + '…' : textPart.content;
547
+ lines.push(`${C.cyberCyan}${C.bold}Kiku:${C.reset} ${C.white}${preview.replace(/\n/g, ' ')}${C.reset}`);
548
+ }
549
+ }
550
+ }
551
+ printSolidPanel({
552
+ title: `Conversation History (last ${recentMessages.length} messages)`,
553
+ lines,
554
+ headerBg: '\x1b[48;2;48;54;70m',
555
+ bg: '\x1b[48;2;36;40;52m'
556
+ });
557
+ return 'HANDLED';
558
+ },
559
+
560
+ '/session': async (ctx) => {
561
+ const { agent, sub } = ctx;
562
+ const sess = agent.session;
563
+ const action = (sub || 'info').toLowerCase();
564
+
565
+ if (action === 'info' || !action) {
566
+ const msgCount = sess.messages.length;
567
+ const userMsgs = sess.messages.filter(m => m.role === 'user').length;
568
+ const assistantMsgs = sess.messages.filter(m => m.role === 'assistant').length;
569
+ const toolCalls = sess.stats.totalToolCalls || 0;
570
+ const totalDur = ((sess.stats.totalDurationMs || 0) / 1000).toFixed(1);
571
+ const createdAt = sess.createdAt ? sess.createdAt.toLocaleString() : 'unknown';
572
+
573
+ printSolidPanel({
574
+ title: `Active Session Details`,
575
+ lines: [
576
+ `${C.gold}Session ID:${C.reset} ${C.white}${sess.id}${C.reset}`,
577
+ `${C.gold}Created At:${C.reset} ${C.white}${createdAt}${C.reset}`,
578
+ `${C.gold}Total Messages:${C.reset} ${C.cyberCyan}${msgCount}${C.reset} total ${C.dim}(${userMsgs} user, ${assistantMsgs} assistant)${C.reset}`,
579
+ `${C.gold}Total Queries:${C.reset} ${C.cyberCyan}${sess.stats.totalQueries}${C.reset}`,
580
+ `${C.gold}Tool Calls:${C.reset} ${C.cyberCyan}${toolCalls}${C.reset}`,
581
+ `${C.gold}Total Duration:${C.reset} ${C.cyberCyan}${totalDur}s${C.reset}`,
582
+ `${C.gold}Thinking Mode:${C.reset} ${C.bold}${C.gold}${sess.thinkingMode.toUpperCase()}${C.reset}`
583
+ ],
584
+ headerBg: '\x1b[48;2;48;54;70m',
585
+ bg: '\x1b[48;2;36;40;52m'
586
+ });
587
+ return 'HANDLED';
588
+ }
589
+
590
+ if (action === 'list') {
591
+ const sessions = agent.memory.getArchivedSessions(15);
592
+ printSolidPanel({
593
+ title: `Archived Sessions (Total: ${sessions.length})`,
594
+ lines: sessions.length === 0
595
+ ? [`${C.dim}No archived sessions found.${C.reset}`]
596
+ : sessions.map(s => {
597
+ const date = new Date(s.createdAt).toLocaleString();
598
+ const dur = ((s.stats?.totalDurationMs || 0) / 1000).toFixed(1);
599
+ return `${C.gold}${s.id}${C.reset} ${C.dim}(${date})${C.reset} — ${s.messageCount} msgs, ${dur}s`;
600
+ }),
601
+ headerBg: '\x1b[48;2;48;54;70m',
602
+ bg: '\x1b[48;2;36;40;52m'
603
+ });
604
+ return 'HANDLED';
605
+ }
606
+
607
+ if (action === 'export') {
608
+ const exported = await agent.session.exportToMarkdown();
609
+ console.log(`\n ${C.emerald}[OK] Session transcript exported.${C.reset}`);
610
+ console.log(` ${C.slate}• File:${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
611
+ return 'HANDLED';
612
+ }
613
+
614
+ if (action === 'clear' || action === 'new' || action === 'reset') {
615
+ await agent.endSession();
616
+ console.log(`\n ${C.emerald}[OK] Previous session archived. Started fresh.${C.reset}\n`);
617
+ return 'HANDLED';
618
+ }
619
+
620
+ console.log(`\n ${C.amber}Unknown session action: "${sub}". Use /session [info|list|export|clear]${C.reset}\n`);
621
+ return 'HANDLED';
622
+ },
623
+
624
+ '/thinking': async (ctx) => {
625
+ const { agent, sub } = ctx;
626
+ const targetMode = (sub || '').toLowerCase();
627
+ if (targetMode === 'show' || targetMode === 'hide') {
628
+ agent.session.setThinkingMode(targetMode);
629
+ console.log(`\n ${C.emerald}[OK] Thinking mode set to ${C.bold}${targetMode.toUpperCase()}${C.reset}\n`);
630
+ } else {
631
+ const current = agent.session.toggleThinkingMode();
632
+ console.log(`\n ${C.emerald}[OK] Thinking mode toggled to ${C.bold}${current.toUpperCase()}${C.reset}\n`);
633
+ }
634
+ return 'HANDLED';
635
+ },
636
+
637
+ '/lang': async (ctx) => {
638
+ const { agent, sub } = ctx;
639
+ const langArg = (sub || '').toLowerCase();
640
+ if (langArg === 'th' || langArg === 'zh' || langArg === 'en' || langArg === 'auto') {
641
+ agent.memory.setPreference('language', langArg === 'auto' ? null : langArg);
642
+ const langNames = { th: 'Thai', zh: 'Chinese', en: 'English', auto: 'Auto-detect' };
643
+ console.log(`\n ${C.emerald}[OK] Language updated to:${C.reset} ${C.bold}${C.white}${langNames[langArg]}${C.reset}\n`);
644
+ } else {
645
+ const current = agent.memory.preferences.language || 'auto';
646
+ printSolidPanel({
647
+ title: `Language Preference`,
648
+ lines: [`${C.gold}Current:${C.reset} ${C.bold}${C.white}${current.toUpperCase()}${C.reset}`, `${C.dim}Usage: /lang [th|zh|en|auto]${C.reset}`],
649
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
650
+ });
651
+ }
652
+ return 'HANDLED';
653
+ },
654
+
655
+ '/theme': async (ctx) => {
656
+ const action = (ctx.sub || 'current').toLowerCase();
657
+ const current = getThemeName();
658
+
659
+ if (action === 'list') {
660
+ printSolidPanel({
661
+ title: `Themes (${THEME_NAMES.length})`,
662
+ lines: THEME_NAMES.map(name => {
663
+ const theme = THEMES[name];
664
+ const marker = name === current ? '[x]' : '[ ]';
665
+ const sample = ansiForeground(theme.colors.primary);
666
+ const preview = `${ansiBackground(theme.colors.panel)}${ansiForeground(theme.colors.text)}${C.bold} Aa ${C.reset}`;
667
+ return `${marker} ${preview} ${sample}${C.bold}${name.padEnd(12)}${C.reset} ${C.dim}${theme.description}${C.reset}`;
668
+ }),
669
+ });
670
+ return 'HANDLED';
671
+ }
672
+
673
+ if (action === 'current') {
674
+ const theme = THEMES[current];
675
+ printSolidPanel({
676
+ title: 'Theme',
677
+ lines: [
678
+ `${C.gold}Current:${C.reset} ${C.bold}${C.white}${theme.label} (${current})${C.reset}`,
679
+ `${C.dim}${theme.description}${C.reset}`,
680
+ `${C.sakura}Primary${C.reset} ${C.roseGlow}Accent${C.reset} ${C.cyberCyan}Info${C.reset} ${C.emerald}Success${C.reset} ${C.amber}Warning${C.reset} ${C.red}Error${C.reset}`,
681
+ `${C.dim}Usage: /theme list or /theme <name>${C.reset}`,
682
+ ],
683
+ });
684
+ return 'HANDLED';
685
+ }
686
+
687
+ const requested = action === 'reset' ? 'sakura' : action;
688
+ const theme = setTheme(requested);
689
+ if (!theme) {
690
+ console.log(`\n ${C.amber}Unknown theme: "${action}".${C.reset}`);
691
+ console.log(` ${C.dim}Available: ${THEME_NAMES.join(', ')}${C.reset}\n`);
692
+ return 'HANDLED';
693
+ }
694
+
695
+ printSolidPanel({
696
+ title: 'Theme Updated',
697
+ lines: [
698
+ `${C.emerald}[OK] Active theme: ${C.bold}${theme.label} (${requested})${C.reset}`,
699
+ `${C.dim}${theme.description}${C.reset}`,
700
+ `${C.sakura}Primary${C.reset} ${C.roseGlow}Accent${C.reset} ${C.cyberCyan}Info${C.reset} ${C.emerald}Success${C.reset} ${C.amber}Warning${C.reset} ${C.red}Error${C.reset}`,
701
+ `${C.dim}Saved for future Kiku Agent sessions.${C.reset}`,
702
+ ],
703
+ });
704
+ return 'HANDLED';
705
+ },
706
+
707
+ '/tool': async (ctx) => {
708
+ const { sub, extra } = ctx;
709
+ const toolName = sub;
710
+ const rawArgs = extra || '';
711
+
712
+ if (!toolName) {
713
+ console.log(`\n ${C.amber}Usage: /tool <tool_name> [args]${C.reset}\n`);
714
+ return 'HANDLED';
715
+ }
716
+
717
+ const schema = toolsSchema.find(t => t.function.name.toLowerCase() === toolName.toLowerCase());
718
+ if (!schema) {
719
+ console.log(`\n ${C.red}Unknown tool: "${toolName}". Use /tools to view all.${C.reset}\n`);
720
+ return 'HANDLED';
721
+ }
722
+
723
+ const actualToolName = schema.function.name;
724
+
725
+ if (!rawArgs || !rawArgs.trim()) {
726
+ const props = schema.function.parameters?.properties || {};
727
+ const required = schema.function.parameters?.required || [];
728
+ console.log(`\n ${C.gold}Tool:${C.reset} ${C.bold}${C.cyberCyan}${actualToolName}${C.reset}`);
729
+ console.log(` ${C.gold}Description:${C.reset} ${C.white}${schema.function.description}${C.reset}`);
730
+ for (const [pName, pMeta] of Object.entries(props)) {
731
+ const reqStr = required.includes(pName) ? `${C.red}[required]${C.reset}` : `${C.dim}[optional]${C.reset}`;
732
+ console.log(` • ${C.bold}${pName}${C.reset} (${pMeta.type || 'any'}) ${reqStr}: ${pMeta.description || ''}`);
733
+ }
734
+ console.log(`\n`);
735
+ return 'HANDLED';
736
+ }
737
+
738
+ let parsedArgs = {};
739
+ if (rawArgs.startsWith('{') && rawArgs.endsWith('}')) {
740
+ try { parsedArgs = JSON.parse(rawArgs); } catch { parsedArgs = { query: rawArgs }; }
741
+ } else {
742
+ const props = schema.function.parameters?.properties || {};
743
+ const firstProp = Object.keys(props)[0] || 'query';
744
+ parsedArgs = { [firstProp]: rawArgs };
745
+ }
746
+
747
+ const runner = createProgressRunner({
748
+ badge: 'RUN',
749
+ title: `Executing ${actualToolName}`,
750
+ step: JSON.stringify(parsedArgs).slice(0, 50),
751
+ badgeBg: '\x1b[48;2;14;165;233m\x1b[38;2;15;23;42m',
752
+ });
753
+ const start = Date.now();
754
+ try {
755
+ const result = await executeTool(actualToolName, parsedArgs);
756
+ const dur = ((Date.now() - start) / 1000).toFixed(2);
757
+ runner.stop();
758
+ console.log(`\n \x1b[48;2;16;185;129m\x1b[38;2;15;23;42m${C.bold} OK ${C.reset} ${C.emerald}Completed in ${dur}s${C.reset}\n`);
759
+ console.log(JSON.stringify(result, null, 2));
760
+ console.log('');
761
+ } catch (err) {
762
+ runner.fail(err);
763
+ }
764
+ return 'HANDLED';
765
+ },
766
+
767
+ '/tools': async (ctx) => {
768
+ const filter = (ctx.sub || '').toLowerCase().trim();
769
+ const filtered = filter
770
+ ? toolsSchema.filter(t => t.function.name.toLowerCase().includes(filter) || t.function.description.toLowerCase().includes(filter))
771
+ : toolsSchema;
772
+
773
+ console.log(`\n ${C.bold}${C.sakura}Registered Tools (${filtered.length}/${toolsSchema.length})${C.reset}\n`);
774
+ for (const [idx, t] of filtered.entries()) {
775
+ const f = t.function;
776
+ const params = Object.keys(f.parameters?.properties || {}).join(', ');
777
+ console.log(` ${C.gold}${String(idx + 1).padStart(2)}. ${C.bold}${C.cyberCyan}${f.name}${C.reset}${C.slate}(${params})${C.reset}`);
778
+ console.log(` ${C.dim}${C.roseGlow}${f.description}${C.reset}`);
779
+ }
780
+ console.log(`\n`);
781
+ return 'HANDLED';
782
+ },
783
+
784
+ '/mem': async (ctx) => {
785
+ const { agent, sub, extra, parts } = ctx;
786
+ const memAction = (sub || '').toLowerCase();
787
+
788
+ if (memAction === 'add') {
789
+ const factText = (parts || []).slice(2).join(' ').trim();
790
+ if (!factText) {
791
+ console.log(`\n ${C.amber}Usage: /mem add <fact>${C.reset}\n`);
792
+ } else {
793
+ const entry = agent.memory.addFact(factText, { source: 'user' });
794
+ console.log(`\n ${C.emerald}[OK] Saved:${C.reset} ${C.white}${factText.slice(0, 80)}${C.reset} (ID: ${entry.id})\n`);
795
+ }
796
+ return 'HANDLED';
797
+ }
798
+
799
+ if (memAction === 'del' || memAction === 'rm') {
800
+ const factId = (parts || [])[2]?.trim();
801
+ if (!factId) {
802
+ console.log(`\n ${C.amber}Usage: /mem del <fact_id>${C.reset}\n`);
803
+ } else if (agent.memory.removeFact(factId)) {
804
+ console.log(`\n ${C.emerald}[OK] Removed [${factId}]${C.reset}\n`);
805
+ } else {
806
+ console.log(`\n ${C.red}Not found [${factId}]${C.reset}\n`);
807
+ }
808
+ return 'HANDLED';
809
+ }
810
+
811
+ if (memAction === 'search') {
812
+ const query = (parts || []).slice(2).join(' ').trim();
813
+ if (!query) {
814
+ console.log(`\n ${C.amber}Usage: /mem search <keyword>${C.reset}\n`);
815
+ } else {
816
+ const results = agent.memory.searchFacts(query);
817
+ printSolidPanel({
818
+ title: `Memory Search: "${query}" (${results.length})`,
819
+ lines: results.map(fact => `${C.white}${fact.content.slice(0, 65)}${C.reset} ${C.dim}(${fact.id})${C.reset}`),
820
+ });
821
+ }
822
+ return 'HANDLED';
823
+ }
824
+
825
+ if (memAction === 'list') {
826
+ const grouped = agent.memory.getFactsByCategory();
827
+ const factLines = [];
828
+ for (const [cat, facts] of Object.entries(grouped)) {
829
+ factLines.push(`${C.bold}${C.cyberCyan}${cat.toUpperCase()}${C.reset} (${facts.length})`);
830
+ for (const fact of facts.slice(0, 6)) {
831
+ factLines.push(` • ${C.white}${fact.content.slice(0, 65)}${C.reset}`);
832
+ }
833
+ }
834
+ printSolidPanel({ title: 'Stored Facts', lines: factLines });
835
+ return 'HANDLED';
836
+ }
837
+
838
+ if (memAction === 'topics') {
839
+ const topics = agent.memory.getTopTopics(10);
840
+ printSolidPanel({
841
+ title: 'Topic Profile',
842
+ lines: topics.map(t => {
843
+ const bar = '█'.repeat(Math.min(t.count, 15));
844
+ return `${C.gold}${t.topic.padEnd(14)}${C.reset} ${C.cyberCyan}${bar}${C.reset} ${C.dim}(${t.count}x)${C.reset}`;
845
+ }),
846
+ });
847
+ return 'HANDLED';
848
+ }
849
+
850
+ if (memAction === 'export') {
851
+ const exported = await agent.memory.exportToJSON();
852
+ console.log(`\n ${C.emerald}[OK] Memory exported.${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
853
+ return 'HANDLED';
854
+ }
855
+
856
+ if (memAction === 'import') {
857
+ const filePath = (parts || []).slice(2).join(' ').trim();
858
+ if (!filePath) {
859
+ console.log(`\n ${C.amber}Usage: /mem import <path>${C.reset}\n`);
860
+ } else {
861
+ const result = await agent.memory.importFromJSON(filePath);
862
+ if (result.success) {
863
+ console.log(`\n ${C.emerald}[OK] Imported ${result.factsImported} facts.${C.reset}\n`);
864
+ } else {
865
+ console.log(`\n ${C.red}[ERROR] ${result.message}${C.reset}\n`);
866
+ }
867
+ }
868
+ return 'HANDLED';
869
+ }
870
+
871
+ if (memAction === 'clear') {
872
+ await agent.memory.clearAll();
873
+ console.log(`\n ${C.emerald}[OK] Memory cleared.${C.reset}\n`);
874
+ return 'HANDLED';
875
+ }
876
+
877
+ const memStatus = agent.memory.getStatus();
878
+ printSolidPanel({
879
+ title: 'Memory Status',
880
+ lines: [`${C.gold}Facts:${C.reset} ${C.cyberCyan}${memStatus.factsCount}${C.reset} ${C.gold}Sessions:${C.reset} ${C.cyberCyan}${memStatus.sessionsCount}${C.reset}`],
881
+ });
882
+ return 'HANDLED';
883
+ },
884
+
885
+ '/play': async (ctx) => {
886
+ const songName = ctx.extra || '';
887
+ if (!songName) {
888
+ console.log(`\n ${C.amber}Usage: /play <song name>${C.reset}\n`);
889
+ } else {
890
+ await audioPlayer.play(songName);
891
+ console.log(`\n ${C.emerald}[OK] Playing: ${C.white}${songName}${C.reset}\n`);
892
+ }
893
+ return 'HANDLED';
894
+ },
895
+
896
+ '/stop': async () => { await audioPlayer.stop(); console.log(`\n ${C.emerald}[OK] Stopped${C.reset}\n`); return 'HANDLED'; },
897
+ '/pause': async () => { const r = await audioPlayer.pause(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
898
+ '/resume': async () => { const r = await audioPlayer.resume(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
899
+ '/next': async () => { const r = await audioPlayer.next(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
900
+ '/prev': async () => { const r = await audioPlayer.prev(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
901
+ '/mute': async () => { const r = await audioPlayer.mute(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
902
+ '/unmute': async () => { const r = await audioPlayer.unmute(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
903
+
904
+ '/add': async (ctx) => {
905
+ const song = ctx.extra || '';
906
+ if (!song) { console.log(`\n ${C.amber}Usage: /add <song>${C.reset}\n`); }
907
+ else { const r = await audioPlayer.addToQueue(song); console.log(`\n ${C.emerald}${r}${C.reset}\n`); }
908
+ return 'HANDLED';
909
+ },
910
+
911
+ '/remove': async (ctx) => {
912
+ const index = parseInt(ctx.sub, 10);
913
+ if (isNaN(index)) { console.log(`\n ${C.amber}Usage: /remove <index>${C.reset}\n`); }
914
+ else { const r = await audioPlayer.removeFromQueue(index); console.log(`\n ${C.emerald}${r}${C.reset}\n`); }
915
+ return 'HANDLED';
916
+ },
917
+
918
+ '/clearqueue': async () => { const r = await audioPlayer.clearQueue(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
919
+
920
+ '/search': async (ctx) => {
921
+ const q = ctx.extra || '';
922
+ if (!q) { console.log(`\n ${C.amber}Usage: /search <query>${C.reset}\n`); return 'HANDLED'; }
923
+ try {
924
+ const results = await audioPlayer.searchYouTube(q);
925
+ if (results.length === 0) { console.log(`\n ${C.amber}No results for "${q}"${C.reset}\n`); }
926
+ else {
927
+ printSolidPanel({
928
+ title: `YouTube: "${q}" (${results.length})`,
929
+ lines: results.map((r, i) => `${C.gold}${i + 1}.${C.reset} ${C.white}${r.title}${C.reset} ${C.dim}(${r.duration})${C.reset}`),
930
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
931
+ });
932
+ }
933
+ } catch (err) { console.log(`\n ${C.red}${err.message}${C.reset}\n`); }
934
+ return 'HANDLED';
935
+ },
936
+
937
+ '/current': async () => {
938
+ const state = audioPlayer.getFullState();
939
+ if (!state.isPlaying && !state.isPaused) {
940
+ console.log(`\n ${C.slate}No music playing.${C.reset}\n`);
941
+ } else {
942
+ printSolidPanel({
943
+ title: `${UI_ICONS.music} Current Track`,
944
+ lines: [
945
+ `${C.gold}Track:${C.reset} ${C.white}${state.currentTrack}${C.reset}`,
946
+ `${C.gold}Status:${C.reset} ${state.isPaused ? C.amber + 'Paused' : C.emerald + 'Playing'}${C.reset}`,
947
+ `${C.gold}Queue:${C.reset} ${C.cyberCyan}${state.queueLength} songs${C.reset}`
948
+ ],
949
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
950
+ });
951
+ }
952
+ return 'HANDLED';
953
+ },
954
+
955
+ '/retry': async (ctx) => {
956
+ if (!ctx.lastQuery) {
957
+ console.log(`\n ${C.amber}No previous query.${C.reset}\n`);
958
+ return 'HANDLED';
959
+ }
960
+ return 'RETRY';
961
+ },
962
+
963
+ '/clear': async (ctx) => {
964
+ try { await ctx.agent.endSession(); } catch {}
965
+ if (ctx.tuiPrompt) {
966
+ ctx.tuiPrompt.viewportLines = [...getWelcomeHeroLines()];
967
+ ctx.tuiPrompt.scrollOffset = 0;
968
+ ctx.tuiPrompt.addViewportLines(` ${C.emerald}[OK] Fresh start.${C.reset}\n`);
969
+ ctx.tuiPrompt.render();
970
+ } else {
971
+ console.clear();
972
+ ctx.printWelcome();
973
+ console.log(` ${C.emerald}[OK] Fresh start.${C.reset}\n`);
974
+ }
975
+ return 'HANDLED';
976
+ },
977
+
978
+ '/save': async (ctx) => {
979
+ const exported = await ctx.agent.session.exportToMarkdown();
980
+ console.log(`\n ${C.emerald}[OK] Exported.${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
981
+ return 'HANDLED';
982
+ },
983
+
984
+ '/provider': async (ctx) => {
985
+ const { CLOUD_PROVIDERS, getProvider, getRecommendedModels } = await import('./config.js');
986
+ const action = (ctx.sub || 'status').toLowerCase();
987
+ const parts = ctx.parts || [];
988
+
989
+ if (action === 'list') {
990
+ const active = ctx.client.provider || 'lmstudio';
991
+ const lines = Object.values(CLOUD_PROVIDERS).map(p => {
992
+ const marker = p.id === active ? '[x]' : '[ ]';
993
+ const isSet = Boolean(p.apiKey && p.apiKey !== 'lm-studio') || p.isLocal;
994
+ const keyBadge = isSet
995
+ ? (p.isLocal ? `${C.slate}local (free)${C.reset}` : `${C.emerald}ready${C.reset}`)
996
+ : `${C.amber}no key${C.reset}`;
997
+ return `${marker} ${C.bold}${C.cyberCyan}${p.id.padEnd(12)}${C.reset} ${C.white}${p.name.padEnd(22)}${C.reset} ${keyBadge.padEnd(20)} ${C.dim}(${p.defaultModel})${C.reset}`;
998
+ });
999
+ printSolidPanel({
1000
+ title: `AI Providers (${Object.keys(CLOUD_PROVIDERS).length})`,
1001
+ lines: [
1002
+ ...lines,
1003
+ '',
1004
+ `${C.dim}Switch: /provider use <name> [model] | Models: /provider models <name> | Docs: /provider info <name>${C.reset}`
1005
+ ],
1006
+ });
1007
+ return 'HANDLED';
1008
+ }
1009
+
1010
+ if (action === 'use') {
1011
+ const targetProvider = (parts[2] || ctx.extra || '').toLowerCase().trim();
1012
+ const targetModel = parts.slice(3).join(' ').trim() || null;
1013
+
1014
+ if (!targetProvider) {
1015
+ console.log(`\n ${C.amber}Usage: /provider use <provider_name> [model_id]${C.reset}`);
1016
+ console.log(` ${C.dim}Example: /provider use groq llama-3.1-8b-instant${C.reset}\n`);
1017
+ return 'HANDLED';
1018
+ }
1019
+
1020
+ try {
1021
+ const options = targetModel ? { model: targetModel } : {};
1022
+ const info = ctx.client.switchProvider(targetProvider, options);
1023
+ if (ctx.agent?.client) ctx.agent.client.switchProvider(targetProvider, options);
1024
+ if (ctx.modelManager) {
1025
+ ctx.modelManager.client = ctx.client;
1026
+ ctx.modelManager.requestedModel = info.model;
1027
+ }
1028
+ ctx.baseUrl = info.baseUrl;
1029
+
1030
+ console.log(`\n ${C.emerald}[OK] Switched provider to ${C.bold}${info.providerName}${C.reset} (${info.provider})`);
1031
+ console.log(` ${C.slate}• Model:${C.reset} ${C.white}${info.model}${C.reset}`);
1032
+ console.log(` ${C.slate}• Base URL:${C.reset} ${C.cyberCyan}${info.baseUrl}${C.reset}`);
1033
+ console.log(` ${C.slate}• API Key:${C.reset} ${info.hasApiKey ? `${C.emerald}Configured` : (info.isLocal ? `${C.slate}Local (not required)` : `${C.amber}Unset (${info.envKey || 'API key required'})`)}${C.reset}`);
1034
+ if (!info.hasApiKey && !info.isLocal) {
1035
+ console.log(` ${C.dim}Tip: Set ${info.envKey} in .env or run /provider setkey <key>${C.reset}`);
1036
+ }
1037
+ console.log('');
1038
+ } catch (err) {
1039
+ console.log(`\n ${C.red}[PROVIDER ERROR] ${err.message}${C.reset}\n`);
1040
+ }
1041
+ return 'HANDLED';
1042
+ }
1043
+
1044
+ if (action === 'models' || action === 'model' || action === 'catalog') {
1045
+ const targetProvider = (ctx.extra || ctx.client.provider || 'lmstudio').toLowerCase().trim();
1046
+ const providerDef = getProvider(targetProvider);
1047
+ if (!providerDef) {
1048
+ console.log(`\n ${C.red}[ERROR] Unknown provider "${targetProvider}". Available: ${Object.keys(CLOUD_PROVIDERS).join(', ')}${C.reset}\n`);
1049
+ return 'HANDLED';
1050
+ }
1051
+
1052
+ const models = getRecommendedModels(targetProvider);
1053
+ const lines = [];
1054
+ lines.push(`${C.gold}Provider:${C.reset} ${C.bold}${C.white}${providerDef.name}${C.reset} (${providerDef.id})`);
1055
+ lines.push(`${C.gold}Pricing:${C.reset} ${C.slate}${providerDef.pricingSummary}${C.reset}`);
1056
+ lines.push('');
1057
+
1058
+ for (const m of models) {
1059
+ const isCurrent = m.id === ctx.client.model;
1060
+ const marker = isCurrent ? `${C.gold}[x]${C.reset}` : `${C.dim}[ ]${C.reset}`;
1061
+ const tierBadge = m.tier === 'free'
1062
+ ? `${C.slate}[FREE]${C.reset}`
1063
+ : m.tier === 'ultra-cheap' || m.tier === 'lowest-cost'
1064
+ ? `${C.emerald}[ULTRA-CHEAP]${C.reset}`
1065
+ : m.tier === 'reasoning'
1066
+ ? `${C.lavender}[REASONING]${C.reset}`
1067
+ : m.tier === 'flagship'
1068
+ ? `${C.sakura}[FLAGSHIP]${C.reset}`
1069
+ : `${C.cyberCyan}[BALANCED]${C.reset}`;
1070
+ lines.push(`${marker} ${C.bold}${C.white}${m.id}${C.reset} ${tierBadge} ${C.dim}• Cost: ${m.cost} • Context: ${m.context}${C.reset}`);
1071
+ lines.push(` ${C.slate}${m.description}${C.reset}`);
1072
+ }
1073
+
1074
+ lines.push('');
1075
+ lines.push(`${C.dim}Use model: /provider use ${providerDef.id} <model_id> or /model use <model_id>${C.reset}`);
1076
+
1077
+ printSolidPanel({
1078
+ title: `${providerDef.name} • Recommended Models (${models.length})`,
1079
+ lines,
1080
+ });
1081
+ return 'HANDLED';
1082
+ }
1083
+
1084
+ if (action === 'info' || action === 'doc' || action === 'docs') {
1085
+ const targetProvider = (ctx.extra || ctx.client.provider || 'lmstudio').toLowerCase().trim();
1086
+ const p = getProvider(targetProvider);
1087
+ if (!p) {
1088
+ console.log(`\n ${C.red}[ERROR] Unknown provider "${targetProvider}".${C.reset}\n`);
1089
+ return 'HANDLED';
1090
+ }
1091
+
1092
+ const isSet = Boolean(p.apiKey && p.apiKey !== 'lm-studio') || p.isLocal;
1093
+ printSolidPanel({
1094
+ title: `${p.name} Developer Details`,
1095
+ lines: [
1096
+ `${C.gold}ID:${C.reset} ${C.white}${p.id}${C.reset}`,
1097
+ `${C.gold}Base URL:${C.reset} ${C.cyberCyan}${p.baseUrl}${C.reset}`,
1098
+ `${C.gold}Default:${C.reset} ${C.white}${p.defaultModel}${C.reset}`,
1099
+ `${C.gold}ENV Key:${C.reset} ${C.white}${p.envKey || 'N/A'}${C.reset} (${isSet ? `${C.emerald}Configured` : `${C.amber}Missing`}${C.reset})`,
1100
+ `${C.gold}Pricing:${C.reset} ${C.slate}${p.pricingSummary || 'N/A'}${C.reset}`,
1101
+ `${C.gold}Features:${C.reset} ${C.slate}${(p.features || []).join(', ')}${C.reset}`,
1102
+ `${C.gold}Docs:${C.reset} ${C.cyberCyan}${p.docUrl || 'N/A'}${C.reset}`,
1103
+ `${C.gold}API Ref:${C.reset} ${C.cyberCyan}${p.apiRefUrl || 'N/A'}${C.reset}`,
1104
+ '',
1105
+ `${C.dim}Switch: /provider use ${p.id} | Models: /provider models ${p.id}${C.reset}`
1106
+ ],
1107
+ });
1108
+ return 'HANDLED';
1109
+ }
1110
+
1111
+ if (action === 'test' || action === 'ping' || action === 'check') {
1112
+ const targetProvider = (ctx.extra || ctx.client.provider || 'lmstudio').toLowerCase().trim();
1113
+ console.log(`\n ${C.cyberCyan}[TESTING] Testing connection to ${targetProvider}...${C.reset}`);
1114
+ let testClient = ctx.client;
1115
+ if (targetProvider !== ctx.client.provider) {
1116
+ const { LLMClient } = await import('./llmClient.js');
1117
+ testClient = new LLMClient({ provider: targetProvider });
1118
+ }
1119
+ const check = await testClient.checkConnection();
1120
+ if (check.connected) {
1121
+ console.log(` ${C.emerald}[OK] Connected to ${check.providerName}!${C.reset}`);
1122
+ console.log(` ${C.slate}• Latency:${C.reset} ${C.emerald}${check.latencyMs || 0}ms${C.reset}`);
1123
+ console.log(` ${C.slate}• Active Model:${C.reset} ${C.white}${check.activeModel || check.model || 'N/A'}${C.reset}`);
1124
+ console.log(` ${C.slate}• Models Found:${C.reset} ${C.cyberCyan}${check.models?.length || 0}${C.reset}\n`);
1125
+ } else {
1126
+ console.log(` ${C.red}[FAILED] Connection check failed for ${check.providerName}.${C.reset}`);
1127
+ console.log(` ${C.amber}• Error:${C.reset} ${check.error}\n`);
1128
+ }
1129
+ return 'HANDLED';
1130
+ }
1131
+
1132
+ if (action === 'setkey' || action === 'key') {
1133
+ const arg1 = parts[2] || '';
1134
+ const arg2 = parts[3] || '';
1135
+ let targetProvider = ctx.client.provider;
1136
+ let newKey = arg1;
1137
+
1138
+ if (arg2 && getProvider(arg1)) {
1139
+ targetProvider = arg1.toLowerCase();
1140
+ newKey = arg2;
1141
+ }
1142
+
1143
+ if (!newKey) {
1144
+ console.log(`\n ${C.amber}Usage: /provider setkey <api_key> or /provider setkey <provider> <api_key>${C.reset}\n`);
1145
+ return 'HANDLED';
1146
+ }
1147
+
1148
+ if (targetProvider === ctx.client.provider) {
1149
+ ctx.client.setApiKey(newKey);
1150
+ if (ctx.agent?.client) ctx.agent.client.setApiKey(newKey);
1151
+ }
1152
+ const pDef = getProvider(targetProvider);
1153
+ if (pDef) pDef.apiKey = newKey;
1154
+
1155
+ const masked = newKey.length > 8
1156
+ ? `${newKey.slice(0, 4)}...${newKey.slice(-4)}`
1157
+ : '****';
1158
+ console.log(`\n ${C.emerald}[OK] API key updated for ${targetProvider}${C.reset} (${masked})\n`);
1159
+ return 'HANDLED';
1160
+ }
1161
+
1162
+ const pInfo = ctx.client.getProviderInfo ? ctx.client.getProviderInfo() : {
1163
+ provider: ctx.client.provider || 'lmstudio',
1164
+ providerName: ctx.client.providerName || 'LM Studio',
1165
+ baseUrl: ctx.client.baseUrl,
1166
+ model: ctx.client.model,
1167
+ hasApiKey: false,
1168
+ };
1169
+
1170
+ printSolidPanel({
1171
+ title: `Active AI Provider • ${pInfo.providerName}`,
1172
+ lines: [
1173
+ `${C.gold}Provider:${C.reset} ${C.bold}${C.white}${pInfo.providerName}${C.reset} (${pInfo.provider})`,
1174
+ `${C.gold}Base URL:${C.reset} ${C.cyberCyan}${pInfo.baseUrl}${C.reset}`,
1175
+ `${C.gold}Model:${C.reset} ${C.white}${pInfo.model}${C.reset}`,
1176
+ `${C.gold}API Key:${C.reset} ${pInfo.hasApiKey ? `${C.emerald}Configured` : (pInfo.isLocal ? `${C.slate}Local (not required)` : `${C.amber}Unset (${pInfo.envKey || 'API key required'})`)}${C.reset}`,
1177
+ `${C.gold}Pricing:${C.reset} ${C.slate}${pInfo.pricingSummary || 'N/A'}${C.reset}`,
1178
+ `${C.gold}Docs:${C.reset} ${C.cyberCyan}${pInfo.docUrl || 'N/A'}${C.reset}`,
1179
+ '',
1180
+ `${C.dim}Commands: /provider list | /provider use <name> [model] | /provider models | /provider test${C.reset}`,
1181
+ ],
1182
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1183
+ });
1184
+ return 'HANDLED';
1185
+ },
1186
+
1187
+ '/model': async (ctx) => {
1188
+ const action = (ctx.sub || 'status').toLowerCase();
1189
+ const providerName = ctx.client.providerName || 'Model';
1190
+ const { getRecommendedModels } = await import('./config.js');
1191
+
1192
+ if (action === 'reconnect') {
1193
+ if (!ctx.modelManager) {
1194
+ console.log(`\n ${C.red}[MODEL ERROR] Runtime model manager is unavailable.${C.reset}\n`);
1195
+ return 'HANDLED';
1196
+ }
1197
+ const resolution = await ctx.modelManager.resolve();
1198
+ if (resolution.connected && resolution.selectedModel) {
1199
+ ctx.setCatalogOnlyMode?.(false);
1200
+ console.log(`\n ${C.emerald}[OK] Connected using ${resolution.selectedModel}.${C.reset}\n`);
1201
+ } else {
1202
+ console.log(`\n ${C.red}[MODEL ERROR] ${resolution.error || 'No chat model is ready.'}${C.reset}\n`);
1203
+ }
1204
+ return 'HANDLED';
1205
+ }
1206
+
1207
+ if (action === 'recommend' || action === 'rec') {
1208
+ const recs = getRecommendedModels(ctx.client.provider);
1209
+ const lines = recs.map(m => {
1210
+ const isCurrent = m.id === ctx.client.model;
1211
+ const marker = isCurrent ? `${C.gold}[x]${C.reset}` : `${C.dim}[ ]${C.reset}`;
1212
+ return `${marker} ${C.bold}${C.white}${m.id.padEnd(30)}${C.reset} ${C.slate}${m.cost.padEnd(16)}${C.reset} ${C.dim}${m.description}${C.reset}`;
1213
+ });
1214
+ printSolidPanel({
1215
+ title: `${providerName} Recommended Models`,
1216
+ lines: lines.length ? lines : [`${C.dim}No specific recommendations for ${providerName}.${C.reset}`],
1217
+ });
1218
+ return 'HANDLED';
1219
+ }
1220
+
1221
+ const liveCheck = ctx.modelManager
1222
+ ? await ctx.modelManager.inspect()
1223
+ : await ctx.client.checkConnection();
1224
+
1225
+ if (action === 'list') {
1226
+ const serverModels = (liveCheck.models || []).slice(0, 40).map(id => `${id === ctx.client.model ? '[x]' : '[ ]'} ${id}`);
1227
+ const recs = getRecommendedModels(ctx.client.provider);
1228
+ const recLines = recs.length > 0
1229
+ ? [
1230
+ `${C.bold}${C.lavender}Recommended Active Models:${C.reset}`,
1231
+ ...recs.map(m => ` ${m.id === ctx.client.model ? C.gold + '[x]' : C.dim + '[ ]'} ${C.white}${m.id}${C.reset} ${C.dim}(${m.cost} • ${m.tier})${C.reset}`),
1232
+ ''
1233
+ ]
1234
+ : [];
1235
+
1236
+ printSolidPanel({
1237
+ title: `${providerName} Models • ${(liveCheck.models || []).length} available`,
1238
+ lines: [
1239
+ ...recLines,
1240
+ `${C.bold}${C.lavender}Reported by Server:${C.reset}`,
1241
+ ...(serverModels.length ? serverModels.map(s => ` ${s}`) : [` ${C.dim}No live model list reported by ${providerName}.${C.reset}`]),
1242
+ '',
1243
+ `${C.dim}Select model: /model use <model_id>${C.reset}`
1244
+ ],
1245
+ });
1246
+ return 'HANDLED';
1247
+ }
1248
+
1249
+ if (action === 'use') {
1250
+ try {
1251
+ const targetModel = ctx.extra?.trim() || '';
1252
+ if (!targetModel) {
1253
+ console.log(`\n ${C.amber}Usage: /model use <model_id>${C.reset}\n`);
1254
+ return 'HANDLED';
1255
+ }
1256
+ if (ctx.modelManager) {
1257
+ const selected = ctx.modelManager.select(targetModel, liveCheck.models || []);
1258
+ console.log(`\n ${C.emerald}[OK] Active model changed to ${selected}.${C.reset}\n`);
1259
+ } else {
1260
+ ctx.client.setModel(targetModel);
1261
+ console.log(`\n ${C.emerald}[OK] Active model changed to ${targetModel}.${C.reset}\n`);
1262
+ }
1263
+ } catch (error) {
1264
+ console.log(`\n ${C.red}[MODEL ERROR] ${error.message}${C.reset}\n`);
1265
+ }
1266
+ return 'HANDLED';
1267
+ }
1268
+
1269
+ printSolidPanel({
1270
+ title: `${providerName} Diagnostics`,
1271
+ lines: [
1272
+ `${C.gold}Provider:${C.reset} ${C.white}${ctx.client.providerName || 'LM Studio'}${C.reset}`,
1273
+ `${C.gold}Status:${C.reset} ${liveCheck.connected ? C.emerald + '● Online' : C.red + '○ Offline'}${C.reset}${liveCheck.latencyMs ? ` ${C.dim}(${liveCheck.latencyMs}ms)${C.reset}` : ''}`,
1274
+ `${C.gold}State:${C.reset} ${C.white}${liveCheck.state || 'ready'}${C.reset}`,
1275
+ `${C.gold}Model:${C.reset} ${C.bold}${C.white}${ctx.client.model || 'None'}${C.reset}`,
1276
+ `${C.gold}Available:${C.reset} ${C.white}${liveCheck.models?.length || 0} model(s)${C.reset}`,
1277
+ `${C.gold}URL:${C.reset} ${C.cyberCyan}${ctx.baseUrl}${C.reset}`,
1278
+ '',
1279
+ `${C.dim}Commands: /model list | /model use <id> | /model recommend | /model reconnect${C.reset}`
1280
+ ],
1281
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1282
+ });
1283
+ return 'HANDLED';
1284
+ },
1285
+
1286
+ '/mcp': async (ctx) => {
1287
+ const action = (ctx.sub || 'status').toLowerCase();
1288
+ const mcpManager = ctx.agent?.mcpManager;
1289
+
1290
+ if (!mcpManager) {
1291
+ console.log(`\n ${C.red}[MCP ERROR] MCP Client Manager is unavailable.${C.reset}\n`);
1292
+ return 'HANDLED';
1293
+ }
1294
+
1295
+ if (action === 'tools') {
1296
+ const tools = mcpManager.getToolsSchema();
1297
+ const lines = [];
1298
+ if (tools.length === 0) {
1299
+ lines.push(`${C.dim}No MCP tools registered. Connect to an MCP server using /mcp connect <name> <url>${C.reset}`);
1300
+ } else {
1301
+ for (const [idx, t] of tools.entries()) {
1302
+ const f = t.function;
1303
+ const params = Object.keys(f.parameters?.properties || {}).join(', ');
1304
+ lines.push(`${C.gold}${idx + 1}.${C.reset} ${C.bold}${C.cyberCyan}${f.name}${C.reset}${C.slate}(${params})${C.reset}`);
1305
+ const desc = f.description || 'No description provided';
1306
+ const wrapped = wrapText(desc, Math.max(30, getFullWidth() - 8));
1307
+ for (const dLine of wrapped) {
1308
+ lines.push(` ${C.dim}${dLine}${C.reset}`);
1309
+ }
1310
+ }
1311
+ }
1312
+ printSolidPanel({
1313
+ title: `Connected MCP Tools (${tools.length})`,
1314
+ lines,
1315
+ });
1316
+ return 'HANDLED';
1317
+ }
1318
+
1319
+ if (action === 'connect') {
1320
+ const parts = (ctx.parts || []).slice(2);
1321
+ const name = parts[0];
1322
+ const targetUrlOrType = parts[1];
1323
+
1324
+ if (!name || !targetUrlOrType) {
1325
+ printSolidPanel({
1326
+ title: 'MCP Connect Usage',
1327
+ lines: [
1328
+ `${C.bold}Connect to Remote HTTP MCP:${C.reset}`,
1329
+ ` ${C.cyberCyan}/mcp connect <name> <url>${C.reset}`,
1330
+ ` ${C.dim}Example: /mcp connect kiku https://jujingyi-kikuuu.vercel.app/mcp${C.reset}`,
1331
+ '',
1332
+ `${C.bold}Connect to Stdio MCP Process:${C.reset}`,
1333
+ ` ${C.cyberCyan}/mcp connect <name> stdio <command> [args...]${C.reset}`,
1334
+ ` ${C.dim}Example: /mcp connect local-kiku stdio node ./server/mcp/stdio.js${C.reset}`,
1335
+ ]
1336
+ });
1337
+ return 'HANDLED';
1338
+ }
1339
+
1340
+ const runner = createProgressRunner({
1341
+ badge: 'RUN',
1342
+ title: `Connecting to MCP Server "${name}"`,
1343
+ step: targetUrlOrType,
1344
+ badgeBg: '\x1b[48;2;14;165;233m\x1b[38;2;15;23;42m',
1345
+ });
1346
+
1347
+ try {
1348
+ let serverConfig;
1349
+ if (targetUrlOrType.toLowerCase() === 'stdio') {
1350
+ const command = parts[2];
1351
+ const args = parts.slice(3);
1352
+ serverConfig = { type: 'stdio', command, args };
1353
+ } else {
1354
+ serverConfig = { type: 'streamable-http', url: targetUrlOrType };
1355
+ }
1356
+
1357
+ const res = await ctx.agent.connectMcp(name, serverConfig);
1358
+
1359
+ runner.done({
1360
+ title: `MCP Server Connected • "${name}"`,
1361
+ lines: [
1362
+ `${C.gold}Server:${C.reset} ${C.bold}${C.white}${name}${C.reset} ${C.dim}(${serverConfig.type})${C.reset}`,
1363
+ `${C.gold}Target:${C.reset} ${C.cyberCyan}${targetUrlOrType}${C.reset}`,
1364
+ `${C.gold}Capabilities:${C.reset} ${C.emerald}${res.toolsCount} Tools${C.reset} ${C.slate}•${C.reset} ${C.cyberCyan}${res.resourcesCount} Resources${C.reset} ${C.slate}•${C.reset} ${C.lavender}${res.promptsCount} Prompts${C.reset}`,
1365
+ `${C.gold}Integration:${C.reset} ${C.dim}Tools prefixed with ${C.cyberCyan}mcp_${name}_*${C.dim} and auto-routed to agent${C.reset}`,
1366
+ '',
1367
+ `${C.dim}Tip: Run /mcp tools to inspect all registered tools or /mcp status for overview.${C.reset}`
1368
+ ],
1369
+ headerBg: '\x1b[48;2;48;54;70m',
1370
+ bg: '\x1b[48;2;36;40;52m'
1371
+ });
1372
+ } catch (err) {
1373
+ runner.fail(err);
1374
+ }
1375
+ return 'HANDLED';
1376
+ }
1377
+
1378
+ if (action === 'disconnect' || action === 'rm' || action === 'del') {
1379
+ const name = (ctx.extra || '').trim();
1380
+ if (!name) {
1381
+ console.log(`\n ${C.amber}Usage: /mcp disconnect <server_name>${C.reset}\n`);
1382
+ return 'HANDLED';
1383
+ }
1384
+ const success = await ctx.agent.disconnectMcp(name);
1385
+ if (success) {
1386
+ console.log(`\n ${C.emerald}[OK] Disconnected MCP server "${name}".${C.reset}\n`);
1387
+ } else {
1388
+ console.log(`\n ${C.amber}MCP server "${name}" not found.${C.reset}\n`);
1389
+ }
1390
+ return 'HANDLED';
1391
+ }
1392
+
1393
+ const status = mcpManager.getStatus();
1394
+ const lines = [
1395
+ `${C.gold}Active Servers:${C.reset} ${C.cyberCyan}${status.activeServersCount}${C.reset} • ${C.gold}Total MCP Tools:${C.reset} ${C.cyberCyan}${status.totalToolsCount}${C.reset}`,
1396
+ ''
1397
+ ];
1398
+
1399
+ if (status.servers.length > 0) {
1400
+ lines.push(`${C.bold}${C.lavender}Connected Servers:${C.reset}`);
1401
+ for (const s of status.servers) {
1402
+ lines.push(` ${C.emerald}●${C.reset} ${C.bold}${C.white}${s.name}${C.reset} ${C.dim}(${s.type})${C.reset} — ${s.toolsCount} tools, ${s.resourcesCount} resources`);
1403
+ }
1404
+ } else {
1405
+ lines.push(`${C.dim}No active MCP servers connected.${C.reset}`);
1406
+ lines.push(`Connect with: ${C.cyberCyan}/mcp connect <name> <url>${C.reset}`);
1407
+ }
1408
+
1409
+ printSolidPanel({
1410
+ title: `Model Context Protocol (MCP) Client`,
1411
+ lines,
1412
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1413
+ });
1414
+ return 'HANDLED';
1415
+ },
1416
+
1417
+ '/ping': async (ctx) => {
1418
+ const start = Date.now();
1419
+ const check = await ctx.client.checkConnection();
1420
+ const ms = Date.now() - start;
1421
+ if (check.connected) {
1422
+ console.log(`\n ${C.emerald}● Connected${C.reset} ${C.dim}(${ms}ms)${C.reset} — ${C.white}${check.activeModel}${C.reset}\n`);
1423
+ } else {
1424
+ console.log(`\n ${C.red}● Disconnected${C.reset} ${C.dim}(${ms}ms)${C.reset}\n`);
1425
+ }
1426
+ return 'HANDLED';
1427
+ },
1428
+
1429
+ '/config': async (ctx) => {
1430
+ const { config } = await import('./config.js');
1431
+ printSolidPanel({
1432
+ title: `Configuration`,
1433
+ lines: [
1434
+ `${C.gold}URL:${C.reset} ${C.cyberCyan}${ctx.baseUrl}${C.reset}`,
1435
+ `${C.gold}Model:${C.reset} ${C.white}${ctx.client.model}${C.reset}`,
1436
+ `${C.gold}Theme:${C.reset} ${C.white}${getThemeName()}${C.reset}`,
1437
+ `${C.gold}Max Iters:${C.reset} ${C.white}${ctx.agent.maxIterations}${C.reset}`,
1438
+ `${C.gold}Window:${C.reset} ${C.white}${ctx.agent.slidingWindowSize}${C.reset}`,
1439
+ `${C.gold}Tools:${C.reset} ${C.cyberCyan}${toolsSchema.length}${C.reset}`
1440
+ ],
1441
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1442
+ });
1443
+ return 'HANDLED';
1444
+ },
1445
+
1446
+ '/doctor': async (ctx) => {
1447
+ const { runDoctor } = await import('./doctor.js');
1448
+ const report = await runDoctor({ client: ctx.client });
1449
+ printSolidPanel({
1450
+ title: `Kiku Doctor • ${report.ok ? 'READY' : 'ACTION REQUIRED'}`,
1451
+ lines: report.checks.map(check => {
1452
+ const marker = check.ok ? '[OK]' : check.required ? '[FAIL]' : '[OPTIONAL]';
1453
+ return `${marker} ${check.id}: ${check.detail}`;
1454
+ }),
1455
+ });
1456
+ return 'HANDLED';
1457
+ },
1458
+
1459
+ '/version': async () => {
1460
+ printSolidPanel({
1461
+ title: 'Version',
1462
+ lines: [
1463
+ `${C.gold}Version:${C.reset} ${C.white}1.0.3${C.reset}`,
1464
+ `${C.gold}Node:${C.reset} ${C.white}${process.version}${C.reset}`,
1465
+ `${C.gold}Platform:${C.reset} ${C.white}${process.platform} ${process.arch}${C.reset}`,
1466
+ ],
1467
+ });
1468
+ return 'HANDLED';
1469
+ },
1470
+
1471
+ '/system': async () => {
1472
+ const os = await import('node:os');
1473
+ const mem = process.memoryUsage();
1474
+ const gpuData = await getGpuMetrics().catch(() => ({ available: false }));
1475
+
1476
+ const lines = [
1477
+ `${C.gold}OS:${C.reset} ${C.white}${os.default.platform()} ${os.default.release()}${C.reset}`,
1478
+ `${C.gold}CPU:${C.reset} ${C.white}${os.default.cpus().length} cores${C.reset}`,
1479
+ `${C.gold}RAM:${C.reset} ${C.white}${(os.default.totalmem() / 1073741824).toFixed(1)} GB${C.reset}`,
1480
+ `${C.gold}Heap:${C.reset} ${C.white}${(mem.heapUsed / 1048576).toFixed(1)} MB${C.reset}`,
1481
+ ];
1482
+
1483
+ if (gpuData.available && gpuData.primary) {
1484
+ const g = gpuData.primary;
1485
+ const vramInfo = g.usedGb != null ? ` (${g.usedGb} / ${g.totalGb} GB VRAM • ${g.usedPercent}%)` : ` (${g.totalGb} GB)`;
1486
+ const utilInfo = g.utilizationGpu != null ? ` • ${g.utilizationGpu}% load` : '';
1487
+ const tempInfo = g.temperatureC != null ? ` • ${g.temperatureC}°C` : '';
1488
+ lines.splice(3, 0, `${C.gold}GPU:${C.reset} ${C.white}${g.name}${C.reset}${C.dim}${vramInfo}${utilInfo}${tempInfo}${C.reset}`);
1489
+ } else {
1490
+ lines.splice(3, 0, `${C.gold}GPU:${C.reset} ${C.dim}No dedicated hardware acceleration detected${C.reset}`);
1491
+ }
1492
+
1493
+ printSolidPanel({
1494
+ title: 'System & Hardware',
1495
+ lines,
1496
+ });
1497
+ return 'HANDLED';
1498
+ },
1499
+
1500
+ '/gpu': async () => {
1501
+ const gpuData = await getGpuMetrics().catch(() => ({ available: false }));
1502
+
1503
+ if (!gpuData.available || !gpuData.primary) {
1504
+ printSolidPanel({
1505
+ title: 'GPU Hardware Resources',
1506
+ lines: [
1507
+ `${C.amber}No dedicated GPU hardware acceleration metrics available.${C.reset}`,
1508
+ `${C.dim}Standard CPU inference / CPU memory active.${C.reset}`,
1509
+ ]
1510
+ });
1511
+ return 'HANDLED';
1512
+ }
1513
+
1514
+ const lines = [];
1515
+ for (const [idx, g] of gpuData.gpus.entries()) {
1516
+ if (gpuData.gpus.length > 1) {
1517
+ lines.push(`${C.bold}${C.lavender}GPU ${idx + 1}:${C.reset} ${C.bold}${C.white}${g.name}${C.reset}`);
1518
+ } else {
1519
+ lines.push(`${C.gold}Device:${C.reset} ${C.bold}${C.white}${g.name}${C.reset} ${C.dim}(Driver ${g.driverVersion})${C.reset}`);
1520
+ }
1521
+
1522
+ if (g.usedGb != null && g.totalMb > 0) {
1523
+ const bar = formatProgressBar(g.usedPercent, 18);
1524
+ lines.push(`${C.gold}VRAM Usage:${C.reset} ${bar} ${C.white}${g.usedGb} / ${g.totalGb} GB${C.reset}`);
1525
+ lines.push(`${C.gold}Free VRAM:${C.reset} ${C.emerald}${g.freeMb} MB (${g.freeGb} GB free)${C.reset}`);
1526
+ } else {
1527
+ lines.push(`${C.gold}VRAM Total:${C.reset} ${C.white}${g.totalGb} GB${C.reset}`);
1528
+ }
1529
+
1530
+ if (g.utilizationGpu != null) {
1531
+ lines.push(`${C.gold}GPU Core:${C.reset} ${C.cyberCyan}${g.utilizationGpu}% Load${C.reset}`);
1532
+ }
1533
+
1534
+ if (g.temperatureC != null) {
1535
+ const tempColor = g.temperatureC >= 80 ? C.roseGlow : g.temperatureC >= 65 ? C.gold : C.emerald;
1536
+ lines.push(`${C.gold}Temperature:${C.reset} ${tempColor}${g.temperatureC}°C${C.reset}`);
1537
+ }
1538
+
1539
+ if (idx < gpuData.gpus.length - 1) lines.push('');
1540
+ }
1541
+
1542
+ printSolidPanel({
1543
+ title: `GPU Hardware Resources • ${gpuData.primary.name}`,
1544
+ lines,
1545
+ headerBg: '\x1b[48;2;48;54;70m',
1546
+ bg: '\x1b[48;2;36;40;52m'
1547
+ });
1548
+ return 'HANDLED';
1549
+ },
1550
+
1551
+ '/about': async () => {
1552
+ printSolidPanel({
1553
+ title: 'About Kiku AI Agent',
1554
+ lines: [
1555
+ `${C.white}Intelligent assistant for Ju Jingyi (鞠婧祎)${C.reset}`,
1556
+ `${C.gold}Engine:${C.reset} ${C.white}LM Studio + Gemma 4${C.reset}`,
1557
+ `${C.gold}Tools:${C.reset} ${C.white}${toolsSchema.length} registered${C.reset}`,
1558
+ `${C.gold}Languages:${C.reset} ${C.white}TH, ZH, EN (auto)${C.reset}`,
1559
+ ],
1560
+ });
1561
+ return 'HANDLED';
1562
+ },
1563
+
1564
+ '/login': async (ctx) => {
1565
+ console.log(`\n ${C.cyberCyan}YouTube Login${C.reset}\n`);
1566
+ resetCookieCheck();
1567
+ const result = await ensureYouTubeCookies({ timeout: 180000 });
1568
+ if (result.ok) {
1569
+ console.log(` ${C.emerald}[OK] Session active (${result.source}).${C.reset}\n`);
1570
+ } else {
1571
+ console.log(` ${C.red}[ERROR] Not available: ${result.reason}${C.reset}\n`);
1572
+ }
1573
+ return 'HANDLED';
1574
+ },
1575
+
1576
+ '/ytstatus': async () => {
1577
+ const status = getCookieStatus();
1578
+ const ensured = status.ensured;
1579
+ printSolidPanel({
1580
+ title: 'YouTube Status',
1581
+ lines: [
1582
+ `${C.gold}Browser:${C.reset} ${status.envBrowser ? C.emerald + status.envBrowser : C.dim + 'n/a'}${C.reset}`,
1583
+ `${C.gold}Cookies:${C.reset} ${status.envCookies ? C.emerald + status.envCookies : C.dim + 'n/a'}${C.reset}`,
1584
+ `${C.gold}Status:${C.reset} ${ensured ? (ensured.ok ? C.emerald + '[OK]' : C.red + '[ERROR] Failed') : C.dim + 'Unchecked'}${C.reset}`
1585
+ ],
1586
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1587
+ });
1588
+ return 'HANDLED';
1589
+ },
1590
+
1591
+ '/queue': async (ctx) => {
1592
+ const { sub } = ctx;
1593
+ if (sub === 'clear') {
1594
+ if (audioPlayer.isPlaying || audioPlayer.queue.length > 0) {
1595
+ console.log(`\n ${C.emerald}${await audioPlayer.clearQueue()}${C.reset}\n`);
1596
+ } else {
1597
+ ctx.clearQueryQueue();
1598
+ console.log(`\n ${C.emerald}[OK] Execution queue cleared.${C.reset}\n`);
1599
+ }
1600
+ } else if (audioPlayer.isPlaying || audioPlayer.queue.length > 0) {
1601
+ const queue = audioPlayer.getQueue();
1602
+ printSolidPanel({
1603
+ title: `${UI_ICONS.music} Queue (${queue.length})`,
1604
+ lines: queue.map((item, idx) => `${item.isCurrent ? C.gold + '[PLAYING]' : C.dim + (idx + 1)} ${C.white}${item.title}${C.reset}`),
1605
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1606
+ });
1607
+ } else {
1608
+ const qLen = ctx.getQueryQueueLength();
1609
+ console.log(`\n ${C.slate}Execution queue: ${qLen} items${C.reset}\n`);
1610
+ }
1611
+ return 'HANDLED';
1612
+ },
1613
+
1614
+ '/stats': async () => {
1615
+ const result = await executeTool('get_system_stats', {});
1616
+ const s = result.stats;
1617
+ printSolidPanel({
1618
+ title: `Database Overview`,
1619
+ lines: [
1620
+ `${C.gold}Works:${C.reset} ${C.cyberCyan}${s.totalWorks}${C.reset}`,
1621
+ `${C.gold}Songs:${C.reset} ${C.cyberCyan}${s.totalUniqueSongs}${C.reset}`,
1622
+ `${C.gold}Brands:${C.reset} ${C.cyberCyan}${s.brandEndorsements}${C.reset}`,
1623
+ `${C.gold}Awards:${C.reset} ${C.cyberCyan}${s.totalAwards}${C.reset}`,
1624
+ `${C.gold}Timeline:${C.reset} ${C.cyberCyan}${s.timelineMilestones}${C.reset}`
1625
+ ],
1626
+ headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
1627
+ });
1628
+ return 'HANDLED';
1629
+ },
1630
+
1631
+ '/prompt': async (ctx) => {
1632
+ const { config } = await import('./config.js');
1633
+ const lines = config.agent.systemPersona.split('\n').slice(0, 20).map(l => `${C.dim}${C.white}${l}${C.reset}`);
1634
+ printSolidPanel({ title: `System Prompt`, lines, headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m' });
1635
+ return 'HANDLED';
1636
+ },
1637
+
1638
+ '/ads': async (ctx) => {
1639
+ ctx.printAd();
1640
+ return 'HANDLED';
1641
+ },
1642
+ };
1643
+
1644
+ const ALIASES = {
1645
+ '/quit': '/exit', '/q': '/exit',
1646
+ '/?': '/help', '/commands': '/help', '/menu': '/help',
1647
+ '/hist': '/history', '/h': '/history',
1648
+ '/sess': '/session', '/sessions': '/session',
1649
+ '/think': '/thinking', '/th': '/thinking',
1650
+ '/detail': '/details', '/d': '/details',
1651
+ '/language': '/lang',
1652
+ '/themes': '/theme',
1653
+ '/exec': '/tool', '/call': '/tool',
1654
+ '/toollist': '/tools',
1655
+ '/metrics': '/stats', '/catalog': '/stats',
1656
+ '/llm': '/model', '/engine': '/model',
1657
+ '/health': '/ping',
1658
+ '/settings': '/config', '/conf': '/config',
1659
+ '/persona': '/prompt', '/sysprompt': '/prompt',
1660
+ '/ver': '/version', '/v': '/version',
1661
+ '/sys': '/system', '/os': '/system',
1662
+ '/vram': '/gpu', '/gpus': '/gpu',
1663
+ '/info': '/about',
1664
+ '/sponsor': '/ads', '/ad': '/ads',
1665
+ '/cls': '/clear', '/reset': '/clear',
1666
+ '/export': '/save',
1667
+ '/r': '/retry',
1668
+ };
1669
+
1670
+ export async function routeCommand(rawInput, ctx) {
1671
+ const trimmed = rawInput.trim();
1672
+ if (!trimmed.startsWith('/')) return 'UNKNOWN';
1673
+
1674
+ const parts = trimmed.split(/\s+/);
1675
+ let rawCmd = parts[0].toLowerCase();
1676
+ const sub = parts[1] || '';
1677
+ const extra = parts.slice(2).join(' ');
1678
+
1679
+ if (ALIASES[rawCmd]) rawCmd = ALIASES[rawCmd];
1680
+
1681
+ const handler = handlers[rawCmd];
1682
+ if (!handler) {
1683
+ const suggestion = findClosestCommand(rawCmd);
1684
+ console.log(`\n ${C.amber}Unknown command: "${rawCmd}"${C.reset}`);
1685
+ if (suggestion) {
1686
+ console.log(` ${C.dim}Did you mean ${C.bold}${C.cyberCyan}${suggestion}${C.reset}${C.dim}?${C.reset}\n`);
1687
+ } else {
1688
+ console.log(` ${C.dim}Type ${C.emerald}/help${C.reset}${C.dim} for available commands.${C.reset}\n`);
1689
+ }
1690
+ return 'UNKNOWN';
1691
+ }
1692
+
1693
+ return await handler({ ...ctx, sub, extra, parts });
1694
+ }