kiku-agent 1.0.7 → 1.0.8

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.
@@ -4,13 +4,16 @@ import { ensureYouTubeCookies, resetCookieCheck, getCookieStatus } from './youtu
4
4
  import { AgentMemory } from './memory.js';
5
5
  import {
6
6
  C, UI_ICONS, printSolidPanel, formatTerminalMarkdown, formatMarkdownLines, randomFanName, createProgressRunner, wrapText, getFullWidth, getWelcomeHeroLines, sanitizeTerminalText,
7
- formatRichCardLines, printRichCard, printRichStatusCard, formatStatusBadge, formatRichTable
7
+ formatRichCardLines, printRichCard, printRichStatusCard, formatStatusBadge, formatRichTable, getStringDisplayWidth
8
8
  } from './ui.js';
9
9
  import { COMMAND_NAMES, getCommandDetails, findCommandDefinition } from './commandRegistry.js';
10
10
  import { THEMES, THEME_NAMES, ansiBackground, ansiForeground, getThemeName, setTheme } from './theme.js';
11
11
  import { getGpuMetrics, formatProgressBar } from './gpuMetrics.js';
12
12
  import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
13
13
  import { UpdateError, UpdateManager, UPDATE_CHANNELS } from './updateManager.js';
14
+ import { QUIZ_QUESTIONS, QUIZ_I18N, LETTER_LABELS, shuffleQuizQuestions } from './quizData.js';
15
+ import crypto from 'node:crypto';
16
+ import { featureCommandHandlers, aiNarration, languageHint } from './commandFeatures.js';
14
17
 
