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.
- package/LICENSE +621 -0
- package/README.md +296 -14
- package/package.json +92 -89
- package/server/agent/agentCore.js +58 -31
- package/server/agent/cli.js +79 -43
- package/server/agent/commandFeatures.js +517 -0
- package/server/agent/commandInput.js +113 -0
- package/server/agent/commandRegistry.js +122 -8
- package/server/agent/commands.js +387 -20
- package/server/agent/jobManager.js +96 -0
- package/server/agent/mcpClient.js +68 -11
- package/server/agent/memory.js +87 -14
- 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/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 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
|
+
}
|