kiku-agent 1.0.6 → 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.
- package/LICENSE +621 -0
- package/README.md +317 -31
- package/package.json +96 -93
- package/server/agent/agentCore.js +58 -31
- package/server/agent/cli.js +101 -56
- package/server/agent/commandFeatures.js +517 -0
- package/server/agent/commandInput.js +113 -0
- package/server/agent/commandRegistry.js +124 -10
- package/server/agent/commands.js +383 -10
- package/server/agent/config.js +64 -48
- package/server/agent/jobManager.js +96 -0
- package/server/agent/llmClient.js +26 -19
- package/server/agent/mcpClient.js +68 -11
- package/server/agent/memory.js +87 -14
- package/server/agent/modelManager.js +30 -13
- package/server/agent/quizData.js +384 -0
- package/server/agent/terminalImage.js +248 -0
- package/server/agent/tools.js +153 -43
- package/server/agent/ui.js +67 -21
- package/server/agent/youtubeAuth.js +1 -2
- package/src/assets/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/src/assets/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
package/server/agent/commands.js
CHANGED
|
@@ -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
|
-
|
|
1025
|
-
|
|
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
|
|
1293
|
+
console.log(` ${C.dim}Example: /provider use groq openai/gpt-oss-120b${C.reset}\n`);
|
|
1202
1294
|
return 'HANDLED';
|
|
1203
1295
|
}
|
|
1204
1296
|
|
|
@@ -1209,6 +1301,12 @@ export const handlers = {
|
|
|
1209
1301
|
if (ctx.modelManager) {
|
|
1210
1302
|
ctx.modelManager.client = ctx.client;
|
|
1211
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
|
+
}
|
|
1212
1310
|
}
|
|
1213
1311
|
ctx.baseUrl = info.baseUrl;
|
|
1214
1312
|
|
|
@@ -1677,12 +1775,18 @@ export const handlers = {
|
|
|
1677
1775
|
for (const s of status.servers) {
|
|
1678
1776
|
const serverName = sanitizeTerminalText(s.name, 128) || 'unnamed';
|
|
1679
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
|
+
: '';
|
|
1680
1784
|
if (s.status === 'connected') {
|
|
1681
|
-
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}`);
|
|
1682
1786
|
} else if (s.status === 'saved_offline') {
|
|
1683
|
-
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}`);
|
|
1684
1788
|
} else {
|
|
1685
|
-
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}`);
|
|
1686
1790
|
}
|
|
1687
1791
|
}
|
|
1688
1792
|
|
|
@@ -2102,10 +2206,15 @@ export const handlers = {
|
|
|
2102
2206
|
return 'HANDLED';
|
|
2103
2207
|
},
|
|
2104
2208
|
|
|
2105
|
-
'/stats': async () => {
|
|
2209
|
+
'/stats': async (ctx) => {
|
|
2106
2210
|
try {
|
|
2107
2211
|
const result = await executeTool('get_system_stats', {});
|
|
2108
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
|
+
});
|
|
2109
2218
|
printSolidPanel({
|
|
2110
2219
|
title: `Database Overview`,
|
|
2111
2220
|
lines: [
|
|
@@ -2113,7 +2222,8 @@ export const handlers = {
|
|
|
2113
2222
|
`${C.gold}Songs:${C.reset} ${C.cyberCyan}${s.totalUniqueSongs || 0}${C.reset}`,
|
|
2114
2223
|
`${C.gold}Brands:${C.reset} ${C.cyberCyan}${s.brandEndorsements || 0}${C.reset}`,
|
|
2115
2224
|
`${C.gold}Awards:${C.reset} ${C.cyberCyan}${s.totalAwards || 0}${C.reset}`,
|
|
2116
|
-
`${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}`] : []),
|
|
2117
2227
|
],
|
|
2118
2228
|
headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
|
|
2119
2229
|
});
|
|
@@ -2198,6 +2308,261 @@ export const handlers = {
|
|
|
2198
2308
|
ctx.printAd();
|
|
2199
2309
|
return 'HANDLED';
|
|
2200
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
|
+
},
|
|
2201
2566
|
};
|
|
2202
2567
|
|
|
2203
2568
|
const ALIASES = {
|
|
@@ -2214,6 +2579,12 @@ const ALIASES = {
|
|
|
2214
2579
|
'/suggest': '/suggestion', '/suggestions': '/suggestion',
|
|
2215
2580
|
'/exec': '/tool', '/call': '/tool',
|
|
2216
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',
|
|
2217
2588
|
'/metrics': '/stats', '/catalog': '/stats',
|
|
2218
2589
|
'/llm': '/model', '/engine': '/model',
|
|
2219
2590
|
'/health': '/ping',
|
|
@@ -2224,9 +2595,11 @@ const ALIASES = {
|
|
|
2224
2595
|
'/vram': '/gpu', '/gpus': '/gpu',
|
|
2225
2596
|
'/info': '/about',
|
|
2226
2597
|
'/sponsor': '/ads', '/ad': '/ads',
|
|
2598
|
+
'/image': '/gallery', '/photo': '/gallery',
|
|
2227
2599
|
'/cls': '/clear', '/reset': '/clear',
|
|
2228
2600
|
'/export': '/save',
|
|
2229
2601
|
'/r': '/retry',
|
|
2602
|
+
'/kvip': '/key',
|
|
2230
2603
|
};
|
|
2231
2604
|
|
|
2232
2605
|
export async function routeCommand(rawInput, ctx) {
|
|
@@ -2256,5 +2629,5 @@ export async function routeCommand(rawInput, ctx) {
|
|
|
2256
2629
|
return handlers['/help']({ ...ctx, sub: rawCmd, extra: '' });
|
|
2257
2630
|
}
|
|
2258
2631
|
|
|
2259
|
-
return await handler({ ...ctx, sub, extra, parts });
|
|
2632
|
+
return await handler({ ...ctx, sub, extra, parts, rawInput: trimmed });
|
|
2260
2633
|
}
|