kiku-agent 1.0.0 → 1.0.4

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 (40) hide show
  1. package/README.md +293 -122
  2. package/package.json +60 -61
  3. package/server/agent/agentCore.js +892 -782
  4. package/server/agent/appPaths.js +21 -0
  5. package/server/agent/audioPlayer.js +739 -673
  6. package/server/agent/cli.js +1414 -1111
  7. package/server/agent/commandRegistry.js +74 -58
  8. package/server/agent/commands.js +1694 -788
  9. package/server/agent/config.js +264 -90
  10. package/server/agent/contextCompactor.js +79 -0
  11. package/server/agent/doctor.js +69 -0
  12. package/server/agent/eventBus.js +107 -103
  13. package/server/agent/gpuMetrics.js +189 -0
  14. package/server/agent/llmClient.js +369 -200
  15. package/server/agent/mcpClient.js +364 -0
  16. package/server/agent/memory.js +670 -566
  17. package/server/agent/modelManager.js +77 -0
  18. package/server/agent/research.js +202 -0
  19. package/server/agent/session.js +244 -239
  20. package/server/agent/subagent.js +116 -0
  21. package/server/agent/theme.js +125 -124
  22. package/server/agent/toolRouter.js +201 -0
  23. package/server/agent/tools.js +1679 -1528
  24. package/server/agent/ui.js +760 -469
  25. package/server/agent/youtubeAuth.js +144 -65
  26. package/server/mcp/claude-desktop-config.example.json +1 -1
  27. package/server/mcp/data/discography.js +36 -36
  28. package/server/mcp/dataAdapter.js +102 -0
  29. package/server/mcp/examples/claude-code.mcp.example.json +19 -0
  30. package/server/mcp/examples/claude-desktop.example.json +11 -0
  31. package/server/mcp/examples/cline.example.json +13 -0
  32. package/server/mcp/examples/cursor.example.json +14 -0
  33. package/server/mcp/examples/gemini-cli.example.json +17 -0
  34. package/server/mcp/examples/opencode.example.json +21 -0
  35. package/server/mcp/examples/vscode.example.json +19 -0
  36. package/server/mcp/examples/windsurf.example.json +14 -0
  37. package/server/mcp/handler.js +85 -159
  38. package/server/mcp/security.js +140 -138
  39. package/server/mcp/server.js +129 -100
  40. package/server/mcp/stdio.js +4 -2
