kiku-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +122 -0
  2. package/package.json +85 -0
  3. package/server/agent/agentCore.js +782 -0
  4. package/server/agent/agentState.js +140 -0
  5. package/server/agent/audioPlayer.js +673 -0
  6. package/server/agent/cli.js +1111 -0
  7. package/server/agent/commandRegistry.js +58 -0
  8. package/server/agent/commands.js +788 -0
  9. package/server/agent/config.js +90 -0
  10. package/server/agent/errors.js +9 -0
  11. package/server/agent/eventBus.js +103 -0
  12. package/server/agent/llmClient.js +200 -0
  13. package/server/agent/memory.js +566 -0
  14. package/server/agent/sanitizer.js +314 -0
  15. package/server/agent/session.js +239 -0
  16. package/server/agent/theme.js +124 -0
  17. package/server/agent/tools.js +1528 -0
  18. package/server/agent/ui.js +469 -0
  19. package/server/agent/webSearch.js +719 -0
  20. package/server/agent/youtubeAuth.js +299 -0
  21. package/server/mcp/claude-desktop-config.example.json +9 -0
  22. package/server/mcp/data/awards.js +14 -0
  23. package/server/mcp/data/brands.js +9 -0
  24. package/server/mcp/data/discography.js +36 -0
  25. package/server/mcp/data/gallery.js +25 -0
  26. package/server/mcp/data/index.js +7 -0
  27. package/server/mcp/data/profile.js +44 -0
  28. package/server/mcp/data/timeline.js +16 -0
  29. package/server/mcp/data/works.js +104 -0
  30. package/server/mcp/handler.js +159 -0
  31. package/server/mcp/prompts.js +143 -0
  32. package/server/mcp/resources.js +54 -0
  33. package/server/mcp/security.js +138 -0
  34. package/server/mcp/server.js +100 -0
  35. package/server/mcp/stdio.js +33 -0
  36. package/server/mcp/tools.js +126 -0
  37. package/src/data/awardsData.js +146 -0
  38. package/src/data/brandsData.js +249 -0
  39. package/src/data/discographyData.js +874 -0
  40. package/src/data/galleryData.js +666 -0
  41. package/src/data/musicVideoData.js +406 -0
  42. package/src/data/profileData.js +156 -0
  43. package/src/data/socialUpdatesData.js +381 -0
  44. package/src/data/timelineData.js +172 -0
  45. package/src/data/worksData.js +516 -0
