kiku-agent 1.0.7 → 1.0.9
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 +95 -92
- 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 +403 -28
- 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/server/agent/updateManager.js +21 -3
- 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,13 +1086,40 @@ 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}`;
|
|
1026
1092
|
}),
|
|
1027
1093
|
});
|
|
1028
1094
|
return 'HANDLED';
|
|
1029
1095
|
}
|
|
1030
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,
|
|
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}`],
|
|
1119
|
+
});
|
|
1120
|
+
return 'HANDLED';
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1031
1123
|
if (memAction === 'export') {
|
|
1032
1124
|
const exported = await agent.memory.exportToJSON();
|
|
1033
1125
|
console.log(`\n ${C.emerald}[OK] Memory exported.${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
|
|
@@ -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
|
|
|
@@ -1859,12 +1957,16 @@ export const handlers = {
|
|
|
1859
1957
|
return 'HANDLED';
|
|
1860
1958
|
}
|
|
1861
1959
|
const channel = await manager.setChannel(requested);
|
|
1960
|
+
const lines = [
|
|
1961
|
+
`${C.emerald}[OK] Future /update checks will use ${C.bold}${channel}${C.reset}${C.emerald}.${C.reset}`,
|
|
1962
|
+
];
|
|
1963
|
+
if (channel !== 'latest') {
|
|
1964
|
+
lines.push(`${C.amber}[NOTE] The "${channel}" channel may not have published builds on npm yet.${C.reset}`);
|
|
1965
|
+
}
|
|
1966
|
+
lines.push(`${C.dim}This changes only the release channel; it does not install anything.${C.reset}`);
|
|
1862
1967
|
printSolidPanel({
|
|
1863
1968
|
title: 'Update Channel Changed',
|
|
1864
|
-
lines
|
|
1865
|
-
`${C.emerald}[OK] Future /update checks will use ${C.bold}${channel}${C.reset}${C.emerald}.${C.reset}`,
|
|
1866
|
-
`${C.dim}This changes only the release channel; it does not install anything.${C.reset}`,
|
|
1867
|
-
],
|
|
1969
|
+
lines,
|
|
1868
1970
|
});
|
|
1869
1971
|
return 'HANDLED';
|
|
1870
1972
|
}
|
|
@@ -1949,12 +2051,16 @@ export const handlers = {
|
|
|
1949
2051
|
return 'HANDLED';
|
|
1950
2052
|
} catch (error) {
|
|
1951
2053
|
const safeMessage = error instanceof UpdateError ? error.message : 'The update operation failed unexpectedly.';
|
|
2054
|
+
const lines = [
|
|
2055
|
+
`${C.red}[${error.code || 'UPDATE_ERROR'}] ${safeMessage}${C.reset}`,
|
|
2056
|
+
];
|
|
2057
|
+
if (error.code === 'CHANNEL_NOT_FOUND') {
|
|
2058
|
+
lines.push(`${C.gold}Tip:${C.reset} ${C.white}Run ${C.bold}/update channel latest${C.reset}${C.white} to return to the active release channel.${C.reset}`);
|
|
2059
|
+
}
|
|
2060
|
+
lines.push(`${C.dim}No session, memory, or environment files were changed.${C.reset}`);
|
|
1952
2061
|
printSolidPanel({
|
|
1953
2062
|
title: 'Update Failed',
|
|
1954
|
-
lines
|
|
1955
|
-
`${C.red}[${error.code || 'UPDATE_ERROR'}] ${safeMessage}${C.reset}`,
|
|
1956
|
-
`${C.dim}No session, memory, or environment files were changed.${C.reset}`,
|
|
1957
|
-
],
|
|
2063
|
+
lines,
|
|
1958
2064
|
});
|
|
1959
2065
|
return 'HANDLED';
|
|
1960
2066
|
}
|
|
@@ -2108,10 +2214,15 @@ export const handlers = {
|
|
|
2108
2214
|
return 'HANDLED';
|
|
2109
2215
|
},
|
|
2110
2216
|
|
|
2111
|
-
'/stats': async () => {
|
|
2217
|
+
'/stats': async (ctx) => {
|
|
2112
2218
|
try {
|
|
2113
2219
|
const result = await executeTool('get_system_stats', {});
|
|
2114
2220
|
const s = result.stats || {};
|
|
2221
|
+
const insight = await aiNarration(ctx, {
|
|
2222
|
+
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.`,
|
|
2223
|
+
user: JSON.stringify({ totalWorks: s.totalWorks || 0, totalSongs: s.totalUniqueSongs || 0, brands: s.brandEndorsements || 0, awards: s.totalAwards || 0, timeline: s.timelineMilestones || 0 }),
|
|
2224
|
+
limit: 240,
|
|
2225
|
+
});
|
|
2115
2226
|
printSolidPanel({
|
|
2116
2227
|
title: `Database Overview`,
|
|
2117
2228
|
lines: [
|
|
@@ -2119,7 +2230,8 @@ export const handlers = {
|
|
|
2119
2230
|
`${C.gold}Songs:${C.reset} ${C.cyberCyan}${s.totalUniqueSongs || 0}${C.reset}`,
|
|
2120
2231
|
`${C.gold}Brands:${C.reset} ${C.cyberCyan}${s.brandEndorsements || 0}${C.reset}`,
|
|
2121
2232
|
`${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}
|
|
2233
|
+
`${C.gold}Timeline:${C.reset} ${C.cyberCyan}${s.timelineMilestones || 0}${C.reset}`,
|
|
2234
|
+
...(insight ? ['', ` ${C.slate}${insight}${C.reset}`] : []),
|
|
2123
2235
|
],
|
|
2124
2236
|
headerBg: '\x1b[48;2;48;54;70m', bg: '\x1b[48;2;36;40;52m'
|
|
2125
2237
|
});
|
|
@@ -2204,6 +2316,261 @@ export const handlers = {
|
|
|
2204
2316
|
ctx.printAd();
|
|
2205
2317
|
return 'HANDLED';
|
|
2206
2318
|
},
|
|
2319
|
+
|
|
2320
|
+
'/gallery': async (ctx) => {
|
|
2321
|
+
const displayed = typeof ctx.showTerminalImage === 'function' && ctx.showTerminalImage();
|
|
2322
|
+
if (!displayed) {
|
|
2323
|
+
printRichCard({
|
|
2324
|
+
title: 'Terminal Gallery Unavailable',
|
|
2325
|
+
status: 'warning',
|
|
2326
|
+
lines: [
|
|
2327
|
+
'Images require an interactive color terminal and packaged files in src/assets/images.',
|
|
2328
|
+
'Set KIKU_TUI_IMAGES=on to override terminal detection, or off to disable images.',
|
|
2329
|
+
],
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
return 'HANDLED';
|
|
2333
|
+
},
|
|
2334
|
+
|
|
2335
|
+
'/quiz': async (ctx) => {
|
|
2336
|
+
const { sub, extra } = ctx;
|
|
2337
|
+
const langArg = (sub === 'lang' ? extra : sub || '').toLowerCase().trim();
|
|
2338
|
+
const quizLang = ['th', 'en', 'zh'].includes(langArg) ? langArg : (ctx.agent?.memory?.preferences?.language || 'en');
|
|
2339
|
+
const i18n = QUIZ_I18N[quizLang] || QUIZ_I18N.en;
|
|
2340
|
+
|
|
2341
|
+
const shuffled = shuffleQuizQuestions(QUIZ_QUESTIONS);
|
|
2342
|
+
let score = 0;
|
|
2343
|
+
let challengeToken = null;
|
|
2344
|
+
let isOnline = false;
|
|
2345
|
+
|
|
2346
|
+
printSolidPanel({
|
|
2347
|
+
title: i18n.title,
|
|
2348
|
+
lines: [
|
|
2349
|
+
`${C.bold}${C.white}${i18n.subtitle}${C.reset}`,
|
|
2350
|
+
`${C.dim}Language: ${quizLang.toUpperCase()} | Questions: 15 | Target: 15/15${C.reset}`,
|
|
2351
|
+
],
|
|
2352
|
+
bg: '\x1b[48;2;48;40;60m'
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
|
|
2356
|
+
const baseUrl = ctx.baseUrl || process.env.KIKU_API_URL || 'http://localhost:3000';
|
|
2357
|
+
const controller = new AbortController();
|
|
2358
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
2359
|
+
try {
|
|
2360
|
+
const res = await fetch(`${baseUrl}/api/v1/quiz/challenge`, { signal: controller.signal });
|
|
2361
|
+
if (res.ok) {
|
|
2362
|
+
const json = await res.json();
|
|
2363
|
+
challengeToken = json.data?.challengeToken;
|
|
2364
|
+
isOnline = true;
|
|
2365
|
+
}
|
|
2366
|
+
} catch {
|
|
2367
|
+
|
|
2368
|
+
} finally {
|
|
2369
|
+
clearTimeout(timeout);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
if (isOnline) {
|
|
2373
|
+
console.log(` ${C.dim}Connected to server. Quiz answers will be verified via API.${C.reset}`);
|
|
2374
|
+
} else {
|
|
2375
|
+
console.log(` ${C.dim}Offline mode. Answers verified locally — key generated on completion.${C.reset}`);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
const answers = [];
|
|
2379
|
+
const stdin = process.stdin;
|
|
2380
|
+
const wasRaw = stdin.isTTY && stdin.isRaw;
|
|
2381
|
+
let rl = null;
|
|
2382
|
+
|
|
2383
|
+
if (typeof ctx.readInput !== 'function') {
|
|
2384
|
+
const { createInterface } = await import('node:readline');
|
|
2385
|
+
if (stdin.isTTY) {
|
|
2386
|
+
try { stdin.setRawMode(false); } catch {}
|
|
2387
|
+
}
|
|
2388
|
+
rl = createInterface({ input: stdin, output: process.stdout });
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
const askQuestion = () => {
|
|
2392
|
+
if (typeof ctx.readInput === 'function') {
|
|
2393
|
+
return ctx.readInput({
|
|
2394
|
+
prompt: i18n.answerPrompt,
|
|
2395
|
+
placeholder: i18n.answerPlaceholder,
|
|
2396
|
+
history: false,
|
|
2397
|
+
cancelValue: 'q',
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
return new Promise(resolve => rl.question(` ${C.roseGlow}>${C.reset} `, resolve));
|
|
2401
|
+
};
|
|
2402
|
+
|
|
2403
|
+
let cancelled = false;
|
|
2404
|
+
try {
|
|
2405
|
+
for (let i = 0; i < shuffled.length; i++) {
|
|
2406
|
+
const q = shuffled[i];
|
|
2407
|
+
const numberedText = q.q[quizLang] || q.q.en;
|
|
2408
|
+
const qText = numberedText.replace(/^\s*\d+[.)]\s*/, '');
|
|
2409
|
+
const opts = q.options[quizLang] || q.options.en;
|
|
2410
|
+
|
|
2411
|
+
printSolidPanel({
|
|
2412
|
+
title: `${i18n.questionOf(i + 1, shuffled.length)} • ${i18n.score(score, shuffled.length)}`,
|
|
2413
|
+
lines: [
|
|
2414
|
+
`${C.bold}${C.white}${qText}${C.reset}`,
|
|
2415
|
+
'',
|
|
2416
|
+
...opts.map((opt, idx) => ` ${C.bold}${C.cyberCyan}${LETTER_LABELS[idx]})${C.reset} ${C.white}${opt}${C.reset}`),
|
|
2417
|
+
],
|
|
2418
|
+
bg: '\x1b[48;2;36;40;52m'
|
|
2419
|
+
});
|
|
2420
|
+
|
|
2421
|
+
let answeredCorrectly = false;
|
|
2422
|
+
while (!answeredCorrectly) {
|
|
2423
|
+
const input = String(await askQuestion() ?? '').trim().toLowerCase();
|
|
2424
|
+
|
|
2425
|
+
if (['q', 'quit', 'exit', 'cancel'].includes(input)) {
|
|
2426
|
+
cancelled = true;
|
|
2427
|
+
break;
|
|
2428
|
+
}
|
|
2429
|
+
if (input === 'h' || input === 'hint') {
|
|
2430
|
+
const hintText = q.hint[quizLang] || q.hint.en;
|
|
2431
|
+
console.log(` ${C.amber}${i18n.hint}:${C.reset} ${C.dim}${hintText}${C.reset}`);
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
let idx = -1;
|
|
2436
|
+
if (['a', 'b', 'c', 'd'].includes(input)) idx = input.charCodeAt(0) - 97;
|
|
2437
|
+
else if (['1', '2', '3', '4'].includes(input)) idx = Number.parseInt(input, 10) - 1;
|
|
2438
|
+
|
|
2439
|
+
if (idx < 0 || idx >= opts.length) {
|
|
2440
|
+
console.log(` ${C.amber}${i18n.invalidAnswer}${C.reset}`);
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const selectedLabel = LETTER_LABELS[idx];
|
|
2445
|
+
const selectedText = opts[idx];
|
|
2446
|
+
if (idx === q.answer) {
|
|
2447
|
+
answers.push({
|
|
2448
|
+
qIndex: i,
|
|
2449
|
+
answerIndex: q.optionOrder?.[idx] ?? idx,
|
|
2450
|
+
});
|
|
2451
|
+
score++;
|
|
2452
|
+
answeredCorrectly = true;
|
|
2453
|
+
console.log(` ${C.emerald}✓ ${i18n.correct.trim()}${C.reset} ${C.dim}— ${selectedLabel}) ${selectedText}${C.reset}`);
|
|
2454
|
+
} else {
|
|
2455
|
+
console.log(` ${C.red}${i18n.incorrect}${C.reset} ${C.dim}— ${selectedLabel}) ${selectedText}${C.reset}`);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
if (cancelled) break;
|
|
2460
|
+
}
|
|
2461
|
+
} finally {
|
|
2462
|
+
rl?.close();
|
|
2463
|
+
if (wasRaw && stdin.isTTY) {
|
|
2464
|
+
try { stdin.setRawMode(true); } catch {}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
if (cancelled) {
|
|
2469
|
+
console.log(` ${C.amber}${i18n.cancelled}${C.reset}`);
|
|
2470
|
+
return 'HANDLED';
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
printSolidPanel({
|
|
2474
|
+
title: `${i18n.score(score, shuffled.length)}`,
|
|
2475
|
+
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}`],
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
if (score < 15) {
|
|
2479
|
+
console.log(`\n ${C.dim}Run ${C.cyberCyan}/quiz${C.reset}${C.dim} to try again.${C.reset}\n`);
|
|
2480
|
+
return 'HANDLED';
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
console.log(`\n ${C.amber}${i18n.generatingKey}${C.reset}\n`);
|
|
2484
|
+
|
|
2485
|
+
let key = null;
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
if (isOnline && challengeToken) {
|
|
2489
|
+
try {
|
|
2490
|
+
const res = await fetch(`${baseUrl}/api/v1/keys/claim`, {
|
|
2491
|
+
method: 'POST',
|
|
2492
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2493
|
+
body: JSON.stringify({ score, answers, challengeToken })
|
|
2494
|
+
});
|
|
2495
|
+
const json = await res.json();
|
|
2496
|
+
if (json.status === 'success' && json.data?.apiKey) {
|
|
2497
|
+
key = json.data.apiKey;
|
|
2498
|
+
}
|
|
2499
|
+
} catch {
|
|
2500
|
+
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
|
|
2505
|
+
if (!key) {
|
|
2506
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
2507
|
+
key = `kiku_vip_sk_${token}`;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
ctx.agent?.memory?.setPreference('vipKey', key);
|
|
2511
|
+
|
|
2512
|
+
printSolidPanel({
|
|
2513
|
+
title: `${i18n.congratsTitle}`,
|
|
2514
|
+
lines: [
|
|
2515
|
+
`${C.bold}${C.white}${i18n.congratsDesc}${C.reset}`,
|
|
2516
|
+
'',
|
|
2517
|
+
`${C.bold}${C.roseGlow}${i18n.keyLabel}:${C.reset}`,
|
|
2518
|
+
` ${C.gold}${key}${C.reset}`,
|
|
2519
|
+
'',
|
|
2520
|
+
`${C.dim}Usage: header 'X-API-Key: ${key}' or query '?apiKey=${key}'${C.reset}`,
|
|
2521
|
+
`${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}`,
|
|
2522
|
+
],
|
|
2523
|
+
bg: '\x1b[48;2;48;40;60m'
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2526
|
+
return 'HANDLED';
|
|
2527
|
+
},
|
|
2528
|
+
|
|
2529
|
+
'/key': async (ctx) => {
|
|
2530
|
+
const { sub, extra } = ctx;
|
|
2531
|
+
const action = (sub || 'show').toLowerCase();
|
|
2532
|
+
const mem = ctx.agent?.memory;
|
|
2533
|
+
|
|
2534
|
+
if (action === 'clear') {
|
|
2535
|
+
mem?.setPreference('vipKey', null);
|
|
2536
|
+
console.log(`\n ${C.emerald}[OK] ${QUIZ_I18N.en.keyCleared}${C.reset}\n`);
|
|
2537
|
+
return 'HANDLED';
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
if (action === 'apply') {
|
|
2541
|
+
const key = extra.trim();
|
|
2542
|
+
if (!key) {
|
|
2543
|
+
console.log(`\n ${C.amber}Usage: /key apply <kiku_vip_sk_...>${C.reset}\n`);
|
|
2544
|
+
return 'HANDLED';
|
|
2545
|
+
}
|
|
2546
|
+
mem?.setPreference('vipKey', key);
|
|
2547
|
+
printSolidPanel({
|
|
2548
|
+
title: 'VIP Key Applied',
|
|
2549
|
+
lines: [
|
|
2550
|
+
`${C.emerald}[OK] Key saved and ready to use!${C.reset}`,
|
|
2551
|
+
`${C.gold}Key:${C.reset} ${C.white}${key}${C.reset}`,
|
|
2552
|
+
],
|
|
2553
|
+
});
|
|
2554
|
+
return 'HANDLED';
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
const storedKey = mem?.preferences?.vipKey;
|
|
2558
|
+
if (storedKey) {
|
|
2559
|
+
printSolidPanel({
|
|
2560
|
+
title: 'Developer VIP Key',
|
|
2561
|
+
lines: [
|
|
2562
|
+
`${C.gold}Key:${C.reset} ${C.bold}${C.white}${storedKey}${C.reset}`,
|
|
2563
|
+
`${C.gold}Tier:${C.reset} ${C.emerald}Developer VIP (1,000 req/min)${C.reset}`,
|
|
2564
|
+
`${C.gold}Usage:${C.reset} ${C.dim}X-API-Key: ${storedKey}${C.reset}`,
|
|
2565
|
+
'',
|
|
2566
|
+
`${C.dim}Clear: /key clear | Apply new: /key apply <key>${C.reset}`,
|
|
2567
|
+
],
|
|
2568
|
+
});
|
|
2569
|
+
} else {
|
|
2570
|
+
console.log(`\n ${C.amber}${QUIZ_I18N.en.noKey}${C.reset}\n`);
|
|
2571
|
+
}
|
|
2572
|
+
return 'HANDLED';
|
|
2573
|
+
},
|
|
2207
2574
|
};
|
|
2208
2575
|
|
|
2209
2576
|
const ALIASES = {
|
|
@@ -2220,6 +2587,12 @@ const ALIASES = {
|
|
|
2220
2587
|
'/suggest': '/suggestion', '/suggestions': '/suggestion',
|
|
2221
2588
|
'/exec': '/tool', '/call': '/tool',
|
|
2222
2589
|
'/toollist': '/tools',
|
|
2590
|
+
'/lookup': '/find', '/findall': '/find',
|
|
2591
|
+
'/cmp': '/compare',
|
|
2592
|
+
'/fav': '/favorites', '/favs': '/favorites',
|
|
2593
|
+
'/latest': '/news',
|
|
2594
|
+
'/job': '/jobs',
|
|
2595
|
+
'/random': '/discover',
|
|
2223
2596
|
'/metrics': '/stats', '/catalog': '/stats',
|
|
2224
2597
|
'/llm': '/model', '/engine': '/model',
|
|
2225
2598
|
'/health': '/ping',
|
|
@@ -2230,9 +2603,11 @@ const ALIASES = {
|
|
|
2230
2603
|
'/vram': '/gpu', '/gpus': '/gpu',
|
|
2231
2604
|
'/info': '/about',
|
|
2232
2605
|
'/sponsor': '/ads', '/ad': '/ads',
|
|
2606
|
+
'/image': '/gallery', '/photo': '/gallery',
|
|
2233
2607
|
'/cls': '/clear', '/reset': '/clear',
|
|
2234
2608
|
'/export': '/save',
|
|
2235
2609
|
'/r': '/retry',
|
|
2610
|
+
'/kvip': '/key',
|
|
2236
2611
|
};
|
|
2237
2612
|
|
|
2238
2613
|
export async function routeCommand(rawInput, ctx) {
|
|
@@ -2262,5 +2637,5 @@ export async function routeCommand(rawInput, ctx) {
|
|
|
2262
2637
|
return handlers['/help']({ ...ctx, sub: rawCmd, extra: '' });
|
|
2263
2638
|
}
|
|
2264
2639
|
|
|
2265
|
-
return await handler({ ...ctx, sub, extra, parts });
|
|
2640
|
+
return await handler({ ...ctx, sub, extra, parts, rawInput: trimmed });
|
|
2266
2641
|
}
|
|
@@ -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
|
+
}
|