15
18
  function levenshteinDistance(a, b) {
16
19
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -85,6 +88,7 @@ export function fuzzyMatchTools(query) {
85
88
  }
86
89
 
87
90
  export const handlers = {
91
+ ...featureCommandHandlers,
88
92
  '/exit': async (ctx) => {
89
93
  try { await ctx.agent.endSession(); } catch {}
90
94
  console.log(`\n ${C.sakura}Session archived. Thank you for supporting Ju Jingyi. Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
@@ -682,6 +686,34 @@ export const handlers = {
682
686
  return 'HANDLED';
683
687
  }
684
688
 
689
+ if (sub === 'summary' || sub === 'summarize') {
690
+ const messages = (agent?.session?.messages || []).map(m => {
691
+ const textPart = m.parts?.find(p => p.type === 'text');
692
+ return { role: m.role, text: textPart ? textPart.content : '' };
693
+ }).filter(m => m.text);
694
+ if (messages.length === 0) {
695
+ printSolidPanel({
696
+ title: 'History Summary',
697
+ lines: [`${C.dim}No conversation history to summarize.${C.reset}`],
698
+ headerBg: '\x1b[48;2;48;54;70m',
699
+ bg: '\x1b[48;2;36;40;52m',
700
+ });
701
+ return 'HANDLED';
702
+ }
703
+ const summary = await aiNarration(ctx, {
704
+ system: `You are the Kiku session archivist. In 2-3 concise sentences, summarize the key topics, queries, and findings discussed across this conversation history. ${languageHint(ctx)} Do not invent facts. No markdown.`,
705
+ user: JSON.stringify({ messageCount: messages.length, messages: messages.slice(-15) }),
706
+ limit: 450,
707
+ });
708
+ printSolidPanel({
709
+ title: `History Summary • ${messages.length} message${messages.length === 1 ? '' : 's'}`,
710
+ lines: summary ? [` ${C.white}${summary}${C.reset}`] : [`${C.dim}Could not generate history summary (requires active chat model).${C.reset}`],
711
+ headerBg: '\x1b[48;2;48;54;70m',
712
+ bg: '\x1b[48;2;36;40;52m',
713
+ });
714
+ return 'HANDLED';
715
+ }
716
+
685
717
  if (sub === 'search' && extra) {
686
718
  const q = extra.toLowerCase().trim();
687
719
  const matches = [];
@@ -768,6 +800,39 @@ export const handlers = {
768
800
  return 'HANDLED';
769
801
  }
770
802
 
803
+ if (action === 'summary' || action === 'summarize') {
804
+ const messages = (sess?.messages || []).map(m => {
805
+ const textPart = m.parts?.find(p => p.type === 'text');
806
+ return { role: m.role, text: textPart ? textPart.content : '' };
807
+ }).filter(m => m.text);
808
+ if (messages.length === 0) {
809
+ printSolidPanel({
810
+ title: 'Session Summary',
811
+ lines: [`${C.dim}No messages in the active session to summarize.${C.reset}`],
812
+ headerBg: '\x1b[48;2;48;54;70m',
813
+ bg: '\x1b[48;2;36;40;52m',
814
+ });
815
+ return 'HANDLED';
816
+ }
817
+ const summary = await aiNarration(ctx, {
818
+ system: `You are the Kiku session analyst. In 2-3 concise sentences, summarize what this session achieved, what tools were used, and key takeaways for the user. ${languageHint(ctx)} Do not invent facts. No markdown.`,
819
+ user: JSON.stringify({
820
+ sessionId: sess.id,
821
+ totalQueries: sess.stats?.totalQueries || 0,
822
+ totalToolCalls: sess.stats?.totalToolCalls || 0,
823
+ recentExchanges: messages.slice(-10),
824
+ }),
825
+ limit: 450,
826
+ });
827
+ printSolidPanel({
828
+ title: `Session Summary • ${sess.id}`,
829
+ lines: summary ? [` ${C.white}${summary}${C.reset}`] : [`${C.dim}Could not generate session summary (requires active chat model).${C.reset}`],
830
+ headerBg: '\x1b[48;2;48;54;70m',
831
+ bg: '\x1b[48;2;36;40;52m',
832
+ });
833
+ return 'HANDLED';
834
+ }
835
+
771
836
  if (action === 'list') {
772
837
  const sessions = agent.memory.getArchivedSessions(15);
773
838
  printSolidPanel({
@@ -1021,9 +1086,36 @@ export const handlers = {
1021
1086
  printSolidPanel({
1022
1087
  title: 'Topic Profile',
1023
1088
  lines: topics.map(t => {
1024
- const bar = '█'.repeat(Math.min(t.count, 15));
1025
- return `${C.gold}${t.topic.padEnd(14)}${C.reset} ${C.cyberCyan}${bar}${C.reset} ${C.dim}(${t.count}x)${C.reset}`;
1089
+ const bar = '█'.repeat(Math.min(t.count, 15));
1090
+ const pad = ' '.repeat(Math.max(0, 14 - getStringDisplayWidth(t.topic)));
1091
+ return `${C.gold}${t.topic}${pad}${C.reset} ${C.cyberCyan}${bar}${C.reset} ${C.dim}(${t.count}x)${C.reset}`;
1092
+ }),
1093
+ });
1094
+ return 'HANDLED';
1095
+ }
1096
+
1097
+ if (memAction === 'summary' || memAction === 'summarize' || memAction === 'profile') {
1098
+ const facts = typeof agent.memory?.getFacts === 'function' ? agent.memory.getFacts() : (agent.memory?.facts || []);
1099
+ const topics = typeof agent.memory?.getTopTopics === 'function' ? agent.memory.getTopTopics(10) : [];
1100
+ if (!facts.length && !topics.length) {
1101
+ printSolidPanel({
1102
+ title: 'Memory Profile Summary',
1103
+ lines: [`${C.dim}No long-term memories or topics stored yet. Use /mem add <fact> first.${C.reset}`]
1104
+ });
1105
+ return 'HANDLED';
1106
+ }
1107
+ const summary = await aiNarration(ctx, {
1108
+ system: `You are the Kiku persona & memory archivist. Synthesize a concise profile (2-3 sentences) of what this user knows, likes, or tracks about Ju Jingyi based on their saved facts and topics. ${languageHint(ctx)} Do not invent facts. No markdown.`,
1109
+ user: JSON.stringify({
1110
+ storedFactsCount: facts.length,
1111
+ sampleFacts: facts.slice(0, 10).map(f => f.content),
1112
+ topTopics: topics,
1026
1113
  }),
1114
+ limit: 450,
1115
+ });
1116
+ printSolidPanel({
1117
+ title: `Memory Profile Summary • ${facts.length} fact${facts.length === 1 ? '' : 's'} • ${topics.length} topic${topics.length === 1 ? '' : 's'}`,
1118
+ lines: summary ? [` ${C.white}${summary}${C.reset}`] : [`${C.dim}Could not generate memory profile summary (requires active chat model).${C.reset}`],
1027
1119
  });
1028
1120
  return 'HANDLED';
1029
1121
  }
@@ -1198,7 +1290,7 @@ export const handlers = {
1198
1290
 
1199
1291
  if (!targetProvider) {
1200
1292
  console.log(`\n ${C.amber}Usage: /provider use <provider_name> [model_id]${C.reset}`);
1201
- console.log(` ${C.dim}Example: /provider use groq openai/gpt-oss-120b${C.reset}\n`);
1293
+ console.log(` ${C.dim}Example: /provider use groq openai/gpt-oss-120b${C.reset}\n`);
1202
1294
  return 'HANDLED';
1203
1295
  }
1204
1296
 
@@ -1206,16 +1298,16 @@ export const handlers = {
1206
1298
  const options = targetModel ? { model: targetModel } : {};
1207
1299
  const info = ctx.client.switchProvider(targetProvider, options);
1208
1300
  if (ctx.agent?.client) ctx.agent.client.switchProvider(targetProvider, options);
1209
- if (ctx.modelManager) {
1210
- ctx.modelManager.client = ctx.client;
1211
- ctx.modelManager.requestedModel = info.model;
1212
- const resolution = await ctx.modelManager.resolve({ requestedModel: info.model });
1213
- if (resolution.connected && resolution.usedFallback) {
1214
- console.log(` ${C.amber}• Requested model unavailable; using ${resolution.selectedModel}${C.reset}`);
1215
- } else if (!resolution.connected) {
1216
- console.log(` ${C.amber}• Provider is not ready: ${resolution.error}${C.reset}`);
1217
- }
1218
- }
1301
+ if (ctx.modelManager) {
1302
+ ctx.modelManager.client = ctx.client;
1303
+ ctx.modelManager.requestedModel = info.model;
1304
+ const resolution = await ctx.modelManager.resolve({ requestedModel: info.model });
1305
+ if (resolution.connected && resolution.usedFallback) {
1306
+ console.log(` ${C.amber}• Requested model unavailable; using ${resolution.selectedModel}${C.reset}`);
1307
+ } else if (!resolution.connected) {
1308
+ console.log(` ${C.amber}• Provider is not ready: ${resolution.error}${C.reset}`);
1309
+ }
1310
+ }
1219
1311
  ctx.baseUrl = info.baseUrl;
1220
1312
 
1221
1313
  console.log(`\n ${C.emerald}[OK] Switched provider to ${C.bold}${info.providerName}${C.reset} (${info.provider})`);
@@ -1683,12 +1775,18 @@ export const handlers = {
1683
1775
  for (const s of status.servers) {
1684
1776
  const serverName = sanitizeTerminalText(s.name, 128) || 'unnamed';
1685
1777
  const serverType = sanitizeTerminalText(s.type, 80) || 'unknown';
1778
+ const sourceNames = (s.sources || [])
1779
+ .map(source => sanitizeTerminalText(String(source).split(/[\\/]/).pop(), 128))
1780
+ .filter(Boolean);
1781
+ const sourceLabel = sourceNames.length > 0
1782
+ ? ` ${C.slate}• source: ${[...new Set(sourceNames)].join(', ')}${C.reset}`
1783
+ : '';
1686
1784
  if (s.status === 'connected') {
1687
- lines.push(` ${C.emerald}● Connected:${C.reset} ${C.bold}${C.white}${serverName}${C.reset} ${C.dim}(${serverType})${C.reset} — ${C.emerald}${s.toolsCount} tools${C.reset}, ${s.resourcesCount} resources`);
1785
+ lines.push(` ${C.emerald}● Connected:${C.reset} ${C.bold}${C.white}${serverName}${C.reset} ${C.dim}(${serverType})${C.reset} — ${C.emerald}${s.toolsCount} tools${C.reset}, ${s.resourcesCount} resources${sourceLabel}`);
1688
1786
  } else if (s.status === 'saved_offline') {
1689
- lines.push(` ${C.slate}○ Saved (Offline):${C.reset} ${C.bold}${C.white}${serverName}${C.reset} ${C.dim}(${serverType})${C.reset} — ${C.dim}type /mcp reconnect ${serverName} to start${C.reset}`);
1787
+ lines.push(` ${C.slate}○ Saved (Offline):${C.reset} ${C.bold}${C.white}${serverName}${C.reset} ${C.dim}(${serverType})${C.reset} — ${C.dim}type /mcp reconnect ${serverName} to start${C.reset}${sourceLabel}`);
1690
1788
  } else {
1691
- lines.push(` ${C.red}✕ Error:${C.reset} ${C.bold}${C.white}${serverName}${C.reset} ${C.dim}(${serverType})${C.reset} — ${C.red}${s.error || 'Connection failed'}${C.reset}`);
1789
+ lines.push(` ${C.red}✕ Error:${C.reset} ${C.bold}${C.white}${serverName}${C.reset} ${C.dim}(${serverType})${C.reset} — ${C.red}${s.error || 'Connection failed'}${C.reset}${sourceLabel}`);
1692
1790
  }
1693
1791
  }
1694
1792
 
@@ -2108,10 +2206,15 @@ export const handlers = {
2108
2206
  return 'HANDLED';
2109
2207
  },
2110
2208
 
2111
- '/stats': async () => {
2209
+ '/stats': async (ctx) => {
2112
2210
  try {
2113
2211
  const result = await executeTool('get_system_stats', {});
2114
2212
  const s = result.stats || {};
2213
+ const insight = await aiNarration(ctx, {
2214
+ system: `You are the Kiku data narrator. In ONE short sentence, point out the most interesting thing about this catalog snapshot. ${languageHint(ctx)} Mention only the numbers provided. Never invent numbers. No markdown.`,
2215
+ user: JSON.stringify({ totalWorks: s.totalWorks || 0, totalSongs: s.totalUniqueSongs || 0, brands: s.brandEndorsements || 0, awards: s.totalAwards || 0, timeline: s.timelineMilestones || 0 }),
2216
+ limit: 240,
2217
+ });
2115
2218
  printSolidPanel({
2116
2219
  title: `Database Overview`,
2117
2220
  lines: [
@@ -2119,7 +2222,8 @@ export const handlers = {
2119
2222
  `${C.gold}Songs:${C.reset} ${C.cyberCyan}${s.totalUniqueSongs || 0}${C.reset}`,
2120
2223
  `${C.gold}Brands:${C.reset} ${C.cyberCyan}${s.brandEndorsements || 0}${C.reset}`,
2121
2224
  `${C.gold}Awards:${C.reset} ${C.cyberCyan}${s.totalAwards || 0}${C.reset}`,
2122
- `${C.gold}Timeline:${C.reset} ${C.cyberCyan}${s.timelineMilestones || 0}${C.reset}`
2225
+ `${C.gold}Timeline:${C.reset} ${C.cyberCyan}${s.timelineMilestones || 0}${C.reset}`,
2226
+ ...(insight ? ['', ` ${C.slate}${insight}${C.reset}`] : []),
2123
2227
  ],
2124
2228
  headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
2125
2229
  });
@@ -2204,6 +2308,261 @@ export const handlers = {
2204
2308
  ctx.printAd();
2205
2309
  return 'HANDLED';
2206
2310
  },
2311
+
2312
+ '/gallery': async (ctx) => {
2313
+ const displayed = typeof ctx.showTerminalImage === 'function' && ctx.showTerminalImage();
2314
+ if (!displayed) {
2315
+ printRichCard({
2316
+ title: 'Terminal Gallery Unavailable',
2317
+ status: 'warning',
2318
+ lines: [
2319
+ 'Images require an interactive color terminal and packaged files in src/assets/images.',
2320
+ 'Set KIKU_TUI_IMAGES=on to override terminal detection, or off to disable images.',
2321
+ ],
2322
+ });
2323
+ }
2324
+ return 'HANDLED';
2325
+ },
2326
+
2327
+ '/quiz': async (ctx) => {
2328
+ const { sub, extra } = ctx;
2329
+ const langArg = (sub === 'lang' ? extra : sub || '').toLowerCase().trim();
2330
+ const quizLang = ['th', 'en', 'zh'].includes(langArg) ? langArg : (ctx.agent?.memory?.preferences?.language || 'en');
2331
+ const i18n = QUIZ_I18N[quizLang] || QUIZ_I18N.en;
2332
+
2333
+ const shuffled = shuffleQuizQuestions(QUIZ_QUESTIONS);
2334
+ let score = 0;
2335
+ let challengeToken = null;
2336
+ let isOnline = false;
2337
+
2338
+ printSolidPanel({
2339
+ title: i18n.title,
2340
+ lines: [
2341
+ `${C.bold}${C.white}${i18n.subtitle}${C.reset}`,
2342
+ `${C.dim}Language: ${quizLang.toUpperCase()} | Questions: 15 | Target: 15/15${C.reset}`,
2343
+ ],
2344
+ bg: '\x1b[48;2;48;40;60m'
2345
+ });
2346
+
2347
+
2348
+ const baseUrl = ctx.baseUrl || process.env.KIKU_API_URL || 'http://localhost:3000';
2349
+ const controller = new AbortController();
2350
+ const timeout = setTimeout(() => controller.abort(), 3000);
2351
+ try {
2352
+ const res = await fetch(`${baseUrl}/api/v1/quiz/challenge`, { signal: controller.signal });
2353
+ if (res.ok) {
2354
+ const json = await res.json();
2355
+ challengeToken = json.data?.challengeToken;
2356
+ isOnline = true;
2357
+ }
2358
+ } catch {
2359
+
2360
+ } finally {
2361
+ clearTimeout(timeout);
2362
+ }
2363
+
2364
+ if (isOnline) {
2365
+ console.log(` ${C.dim}Connected to server. Quiz answers will be verified via API.${C.reset}`);
2366
+ } else {
2367
+ console.log(` ${C.dim}Offline mode. Answers verified locally — key generated on completion.${C.reset}`);
2368
+ }
2369
+
2370
+ const answers = [];
2371
+ const stdin = process.stdin;
2372
+ const wasRaw = stdin.isTTY && stdin.isRaw;
2373
+ let rl = null;
2374
+
2375
+ if (typeof ctx.readInput !== 'function') {
2376
+ const { createInterface } = await import('node:readline');
2377
+ if (stdin.isTTY) {
2378
+ try { stdin.setRawMode(false); } catch {}
2379
+ }
2380
+ rl = createInterface({ input: stdin, output: process.stdout });
2381
+ }
2382
+
2383
+ const askQuestion = () => {
2384
+ if (typeof ctx.readInput === 'function') {
2385
+ return ctx.readInput({
2386
+ prompt: i18n.answerPrompt,
2387
+ placeholder: i18n.answerPlaceholder,
2388
+ history: false,
2389
+ cancelValue: 'q',
2390
+ });
2391
+ }
2392
+ return new Promise(resolve => rl.question(` ${C.roseGlow}>${C.reset} `, resolve));
2393
+ };
2394
+
2395
+ let cancelled = false;
2396
+ try {
2397
+ for (let i = 0; i < shuffled.length; i++) {
2398
+ const q = shuffled[i];
2399
+ const numberedText = q.q[quizLang] || q.q.en;
2400
+ const qText = numberedText.replace(/^\s*\d+[.)]\s*/, '');
2401
+ const opts = q.options[quizLang] || q.options.en;
2402
+
2403
+ printSolidPanel({
2404
+ title: `${i18n.questionOf(i + 1, shuffled.length)} • ${i18n.score(score, shuffled.length)}`,
2405
+ lines: [
2406
+ `${C.bold}${C.white}${qText}${C.reset}`,
2407
+ '',
2408
+ ...opts.map((opt, idx) => ` ${C.bold}${C.cyberCyan}${LETTER_LABELS[idx]})${C.reset} ${C.white}${opt}${C.reset}`),
2409
+ ],
2410
+ bg: '\x1b[48;2;36;40;52m'
2411
+ });
2412
+
2413
+ let answeredCorrectly = false;
2414
+ while (!answeredCorrectly) {
2415
+ const input = String(await askQuestion() ?? '').trim().toLowerCase();
2416
+
2417
+ if (['q', 'quit', 'exit', 'cancel'].includes(input)) {
2418
+ cancelled = true;
2419
+ break;
2420
+ }
2421
+ if (input === 'h' || input === 'hint') {
2422
+ const hintText = q.hint[quizLang] || q.hint.en;
2423
+ console.log(` ${C.amber}${i18n.hint}:${C.reset} ${C.dim}${hintText}${C.reset}`);
2424
+ continue;
2425
+ }
2426
+
2427
+ let idx = -1;
2428
+ if (['a', 'b', 'c', 'd'].includes(input)) idx = input.charCodeAt(0) - 97;
2429
+ else if (['1', '2', '3', '4'].includes(input)) idx = Number.parseInt(input, 10) - 1;
2430
+
2431
+ if (idx < 0 || idx >= opts.length) {
2432
+ console.log(` ${C.amber}${i18n.invalidAnswer}${C.reset}`);
2433
+ continue;
2434
+ }
2435
+
2436
+ const selectedLabel = LETTER_LABELS[idx];
2437
+ const selectedText = opts[idx];
2438
+ if (idx === q.answer) {
2439
+ answers.push({
2440
+ qIndex: i,
2441
+ answerIndex: q.optionOrder?.[idx] ?? idx,
2442
+ });
2443
+ score++;
2444
+ answeredCorrectly = true;
2445
+ console.log(` ${C.emerald}✓ ${i18n.correct.trim()}${C.reset} ${C.dim}— ${selectedLabel}) ${selectedText}${C.reset}`);
2446
+ } else {
2447
+ console.log(` ${C.red}${i18n.incorrect}${C.reset} ${C.dim}— ${selectedLabel}) ${selectedText}${C.reset}`);
2448
+ }
2449
+ }
2450
+
2451
+ if (cancelled) break;
2452
+ }
2453
+ } finally {
2454
+ rl?.close();
2455
+ if (wasRaw && stdin.isTTY) {
2456
+ try { stdin.setRawMode(true); } catch {}
2457
+ }
2458
+ }
2459
+
2460
+ if (cancelled) {
2461
+ console.log(` ${C.amber}${i18n.cancelled}${C.reset}`);
2462
+ return 'HANDLED';
2463
+ }
2464
+
2465
+ printSolidPanel({
2466
+ title: `${i18n.score(score, shuffled.length)}`,
2467
+ lines: [`${C.bold}${score === 15 ? C.emerald : C.amber}${score === 15 ? 'Perfect Score!' : `${score}/15 — Need 15/15 to claim a VIP Key.`}${C.reset}`],
2468
+ });
2469
+
2470
+ if (score < 15) {
2471
+ console.log(`\n ${C.dim}Run ${C.cyberCyan}/quiz${C.reset}${C.dim} to try again.${C.reset}\n`);
2472
+ return 'HANDLED';
2473
+ }
2474
+
2475
+ console.log(`\n ${C.amber}${i18n.generatingKey}${C.reset}\n`);
2476
+
2477
+ let key = null;
2478
+
2479
+
2480
+ if (isOnline && challengeToken) {
2481
+ try {
2482
+ const res = await fetch(`${baseUrl}/api/v1/keys/claim`, {
2483
+ method: 'POST',
2484
+ headers: { 'Content-Type': 'application/json' },
2485
+ body: JSON.stringify({ score, answers, challengeToken })
2486
+ });
2487
+ const json = await res.json();
2488
+ if (json.status === 'success' && json.data?.apiKey) {
2489
+ key = json.data.apiKey;
2490
+ }
2491
+ } catch {
2492
+
2493
+ }
2494
+ }
2495
+
2496
+
2497
+ if (!key) {
2498
+ const token = crypto.randomBytes(16).toString('hex');
2499
+ key = `kiku_vip_sk_${token}`;
2500
+ }
2501
+
2502
+ ctx.agent?.memory?.setPreference('vipKey', key);
2503
+
2504
+ printSolidPanel({
2505
+ title: `${i18n.congratsTitle}`,
2506
+ lines: [
2507
+ `${C.bold}${C.white}${i18n.congratsDesc}${C.reset}`,
2508
+ '',
2509
+ `${C.bold}${C.roseGlow}${i18n.keyLabel}:${C.reset}`,
2510
+ ` ${C.gold}${key}${C.reset}`,
2511
+ '',
2512
+ `${C.dim}Usage: header 'X-API-Key: ${key}' or query '?apiKey=${key}'${C.reset}`,
2513
+ `${C.dim}Key saved automatically. Use ${C.cyberCyan}/key show${C.reset}${C.dim} to view or ${C.cyberCyan}/key clear${C.reset}${C.dim} to remove.${C.reset}`,
2514
+ ],
2515
+ bg: '\x1b[48;2;48;40;60m'
2516
+ });
2517
+
2518
+ return 'HANDLED';
2519
+ },
2520
+
2521
+ '/key': async (ctx) => {
2522
+ const { sub, extra } = ctx;
2523
+ const action = (sub || 'show').toLowerCase();
2524
+ const mem = ctx.agent?.memory;
2525
+
2526
+ if (action === 'clear') {
2527
+ mem?.setPreference('vipKey', null);
2528
+ console.log(`\n ${C.emerald}[OK] ${QUIZ_I18N.en.keyCleared}${C.reset}\n`);
2529
+ return 'HANDLED';
2530
+ }
2531
+
2532
+ if (action === 'apply') {
2533
+ const key = extra.trim();
2534
+ if (!key) {
2535
+ console.log(`\n ${C.amber}Usage: /key apply <kiku_vip_sk_...>${C.reset}\n`);
2536
+ return 'HANDLED';
2537
+ }
2538
+ mem?.setPreference('vipKey', key);
2539
+ printSolidPanel({
2540
+ title: 'VIP Key Applied',
2541
+ lines: [
2542
+ `${C.emerald}[OK] Key saved and ready to use!${C.reset}`,
2543
+ `${C.gold}Key:${C.reset} ${C.white}${key}${C.reset}`,
2544
+ ],
2545
+ });
2546
+ return 'HANDLED';
2547
+ }
2548
+
2549
+ const storedKey = mem?.preferences?.vipKey;
2550
+ if (storedKey) {
2551
+ printSolidPanel({
2552
+ title: 'Developer VIP Key',
2553
+ lines: [
2554
+ `${C.gold}Key:${C.reset} ${C.bold}${C.white}${storedKey}${C.reset}`,
2555
+ `${C.gold}Tier:${C.reset} ${C.emerald}Developer VIP (1,000 req/min)${C.reset}`,
2556
+ `${C.gold}Usage:${C.reset} ${C.dim}X-API-Key: ${storedKey}${C.reset}`,
2557
+ '',
2558
+ `${C.dim}Clear: /key clear | Apply new: /key apply <key>${C.reset}`,
2559
+ ],
2560
+ });
2561
+ } else {
2562
+ console.log(`\n ${C.amber}${QUIZ_I18N.en.noKey}${C.reset}\n`);
2563
+ }
2564
+ return 'HANDLED';
2565
+ },
2207
2566
  };
2208
2567
 
2209
2568
  const ALIASES = {
@@ -2220,6 +2579,12 @@ const ALIASES = {
2220
2579
  '/suggest': '/suggestion', '/suggestions': '/suggestion',
2221
2580
  '/exec': '/tool', '/call': '/tool',
2222
2581
  '/toollist': '/tools',
2582
+ '/lookup': '/find', '/findall': '/find',
2583
+ '/cmp': '/compare',
2584
+ '/fav': '/favorites', '/favs': '/favorites',
2585
+ '/latest': '/news',
2586
+ '/job': '/jobs',
2587
+ '/random': '/discover',
2223
2588
  '/metrics': '/stats', '/catalog': '/stats',
2224
2589
  '/llm': '/model', '/engine': '/model',
2225
2590
  '/health': '/ping',
@@ -2230,9 +2595,11 @@ const ALIASES = {
2230
2595
  '/vram': '/gpu', '/gpus': '/gpu',
2231
2596
  '/info': '/about',
2232
2597
  '/sponsor': '/ads', '/ad': '/ads',
2598
+ '/image': '/gallery', '/photo': '/gallery',
2233
2599
  '/cls': '/clear', '/reset': '/clear',
2234
2600
  '/export': '/save',
2235
2601
  '/r': '/retry',
2602
+ '/kvip': '/key',
2236
2603
  };
2237
2604
 
2238
2605
  export async function routeCommand(rawInput, ctx) {
@@ -2262,5 +2629,5 @@ export async function routeCommand(rawInput, ctx) {
2262
2629
  return handlers['/help']({ ...ctx, sub: rawCmd, extra: '' });
2263
2630
  }
2264
2631
 
2265
- return await handler({ ...ctx, sub, extra, parts });
2632
+ return await handler({ ...ctx, sub, extra, parts, rawInput: trimmed });
2266
2633
  }
@@ -0,0 +1,96 @@
1
+ function errorMessage(error) {
2
+ return error?.message || String(error || 'Unknown error');
3
+ }
4
+
5
+ export class AgentJobManager {
6
+ constructor(limit = 50) {
7
+ this.limit = limit;
8
+ this.jobs = [];
9
+ }
10
+
11
+ create(type, label) {
12
+ const controller = new AbortController();
13
+ const job = {
14
+ id: `job_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
15
+ type: String(type || 'task'),
16
+ label: String(label || type || 'Task').slice(0, 200),
17
+ state: 'running',
18
+ createdAt: new Date().toISOString(),
19
+ startedAt: new Date().toISOString(),
20
+ completedAt: null,
21
+ error: null,
22
+ result: null,
23
+ controller,
24
+ };
25
+ this.jobs.unshift(job);
26
+ this.jobs = this.jobs.slice(0, this.limit);
27
+ return job;
28
+ }
29
+
30
+ async run(type, label, runner, options = {}) {
31
+ const job = this.create(type, label);
32
+ const abortParent = () => job.controller.abort(options.signal?.reason);
33
+ options.signal?.addEventListener('abort', abortParent, { once: true });
34
+ options.onJob?.(this.publicJob(job));
35
+ try {
36
+ const result = await runner({ signal: job.controller.signal, job: this.publicJob(job) });
37
+ job.state = job.controller.signal.aborted ? 'cancelled' : 'completed';
38
+ job.result = result;
39
+ job.completedAt = new Date().toISOString();
40
+ return result;
41
+ } catch (error) {
42
+ job.state = job.controller.signal.aborted || error?.name === 'AbortError' ? 'cancelled' : 'failed';
43
+ job.error = errorMessage(error);
44
+ job.completedAt = new Date().toISOString();
45
+ throw error;
46
+ } finally {
47
+ options.signal?.removeEventListener('abort', abortParent);
48
+ }
49
+ }
50
+
51
+ list() {
52
+ return this.jobs.map(job => this.publicJob(job));
53
+ }
54
+
55
+ get(id) {
56
+ const job = this.jobs.find(item => item.id === id);
57
+ return job ? this.publicJob(job) : null;
58
+ }
59
+
60
+ cancel(id) {
61
+ const job = this.jobs.find(item => item.id === id);
62
+ if (!job || job.state !== 'running') return false;
63
+ job.state = 'cancelled';
64
+ job.completedAt = new Date().toISOString();
65
+ job.controller.abort(new DOMException('Cancelled by user', 'AbortError'));
66
+ return true;
67
+ }
68
+
69
+ cancelType(type) {
70
+ let cancelled = false;
71
+ for (const job of this.jobs) {
72
+ if (job.type === type && job.state === 'running') cancelled = this.cancel(job.id) || cancelled;
73
+ }
74
+ return cancelled;
75
+ }
76
+
77
+ clear() {
78
+ const before = this.jobs.length;
79
+ this.jobs = this.jobs.filter(job => job.state === 'running');
80
+ return before - this.jobs.length;
81
+ }
82
+
83
+ publicJob(job) {
84
+ return {
85
+ id: job.id,
86
+ type: job.type,
87
+ label: job.label,
88
+ state: job.state,
89
+ createdAt: job.createdAt,
90
+ startedAt: job.startedAt,
91
+ completedAt: job.completedAt,
92
+ error: job.error,
93
+ result: job.result,
94
+ };
95
+ }
96
+ }