kiku-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +122 -0
  2. package/package.json +85 -0
  3. package/server/agent/agentCore.js +782 -0
  4. package/server/agent/agentState.js +140 -0
  5. package/server/agent/audioPlayer.js +673 -0
  6. package/server/agent/cli.js +1111 -0
  7. package/server/agent/commandRegistry.js +58 -0
  8. package/server/agent/commands.js +788 -0
  9. package/server/agent/config.js +90 -0
  10. package/server/agent/errors.js +9 -0
  11. package/server/agent/eventBus.js +103 -0
  12. package/server/agent/llmClient.js +200 -0
  13. package/server/agent/memory.js +566 -0
  14. package/server/agent/sanitizer.js +314 -0
  15. package/server/agent/session.js +239 -0
  16. package/server/agent/theme.js +124 -0
  17. package/server/agent/tools.js +1528 -0
  18. package/server/agent/ui.js +469 -0
  19. package/server/agent/webSearch.js +719 -0
  20. package/server/agent/youtubeAuth.js +299 -0
  21. package/server/mcp/claude-desktop-config.example.json +9 -0
  22. package/server/mcp/data/awards.js +14 -0
  23. package/server/mcp/data/brands.js +9 -0
  24. package/server/mcp/data/discography.js +36 -0
  25. package/server/mcp/data/gallery.js +25 -0
  26. package/server/mcp/data/index.js +7 -0
  27. package/server/mcp/data/profile.js +44 -0
  28. package/server/mcp/data/timeline.js +16 -0
  29. package/server/mcp/data/works.js +104 -0
  30. package/server/mcp/handler.js +159 -0
  31. package/server/mcp/prompts.js +143 -0
  32. package/server/mcp/resources.js +54 -0
  33. package/server/mcp/security.js +138 -0
  34. package/server/mcp/server.js +100 -0
  35. package/server/mcp/stdio.js +33 -0
  36. package/server/mcp/tools.js +126 -0
  37. package/src/data/awardsData.js +146 -0
  38. package/src/data/brandsData.js +249 -0
  39. package/src/data/discographyData.js +874 -0
  40. package/src/data/galleryData.js +666 -0
  41. package/src/data/musicVideoData.js +406 -0
  42. package/src/data/profileData.js +156 -0
  43. package/src/data/socialUpdatesData.js +381 -0
  44. package/src/data/timelineData.js +172 -0
  45. package/src/data/worksData.js +516 -0
@@ -0,0 +1,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 } 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
+ }