@@ -1,1111 +1,1414 @@
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
- });
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 { LLMClient, LMStudioClient } from './llmClient.js';
9
+ import { toolsSchema, toolHandlers, executeTool } from './tools.js';
10
+ import { config, CLOUD_PROVIDERS } 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
+ import { ModelManager } from './modelManager.js';
19
+
20
+ import {
21
+ C, getStringDisplayWidth, getFullWidth, wrapText, stripAnsi, fitStyledText,
22
+ previousGraphemeBoundary, nextGraphemeBoundary, sliceLineForDisplay,
23
+ printSolidBannerRow, printSolidPanel, formatSolidPanelLines, formatTerminalMarkdown,
24
+ parseMarkdownTableRow, isMarkdownTableSeparator, expandMarkdownLinks,
25
+ printWelcomeHero, getWelcomeHeroLines, randomFanName, setupOutputInterception,
26
+ rawWrite, INPUT_PLACEHOLDER, UI_ICONS,
27
+ spinnerFrames, thinkingFrames, createProgressRunner,
28
+ enterAlternateScreen, exitAlternateScreen, VirtualScreenBuffer,
29
+ } from './ui.js';
30
+
31
+ let activeAbortController = null;
32
+ let isProcessing = false;
33
+ let lastQuery = null;
34
+ let queryCount = 0;
35
+ let nextQueryNumber = 1;
36
+ let activeQueryNumber = null;
37
+ const queryQueue = [];
38
+ const deferredCommands = [];
39
+ let globalTuiPrompt = null;
40
+ let lastQueryDetails = null;
41
+ const queryDetails = new Map();
42
+
43
+ function parseCliArgs() {
44
+ const args = process.argv.slice(2);
45
+ const options = {
46
+ query: null,
47
+ provider: null,
48
+ apiKey: null,
49
+ model: null,
50
+ url: null,
51
+ mcp: null,
52
+ showTools: false,
53
+ showStats: false,
54
+ thinking: 'show',
55
+ verbose: false,
56
+ help: false
57
+ };
58
+
59
+ for (let i = 0; i < args.length; i++) {
60
+ const arg = args[i];
61
+ if (arg === '--help' || arg === '-h') options.help = true;
62
+ else if (arg === '--tools' || arg === '-t') options.showTools = true;
63
+ else if (arg === '--stats' || arg === '-s') options.showStats = true;
64
+ else if (arg === '--verbose' || arg === '-v') { options.verbose = true; options.thinking = 'show'; }
65
+ else if (arg === '--thinking') options.thinking = args[++i] || 'show';
66
+ else if (arg === '--provider' || arg === '-P') options.provider = args[++i];
67
+ else if (arg === '--api-key' || arg === '-k') options.apiKey = args[++i];
68
+ else if (arg === '--mcp') options.mcp = args[++i];
69
+ else if (arg === '--model' || arg === '-m') options.model = args[++i];
70
+ else if (arg === '--url' || arg === '-u') options.url = args[++i];
71
+ else if (arg === '--query' || arg === '-q') options.query = args[++i];
72
+ else if (!arg.startsWith('-') && !options.query) { options.query = args.slice(i).join(' '); break; }
73
+ }
74
+ return options;
75
+ }
76
+
77
+ async function waitForLMStudio(baseUrl, timeoutMs = 30000) {
78
+ const client = new LMStudioClient({ baseUrl });
79
+ const start = Date.now();
80
+ while (Date.now() - start < timeoutMs) {
81
+ const health = await client.checkConnection();
82
+ if (health.connected) return health;
83
+ await new Promise(r => setTimeout(r, 1000));
84
+ }
85
+ return null;
86
+ }
87
+
88
+ async function attemptAutoStartLMStudio(baseUrl, model) {
89
+ const isWin = process.platform === 'win32';
90
+ const isMac = process.platform === 'darwin';
91
+
92
+ const candidates = isWin
93
+ ? [
94
+ `${process.env.LOCALAPPDATA}\\LM Studio\\LM Studio.exe`,
95
+ `${process.env.LOCALAPPDATA}\\Programs\\LM Studio\\LM Studio.exe`,
96
+ 'C:\\Program Files\\LM Studio\\LM Studio.exe'
97
+ ]
98
+ : isMac
99
+ ? ['/Applications/LM Studio.app/Contents/MacOS/LM Studio']
100
+ : ['lmstudio', '/usr/local/bin/lmstudio', '/opt/lmstudio/bin/lmstudio'];
101
+
102
+ const exe = candidates.find(p => {
103
+ if (p.includes('/') || p.includes('\\')) {
104
+ try { return existsSync(p); } catch { return false; }
105
+ }
106
+ return true;
107
+ });
108
+
109
+ if (!exe) return null;
110
+ console.log(` ${C.cyberCyan}attempting to auto-start LM Studio...${C.reset}`);
111
+
112
+ try {
113
+ let port = '1234';
114
+ try { const parsedUrl = new URL(baseUrl); if (parsedUrl.port) port = parsedUrl.port; } catch {}
115
+
116
+ const child = spawn(exe, isWin
117
+ ? ['--model', model, '--serve', '--port', port]
118
+ : ['serve', '--model', model, '--port', port],
119
+ { detached: true, stdio: 'ignore', windowsHide: true }
120
+ );
121
+ child.unref();
122
+
123
+ return await waitForLMStudio(baseUrl, 45000);
124
+ } catch { return null; }
125
+ }
126
+
127
+ function extractPreview(result, toolName) {
128
+ const MAX = 80;
129
+ let raw = '';
130
+ if (result.content) raw = result.content;
131
+ else if (result.results && Array.isArray(result.results) && result.results.length > 0) {
132
+ const first = result.results[0];
133
+ raw = first.title || first.snippet || first.url || '';
134
+ } else if (result.works) raw = result.works.map(w => w.titleTh || w.titleCn).filter(Boolean).join(', ');
135
+ else if (result.songs) raw = result.songs.map(s => s.title).filter(Boolean).join(', ');
136
+ else if (result.brands) raw = result.brands.map(b => b.brand).filter(Boolean).join(', ');
137
+ else if (result.profile) raw = `${result.profile.nameTh || ''} ${result.profile.nameCn || ''}`.trim();
138
+ else if (result.stats) raw = `${result.stats.totalWorks || 0} works`;
139
+
140
+ if (!raw) return '';
141
+ raw = raw.replace(/\s+/g, ' ').trim();
142
+ return raw.length > MAX ? raw.slice(0, MAX) + '…' : raw;
143
+ }
144
+
145
+ function cleanThinkingLine(line) {
146
+ if (!line) return '';
147
+ return line
148
+ .replace(/\*\*\*?([^*]+)\*\*\*/g, '$1')
149
+ .replace(/\*([^\*\n]+)\*/g, '$1')
150
+ .replace(/^(\s*)[-*]\s+/gm, '$1• ')
151
+ .replace(/\*/g, '')
152
+ .trim();
153
+ }
154
+
155
+ function printQueryDetails(reference = '') {
156
+ const normalized = String(reference || '').trim().replace(/^q/i, '');
157
+ const requestedNumber = normalized ? Number.parseInt(normalized, 10) : lastQueryDetails?.number;
158
+ const details = normalized
159
+ ? (Number.isInteger(requestedNumber) ? queryDetails.get(requestedNumber) : null)
160
+ : lastQueryDetails;
161
+
162
+ if (!details) {
163
+ console.log(`\n ${C.amber}[NOT FOUND] No completed query details. Use ${C.cyberCyan}/details Q1${C.reset}${C.amber}.${C.reset}\n`);
164
+ return;
165
+ }
166
+
167
+ const width = Math.max(16, getFullWidth() - 8);
168
+ const safeLines = (label, value) => {
169
+ const text = `${label}: ${String(value || '—')}`;
170
+ return wrapText(expandMarkdownLinks(text), width)
171
+ .map(line => `${C.white}${formatTerminalMarkdown(line)}${C.reset}`);
172
+ };
173
+
174
+ printSolidPanel({
175
+ title: `Details • Q${details.number}`,
176
+ lines: safeLines('Question', details.prompt),
177
+ });
178
+
179
+ const thinking = String(details.thinking || '').trim();
180
+ printSolidPanel({
181
+ title: `Thinking • Q${details.number}`,
182
+ lines: thinking
183
+ ? thinking.split('\n').flatMap(line => wrapText(cleanThinkingLine(line), width)).map(line => `${C.dim}${C.slate}${line}${C.reset}`)
184
+ : [`${C.dim}${C.slate}No thinking trace was recorded.${C.reset}`],
185
+ });
186
+
187
+ if (!details.tools.length) {
188
+ printSolidPanel({
189
+ title: `Tools • Q${details.number}`,
190
+ lines: [`${C.dim}${C.slate}No tools were used.${C.reset}`],
191
+ });
192
+ return;
193
+ }
194
+
195
+ for (const [index, tool] of details.tools.entries()) {
196
+ const args = JSON.stringify(tool.args ?? {}, null, 2);
197
+ const serializedResult = JSON.stringify(tool.result ?? {}, null, 2);
198
+ const result = serializedResult.length > 6000
199
+ ? serializedResult.slice(0, 6000) + '\n… result truncated'
200
+ : serializedResult;
201
+ const lines = [
202
+ ...safeLines('Duration', tool.duration == null ? 'n/a' : `${(tool.duration / 1000).toFixed(2)}s`),
203
+ ...safeLines('Arguments', args),
204
+ ...safeLines('Result', result),
205
+ ];
206
+ printSolidPanel({ title: `Tool ${index + 1} • ${tool.name} • Q${details.number}`, lines });
207
+ }
208
+ }
209
+
210
+ export function printHelp(specificCmd = null) {
211
+ if (specificCmd) {
212
+ const definition = findCommandDefinition(specificCmd);
213
+ if (!definition) {
214
+ console.log(`\n ${C.amber}Unknown command: ${specificCmd}${C.reset}`);
215
+ console.log(` ${C.dim}Type /help to see all commands.${C.reset}\n`);
216
+ return;
217
+ }
218
+ const maxContentWidth = Math.max(40, getFullWidth() - 8);
219
+ const wrappedDesc = wrapText(definition.desc, maxContentWidth);
220
+ const wrappedUsage = wrapText(`Usage: ${definition.usage}`, maxContentWidth);
221
+ printSolidPanel({
222
+ title: `${UI_ICONS.help} ${definition.name}`,
223
+ lines: [
224
+ ...wrappedDesc.map(d => `${C.white}${d}${C.reset}`),
225
+ ...wrappedUsage.map(u => `${C.dim}${u}${C.reset}`),
226
+ ],
227
+ });
228
+ return;
229
+ }
230
+
231
+ const termWidth = getFullWidth();
232
+ const maxContentWidth = Math.max(40, termWidth - 8);
233
+ const helpLines = [];
234
+
235
+ for (const command of COMMAND_DEFINITIONS) {
236
+ const usageWidth = 30;
237
+ const descWidth = maxContentWidth - usageWidth - 2;
238
+
239
+ if (command.usage.length > 28 || command.desc.length > descWidth) {
240
+ const wrappedDesc = wrapText(command.desc, maxContentWidth - 6);
241
+ helpLines.push(`${C.emerald}${command.usage}${C.reset}`);
242
+ for (const d of wrappedDesc) {
243
+ helpLines.push(` ${C.dim}↳${C.reset} ${C.white}${d}${C.reset}`);
244
+ }
245
+ } else {
246
+ helpLines.push(`${C.emerald}${command.usage.padEnd(usageWidth)}${C.reset} ${C.white}${command.desc}${C.reset}`);
247
+ }
248
+ }
249
+
250
+ helpLines.push(`${C.dim}Tip: Press Tab for auto-completion • Type /help <command> for details.${C.reset}`);
251
+
252
+ printSolidPanel({
253
+ title: `${UI_ICONS.help} Kiku AI Agent — ${COMMAND_DEFINITIONS.length} Commands`,
254
+ lines: helpLines,
255
+ });
256
+ }
257
+
258
+ const ads = [
259
+ {
260
+ title: 'DashSecurity', subtitle: 'Cybersecurity Knowledge Cloud',
261
+ url: 'https://dashsecurity.kpltgroup.com/',
262
+ features: ['AI-Powered SAST Scanner', 'OWASP Top 10 Detection', 'Certification Prep Labs'],
263
+ },
264
+ {
265
+ title: 'Ju Jingyi Kikuuu', subtitle: 'Official Portfolio',
266
+ url: 'https://jujingyi-kikuuu.vercel.app/',
267
+ features: ['45+ Music Releases', 'Complete Filmography', 'REST API v1'],
268
+ },
269
+ {
270
+ title: 'ThemeHackers', subtitle: 'Security Research',
271
+ url: 'https://github.com/Themehackers/',
272
+ features: ['Penetration Testing', 'Open-Source Tools', '10+ Certifications'],
273
+ },
274
+ ];
275
+
276
+ let adIndex = 0;
277
+ function printAd() {
278
+ const ad = ads[adIndex % ads.length];
279
+ adIndex++;
280
+ const w = getFullWidth();
281
+ console.log('');
282
+ 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);
283
+ printSolidBannerRow(` ${ad.features.map(f => `${C.white}${f}${C.reset}`).join(' • ')}`, w, C.bgPanel);
284
+ printSolidBannerRow(` ${C.cyberCyan}${C.underline}${ad.url}${C.reset}`, w, C.bgPanel);
285
+ console.log('');
286
+ }
287
+
288
+ class InteractiveTuiPrompt {
289
+ constructor(options = {}) {
290
+ this.promptText = options.prompt || 'Kiku ❯ ';
291
+ this.agent = options.agent || null;
292
+ this.modelName = options.modelName || 'gemma-4-e4b';
293
+ this.provider = options.provider || 'lmstudio';
294
+ this.history = [];
295
+ this.historyIndex = -1;
296
+ this.line = '';
297
+ this.cursor = 0;
298
+ this.popoverIndex = 0;
299
+ this.popoverActive = false;
300
+ this.popoverItems = [];
301
+ this.ghostText = '';
302
+ this.isResolving = false;
303
+ this.activeResolve = null;
304
+ this.activeReject = null;
305
+ this.onSigInt = options.onSigInt || null;
306
+ this._keyHandler = null;
307
+ this._renderTimer = null;
308
+ this.agentState = '';
309
+ this.agentStateTime = 0;
310
+ this._lastCtrlCTime = 0;
311
+
312
+ this.viewportLines = getWelcomeHeroLines();
313
+ this.scrollOffset = 0;
314
+ this.screenBuffer = new VirtualScreenBuffer(
315
+ Math.max(20, process.stdout.columns || 80),
316
+ Math.max(10, process.stdout.rows || 24)
317
+ );
318
+ }
319
+
320
+ addViewportLines(lines) {
321
+ if (!lines) return;
322
+ if (!Array.isArray(lines)) lines = [lines];
323
+ for (const l of lines) {
324
+ if (typeof l === 'string') {
325
+ const sublines = l.split('\n');
326
+ for (const sl of sublines) this.viewportLines.push(sl);
327
+ }
328
+ }
329
+ if (this.viewportLines.length > 5000) {
330
+ this.viewportLines.splice(0, this.viewportLines.length - 5000);
331
+ }
332
+ if (this.scrollOffset === 0) {
333
+ this.scrollOffset = 0;
334
+ }
335
+ this.render();
336
+ }
337
+
338
+ updateLastViewportLine(line) {
339
+ if (this.viewportLines.length === 0) {
340
+ this.viewportLines.push(line);
341
+ } else {
342
+ this.viewportLines[this.viewportLines.length - 1] = line;
343
+ }
344
+ this.render();
345
+ }
346
+
347
+ clearViewport() {
348
+ this.viewportLines = getWelcomeHeroLines();
349
+ this.scrollOffset = 0;
350
+ this.render();
351
+ }
352
+
353
+ clearPrompt() {
354
+
355
+ }
356
+
357
+ clearPopover() {
358
+ if (this.popoverActive) {
359
+ this.popoverActive = false;
360
+ this.popoverItems = [];
361
+ this.ghostText = '';
362
+ }
363
+ }
364
+
365
+ _cleanupInput() {
366
+ if (this._renderTimer) { clearTimeout(this._renderTimer); this._renderTimer = null; }
367
+ if (this._keyHandler) { input.removeListener('keypress', this._keyHandler); this._keyHandler = null; }
368
+ try { if (input.isTTY) input.setRawMode(false); } catch {}
369
+ }
370
+
371
+ async readLine() {
372
+ return new Promise((resolve, reject) => {
373
+ this._cleanupInput();
374
+ this.line = '';
375
+ this.cursor = 0;
376
+ this.historyIndex = -1;
377
+ this.popoverIndex = 0;
378
+ this.popoverActive = false;
379
+ this.popoverItems = [];
380
+ this.ghostText = '';
381
+ this.isResolving = false;
382
+ this.activeResolve = resolve;
383
+ this.activeReject = reject;
384
+
385
+ try {
386
+ this.updateState();
387
+ readline.emitKeypressEvents(input);
388
+ if (input.isTTY) input.setRawMode(true);
389
+ this._keyHandler = (str, key) => {
390
+ try { this.handleKeypress(str, key); } catch (error) { this.fail(error); }
391
+ };
392
+ input.on('keypress', this._keyHandler);
393
+ this.render();
394
+ } catch (error) {
395
+ this.fail(error);
396
+ }
397
+ });
398
+ }
399
+
400
+ finish(result) {
401
+ if (this.isResolving) return;
402
+ this.isResolving = true;
403
+ this._cleanupInput();
404
+ this.line = '';
405
+ this.cursor = 0;
406
+ this.popoverActive = false;
407
+ this.popoverItems = [];
408
+ if (result && result.trim()) this.history.push(result);
409
+ const resolve = this.activeResolve;
410
+ this.activeResolve = null;
411
+ this.activeReject = null;
412
+ if (resolve) resolve(result);
413
+ }
414
+
415
+ fail(error) {
416
+ if (this.isResolving) return;
417
+ this.isResolving = true;
418
+ this._cleanupInput();
419
+ const reject = this.activeReject;
420
+ this.activeResolve = null;
421
+ this.activeReject = null;
422
+ if (reject) reject(error);
423
+ }
424
+
425
+ dispose() {
426
+ this.isDisposed = true;
427
+ this._cleanupInput();
428
+ this.screenBuffer = null;
429
+ this.activeResolve = null;
430
+ this.activeReject = null;
431
+ this.isResolving = true;
432
+ }
433
+
434
+ updateState() {
435
+ if (this.line.startsWith('/') || this.line.includes('@')) {
436
+ const items = getSuggestionsForLine(this.line, this.cursor, this.agent);
437
+ this.popoverItems = items;
438
+ this.popoverActive = items.length > 0;
439
+ if (this.popoverIndex >= items.length) this.popoverIndex = Math.max(0, items.length - 1);
440
+
441
+ if (items.length > 0 && this.cursor === this.line.length) {
442
+ const topItem = items[this.popoverIndex] || items[0];
443
+ const targetStr = topItem.insert || topItem.name;
444
+ if (targetStr.toLowerCase().startsWith(this.line.toLowerCase()) && targetStr.length > this.line.length) {
445
+ this.ghostText = targetStr.slice(this.line.length);
446
+ } else { this.ghostText = ''; }
447
+ } else { this.ghostText = ''; }
448
+ } else {
449
+ this.popoverActive = false;
450
+ this.popoverItems = [];
451
+ this.popoverIndex = 0;
452
+ this.ghostText = '';
453
+ }
454
+ }
455
+
456
+ render() {
457
+ if (this.isDisposed || !this.screenBuffer) return;
458
+ const termCols = Math.max(20, process.stdout.columns || 80);
459
+ const termRows = Math.max(10, process.stdout.rows || 24);
460
+
461
+ this.screenBuffer.resize(termCols, termRows);
462
+ this.screenBuffer.clear(C.bgDark);
463
+
464
+
465
+ const shortModel = (this.modelName || 'gemma-4-e4b').replace(/^.*\//, '');
466
+ const modelLabel = `${C.gold}✦ ${shortModel}${C.reset}`;
467
+ const mcpCount = this.agent?.mcpManager?.servers?.size || 0;
468
+ const mcpLabel = mcpCount > 0 ? `${C.emerald}● ${mcpCount} MCP${C.reset}` : `${C.slate}○ 0 MCP${C.reset}`;
469
+
470
+ const headerLeft = ` ${C.bold}${C.sakura}◆ KIKU AGENT${C.reset} ${C.slate}v1.0.4${C.reset}`;
471
+ const headerRight = `${modelLabel} ${C.slate}│${C.reset} ${mcpLabel} ${C.slate}│${C.reset} ${C.roseGlow}◆ Sakura${C.reset} `;
472
+ const safeCols = Math.max(1, termCols - 1);
473
+ const headerPad = Math.max(1, safeCols - getStringDisplayWidth(headerLeft) - getStringDisplayWidth(headerRight));
474
+ this.screenBuffer.setRow(0, `${headerLeft}${' '.repeat(headerPad)}${headerRight}`, C.bgPanelHeader);
475
+
476
+
477
+ const vpStartRow = 1;
478
+ const vpHeight = Math.max(1, termRows - 4);
479
+ const totalLines = this.viewportLines.length;
480
+ const maxOffset = Math.max(0, totalLines - vpHeight);
481
+ const currentOffset = Math.min(this.scrollOffset, maxOffset);
482
+ const startIndex = Math.max(0, totalLines - vpHeight - currentOffset);
483
+ const visibleLines = this.viewportLines.slice(startIndex, startIndex + vpHeight);
484
+
485
+ for (let r = 0; r < vpHeight; r++) {
486
+ const lineContent = visibleLines[r] || '';
487
+ this.screenBuffer.setRow(vpStartRow + r, lineContent, C.bgDark);
488
+ }
489
+
490
+
491
+ const statusBarRow = termRows - 4;
492
+ const musicStatus = audioPlayer.getStatus();
493
+ let statusStyled = '';
494
+
495
+ if (isProcessing) {
496
+ const frame = spinnerFrames[Math.floor(Date.now() / 70) % spinnerFrames.length];
497
+ const stateLabel = stripAnsi(this.agentState || 'Processing...');
498
+ const elapsed = this.agentState
499
+ ? ` ${((Date.now() - (this.agentStateTime || Date.now())) / 1000).toFixed(1)}s`
500
+ : '';
501
+ const queueLabel = queryQueue.length > 0 ? ` Queue: ${queryQueue.length}` : '';
502
+ const activeLabel = activeQueryNumber == null ? '' : `${C.bold}Q${activeQueryNumber}${C.reset}${C.cyberCyan} • `;
503
+ statusStyled = ` ${C.cyberCyan}${frame} ${activeLabel}${stateLabel}${elapsed}${C.reset}${C.slate}${queueLabel}${C.reset}`;
504
+ } else if (musicStatus) {
505
+ statusStyled = ` ${C.cyberCyan}🎵 ${musicStatus}${C.reset}`;
506
+ } else if (currentOffset > 0) {
507
+ statusStyled = ` ${C.amber}[↑ SCROLLED +${currentOffset} lines — press [End] to return to live stream]${C.reset}`;
508
+ } else {
509
+ statusStyled = ` ${C.emerald}●${C.reset} ${C.slate}IDLE • Ask anything about Ju Jingyi or type / for commands${C.reset}`;
510
+ }
511
+
512
+ this.screenBuffer.setRow(statusBarRow, statusStyled, C.bgPanelHeader);
513
+
514
+
515
+ if (this.popoverActive && this.popoverItems.length > 0) {
516
+ const maxItems = Math.min(5, Math.max(1, vpHeight - 1));
517
+ const startIdx = Math.max(0, Math.min(this.popoverIndex - 2, this.popoverItems.length - maxItems));
518
+ const visibleItems = this.popoverItems.slice(startIdx, startIdx + maxItems);
519
+ const popoverHeight = visibleItems.length + 1;
520
+ const popoverStartRow = Math.max(vpStartRow, statusBarRow - popoverHeight);
521
+
522
+ this.screenBuffer.setRow(
523
+ popoverStartRow,
524
+ ` ${C.bold}${C.roseGlow}Suggestions${C.reset} ${C.slate}— ↑/↓ navigate, Tab/Enter select, Esc close${C.reset}`,
525
+ C.bgPanelHeader
526
+ );
527
+
528
+ for (let i = 0; i < visibleItems.length; i++) {
529
+ const actualIdx = startIdx + i;
530
+ const item = visibleItems[i];
531
+ const isSelected = actualIdx === this.popoverIndex;
532
+ const pointer = isSelected ? '❯' : ' ';
533
+ const pointerColor = isSelected ? C.gold : C.darkGray;
534
+ const nameColor = isSelected ? `${C.bold}${C.white}` : C.cyberCyan;
535
+ this.screenBuffer.setRow(
536
+ popoverStartRow + 1 + i,
537
+ ` ${pointerColor}${pointer}${C.reset} ${nameColor}${item.name.padEnd(24)}${C.reset} ${C.slate}${stripAnsi(item.desc)}${C.reset}`,
538
+ C.bgPanel
539
+ );
540
+ }
541
+ }
542
+
543
+
544
+ const promptRow = termRows - 2;
545
+ let promptStyled = `${C.bold}${C.sakura}${this.promptText}${C.reset}`;
546
+ const promptVisualWidth = getStringDisplayWidth(this.promptText);
547
+ const maxLineWidth = Math.max(1, termCols - promptVisualWidth - 3);
548
+
549
+ const lineVisualWidth = getStringDisplayWidth(this.line);
550
+ const { displayLine, cursorVisualOffset } = sliceLineForDisplay(this.line, this.cursor, maxLineWidth);
551
+
552
+ let trailingStyled = '';
553
+ if (this.ghostText && this.cursor === this.line.length && lineVisualWidth <= maxLineWidth) {
554
+ const remainingWidth = Math.max(0, maxLineWidth - lineVisualWidth);
555
+ const ghostDisplay = remainingWidth > 0
556
+ ? sliceLineForDisplay(this.ghostText, 0, remainingWidth).displayLine.replace(/…$/, '')
557
+ : '';
558
+ if (ghostDisplay) {
559
+ trailingStyled = `${C.dim}${C.slate}${ghostDisplay}${C.reset}`;
560
+ }
561
+ } else if (!this.line) {
562
+ const placeholder = sliceLineForDisplay(INPUT_PLACEHOLDER, 0, maxLineWidth).displayLine;
563
+ trailingStyled = `${C.dim}${C.slate}${placeholder}${C.reset}`;
564
+ }
565
+
566
+ const rowStyled = ` ❯ ${promptStyled}${C.white}${displayLine}${trailingStyled}`;
567
+ this.screenBuffer.setRow(promptRow, rowStyled, C.bgPanel);
568
+
569
+ this.screenBuffer.setRow(termRows - 1, '─'.repeat(termCols), C.bgDark);
570
+
571
+ const cursorCol = 3 + promptVisualWidth + cursorVisualOffset;
572
+ this.screenBuffer.setCursor(promptRow, cursorCol, true);
573
+ this.screenBuffer.render();
574
+ }
575
+
576
+ handleKeypress(str, key) {
577
+ if (!key) return;
578
+
579
+ if (key.ctrl && key.name === 'c') {
580
+ if (this.line.length > 0) { this.line = ''; this.cursor = 0; this.updateState(); this.render(); return; }
581
+ if (isProcessing) { if (this.onSigInt) this.onSigInt(); return; }
582
+ const now = Date.now();
583
+ if (!this._lastCtrlCTime || now - this._lastCtrlCTime > 2000) {
584
+ this._lastCtrlCTime = now;
585
+ this.addViewportLines(` ${C.amber}[Press Ctrl+C again or type /exit to quit]${C.reset}`);
586
+ return;
587
+ }
588
+ if (this.onSigInt) this.onSigInt(); else process.exit(0);
589
+ return;
590
+ }
591
+
592
+ if (key.name === 'pageup') {
593
+ const termRows = Math.max(10, process.stdout.rows || 24);
594
+ const vpHeight = Math.max(1, termRows - 4);
595
+ const maxOffset = Math.max(0, this.viewportLines.length - vpHeight);
596
+ this.scrollOffset = Math.min(maxOffset, this.scrollOffset + Math.max(1, vpHeight - 2));
597
+ this.render();
598
+ return;
599
+ }
600
+
601
+ if (key.name === 'pagedown') {
602
+ const termRows = Math.max(10, process.stdout.rows || 24);
603
+ const vpHeight = Math.max(1, termRows - 4);
604
+ this.scrollOffset = Math.max(0, this.scrollOffset - Math.max(1, vpHeight - 2));
605
+ this.render();
606
+ return;
607
+ }
608
+
609
+ if (key.ctrl && key.name === 'l') {
610
+ this.render();
611
+ return;
612
+ }
613
+
614
+ if (key.name === 'escape') {
615
+ if (this.popoverActive) {
616
+ this.popoverActive = false;
617
+ this.popoverItems = [];
618
+ this.ghostText = '';
619
+ this.render();
620
+ } else if (this.scrollOffset > 0) {
621
+ this.scrollOffset = 0;
622
+ this.render();
623
+ }
624
+ return;
625
+ }
626
+
627
+ if (key.name === 'f2') {
628
+ this.finish('/details');
629
+ return;
630
+ }
631
+
632
+ if (key.name === 'tab') {
633
+ if (this.popoverActive && this.popoverItems.length > 0) {
634
+ const item = this.popoverItems[this.popoverIndex];
635
+ if (item) {
636
+ this.line = item.insert || item.name;
637
+ this.cursor = item.newCursor !== undefined ? item.newCursor : this.line.length;
638
+ this.updateState();
639
+ this.render();
640
+ return;
641
+ }
642
+ } else if (this.ghostText) {
643
+ this.line += this.ghostText;
644
+ this.cursor = this.line.length;
645
+ this.updateState();
646
+ this.render();
647
+ return;
648
+ }
649
+ return;
650
+ }
651
+
652
+ if (key.name === 'return' || key.name === 'enter') {
653
+ if (this.popoverActive && this.popoverItems.length > 0) {
654
+ const item = this.popoverItems[this.popoverIndex];
655
+ if (item) {
656
+ const insertVal = item.insert || item.name;
657
+ if (insertVal.endsWith(' ')) {
658
+ this.line = insertVal;
659
+ this.cursor = item.newCursor !== undefined ? item.newCursor : this.line.length;
660
+ this.updateState();
661
+ this.render();
662
+ return;
663
+ }
664
+ this.line = insertVal;
665
+ this.finish(this.line);
666
+ return;
667
+ }
668
+ }
669
+ this.scrollOffset = 0;
670
+ this.finish(this.line);
671
+ return;
672
+ }
673
+
674
+ if (key.name === 'up' || (key.ctrl && key.name === 'p')) {
675
+ if (this.popoverActive && this.popoverItems.length > 0) {
676
+ this.popoverIndex = (this.popoverIndex - 1 + this.popoverItems.length) % this.popoverItems.length;
677
+ this.updateState();
678
+ this.render();
679
+ return;
680
+ }
681
+ if (key.ctrl && this.history.length > 0) {
682
+ if (this.historyIndex === -1) this.historyIndex = this.history.length - 1;
683
+ else if (this.historyIndex > 0) this.historyIndex--;
684
+ this.line = this.history[this.historyIndex] || '';
685
+ this.cursor = this.line.length;
686
+ this.updateState();
687
+ this.render();
688
+ return;
689
+ }
690
+ const termRows = Math.max(10, process.stdout.rows || 24);
691
+ const vpHeight = Math.max(1, termRows - 4);
692
+ const maxOffset = Math.max(0, this.viewportLines.length - vpHeight);
693
+ this.scrollOffset = Math.min(maxOffset, this.scrollOffset + 1);
694
+ this.render();
695
+ return;
696
+ }
697
+
698
+ if (key.name === 'down' || (key.ctrl && key.name === 'n')) {
699
+ if (this.popoverActive && this.popoverItems.length > 0) {
700
+ this.popoverIndex = (this.popoverIndex + 1) % this.popoverItems.length;
701
+ this.updateState();
702
+ this.render();
703
+ return;
704
+ }
705
+ if (key.ctrl && this.history.length > 0 && this.historyIndex !== -1) {
706
+ if (this.historyIndex < this.history.length - 1) {
707
+ this.historyIndex++;
708
+ this.line = this.history[this.historyIndex] || '';
709
+ } else {
710
+ this.historyIndex = -1;
711
+ this.line = '';
712
+ }
713
+ this.cursor = this.line.length;
714
+ this.updateState();
715
+ this.render();
716
+ return;
717
+ }
718
+ if (this.scrollOffset > 0) {
719
+ this.scrollOffset = Math.max(0, this.scrollOffset - 1);
720
+ this.render();
721
+ }
722
+ return;
723
+ }
724
+
725
+ if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
726
+ if (this.line === '') {
727
+ const termRows = Math.max(10, process.stdout.rows || 24);
728
+ const vpHeight = Math.max(1, termRows - 6);
729
+ this.scrollOffset = Math.max(0, this.viewportLines.length - vpHeight);
730
+ this.render();
731
+ } else {
732
+ this.cursor = 0;
733
+ this.render();
734
+ }
735
+ return;
736
+ }
737
+
738
+ if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
739
+ if (this.line === '') {
740
+ this.scrollOffset = 0;
741
+ this.render();
742
+ } else {
743
+ this.cursor = this.line.length;
744
+ this.render();
745
+ }
746
+ return;
747
+ }
748
+
749
+ if (key.name === 'right') { if (this.cursor < this.line.length) { this.cursor = nextGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
750
+ if (key.name === 'left') { if (this.cursor > 0) { this.cursor = previousGraphemeBoundary(this.line, this.cursor); this.render(); } return; }
751
+
752
+ 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; }
753
+ 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; }
754
+
755
+ if (str && !key.ctrl && !key.meta) {
756
+ if (
757
+ str.startsWith('\x1b') ||
758
+ /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(str) ||
759
+ /^[0-9;]+[Mm]$/.test(str) ||
760
+ /^<*[0-9;]+[Mm]/.test(str)
761
+ ) return;
762
+ this.line = this.line.slice(0, this.cursor) + str + this.line.slice(this.cursor);
763
+ this.cursor += str.length;
764
+ this.updateState();
765
+ this.render();
766
+ }
767
+ }
768
+ }
769
+
770
+ function getSuggestionsForLine(line, cursor = (line || '').length, agent = null) {
771
+ const textBeforeCursor = (line || '').slice(0, cursor);
772
+ const atMatch = textBeforeCursor.match(/@([a-zA-Z0-9_-]*)$/);
773
+
774
+ if (atMatch) {
775
+ const atQuery = atMatch[1].toLowerCase();
776
+ const startIndex = atMatch.index;
777
+ const allTools = agent ? agent.getAllToolsSchema() : toolsSchema;
778
+
779
+ return allTools
780
+ .filter(t => {
781
+ const name = t.function?.name?.toLowerCase() || '';
782
+ const desc = t.function?.description?.toLowerCase() || '';
783
+ return name.includes(atQuery) || desc.includes(atQuery);
784
+ })
785
+ .slice(0, 10)
786
+ .map(t => {
787
+ const f = t.function;
788
+ const insertText = line.slice(0, startIndex) + `@${f.name} ` + line.slice(cursor);
789
+ const newCursor = startIndex + `@${f.name} `.length;
790
+ return {
791
+ name: `@${f.name}`,
792
+ desc: f.description || '',
793
+ insert: insertText,
794
+ newCursor,
795
+ toolName: f.name,
796
+ };
797
+ });
798
+ }
799
+
800
+ const trimmed = line.trimStart();
801
+ if (!trimmed.startsWith('/')) return [];
802
+ const parts = trimmed.split(/\s+/);
803
+ const baseCmd = parts[0].toLowerCase();
804
+ const allCmds = COMMAND_DEFINITIONS;
805
+
806
+ if (baseCmd === '/tool' && (parts.length > 1 || line.includes(' '))) {
807
+ const q = parts.slice(1).join(' ').toLowerCase();
808
+ const allTools = agent ? agent.getAllToolsSchema() : toolsSchema;
809
+ if (!q) return allTools.map(t => ({ name: `/tool ${t.function.name}`, desc: t.function.description.slice(0, 50), insert: `/tool ${t.function.name} ` }));
810
+ return allTools
811
+ .filter(t => t.function.name.toLowerCase().includes(q))
812
+ .slice(0, 8)
813
+ .map(t => ({ name: `/tool ${t.function.name}`, desc: t.function.description.slice(0, 50), insert: `/tool ${t.function.name} ` }));
814
+ }
815
+
816
+ if (parts.length > 1 || (parts.length === 1 && line.includes(' '))) {
817
+ const subs = SUB_COMMANDS[baseCmd] || [];
818
+ const query = parts.slice(1).join(' ').toLowerCase();
819
+ return subs
820
+ .filter(s => s.toLowerCase().includes(query))
821
+ .map(s => ({ name: `${baseCmd} ${s}`, desc: '', insert: `${baseCmd} ${s} ` }));
822
+ }
823
+
824
+ return allCmds
825
+ .map(c => ({ ...c, score: c.name === baseCmd ? 200 : c.name.startsWith(baseCmd) ? 100 : c.name.includes(baseCmd) ? 70 : 0 }))
826
+ .filter(c => c.score > 0)
827
+ .sort((a, b) => b.score - a.score);
828
+ }
829
+
830
+ async function executeQueryWithUI(agent, prompt, isThinkingVisible = false, signal = null, queryNumber = null) {
831
+ let spinnerInterval = null;
832
+ let hasPrintedResponseHeader = false;
833
+ let responsePanelStarted = false;
834
+ let toolSectionStarted = false;
835
+ let responseLineBuffer = '';
836
+ let pendingTableHeader = null;
837
+ let activeTableHeaders = null;
838
+ let tableRowNumber = 0;
839
+ const boxWidth = getFullWidth();
840
+ const bg = '\x1b[48;2;34;37;48m';
841
+ const bgHeader = '\x1b[48;2;48;52;68m';
842
+ const colorizeResponseText = (formatted) => (
843
+ `${C.white}${formatted.replace(/\x1b\[0m/g, C.reset + C.white)}${C.reset}`
844
+ );
845
+ const queryTag = queryNumber == null ? '' : `Q${queryNumber}`;
846
+ const answerTitle = queryTag
847
+ ? `Answer • ${queryTag} — ${prompt.length > 52 ? prompt.slice(0, 52) + '…' : prompt}`
848
+ : 'Answer';
849
+
850
+ const ensureResponsePanel = () => {
851
+ if (responsePanelStarted) return;
852
+ console.log('');
853
+ printSolidBannerRow(` ${C.bold}${C.roseGlow}${answerTitle}${C.reset}`, boxWidth, C.bgPanelHeader);
854
+ responsePanelStarted = true;
855
+ };
856
+
857
+ const printPlainResponseLine = (line) => {
858
+ ensureResponsePanel();
859
+ const wrapSafeLine = expandMarkdownLinks(line);
860
+ const formatted = formatTerminalMarkdown(wrapSafeLine);
861
+ const maxWidth = getFullWidth() - 4;
862
+ const visualLen = getStringDisplayWidth(formatted);
863
+ if (visualLen > maxWidth) {
864
+ for (const wl of wrapText(wrapSafeLine, maxWidth)) {
865
+ printSolidBannerRow(` ${colorizeResponseText(formatTerminalMarkdown(wl))}`, boxWidth, C.bgPanel);
866
+ }
867
+ } else {
868
+ printSolidBannerRow(` ${colorizeResponseText(formatted)}`, boxWidth, C.bgPanel);
869
+ }
870
+ };
871
+
872
+ const printTableRecord = (cells) => {
873
+ ensureResponsePanel();
874
+ tableRowNumber++;
875
+ printSolidBannerRow(` ${C.bold}${C.cyberCyan}Item ${tableRowNumber}${C.reset}`, boxWidth, C.bgPanelHeader);
876
+
877
+ const maxWidth = Math.max(16, getFullWidth() - 8);
878
+ for (let index = 0; index < activeTableHeaders.length; index++) {
879
+ const header = activeTableHeaders[index] || `Column ${index + 1}`;
880
+ const value = expandMarkdownLinks(cells[index] || '—');
881
+ const prefix = `${header}: `;
882
+ const wrapped = wrapText(prefix + value, maxWidth);
883
+ for (const line of wrapped) {
884
+ printSolidBannerRow(` ${colorizeResponseText(formatTerminalMarkdown(line))}`, boxWidth, C.bgPanel);
885
+ }
886
+ }
887
+ };
888
+
889
+ const flushPendingTableHeader = () => {
890
+ if (!pendingTableHeader) return;
891
+ printPlainResponseLine(`| ${pendingTableHeader.join(' | ')} |`);
892
+ pendingTableHeader = null;
893
+ };
894
+
895
+ const printResponseLine = (line) => {
896
+ const cells = parseMarkdownTableRow(line);
897
+
898
+ if (pendingTableHeader) {
899
+ if (cells && cells.length === pendingTableHeader.length && isMarkdownTableSeparator(cells)) {
900
+ activeTableHeaders = pendingTableHeader;
901
+ pendingTableHeader = null;
902
+ tableRowNumber = 0;
903
+ return;
904
+ }
905
+ flushPendingTableHeader();
906
+ }
907
+
908
+ if (activeTableHeaders) {
909
+ if (cells && cells.length === activeTableHeaders.length && !isMarkdownTableSeparator(cells)) {
910
+ printTableRecord(cells);
911
+ return;
912
+ }
913
+ activeTableHeaders = null;
914
+ tableRowNumber = 0;
915
+ }
916
+
917
+ if (cells && !isMarkdownTableSeparator(cells)) {
918
+ pendingTableHeader = cells;
919
+ return;
920
+ }
921
+
922
+ printPlainResponseLine(line);
923
+ };
924
+
925
+ const flushResponseBuffer = (isFinal = false) => {
926
+ if (responseLineBuffer.length > 0) {
927
+ if (isFinal) {
928
+ for (const line of responseLineBuffer.split('\n')) { if (line.length > 0) printResponseLine(line); }
929
+ responseLineBuffer = '';
930
+ } else if (responseLineBuffer.includes('\n')) {
931
+ const lines = responseLineBuffer.split('\n');
932
+ responseLineBuffer = lines.pop();
933
+ for (const line of lines) printResponseLine(line);
934
+ }
935
+ }
936
+ if (isFinal) flushPendingTableHeader();
937
+ };
938
+
939
+ const startSpinner = (label) => {
940
+ if (globalTuiPrompt) { globalTuiPrompt.agentState = label; globalTuiPrompt.agentStateTime = Date.now(); }
941
+ if (!spinnerInterval) {
942
+ spinnerInterval = setInterval(() => { if (globalTuiPrompt && isProcessing) globalTuiPrompt.render(); }, 80);
943
+ }
944
+ };
945
+
946
+ const stopSpinner = () => {
947
+ if (spinnerInterval) { clearInterval(spinnerInterval); spinnerInterval = null; }
948
+ if (globalTuiPrompt) { globalTuiPrompt.agentState = ''; globalTuiPrompt.render(); }
949
+ };
950
+
951
+ let thinkingShown = false;
952
+ let thinkingBuffer = '';
953
+ let thinkingStartTime = 0;
954
+ let accumulatedThinking = '';
955
+ let recordedTools = [];
956
+ let currentToolArgs = {};
957
+
958
+ startSpinner(`${C.slate}Thinking…${C.reset}`);
959
+
960
+ try {
961
+ await agent.ask(prompt, {
962
+ signal,
963
+ onStepStart: () => {},
964
+ onReasoningStart: () => {
965
+ stopSpinner();
966
+ thinkingStartTime = Date.now();
967
+ thinkingBuffer = '';
968
+ if (!thinkingShown) {
969
+ console.log('');
970
+ const icon = thinkingFrames[Math.floor(Date.now() / 200) % thinkingFrames.length];
971
+ printSolidBannerRow(` ${C.lavender}${C.bold}${icon} Thinking${queryTag ? ` • ${queryTag}` : ''}${C.reset}`, boxWidth, bgHeader);
972
+ }
973
+ },
974
+ onReasoningDelta: (chunk) => {
975
+ accumulatedThinking += chunk;
976
+ thinkingBuffer += chunk;
977
+ if (!thinkingShown) {
978
+ const lines = thinkingBuffer.split('\n');
979
+ thinkingBuffer = lines.pop();
980
+ for (const line of lines) {
981
+ if (line.trim()) {
982
+ for (const w of wrapText(cleanThinkingLine(line), boxWidth - 6)) {
983
+ printSolidBannerRow(` ${C.dim}${C.slate}${w}${C.reset}`, boxWidth, bg);
984
+ }
985
+ }
986
+ }
987
+ }
988
+ },
989
+ onReasoningEnd: () => {
990
+ const elapsed = ((Date.now() - thinkingStartTime) / 1000).toFixed(1);
991
+ if (!thinkingShown) {
992
+ if (thinkingBuffer.trim()) {
993
+ for (const w of wrapText(cleanThinkingLine(thinkingBuffer), boxWidth - 6)) {
994
+ printSolidBannerRow(` ${C.dim}${C.slate}${w}${C.reset}`, boxWidth, bg);
995
+ }
996
+ }
997
+ printSolidBannerRow(` ${C.emerald}[DONE] Thought for ${elapsed}s${C.reset}`, boxWidth, bg);
998
+ console.log('');
999
+ } else {
1000
+ stopSpinner();
1001
+ const detailHint = queryTag ? ` — F2 or /details ${queryTag}` : '';
1002
+ console.log(`\n ${C.dim}${C.slate}Thinking${queryTag ? ` • ${queryTag}` : ''} (${elapsed}s)${detailHint}${C.reset}`);
1003
+ }
1004
+ thinkingShown = true;
1005
+ },
1006
+
1007
+ onToolStart: (toolName, args) => {
1008
+ stopSpinner();
1009
+ if (!toolSectionStarted) {
1010
+ console.log('');
1011
+ const detailHint = queryTag ? ` ${C.dim}Details: F2 or /details ${queryTag}${C.reset}` : '';
1012
+ printSolidBannerRow(` ${C.bold}${C.cyberCyan}Tools${queryTag ? ` • ${queryTag}` : ''}${C.reset}${detailHint}`, boxWidth, C.bgPanelHeader);
1013
+ toolSectionStarted = true;
1014
+ }
1015
+ currentToolArgs = args || {};
1016
+ const queryVal = args?.query || args?.url || Object.values(args || {})[0] || '';
1017
+ const displayQuery = typeof queryVal === 'string' && queryVal.length > 30 ? queryVal.slice(0, 30) + '…' : queryVal;
1018
+ startSpinner(`${C.cyberCyan}• ${C.bold}${C.white}${toolName}${C.reset}${displayQuery ? ` ${C.slate}"${displayQuery}"${C.reset}` : ''}`);
1019
+ },
1020
+
1021
+ onToolResult: (toolName, result, durationMs) => {
1022
+ stopSpinner();
1023
+ recordedTools.push({ name: toolName, args: currentToolArgs, result, duration: durationMs });
1024
+
1025
+ const count = result.count ?? result.returnedCount;
1026
+ const isError = result.status === 'error';
1027
+ const isNotFound = result.status === 'not_found' || count === 0;
1028
+ const statusLabel = isError
1029
+ ? `${C.red}[ERROR]`
1030
+ : isNotFound
1031
+ ? `${C.amber}[NOT FOUND]`
1032
+ : `${C.emerald}[OK]`;
1033
+ const countTag = count != null && !isError ? ` • ${C.cyberCyan}${count} results${C.reset}` : '';
1034
+ const timerTag = durationMs !== undefined ? ` • ${C.dim}${(durationMs / 1000).toFixed(2)}s${C.reset}` : '';
1035
+
1036
+ 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);
1037
+
1038
+ if (!isError && !isNotFound) {
1039
+ const preview = extractPreview(result, toolName);
1040
+ if (preview) {
1041
+ for (const pLine of wrapText('• ' + preview, boxWidth - 6)) {
1042
+ printSolidBannerRow(` ${C.dim}${C.slate}${pLine}${C.reset}`, boxWidth, bg);
1043
+ }
1044
+ }
1045
+ }
1046
+ },
1047
+
1048
+ onTokenStart: () => { stopSpinner(); },
1049
+
1050
+ onTokenDelta: (token) => {
1051
+ if (!hasPrintedResponseHeader) { stopSpinner(); hasPrintedResponseHeader = true; }
1052
+ responseLineBuffer += token;
1053
+ flushResponseBuffer(false);
1054
+ },
1055
+
1056
+ onFinalAnswer: (answer) => {
1057
+ stopSpinner();
1058
+ if (!hasPrintedResponseHeader) {
1059
+ for (const line of (answer || '').split('\n')) { if (line.length > 0) printResponseLine(line); }
1060
+ flushPendingTableHeader();
1061
+ } else {
1062
+ flushResponseBuffer(true);
1063
+ }
1064
+ },
1065
+
1066
+ onError: (err) => {
1067
+ stopSpinner();
1068
+ console.log(`\n ${C.red}[ERROR] ${err.message}${C.reset}\n`);
1069
+ },
1070
+ });
1071
+
1072
+ lastQueryDetails = { number: queryNumber, prompt, thinking: accumulatedThinking, tools: recordedTools, timestamp: Date.now() };
1073
+ if (queryNumber != null) {
1074
+ queryDetails.set(queryNumber, lastQueryDetails);
1075
+ if (queryDetails.size > 50) queryDetails.delete(queryDetails.keys().next().value);
1076
+ }
1077
+ if (responsePanelStarted) console.log('');
1078
+ } catch (err) {
1079
+ stopSpinner();
1080
+ if (!isAbortError(err)) {
1081
+ console.log(`\n ${C.red}[ERROR] ${err.message}${C.reset}\n`);
1082
+ } else {
1083
+ console.log(`\n ${C.amber}[INTERRUPTED] Query cancelled.${C.reset}\n`);
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ async function main() {
1089
+ try { process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l'); } catch {}
1090
+
1091
+ const opts = parseCliArgs();
1092
+
1093
+ if (opts.help) { printWelcomeHero(); printHelp(); process.exit(0); }
1094
+ if (opts.showTools) {
1095
+ printWelcomeHero();
1096
+ console.log(`\n ${C.bold}${C.sakura}Tools (${toolsSchema.length})${C.reset}\n`);
1097
+ for (const [i, t] of toolsSchema.entries()) {
1098
+ 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}`);
1099
+ }
1100
+ console.log('');
1101
+ process.exit(0);
1102
+ }
1103
+
1104
+ if (opts.showStats) { printWelcomeHero(); const r = await executeTool('get_system_stats', {}); console.log(JSON.stringify(r, null, 2)); process.exit(0); }
1105
+
1106
+ if (opts.query) {
1107
+ printWelcomeHero();
1108
+ const client = new LLMClient({
1109
+ provider: opts.provider || config.activeProvider,
1110
+ baseUrl: opts.url,
1111
+ apiKey: opts.apiKey,
1112
+ model: opts.model
1113
+ });
1114
+ console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
1115
+ const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1116
+ await executeQueryWithUI(agent, opts.query, agent.session.thinkingMode === 'show', null, 1);
1117
+ process.exit(0);
1118
+ }
1119
+
1120
+
1121
+ enterAlternateScreen();
1122
+
1123
+ const client = new LLMClient({
1124
+ provider: opts.provider || config.activeProvider,
1125
+ baseUrl: opts.url,
1126
+ apiKey: opts.apiKey,
1127
+ model: opts.model
1128
+ });
1129
+ const baseUrl = client.baseUrl;
1130
+ const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1131
+
1132
+ const tuiPrompt = new InteractiveTuiPrompt({
1133
+ prompt: 'Kiku ❯ ',
1134
+ agent,
1135
+ modelName: opts.model || client.model || 'gemma-4-e4b',
1136
+ provider: client.providerName,
1137
+ onSigInt: async () => {
1138
+ if (activeAbortController) {
1139
+ activeAbortController.abort();
1140
+ activeAbortController = null;
1141
+ queryQueue.length = 0;
1142
+ deferredCommands.length = 0;
1143
+ } else {
1144
+ try { await agent.endSession(); } catch {}
1145
+ if (globalTuiPrompt) {
1146
+ globalTuiPrompt.dispose();
1147
+ globalTuiPrompt = null;
1148
+ }
1149
+ exitAlternateScreen();
1150
+ console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
1151
+ process.exit(0);
1152
+ }
1153
+ }
1154
+ });
1155
+
1156
+ globalTuiPrompt = tuiPrompt;
1157
+ setupOutputInterception(tuiPrompt, () => isProcessing);
1158
+
1159
+ let health = await client.checkConnection();
1160
+ let catalogOnlyMode = false;
1161
+
1162
+ if (!health.connected) {
1163
+ console.log(` ${C.amber}!! ${client.providerName} offline or unreachable${C.reset}`);
1164
+ console.log(` ${C.dim}• ${health.error}${C.reset}\n`);
1165
+
1166
+ if (client.provider === 'lmstudio') {
1167
+ console.log(` ${C.bold}${C.sakura}Attempting auto-start...${C.reset}`);
1168
+ const autoHealth = await attemptAutoStartLMStudio(baseUrl, opts.model || client.model);
1169
+ if (autoHealth) {
1170
+ health = autoHealth;
1171
+ console.log(` ${C.emerald}[OK] LM Studio started.${C.reset}\n`);
1172
+ } else {
1173
+ catalogOnlyMode = true;
1174
+ console.log(` ${C.amber}[CATALOG-ONLY] Kiku will remain available without AI synthesis.${C.reset}\n`);
1175
+ }
1176
+ } else {
1177
+ catalogOnlyMode = true;
1178
+ console.log(` ${C.amber}[CATALOG-ONLY] Kiku will remain available without AI synthesis.${C.reset}\n`);
1179
+ }
1180
+ }
1181
+
1182
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1183
+ const modelResolution = catalogOnlyMode
1184
+ ? { connected: false, state: 'catalog_only' }
1185
+ : await modelManager.resolve({ requestedModel: opts.model || client.model });
1186
+ if (!catalogOnlyMode && (!modelResolution.connected || !modelResolution.selectedModel)) {
1187
+ console.log(` ${C.amber}[MODEL WARNING] ${modelResolution.error}${C.reset}`);
1188
+ catalogOnlyMode = true;
1189
+ console.log(` ${C.amber}[CATALOG-ONLY] Load or select a chat model, then use /model reconnect.${C.reset}\n`);
1190
+ }
1191
+ if (modelResolution.selectedModel) {
1192
+ tuiPrompt.modelName = modelResolution.selectedModel;
1193
+ }
1194
+ if (modelResolution.usedFallback) {
1195
+ console.log(` ${C.amber}[MODEL FALLBACK] Requested ${opts.model}, using ${modelResolution.selectedModel}.${C.reset}\n`);
1196
+ }
1197
+
1198
+ if (opts.mcp) {
1199
+ try {
1200
+ if (opts.mcp.endsWith('.json')) {
1201
+ await agent.mcpManager.loadConfigFile(opts.mcp);
1202
+ console.log(` ${C.emerald}[OK] Loaded MCP configuration from ${opts.mcp}${C.reset}\n`);
1203
+ } else {
1204
+ const res = await agent.connectMcp('remote', { url: opts.mcp });
1205
+ console.log(` ${C.emerald}[OK] MCP Connected: ${opts.mcp} (${res.toolsCount} Tools)${C.reset}\n`);
1206
+ }
1207
+ } catch (err) {
1208
+ console.log(` ${C.red}[MCP Error] ${err.message}${C.reset}\n`);
1209
+ }
1210
+ }
1211
+
1212
+ await agent._ready;
1213
+
1214
+ if (agent.restoredMessageCount > 0) {
1215
+ const s = agent.session;
1216
+ console.log(`\n ${C.sakura}${C.bold}[SESSION RESTORED]${C.reset} ${C.dim}${s.id} — ${s.messages.length} messages${C.reset}\n`);
1217
+ }
1218
+
1219
+ agent.stateMachine.on('enter:' + AgentStates.FETCHING_TOOL, ({ toolName }) => {
1220
+ if (globalTuiPrompt) {
1221
+ globalTuiPrompt.agentState = `Tool: ${toolName}`;
1222
+ globalTuiPrompt.agentStateTime = Date.now();
1223
+ }
1224
+ });
1225
+
1226
+ agent.stateMachine.on('enter:' + AgentStates.STREAMING_THOUGHTS, () => {
1227
+ if (globalTuiPrompt) {
1228
+ globalTuiPrompt.agentState = 'Thinking…';
1229
+ globalTuiPrompt.agentStateTime = Date.now();
1230
+ }
1231
+ });
1232
+
1233
+ agent.stateMachine.on('enter:' + AgentStates.STREAMING_ANSWER, () => {
1234
+ if (globalTuiPrompt) {
1235
+ globalTuiPrompt.agentState = 'Streaming…';
1236
+ globalTuiPrompt.agentStateTime = Date.now();
1237
+ }
1238
+ });
1239
+
1240
+ agent.stateMachine.on('enter:' + AgentStates.IDLE, () => {
1241
+ if (globalTuiPrompt) {
1242
+ globalTuiPrompt.agentState = '';
1243
+ }
1244
+ });
1245
+
1246
+ async function processNextQueueItem() {
1247
+ if (isProcessing || queryQueue.length === 0) return;
1248
+
1249
+ isProcessing = true;
1250
+ while (queryQueue.length > 0) {
1251
+ const nextItem = queryQueue.shift();
1252
+ lastQuery = nextItem.prompt;
1253
+ activeQueryNumber = nextItem.number;
1254
+
1255
+ activeAbortController = new AbortController();
1256
+ try {
1257
+ if (catalogOnlyMode) {
1258
+ let targetTool = 'global_search';
1259
+ let queryArgs = { query: nextItem.prompt };
1260
+ const atMatch = nextItem.prompt.match(/^@([a-zA-Z0-9_-]+)(?:\s+(.*))?$/);
1261
+ if (atMatch) {
1262
+ targetTool = atMatch[1];
1263
+ const rawArg = (atMatch[2] || '').trim();
1264
+ if (rawArg) {
1265
+ try { queryArgs = JSON.parse(rawArg); } catch { queryArgs = { query: rawArg }; }
1266
+ } else {
1267
+ queryArgs = {};
1268
+ }
1269
+ }
1270
+
1271
+ let result;
1272
+ if (agent.mcpManager?.isMcpTool(targetTool)) {
1273
+ result = await agent.mcpManager.callTool(targetTool, queryArgs);
1274
+ } else {
1275
+ result = await executeTool(targetTool, queryArgs);
1276
+ }
1277
+
1278
+ const serialized = JSON.stringify(result, null, 2);
1279
+ printSolidPanel({
1280
+ title: `Direct Tool Result • ${targetTool} • Q${nextItem.number}`,
1281
+ lines: [
1282
+ ...serialized.slice(0, 6000).split('\n'),
1283
+ ...(serialized.length > 6000 ? [`${C.dim}… result truncated${C.reset}`] : []),
1284
+ ],
1285
+ });
1286
+ } else {
1287
+ await executeQueryWithUI(agent, nextItem.prompt, agent.session.thinkingMode === 'show', activeAbortController.signal, nextItem.number);
1288
+ }
1289
+ } finally {
1290
+ activeAbortController = null;
1291
+ queryCount++;
1292
+ printAd();
1293
+ activeQueryNumber = null;
1294
+ }
1295
+ }
1296
+ isProcessing = false;
1297
+ }
1298
+
1299
+ while (true) {
1300
+ let rawInput;
1301
+
1302
+ if (deferredCommands.length > 0 && !isProcessing) {
1303
+ rawInput = deferredCommands.shift();
1304
+ console.log(`\n ${C.cyberCyan}[DEFERRED] ${rawInput}${C.reset}\n`);
1305
+ } else {
1306
+ rawInput = await tuiPrompt.readLine();
1307
+ }
1308
+
1309
+ if (rawInput === null) break;
1310
+ const trimmed = rawInput.trim();
1311
+ if (!trimmed) continue;
1312
+
1313
+ const queryNumber = !trimmed.startsWith('/') ? nextQueryNumber++ : null;
1314
+
1315
+ if (!trimmed.startsWith('/')) {
1316
+ const wrapped = wrapText(trimmed, getFullWidth() - 8);
1317
+ const queuePosition = isProcessing ? queryQueue.length + 1 : 0;
1318
+ const queuedLabel = queuePosition > 0 ? ` • Queued ${queuePosition}` : '';
1319
+ printSolidPanel({
1320
+ title: `${C.roseGlow}${C.bold}Question ${queryNumber} • ${randomFanName()}${queuedLabel}${C.reset}`,
1321
+ lines: wrapped.map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
1322
+ bg: '\x1b[48;2;36;40;52m'
1323
+ });
1324
+ }
1325
+
1326
+ if (trimmed.startsWith('/')) {
1327
+ if (isProcessing) {
1328
+ deferredCommands.push(trimmed);
1329
+ console.log(`\n ${C.cyberCyan}[DEFERRED] ${trimmed}${C.reset}\n`);
1330
+ tuiPrompt.render();
1331
+ continue;
1332
+ }
1333
+
1334
+ const cmdCtx = {
1335
+ tuiPrompt,
1336
+ agent,
1337
+ client,
1338
+ baseUrl,
1339
+ modelManager,
1340
+ catalogOnlyMode,
1341
+ setCatalogOnlyMode: value => { catalogOnlyMode = Boolean(value); },
1342
+ sub: trimmed.split(/\s+/)[1] || '',
1343
+ extra: trimmed.split(/\s+/).slice(2).join(' '),
1344
+ parts: trimmed.split(/\s+/),
1345
+ lastQuery,
1346
+ printHelp,
1347
+ printQueryDetails,
1348
+ printWelcome: printWelcomeHero,
1349
+ printAd,
1350
+ enqueueQuery: async (prompt, isRetry) => {
1351
+ queryQueue.push({ id: 'q_' + Date.now(), number: nextQueryNumber++, prompt, addedAt: Date.now() });
1352
+ await processNextQueueItem();
1353
+ },
1354
+ clearQueryQueue: () => { queryQueue.length = 0; },
1355
+ getQueryQueueLength: () => queryQueue.length,
1356
+ };
1357
+
1358
+ const result = await routeCommand(trimmed, cmdCtx);
1359
+
1360
+ if (result === 'EXIT') break;
1361
+ if (result === 'RETRY' && cmdCtx.lastQuery) {
1362
+ const retryNumber = nextQueryNumber++;
1363
+ printSolidPanel({
1364
+ title: `${C.roseGlow}${C.bold}Question ${retryNumber} • Retry${C.reset}`,
1365
+ lines: wrapText(cmdCtx.lastQuery, getFullWidth() - 8)
1366
+ .map(wLine => `${C.cyberCyan}❯${C.reset} ${C.white}${wLine}${C.reset}`),
1367
+ bg: '\x1b[48;2;36;40;52m'
1368
+ });
1369
+ queryQueue.push({ id: 'q_' + Date.now(), number: retryNumber, prompt: cmdCtx.lastQuery, addedAt: Date.now() });
1370
+ await processNextQueueItem();
1371
+ }
1372
+ continue;
1373
+ }
1374
+
1375
+ const { sanitized, warnings } = sanitizeQuery(trimmed);
1376
+ if (warnings.length > 0) {
1377
+ console.log(`\n ${C.amber}[WARNING] ${warnings.join('; ')}${C.reset}\n`);
1378
+ }
1379
+
1380
+ lastQuery = trimmed;
1381
+ queryQueue.push({ id: 'q_' + Date.now(), number: queryNumber, prompt: trimmed, addedAt: Date.now() });
1382
+ await processNextQueueItem();
1383
+ }
1384
+
1385
+ if (globalTuiPrompt) {
1386
+ globalTuiPrompt.dispose();
1387
+ globalTuiPrompt = null;
1388
+ }
1389
+ exitAlternateScreen();
1390
+ console.log(`\n ${C.sakura}Goodbye. (鞠躬 ~ 谢谢!)${C.reset}\n`);
1391
+ process.exit(0);
1392
+ }
1393
+
1394
+ process.on('exit', () => {
1395
+ try { globalTuiPrompt?.dispose(); } catch {}
1396
+ exitAlternateScreen();
1397
+ });
1398
+ process.on('uncaughtException', (err) => {
1399
+ try { globalTuiPrompt?.dispose(); } catch {}
1400
+ exitAlternateScreen();
1401
+ console.error(`\n Uncaught: ${err.message}`);
1402
+ process.exit(1);
1403
+ });
1404
+ process.on('SIGINT', () => {
1405
+ try { globalTuiPrompt?.dispose(); } catch {}
1406
+ exitAlternateScreen();
1407
+ process.exit(130);
1408
+ });
1409
+
1410
+ main().catch(err => {
1411
+ exitAlternateScreen();
1412
+ console.error(`\n ${C.red}Fatal: ${err.message}${C.reset}\n`);
1413
+ process.exit(1);
1414
+ });