@@ -0,0 +1,1111 @@
1
+ #!/usr/bin/env node
2
+
3
+ import readline from 'node:readline';
4
+ import { stdin as input } from 'node:process';
5
+ import { existsSync } from 'node:fs';
6
+ import { spawn } from 'node:child_process';
7
+ import { KikuAgent } from './agentCore.js';
8
+ import { LMStudioClient } from './llmClient.js';
9
+ import { toolsSchema, toolHandlers, executeTool } from './tools.js';
10
+ import { config } from './config.js';
11
+ import { audioPlayer } from './audioPlayer.js';
12
+ import { routeCommand } from './commands.js';
13
+ import { eventBus, Events } from './eventBus.js';
14
+ import { AgentStates } from './agentState.js';
15
+ import { sanitizeQuery } from './sanitizer.js';
16
+ import { isAbortError } from './errors.js';
17
+ import { COMMAND_DEFINITIONS, SUB_COMMANDS, findCommandDefinition } from './commandRegistry.js';
18
+
19
+ import {
20
+ C, getStringDisplayWidth, getFullWidth, wrapText, stripAnsi, fitStyledText,
21
+ previousGraphemeBoundary, nextGraphemeBoundary, sliceLineForDisplay,
22
+ printSolidBannerRow, printSolidPanel, formatTerminalMarkdown,
23
+ parseMarkdownTableRow, isMarkdownTableSeparator, expandMarkdownLinks,
24
+ printWelcomeHero, randomFanName, setupOutputInterception,
25
+ rawWrite, INPUT_PLACEHOLDER, UI_ICONS,
26
+ spinnerFrames, thinkingFrames,
27
+ } from './ui.js';
28
+
29
+ let activeAbortController = null;
30
+ let isProcessing = false;
31
+ let lastQuery = null;
32
+ let queryCount = 0;
33
+ let nextQueryNumber = 1;
34
+ let activeQueryNumber = null;
35
+ const queryQueue = [];
36
+ const deferredCommands = [];
37
+ let globalTuiPrompt = null;
38
+ let lastQueryDetails = null;
39
+ const queryDetails = new Map();
40
+
41
+ function parseCliArgs() {
42
+ const args = process.argv.slice(2);
43
+ const options = {
44
+ query: null,
45
+ model: 'google/gemma-4-e4b',
46
+ url: null,
47
+ showTools: false,
48
+ showStats: false,
49
+ thinking: 'show',
50
+ verbose: false,
51
+ help: false
52
+ };
53
+
54
+ for (let i = 0; i < args.length; i++) {
55
+ const arg = args[i];
56
+ if (arg === '--help' || arg === '-h') options.help = true;
57
+ else if (arg === '--tools' || arg === '-t') options.showTools = true;
58
+ else if (arg === '--stats' || arg === '-s') options.showStats = true;
59
+ else if (arg === '--verbose' || arg === '-v') { options.verbose = true; options.thinking = 'show'; }
60
+ else if (arg === '--thinking') options.thinking = args[++i] || 'show';
61
+ else if (arg === '--model' || arg === '-m') options.model = args[++i] || options.model;
62
+ else if (arg === '--url' || arg === '-u') options.url = args[++i];
63
+ else if (arg === '--query' || arg === '-q') options.query = args[++i];
64
+ else if (!arg.startsWith('-') && !options.query) { options.query = args.slice(i).join(' '); break; }
65
+ }
66
+ return options;
67
+ }
68
+
69
+ async function waitForLMStudio(baseUrl, timeoutMs = 30000) {
70
+ const client = new LMStudioClient({ baseUrl });
71
+ const start = Date.now();
72
+ while (Date.now() - start < timeoutMs) {
73
+ const health = await client.checkConnection();
74
+ if (health.connected) return health;
75
+ await new Promise(r => setTimeout(r, 1000));
76
+ }
77
+ return null;
78
+ }
79
+
80
+ async function attemptAutoStartLMStudio(baseUrl, model) {
81
+ const isWin = process.platform === 'win32';
82
+ const isMac = process.platform === 'darwin';
83
+
84
+ const candidates = isWin
85
+ ? [
86
+ `${process.env.LOCALAPPDATA}\\LM Studio\\LM Studio.exe`,
87
+ `${process.env.LOCALAPPDATA}\\Programs\\LM Studio\\LM Studio.exe`,
88
+ 'C:\\Program Files\\LM Studio\\LM Studio.exe'
89
+ ]
90
+ : isMac
91
+ ? ['/Applications/LM Studio.app/Contents/MacOS/LM Studio']
92
+ : ['lmstudio', '/usr/local/bin/lmstudio', '/opt/lmstudio/bin/lmstudio'];
93
+
94
+ const exe = candidates.find(p => {
95
+ if (p.includes('/') || p.includes('\\')) {
96
+ try { return existsSync(p); } catch { return false; }
97
+ }
98
+ return true;
99
+ });
100
+
101
+ if (!exe) return null;
102
+ console.log(` ${C.cyberCyan}attempting to auto-start LM Studio...${C.reset}`);
103
+
104
+ try {
105
+ let port = '1234';
106
+ try { const parsedUrl = new URL(baseUrl); if (parsedUrl.port) port = parsedUrl.port; } catch {}
107
+
108
+ const child = spawn(exe, isWin
109
+ ? ['--model', model, '--serve', '--port', port]
110
+ : ['serve', '--model', model, '--port', port],
111
+ { detached: true, stdio: 'ignore', windowsHide: true }
112
+ );
113
+ child.unref();
114
+
115
+ return await waitForLMStudio(baseUrl, 45000);
116
+ } catch { return null; }
117
+ }
118
+
119
+ function extractPreview(result, toolName) {
120
+ const MAX = 80;
121
+ let raw = '';
122
+ if (result.content) raw = result.content;
123
+ else if (result.results && Array.isArray(result.results) && result.results.length > 0) {
124
+ const first = result.results[0];
125
+ raw = first.title || first.snippet || first.url || '';
126
+ } else if (result.works) raw = result.works.map(w => w.titleTh || w.titleCn).filter(Boolean).join(', ');
127
+ else if (result.songs) raw = result.songs.map(s => s.title).filter(Boolean).join(', ');
128
+ else if (result.brands) raw = result.brands.map(b => b.brand).filter(Boolean).join(', ');
129
+ else if (result.profile) raw = `${result.profile.nameTh || ''} ${result.profile.nameCn || ''}`.trim();
130
+ else if (result.stats) raw = `${result.stats.totalWorks || 0} works`;
131
+
132
+ if (!raw) return '';
133
+ raw = raw.replace(/\s+/g, ' ').trim();
134
+ return raw.length > MAX ? raw.slice(0, MAX) + '…' : raw;
135
+ }
136
+
137
+ function cleanThinkingLine(line) {
138
+ if (!line) return '';
139
+ return line
140
+ .replace(/\*\*\*?([^*]+)\*\*\*/g, '$1')
141
+ .replace(/\*([^\*\n]+)\*/g, '$1')
142
+ .replace(/^(\s*)[-*]\s+/gm, '$1• ')
143
+ .replace(/\*/g, '')
144
+ .trim();
145
+ }
146
+
147
+ function printQueryDetails(reference = '') {
148
+ const normalized = String(reference || '').trim().replace(/^q/i, '');
149
+ const requestedNumber = normalized ? Number.parseInt(normalized, 10) : lastQueryDetails?.number;
150
+ const details = normalized
151
+ ? (Number.isInteger(requestedNumber) ? queryDetails.get(requestedNumber) : null)
152
+ : lastQueryDetails;
153
+
154
+ if (!details) {
155
+ console.log(`\n ${C.amber}[NOT FOUND] No completed query details. Use ${C.cyberCyan}/details Q1${C.reset}${C.amber}.${C.reset}\n`);
156
+ return;
157
+ }
158
+
159
+ const width = Math.max(16, getFullWidth() - 8);
160
+ const safeLines = (label, value) => {
161
+ const text = `${label}: ${String(value || '—')}`;
162
+ return wrapText(expandMarkdownLinks(text), width)
163
+ .map(line => `${C.white}${formatTerminalMarkdown(line)}${C.reset}`);
164
+ };
165
+
166
+ printSolidPanel({
167
+ title: `Details • Q${details.number}`,
168
+ lines: safeLines('Question', details.prompt),
169
+ });
170
+
171
+ const thinking = String(details.thinking || '').trim();
172
+ printSolidPanel({
173
+ title: `Thinking • Q${details.number}`,
174
+ lines: thinking
175
+ ? thinking.split('\n').flatMap(line => wrapText(cleanThinkingLine(line), width)).map(line => `${C.dim}${C.slate}${line}${C.reset}`)
176
+ : [`${C.dim}${C.slate}No thinking trace was recorded.${C.reset}`],
177
+ });
178
+
179
+ if (!details.tools.length) {
180
+ printSolidPanel({
181
+ title: `Tools • Q${details.number}`,
182
+ lines: [`${C.dim}${C.slate}No tools were used.${C.reset}`],
183
+ });
184
+ return;
185
+ }
186
+
187
+ for (const [index, tool] of details.tools.entries()) {
188
+ const args = JSON.stringify(tool.args ?? {}, null, 2);
189
+ const serializedResult = JSON.stringify(tool.result ?? {}, null, 2);
190
+ const result = serializedResult.length > 6000
191
+ ? serializedResult.slice(0, 6000) + '\n… result truncated'
192
+ : serializedResult;
193
+ const lines = [
194
+ ...safeLines('Duration', tool.duration == null ? 'n/a' : `${(tool.duration / 1000).toFixed(2)}s`),
195
+ ...safeLines('Arguments', args),
196
+ ...safeLines('Result', result),
197
+ ];
198
+ printSolidPanel({ title: `Tool ${index + 1} • ${tool.name} • Q${details.number}`, lines });
199
+ }
200
+ }
201
+
202
+ function printHelp(specificCmd = null) {
203
+ if (specificCmd) {
204
+ const definition = findCommandDefinition(specificCmd);
205
+ if (!definition) {
206
+ console.log(`\n ${C.amber}Unknown command: ${specificCmd}${C.reset}`);
207
+ console.log(` ${C.dim}Type /help to see all commands.${C.reset}\n`);
208
+ return;
209
+ }
210
+ printSolidPanel({
211
+ title: `${UI_ICONS.help} ${definition.name}`,
212
+ lines: [`${C.white}${definition.desc}${C.reset}`, `${C.dim}Usage: ${definition.usage}${C.reset}`],
213
+ });
214
+ return;
215
+ }
216
+
217
+ printSolidPanel({
218
+ title: `${UI_ICONS.help} Kiku AI Agent — ${COMMAND_DEFINITIONS.length} Commands`,
219
+ lines: [
220
+ ...COMMAND_DEFINITIONS.map(command => `${C.emerald}${command.usage.padEnd(30)}${C.reset} ${C.white}${command.desc}${C.reset}`),
221
+ `${C.dim}Tip: Press Tab for auto-completion.${C.reset}`,
222
+ ],
223
+ });
224
+ }
225
+
226
+ const ads = [
227
+ {
228
+ title: 'DashSecurity', subtitle: 'Cybersecurity Knowledge Cloud',
229
+ url: 'https://dashsecurity.kpltgroup.com/',
230
+ features: ['AI-Powered SAST Scanner', 'OWASP Top 10 Detection', 'Certification Prep Labs'],
231
+ },
232
+ {
233
+ title: 'Ju Jingyi Kikuuu', subtitle: 'Official Portfolio',
234
+ url: 'https://jujingyi-kikuuu.vercel.app/',
235
+ features: ['45+ Music Releases', 'Complete Filmography', 'REST API v1'],
236
+ },
237
+ {
238
+ title: 'ThemeHackers', subtitle: 'Security Research',
239
+ url: 'https://github.com/Themehackers/',
240
+ features: ['Penetration Testing', 'Open-Source Tools', '10+ Certifications'],
241
+ },
242
+ ];
243
+
244
+ let adIndex = 0;
245
+ function printAd() {
246
+ const ad = ads[adIndex % ads.length];
247
+ adIndex++;
248
+ const w = getFullWidth();
249
+ console.log('');
250
+ printSolidBannerRow(` ${C.gold}${C.bold}${UI_ICONS.sponsor} SPONSOR${C.reset} ${C.bold}${C.white}${ad.title}${C.reset} — ${C.roseGlow}${ad.subtitle}${C.reset}`, w, C.bgPanelHeader);
251
+ printSolidBannerRow(` ${ad.features.map(f => `${C.white}${f}${C.reset}`).join(' • ')}`, w, C.bgPanel);
252
+ printSolidBannerRow(` ${C.cyberCyan}${C.underline}${ad.url}${C.reset}`, w, C.bgPanel);
253
+ console.log('');
254
+ }
255
+
256
+ class InteractiveTuiPrompt {
257
+ constructor(options = {}) {
258
+ this.promptText = options.prompt || 'Kiku ❯ ';
259
+ this.agent = options.agent || null;
260
+ this.history = [];
261
+ this.historyIndex = -1;
262
+ this.line = '';
263
+ this.cursor = 0;
264
+ this.popoverIndex = 0;
265
+ this.popoverActive = false;
266
+ this.popoverItems = [];
267
+ this.ghostText = '';
268
+ this.lastRenderedLines = 0;
269
+ this.hasTopPadding = false;
270
+ this.isResolving = false;
271
+ this.activeResolve = null;
272
+ this.activeReject = null;
273
+ this.onSigInt = options.onSigInt || null;
274
+ this._keyHandler = null;
275
+ this._renderTimer = null;
276
+ this.agentState = '';
277
+ this.agentStateTime = 0;
278
+ this._lastCtrlCTime = 0;
279
+ }
280
+
281
+ _scheduleRender() {
282
+ if (this._renderTimer) return;
283
+ this._renderTimer = setTimeout(() => {
284
+ this._renderTimer = null;
285
+ try { this.render(); } catch (error) { this.fail(error); }
286
+ }, 0);
287
+ }
288
+
289
+ _cleanupInput() {
290
+ if (this._renderTimer) { clearTimeout(this._renderTimer); this._renderTimer = null; }
291
+ if (this._keyHandler) { input.removeListener('keypress', this._keyHandler); this._keyHandler = null; }
292
+ try { if (input.isTTY) input.setRawMode(false); } catch {}
293
+ }
294
+
295
+ async readLine() {
296
+ return new Promise((resolve, reject) => {
297
+ this._cleanupInput();
298
+ try { this.clearPrompt(); } catch {}
299
+ this.line = '';
300
+ this.cursor = 0;
301
+ this.historyIndex = -1;
302
+ this.popoverIndex = 0;
303
+ this.popoverActive = false;
304
+ this.popoverItems = [];
305
+ this.ghostText = '';
306
+ this.lastRenderedLines = 0;
307
+ this.hasTopPadding = false;
308
+ this.isResolving = false;
309
+ this.activeResolve = resolve;
310
+ this.activeReject = reject;
311
+
312
+ try {
313
+ this.updateState();
314
+ readline.emitKeypressEvents(input);
315
+ if (input.isTTY) input.setRawMode(true);
316
+ this._keyHandler = (str, key) => {
317
+ try { this.handleKeypress(str, key); } catch (error) { this.fail(error); }
318
+ };
319
+ input.on('keypress', this._keyHandler);
320
+ this.render();
321
+ } catch (error) {
322
+ this.fail(error);
323
+ }
324
+ });
325
+ }
326
+
327
+ finish(result) {
328
+ if (this.isResolving) return;
329
+ this.isResolving = true;
330
+ try { this.clearPrompt(); } catch {}
331
+ this._cleanupInput();
332
+ this.line = '';
333
+ this.cursor = 0;
334
+ if (result && result.trim()) this.history.push(result);
335
+ const resolve = this.activeResolve;
336
+ this.activeResolve = null;
337
+ this.activeReject = null;
338
+ if (resolve) resolve(result);
339
+ }
340
+
341
+ fail(error) {
342
+ if (this.isResolving) return;
343
+ this.isResolving = true;
344
+ try { this.clearPrompt(); } catch {}
345
+ this._cleanupInput();
346
+ const reject = this.activeReject;
347
+ this.activeResolve = null;
348
+ this.activeReject = null;
349
+ if (reject) reject(error);
350
+ }
351
+
352
+ dispose() {
353
+ this._cleanupInput();
354
+ this.activeResolve = null;
355
+ this.activeReject = null;
356
+ this.isResolving = true;
357
+ }
358
+
359
+ clearPrompt() {
360
+ this.clearPopover();
361
+ rawWrite('\r\x1b[2K');
362
+ if (this.hasTopPadding) {
363
+ rawWrite('\x1b[1A\r\x1b[2K');
364
+ this.hasTopPadding = false;
365
+ }
366
+ }
367
+
368
+ clearPopover() {
369
+ if (this.lastRenderedLines > 0) {
370
+ let clearStr = '';
371
+ for (let i = 0; i < this.lastRenderedLines; i++) clearStr += '\x1b[1B\r\x1b[2K';
372
+ clearStr += `\x1b[${this.lastRenderedLines}A\r`;
373
+ rawWrite(clearStr);
374
+ this.lastRenderedLines = 0;
375
+ }
376
+ }
377
+
378
+ updateState() {
379
+ if (this.line.startsWith('/')) {
380
+ const items = getSuggestionsForLine(this.line);
381
+ this.popoverItems = items;
382
+ this.popoverActive = items.length > 0;
383
+ if (this.popoverIndex >= items.length) this.popoverIndex = Math.max(0, items.length - 1);
384
+
385
+ if (items.length > 0 && this.cursor === this.line.length) {
386
+ const topItem = items[this.popoverIndex] || items[0];
387
+ const targetStr = topItem.insert || topItem.name;
388
+ if (targetStr.toLowerCase().startsWith(this.line.toLowerCase()) && targetStr.length > this.line.length) {
389
+ this.ghostText = targetStr.slice(this.line.length);
390
+ } else { this.ghostText = ''; }
391
+ } else { this.ghostText = ''; }
392
+ } else {
393
+ this.popoverActive = false;
394
+ this.popoverItems = [];
395
+ this.popoverIndex = 0;
396
+ this.ghostText = '';
397
+ }
398
+ }
399
+
400
+ render() {
401
+ this.clearPopover();
402
+ const termCols = Math.max(10, process.stdout.columns || 80);
403
+ const inputRowWidth = Math.max(1, termCols - 1);
404
+
405
+ const musicStatus = audioPlayer.getStatus();
406
+ let statusStyled = '';
407
+ if (isProcessing) {
408
+ const frame = spinnerFrames[Math.floor(Date.now() / 70) % spinnerFrames.length];
409
+ const stateLabel = stripAnsi(this.agentState || 'Processing...');
410
+ const elapsed = this.agentState
411
+ ? ` ${((Date.now() - (this.agentStateTime || Date.now())) / 1000).toFixed(1)}s`
412
+ : '';
413
+ const queueLabel = queryQueue.length > 0 ? ` Queue: ${queryQueue.length}` : '';
414
+ const activeLabel = activeQueryNumber == null ? '' : `${C.bold}Q${activeQueryNumber}${C.reset}${C.cyberCyan} • `;
415
+ statusStyled = `${C.cyberCyan}${frame} ${activeLabel}${stateLabel}${elapsed}${C.reset}${C.slate}${queueLabel}${C.reset}`;
416
+ } else if (musicStatus) {
417
+ statusStyled = `${C.cyberCyan}${musicStatus}${C.reset}`;
418
+ }
419
+
420
+ if (this.hasTopPadding) rawWrite('\x1b[1A\r\x1b[2K');
421
+ const statusBackground = isProcessing ? C.bgPanelHeader : C.bgPanel;
422
+ const statusLine = fitStyledText(statusStyled ? ` ${statusStyled}` : '', inputRowWidth);
423
+ const themedStatus = statusLine.replace(/\x1b\[0m/g, C.reset + statusBackground + C.white);
424
+ const statusPadding = Math.max(0, inputRowWidth - getStringDisplayWidth(statusLine));
425
+ rawWrite(`${statusBackground}${C.white}${themedStatus}${' '.repeat(statusPadding)}${C.reset}\n\r\x1b[2K`);
426
+ this.hasTopPadding = true;
427
+
428
+ let promptStyled = `${C.bold}${C.sakura}${this.promptText}${C.reset}`;
429
+ let currentPrompt = this.promptText;
430
+
431
+ if (getStringDisplayWidth(currentPrompt) >= termCols - 2) {
432
+ currentPrompt = this.promptText;
433
+ promptStyled = `${C.bold}${C.sakura}${this.promptText}${C.reset}`;
434
+ }
435
+ const promptVisualWidth = getStringDisplayWidth(currentPrompt);
436
+ const maxLineWidth = Math.max(1, termCols - promptVisualWidth - 1);
437
+
438
+ const lineVisualWidth = getStringDisplayWidth(this.line);
439
+ const { displayLine, cursorVisualOffset } = sliceLineForDisplay(this.line, this.cursor, maxLineWidth);
440
+
441
+ let trailingStyled = '';
442
+ let trailingVisualWidth = 0;
443
+ if (this.ghostText && this.cursor === this.line.length && lineVisualWidth <= maxLineWidth) {
444
+ const remainingWidth = Math.max(0, maxLineWidth - lineVisualWidth);
445
+ const ghostDisplay = remainingWidth > 0
446
+ ? sliceLineForDisplay(this.ghostText, 0, remainingWidth).displayLine.replace(/…$/, '')
447
+ : '';
448
+ if (ghostDisplay) {
449
+ trailingStyled = `${C.dim}${C.slate}${ghostDisplay}${C.reset}`;
450
+ trailingVisualWidth = getStringDisplayWidth(ghostDisplay);
451
+ }
452
+ } else if (!this.line) {
453
+ const placeholder = sliceLineForDisplay(INPUT_PLACEHOLDER, 0, maxLineWidth).displayLine;
454
+ trailingStyled = `${C.dim}${C.slate}${placeholder}${C.reset}`;
455
+ trailingVisualWidth = getStringDisplayWidth(placeholder);
456
+ }
457
+
458
+ const rowStyled = `${promptStyled}${C.white}${displayLine}${trailingStyled}`
459
+ .replace(/\x1b\[0m/g, C.reset + C.bgPanel + C.white);
460
+ const rowVisualWidth = promptVisualWidth + getStringDisplayWidth(displayLine) + trailingVisualWidth;
461
+ const rowPadding = Math.max(0, inputRowWidth - rowVisualWidth);
462
+ rawWrite(`${C.bgPanel}${rowStyled}${' '.repeat(rowPadding)}${C.reset}`);
463
+
464
+ rawWrite(`\n\r\x1b[2K${C.bgPanel}${' '.repeat(inputRowWidth)}${C.reset}`);
465
+ let renderedLinesBelow = 1;
466
+
467
+ if (this.popoverActive && this.popoverItems.length > 0) {
468
+ const maxItems = 5;
469
+ const startIdx = Math.max(0, Math.min(this.popoverIndex - 2, this.popoverItems.length - maxItems));
470
+ const visibleItems = this.popoverItems.slice(startIdx, startIdx + maxItems);
471
+ const boxWidth = Math.max(1, termCols - 1);
472
+ const writeSurfaceRow = (styledText) => {
473
+ const fitted = fitStyledText(styledText, boxWidth);
474
+ const pad = Math.max(0, boxWidth - getStringDisplayWidth(fitted));
475
+ const themed = fitted.replace(/\x1b\[0m/g, C.reset + C.bgPanel + C.white);
476
+ rawWrite(`\n\r\x1b[2K${C.bgPanel}${C.white}${themed}${' '.repeat(pad)}${C.reset}`);
477
+ };
478
+
479
+ writeSurfaceRow(` ${C.bold}${C.roseGlow}Suggestions${C.reset} ${C.slate}— ↑/↓ navigate, Tab/Enter select, Esc close${C.reset}`);
480
+
481
+ for (let i = 0; i < visibleItems.length; i++) {
482
+ const actualIdx = startIdx + i;
483
+ const item = visibleItems[i];
484
+ const isSelected = actualIdx === this.popoverIndex;
485
+ const pointer = isSelected ? '❯' : ' ';
486
+ const pointerColor = isSelected ? C.gold : C.darkGray;
487
+ const nameColor = isSelected ? `${C.bold}${C.white}` : C.cyberCyan;
488
+ writeSurfaceRow(` ${pointerColor}${pointer}${C.reset} ${nameColor}${item.name.padEnd(22)}${C.reset} ${C.slate}${stripAnsi(item.desc)}${C.reset}`);
489
+ }
490
+ renderedLinesBelow += visibleItems.length + 1;
491
+ }
492
+
493
+ this.lastRenderedLines = renderedLinesBelow;
494
+ rawWrite(`\x1b[${this.lastRenderedLines}A\r`);
495
+
496
+ const cursorColumn = promptVisualWidth + cursorVisualOffset;
497
+ rawWrite(`\r${cursorColumn > 0 ? `\x1b[${cursorColumn}C` : ''}`);
498
+ }
499
+
500
+ handleKeypress(str, key) {
501
+ if (!key) return;
502
+
503
+ if (key.ctrl && key.name === 'c') {
504
+ if (this.line.length > 0) { this.line = ''; this.cursor = 0; this.updateState(); this.render(); return; }
505
+ if (isProcessing) { if (this.onSigInt) this.onSigInt(); return; }
506
+ const now = Date.now();
507
+ if (!this._lastCtrlCTime || now - this._lastCtrlCTime > 2000) {
508
+ this._lastCtrlCTime = now;
509
+ rawWrite(`\n ${C.dim}(Press Ctrl+C again or type /exit to quit)${C.reset}\n`);
510
+ this.render(); return;
511
+ }
512
+ if (this.onSigInt) this.onSigInt(); else process.exit(0);
513
+ return;
514
+ }
515
+
516
+ if (key.ctrl && key.name === 'l') { console.clear(); printWelcomeHero(); this.updateState(); this.render(); return; }
517
+ if (key.name === 'escape') { if (this.popoverActive) { this.popoverActive = false; this.popoverItems = []; this.ghostText = ''; this.render(); } return; }
518
+ if (key.name === 'f2') { this.finish('/details'); return; }
519
+
520
+ if (key.name === 'tab') {
521
+ if (this.popoverActive && this.popoverItems.length > 0) {
522
+ const item = this.popoverItems[this.popoverIndex];
523
+ if (item) { this.line = item.insert || item.name; this.cursor = this.line.length; this.updateState(); this.render(); return; }
524
+ } else if (this.ghostText) {
525
+ this.line += this.ghostText; this.cursor = this.line.length; this.updateState(); this.render(); return;
526
+ }
527
+ return;
528
+ }
529
+
530
+ if (key.name === 'return' || key.name === 'enter') {
531
+ if (this.popoverActive && this.popoverItems.length > 0) {
532
+ const item = this.popoverItems[this.popoverIndex];
533
+ if (item) {
534
+ const insertVal = item.insert || item.name;
535
+ if (insertVal.endsWith(' ')) { this.line = insertVal; this.cursor = this.line.length; this.updateState(); this.render(); return; }
536
+ this.line = insertVal; this.finish(this.line); return;
537
+ }
538
+ }
539
+ this.finish(this.line); return;
540
+ }
541
+
542
+ if (key.name === 'up') {
543
+ if (this.popoverActive && this.popoverItems.length > 0) {
544
+ this.popoverIndex = (this.popoverIndex - 1 + this.popoverItems.length) % this.popoverItems.length;
545
+ this.updateState(); this.render(); return;
546
+ }
547
+ if (this.history.length > 0) {
548
+ if (this.historyIndex === -1) this.historyIndex = this.history.length - 1;
549
+ else if (this.historyIndex > 0) this.historyIndex--;
550
+ this.line = this.history[this.historyIndex] || ''; this.cursor = this.line.length;
551
+ this.updateState(); this.render();
552
+ }
553
+ return;
554
+ }
555
+
556
+ if (key.name === 'down') {
557
+ if (this.popoverActive && this.popoverItems.length > 0) {
558
+ this.popoverIndex = (this.popoverIndex + 1) % this.popoverItems.length;
559
+ this.updateState(); this.render(); return;
560
+ }
561
+ if (this.history.length > 0 && this.historyIndex !== -1) {
562
+ if (this.historyIndex < this.history.length - 1) { this.historyIndex++; this.line = this.history[this.historyIndex] || ''; }
563
+ else { this.historyIndex = -1; this.line = ''; }
564
+ this.cursor = this.line.length; this.updateState(); this.render();
565
+ }
566
+ return;
567
+ }
568
+
569
+ if (key.name === 'right') { if (this.cursor < this.line.length) { this.cursor = nextGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
570
+ if (key.name === 'left') { if (this.cursor > 0) { this.cursor = previousGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
571
+ if (key.name === 'home' || (key.ctrl && key.name === 'a')) { this.cursor = 0; this.render(); return; }
572
+ if (key.name === 'end' || (key.ctrl && key.name === 'e')) { this.cursor = this.line.length; this.render(); return; }
573
+
574
+ if (key.name === 'backspace') { if (this.cursor > 0) { const previous = previousGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, previous) + this.line.slice(this.cursor); this.cursor = previous; this.updateState(); this.render(); } return; }
575
+ if (key.name === 'delete') { if (this.cursor < this.line.length) { const next = nextGraphemeBoundary(this.line, this.cursor); this.line = this.line.slice(0, this.cursor) + this.line.slice(next); this.updateState(); this.render(); } return; }
576
+
577
+ if (str && !key.ctrl && !key.meta) {
578
+ if (str.startsWith('\x1b') || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(str)) return;
579
+ this.line = this.line.slice(0, this.cursor) + str + this.line.slice(this.cursor);
580
+ this.cursor += str.length;
581
+ this.updateState(); this.render();
582
+ }
583
+ }
584
+ }
585
+
586
+ function getSuggestionsForLine(line) {
587
+ const trimmed = line.trimStart();
588
+ if (!trimmed.startsWith('/')) return [];
589
+ const parts = trimmed.split(/\s+/);
590
+ const baseCmd = parts[0].toLowerCase();
591
+ const allCmds = COMMAND_DEFINITIONS;
592
+
593
+ if (baseCmd === '/tool' && (parts.length > 1 || line.includes(' '))) {
594
+ const q = parts.slice(1).join(' ').toLowerCase();
595
+ if (!q) return toolsSchema.map(t => ({ name: `/tool ${t.function.name}`, desc: t.function.description.slice(0, 50), insert: `/tool ${t.function.name} ` }));
596
+ return toolsSchema
597
+ .filter(t => t.function.name.toLowerCase().includes(q))
598
+ .slice(0, 8)
599
+ .map(t => ({ name: `/tool ${t.function.name}`, desc: t.function.description.slice(0, 50), insert: `/tool ${t.function.name} ` }));
600
+ }
601
+
602
+ if (parts.length > 1 || (parts.length === 1 && line.includes(' '))) {
603
+ const subs = SUB_COMMANDS[baseCmd] || [];
604
+ const query = parts.slice(1).join(' ').toLowerCase();
605
+ return subs
606
+ .filter(s => s.toLowerCase().includes(query))
607
+ .map(s => ({ name: `${baseCmd} ${s}`, desc: '', insert: `${baseCmd} ${s} ` }));
608
+ }
609
+
610
+ return allCmds
611
+ .map(c => ({ ...c, score: c.name === baseCmd ? 200 : c.name.startsWith(baseCmd) ? 100 : c.name.includes(baseCmd) ? 70 : 0 }))
612
+ .filter(c => c.score > 0)
613
+ .sort((a, b) => b.score - a.score);
614
+ }
615
+
616
+ async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, signal = null, queryNumber = null) {
617
+ let spinnerInterval = null;
618
+ let hasPrintedResponseHeader = false;
619
+ let responsePanelStarted = false;
620
+ let toolSectionStarted = false;
621
+ let responseLineBuffer = '';
622
+ let pendingTableHeader = null;
623
+ let activeTableHeaders = null;
624
+ let tableRowNumber = 0;
625
+ const boxWidth = getFullWidth();
626
+ const bg = '\x1b[48;2;34;37;48m';
627
+ const bgHeader = '\x1b[48;2;48;52;68m';
628
+ const colorizeResponseText = (formatted) => (
629
+ `${C.white}${formatted.replace(/\x1b\[0m/g, C.reset + C.white)}${C.reset}`
630
+ );
631
+ const queryTag = queryNumber == null ? '' : `Q${queryNumber}`;
632
+ const answerTitle = queryTag
633
+ ? `Answer • ${queryTag} — ${prompt.length > 52 ? prompt.slice(0, 52) + '…' : prompt}`
634
+ : 'Answer';
635
+
636
+ const ensureResponsePanel = () => {
637
+ if (responsePanelStarted) return;
638
+ console.log('');
639
+ printSolidBannerRow(` ${C.bold}${C.roseGlow}${answerTitle}${C.reset}`, boxWidth, C.bgPanelHeader);
640
+ responsePanelStarted = true;
641
+ };
642
+
643
+ const printPlainResponseLine = (line) => {
644
+ ensureResponsePanel();
645
+ const wrapSafeLine = expandMarkdownLinks(line);
646
+ const formatted = formatTerminalMarkdown(wrapSafeLine);
647
+ const maxWidth = getFullWidth() - 4;
648
+ const visualLen = getStringDisplayWidth(formatted);
649
+ if (visualLen > maxWidth) {
650
+ for (const wl of wrapText(wrapSafeLine, maxWidth)) {
651
+ printSolidBannerRow(` ${colorizeResponseText(formatTerminalMarkdown(wl))}`, boxWidth, C.bgPanel);
652
+ }
653
+ } else {
654
+ printSolidBannerRow(` ${colorizeResponseText(formatted)}`, boxWidth, C.bgPanel);
655
+ }
656
+ };
657
+
658
+ const printTableRecord = (cells) => {
659
+ ensureResponsePanel();
660
+ tableRowNumber++;
661
+ printSolidBannerRow(` ${C.bold}${C.cyberCyan}Item ${tableRowNumber}${C.reset}`, boxWidth, C.bgPanelHeader);
662
+
663
+ const maxWidth = Math.max(16, getFullWidth() - 8);
664
+ for (let index = 0; index < activeTableHeaders.length; index++) {
665
+ const header = activeTableHeaders[index] || `Column ${index + 1}`;
666
+ const value = expandMarkdownLinks(cells[index] || '—');
667
+ const prefix = `${header}: `;
668
+ const wrapped = wrapText(prefix + value, maxWidth);
669
+ for (const line of wrapped) {
670
+ printSolidBannerRow(` ${colorizeResponseText(formatTerminalMarkdown(line))}`, boxWidth, C.bgPanel);
671
+ }
672
+ }
673
+ };
674
+
675
+ const flushPendingTableHeader = () => {
676
+ if (!pendingTableHeader) return;
677
+ printPlainResponseLine(`| ${pendingTableHeader.join(' | ')} |`);
678
+ pendingTableHeader = null;
679
+ };
680
+
681
+ const printResponseLine = (line) => {
682
+ const cells = parseMarkdownTableRow(line);
683
+
684
+ if (pendingTableHeader) {
685
+ if (cells && cells.length === pendingTableHeader.length && isMarkdownTableSeparator(cells)) {
686
+ activeTableHeaders = pendingTableHeader;
687
+ pendingTableHeader = null;
688
+ tableRowNumber = 0;
689
+ return;
690
+ }
691
+ flushPendingTableHeader();
692
+ }
693
+
694
+ if (activeTableHeaders) {
695
+ if (cells && cells.length === activeTableHeaders.length && !isMarkdownTableSeparator(cells)) {
696
+ printTableRecord(cells);
697
+ return;
698
+ }
699
+ activeTableHeaders = null;
700
+ tableRowNumber = 0;
701
+ }
702
+
703
+ if (cells && !isMarkdownTableSeparator(cells)) {
704
+ pendingTableHeader = cells;
705
+ return;
706
+ }
707
+
708
+ printPlainResponseLine(line);
709
+ };
710
+
711
+ const flushResponseBuffer = (isFinal = false) => {
712
+ if (responseLineBuffer.length > 0) {
713
+ if (isFinal) {
714
+ for (const line of responseLineBuffer.split('\n')) { if (line.length > 0) printResponseLine(line); }
715
+ responseLineBuffer = '';
716
+ } else if (responseLineBuffer.includes('\n')) {
717
+ const lines = responseLineBuffer.split('\n');
718
+ responseLineBuffer = lines.pop();
719
+ for (const line of lines) printResponseLine(line);
720
+ }
721
+ }
722
+ if (isFinal) flushPendingTableHeader();
723
+ };
724
+
725
+ const startSpinner = (label) => {
726
+ if (globalTuiPrompt) { globalTuiPrompt.agentState = label; globalTuiPrompt.agentStateTime = Date.now(); }
727
+ if (!spinnerInterval) {
728
+ spinnerInterval = setInterval(() => { if (globalTuiPrompt && isProcessing) globalTuiPrompt.render(); }, 80);
729
+ }
730
+ };
731
+
732
+ const stopSpinner = () => {
733
+ if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
734
+ if (globalTuiPrompt) { globalTuiPrompt.agentState = ''; globalTuiPrompt.render(); }
735
+ };
736
+
737
+ let thinkingShown = false;
738
+ let thinkingBuffer = '';
739
+ let thinkingStartTime = 0;
740
+ let accumulatedThinking = '';
741
+ let recordedTools = [];
742
+ let currentToolArgs = {};
743
+
744
+ startSpinner(`${C.slate}Thinking…${C.reset}`);
745
+
746
+ try {
747
+ await agent.ask(prompt, {
748
+ signal,
749
+ onStepStart: () => {},
750
+ onReasoningStart: () => {
751
+ stopSpinner();
752
+ thinkingStartTime = Date.now();
753
+ thinkingBuffer = '';
754
+ if (!thinkingShown) {
755
+ console.log('');
756
+ const icon = thinkingFrames[Math.floor(Date.now() / 200) % thinkingFrames.length];
757
+ printSolidBannerRow(` ${C.lavender}${C.bold}${icon} Thinking${queryTag ? ` • ${queryTag}` : ''}${C.reset}`, boxWidth, bgHeader);
758
+ }
759
+ },
760
+ onReasoningDelta: (chunk) => {
761
+ accumulatedThinking += chunk;
762
+ thinkingBuffer += chunk;
763
+ if (!thinkingShown) {
764
+ const lines = thinkingBuffer.split('\n');
765
+ thinkingBuffer = lines.pop();
766
+ for (const line of lines) {
767
+ if (line.trim()) {
768
+ for (const w of wrapText(cleanThinkingLine(line), boxWidth - 6)) {
769
+ printSolidBannerRow(` ${C.dim}${C.slate}${w}${C.reset}`, boxWidth, bg);
770
+ }
771
+ }
772
+ }
773
+ }
774
+ },
775
+ onReasoningEnd: () => {
776
+ const elapsed = ((Date.now() - thinkingStartTime) / 1000).toFixed(1);
777
+ if (!thinkingShown) {
778
+ if (thinkingBuffer.trim()) {
779
+ for (const w of wrapText(cleanThinkingLine(thinkingBuffer), boxWidth - 6)) {
780
+ printSolidBannerRow(` ${C.dim}${C.slate}${w}${C.reset}`, boxWidth, bg);
781
+ }
782
+ }
783
+ printSolidBannerRow(` ${C.emerald}[DONE] Thought for ${elapsed}s${C.reset}`, boxWidth, bg);
784
+ console.log('');
785
+ } else {
786
+ stopSpinner();
787
+ const detailHint = queryTag ? ` — F2 or /details ${queryTag}` : '';
788
+ console.log(`\n ${C.dim}${C.slate}Thinking${queryTag ? ` • ${queryTag}` : ''} (${elapsed}s)${detailHint}${C.reset}`);
789
+ }
790
+ thinkingShown = true;
791
+ },
792
+
793
+ onToolStart: (toolName, args) => {
794
+ stopSpinner();
795
+ if (!toolSectionStarted) {
796
+ console.log('');
797
+ const detailHint = queryTag ? ` ${C.dim}Details: F2 or /details ${queryTag}${C.reset}` : '';
798
+ printSolidBannerRow(` ${C.bold}${C.cyberCyan}Tools${queryTag ? ` • ${queryTag}` : ''}${C.reset}${detailHint}`, boxWidth, C.bgPanelHeader);
799
+ toolSectionStarted = true;
800
+ }
801
+ currentToolArgs = args || {};
802
+ const queryVal = args?.query || args?.url || Object.values(args || {})[0] || '';
803
+ const displayQuery = typeof queryVal === 'string' && queryVal.length > 30 ? queryVal.slice(0, 30) + '…' : queryVal;
804
+ startSpinner(`${C.cyberCyan}• ${C.bold}${C.white}${toolName}${C.reset}${displayQuery ? ` ${C.slate}"${displayQuery}"${C.reset}` : ''}`);
805
+ },
806
+
807
+ onToolResult: (toolName, result, durationMs) => {
808
+ stopSpinner();
809
+ recordedTools.push({ name: toolName, args: currentToolArgs, result, duration: durationMs });
810
+
811
+ const count = result.count ?? result.returnedCount;
812
+ const isError = result.status === 'error';
813
+ const isNotFound = result.status === 'not_found' || count === 0;
814
+ const statusLabel = isError
815
+ ? `${C.red}[ERROR]`
816
+ : isNotFound
817
+ ? `${C.amber}[NOT FOUND]`
818
+ : `${C.emerald}[OK]`;
819
+ const countTag = count != null && !isError ? ` • ${C.cyberCyan}${count} results${C.reset}` : '';
820
+ const timerTag = durationMs !== undefined ? ` • ${C.dim}${(durationMs / 1000).toFixed(2)}s${C.reset}` : '';
821
+
822
+ printSolidBannerRow(` ${C.slate}• ${C.bold}${C.cyberCyan}${toolName}${C.reset} ${statusLabel} ${(isError ? C.red + 'Failed' : isNotFound ? C.amber + 'No matching data' : C.white + 'OK') + C.reset}${countTag}${timerTag}`, boxWidth, C.bgPanel);
823
+
824
+ if (!isError && !isNotFound) {
825
+ const preview = extractPreview(result, toolName);
826
+ if (preview) {
827
+ for (const pLine of wrapText('• ' + preview, boxWidth - 6)) {
828
+ printSolidBannerRow(` ${C.dim}${C.slate}${pLine}${C.reset}`, boxWidth, bg);
829
+ }
830
+ }
831
+ }
832
+ },
833
+
834
+ onTokenStart: () => { stopSpinner(); },
835
+
836
+ onTokenDelta: (token) => {
837
+ if (!hasPrintedResponseHeader) { stopSpinner(); hasPrintedResponseHeader = true; }
838
+ responseLineBuffer += token;
839
+ flushResponseBuffer(false);
840
+ },
841
+
842
+ onFinalAnswer: (answer) => {
843
+ stopSpinner();
844
+ if (!hasPrintedResponseHeader) {
845
+ for (const line of (answer || '').split('\n')) { if (line.length > 0) printResponseLine(line); }
846
+ flushPendingTableHeader();
847
+ } else {
848
+ flushResponseBuffer(true);
849
+ }
850
+ },
851
+
852
+ onError: (err) => {
853
+ stopSpinner();
854
+ console.log(`\n ${C.red}[ERROR] ${err.message}${C.reset}\n`);
855
+ },
856
+ });
857
+
858
+ lastQueryDetails = { number: queryNumber, prompt, thinking: accumulatedThinking, tools: recordedTools, timestamp: Date.now() };
859
+ if (queryNumber != null) {
860
+ queryDetails.set(queryNumber, lastQueryDetails);
861
+ if (queryDetails.size > 50) queryDetails.delete(queryDetails.keys().next().value);
862
+ }
863
+ if (responsePanelStarted) console.log('');
864
+ } catch (err) {
865
+ stopSpinner();
866
+ if (!isAbortError(err)) {
867
+ console.log(`\n ${C.red}[ERROR] ${err.message}${C.reset}\n`);
868
+ } else {
869
+ console.log(`\n ${C.amber}[INTERRUPTED] Query cancelled.${C.reset}\n`);
870
+ }
871
+ }
872
+ }
873
+
874
+ async function main() {
875
+ try { process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l'); } catch {}
876
+
877
+ const opts = parseCliArgs();
878
+
879
+ if (opts.help) { printWelcomeHero(); printHelp(); process.exit(0); }
880
+ if (opts.showTools) {
881
+ printWelcomeHero();
882
+ console.log(`\n ${C.bold}${C.sakura}Tools (${toolsSchema.length})${C.reset}\n`);
883
+ for (const [i, t] of toolsSchema.entries()) {
884
+ console.log(` ${C.gold}${String(i + 1).padStart(2)}${C.reset} ${C.cyberCyan}${t.function.name}${C.reset} — ${C.dim}${t.function.description.slice(0, 60)}${C.reset}`);
885
+ }
886
+ console.log('');
887
+ process.exit(0);
888
+ }
889
+
890
+ if (opts.showStats) { printWelcomeHero(); const r = await executeTool('get_system_stats', {}); console.log(JSON.stringify(r, null, 2)); process.exit(0); }
891
+
892
+ const baseUrl = opts.url || config.lmStudio.baseUrl;
893
+ const client = new LMStudioClient({ baseUrl, model: opts.model });
894
+ const health = await client.checkConnection();
895
+
896
+ printWelcomeHero();
897
+
898
+ if (!health.connected) {
899
+ console.log(` ${C.amber}!! LM Studio offline${C.reset}`);
900
+ console.log(` ${C.dim}• ${health.error}${C.reset}\n`);
901
+ console.log(` ${C.bold}${C.sakura}Attempting auto-start...${C.reset}`);
902
+
903
+ const autoHealth = await attemptAutoStartLMStudio(baseUrl, opts.model || config.lmStudio.model);
904
+ if (autoHealth) {
905
+ console.log(` ${C.emerald}[OK] LM Studio started.${C.reset}\n`);
906
+ } else {
907
+ console.log(` ${C.sakura}Setup: Open LM Studio → load google/gemma-4-e4b → Start Server :1234${C.reset}\n`);
908
+ if (opts.query) {
909
+ const directResult = await executeTool('global_search', { query: opts.query });
910
+ console.log(JSON.stringify(directResult, null, 2));
911
+ process.exit(0);
912
+ }
913
+ process.exit(1);
914
+ }
915
+ }
916
+
917
+ const agent = new KikuAgent({ baseUrl, model: health.activeModel || opts.model, thinkingMode: opts.thinking });
918
+
919
+ if (opts.query) {
920
+ console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
921
+ await executeQueryWithUI(agent, opts.query, agent.session.thinkingMode === 'show', null, 1);
922
+ process.exit(0);
923
+ }
924
+
925
+ await agent._ready;
926
+
927
+ if (agent.restoredMessageCount > 0) {
928
+ const s = agent.session;
929
+ console.log(`\n ${C.sakura}${C.bold}[SESSION RESTORED]${C.reset} ${C.dim}${s.id} — ${s.messages.length} messages${C.reset}\n`);
930
+ }
931
+
932
+ const tuiPrompt = new InteractiveTuiPrompt({
933
+ prompt: 'Kiku ❯ ',
934
+ agent,
935
+ onSigInt: async () => {
936
+ if (activeAbortController) {
937
+ activeAbortController.abort();
938
+ activeAbortController = null;
939
+ queryQueue.length = 0;
940
+ deferredCommands.length = 0;
941
+ } else {
942
+ try { await agent.endSession(); } catch {}
943
+ globalTuiPrompt?.dispose();
944
+ console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
945
+ process.exit(0);
946
+ }
947
+ }
948
+ });
949
+
950
+ globalTuiPrompt = tuiPrompt;
951
+
952
+ setupOutputInterception(tuiPrompt, () => isProcessing);
953
+
954
+ agent.stateMachine.on('enter:' + AgentStates.FETCHING_TOOL, ({ toolName }) => {
955
+ if (globalTuiPrompt) {
956
+ globalTuiPrompt.agentState = `Tool: ${toolName}`;
957
+ globalTuiPrompt.agentStateTime = Date.now();
958
+ }
959
+ });
960
+
961
+ agent.stateMachine.on('enter:' + AgentStates.STREAMING_THOUGHTS, () => {
962
+ if (globalTuiPrompt) {
963
+ globalTuiPrompt.agentState = 'Thinking…';
964
+ globalTuiPrompt.agentStateTime = Date.now();
965
+ }
966
+ });
967
+
968
+ agent.stateMachine.on('enter:' + AgentStates.STREAMING_ANSWER, () => {
969
+ if (globalTuiPrompt) {
970
+ globalTuiPrompt.agentState = 'Streaming…';
971
+ globalTuiPrompt.agentStateTime = Date.now();
972
+ }
973
+ });
974
+
975
+ agent.stateMachine.on('enter:' + AgentStates.IDLE, () => {
976
+ if (globalTuiPrompt) {
977
+ globalTuiPrompt.agentState = '';
978
+ }
979
+ });
980
+
981
+ async function processNextQueueItem() {
982
+ if (isProcessing || queryQueue.length === 0) return;
983
+
984
+ isProcessing = true;
985
+ const nextItem = queryQueue.shift();
986
+ lastQuery = nextItem.prompt;
987
+ activeQueryNumber = nextItem.number;
988
+
989
+ tuiPrompt.clearPrompt();
990
+ tuiPrompt.render();
991
+
992
+ console.log('');
993
+ activeAbortController = new AbortController();
994
+ try {
995
+ await executeQueryWithUI(agent, nextItem.prompt, agent.session.thinkingMode === 'show', activeAbortController.signal, nextItem.number);
996
+ } finally {
997
+ activeAbortController = null;
998
+ queryCount++;
999
+ printAd();
1000
+ console.log('');
1001
+ isProcessing = false;
1002
+ activeQueryNumber = null;
1003
+ if (globalTuiPrompt) globalTuiPrompt.render();
1004
+
1005
+ if (queryQueue.length > 0) {
1006
+ setTimeout(processNextQueueItem, 100);
1007
+ } else if (deferredCommands.length > 0 && globalTuiPrompt && globalTuiPrompt.activeResolve) {
1008
+ globalTuiPrompt.finish(' ');
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ while (true) {
1014
+ let rawInput;
1015
+
1016
+ if (deferredCommands.length > 0 && !isProcessing) {
1017
+ rawInput = deferredCommands.shift();
1018
+ console.log(`\n ${C.cyberCyan}[DEFERRED] ${rawInput}${C.reset}\n`);
1019
+ } else {
1020
+ rawInput = await tuiPrompt.readLine();
1021
+ }
1022
+
1023
+ if (rawInput === null) break;
1024
+ const trimmed = rawInput.trim();
1025
+ if (!trimmed) continue;
1026
+
1027
+ const queryNumber = !trimmed.startsWith('/') ? nextQueryNumber++ : null;
1028
+
1029
+ if (!trimmed.startsWith('/')) {
1030
+ const wrapped = wrapText(trimmed, getFullWidth() - 8);
1031
+ const queuePosition = isProcessing ? queryQueue.length + 1 : 0;
1032
+ const queuedLabel = queuePosition > 0 ? ` • Queued ${queuePosition}` : '';
1033
+ printSolidPanel({
1034
+ title: `${C.roseGlow}${C.bold}Question ${queryNumber} • ${randomFanName()}${queuedLabel}${C.reset}`,
1035
+ lines: wrapped.map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
1036
+ bg: '\x1b[48;2;36;40;52m'
1037
+ });
1038
+ }
1039
+
1040
+ if (trimmed.startsWith('/')) {
1041
+ if (isProcessing) {
1042
+ deferredCommands.push(trimmed);
1043
+ console.log(`\n ${C.cyberCyan}[DEFERRED] ${trimmed}${C.reset}\n`);
1044
+ tuiPrompt.render();
1045
+ continue;
1046
+ }
1047
+
1048
+ const cmdCtx = {
1049
+ agent,
1050
+ client,
1051
+ baseUrl,
1052
+ sub: trimmed.split(/\s+/)[1] || '',
1053
+ extra: trimmed.split(/\s+/).slice(2).join(' '),
1054
+ parts: trimmed.split(/\s+/),
1055
+ lastQuery,
1056
+ printHelp,
1057
+ printQueryDetails,
1058
+ printWelcome: printWelcomeHero,
1059
+ printAd,
1060
+ enqueueQuery: (prompt, isRetry) => {
1061
+ queryQueue.push({ id: 'q_' + Date.now(), number: nextQueryNumber++, prompt, addedAt: Date.now() });
1062
+ if (!isProcessing) processNextQueueItem();
1063
+ },
1064
+ clearQueryQueue: () => { queryQueue.length = 0; },
1065
+ getQueryQueueLength: () => queryQueue.length,
1066
+ };
1067
+
1068
+ const result = await routeCommand(trimmed, cmdCtx);
1069
+
1070
+ if (result === 'EXIT') break;
1071
+ if (result === 'RETRY' && cmdCtx.lastQuery) {
1072
+ const retryNumber = nextQueryNumber++;
1073
+ printSolidPanel({
1074
+ title: `${C.roseGlow}${C.bold}Question ${retryNumber} • Retry${C.reset}`,
1075
+ lines: wrapText(cmdCtx.lastQuery, getFullWidth() - 8)
1076
+ .map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
1077
+ bg: '\x1b[48;2;36;40;52m'
1078
+ });
1079
+ queryQueue.push({ id: 'q_' + Date.now(), number: retryNumber, prompt: cmdCtx.lastQuery, addedAt: Date.now() });
1080
+ if (!isProcessing) processNextQueueItem();
1081
+ }
1082
+ continue;
1083
+ }
1084
+
1085
+ const { sanitized, warnings } = sanitizeQuery(trimmed);
1086
+ if (warnings.length > 0) {
1087
+ console.log(`\n ${C.amber}[WARNING] ${warnings.join('; ')}${C.reset}\n`);
1088
+ }
1089
+
1090
+ lastQuery = trimmed;
1091
+ queryQueue.push({ id: 'q_' + Date.now(), number: queryNumber, prompt: trimmed, addedAt: Date.now() });
1092
+ if (isProcessing) { tuiPrompt.render(); continue; }
1093
+ processNextQueueItem();
1094
+ }
1095
+ }
1096
+
1097
+ process.on('exit', () => { try { globalTuiPrompt?.dispose(); } catch {} });
1098
+ process.on('uncaughtException', (err) => {
1099
+ try { globalTuiPrompt?.dispose(); } catch {}
1100
+ console.error(`\n Uncaught: ${err.message}`);
1101
+ process.exit(1);
1102
+ });
1103
+ process.on('SIGINT', () => {
1104
+ try { globalTuiPrompt?.dispose(); } catch {}
1105
+ process.exit(130);
1106
+ });
1107
+
1108
+ main().catch(err => {
1109
+ console.error(`\n ${C.red}Fatal: ${err.message}${C.reset}\n`);
1110
+ process.exit(1);
1111
+ });