kiku-agent 1.0.0 → 1.0.3

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 (39) 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/mcp/claude-desktop-config.example.json +1 -1
  26. package/server/mcp/data/discography.js +36 -36
  27. package/server/mcp/dataAdapter.js +102 -0
  28. package/server/mcp/examples/claude-code.mcp.example.json +19 -0
  29. package/server/mcp/examples/claude-desktop.example.json +11 -0
  30. package/server/mcp/examples/cline.example.json +13 -0
  31. package/server/mcp/examples/cursor.example.json +14 -0
  32. package/server/mcp/examples/gemini-cli.example.json +17 -0
  33. package/server/mcp/examples/opencode.example.json +21 -0
  34. package/server/mcp/examples/vscode.example.json +19 -0
  35. package/server/mcp/examples/windsurf.example.json +14 -0
  36. package/server/mcp/handler.js +85 -159
  37. package/server/mcp/security.js +140 -138
  38. package/server/mcp/server.js +129 -100
  39. package/server/mcp/stdio.js +4 -2
@@ -1,469 +1,760 @@
1
- import readline from 'node:readline';
2
- import { stdin as input, stdout as output } from 'node:process';
3
- import { eventBus, Events } from './eventBus.js';
4
- import { AgentStates } from './agentState.js';
5
- import { ansiBackground, ansiForeground, getThemePalette } from './theme.js';
6
-
7
-
8
- export const C = Object.freeze({
9
- reset: '\x1b[0m',
10
- bold: '\x1b[1m',
11
- dim: '\x1b[2m',
12
- italic: '\x1b[3m',
13
- underline: '\x1b[4m',
14
-
15
- get sakura() { return ansiForeground(getThemePalette().primary); },
16
- get roseGlow() { return ansiForeground(getThemePalette().secondary); },
17
- get gold() { return ansiForeground(getThemePalette().highlight); },
18
- get amber() { return ansiForeground(getThemePalette().warning); },
19
- get cyberCyan() { return ansiForeground(getThemePalette().info); },
20
- get emerald() { return ansiForeground(getThemePalette().success); },
21
- get lavender() { return ansiForeground(getThemePalette().tertiary); },
22
- get slate() { return ansiForeground(getThemePalette().muted); },
23
- get darkGray() { return ansiForeground(getThemePalette().subdued); },
24
- get white() { return ansiForeground(getThemePalette().text); },
25
- get red() { return ansiForeground(getThemePalette().error); },
26
- get bgPanel() { return ansiBackground(getThemePalette().panel); },
27
- get bgPanelHeader() { return ansiBackground(getThemePalette().panelHeader); },
28
- get bgDark() { return ansiBackground(getThemePalette().panel); },
29
- get bgTool() { return ansiBackground(getThemePalette().panel); },
30
- get bgToolResult() { return ansiBackground(getThemePalette().panel); },
31
- get bgToolError() { return ansiBackground(getThemePalette().panel); },
32
- get bgThinking() { return ansiBackground(getThemePalette().panel); },
33
- get bgResponse() { return ansiBackground(getThemePalette().panel); },
34
- get bgError() { return ansiBackground(getThemePalette().panel); },
35
- });
36
-
37
- const _rawStdoutWrite = process.stdout.write.bind(process.stdout);
38
- const _rawConsoleLog = console.log.bind(console);
39
- const _rawConsoleClear = console.clear.bind(console);
40
-
41
-
42
-
43
- export const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
44
- export const processingFrames = spinnerFrames;
45
- export const thinkingFrames = spinnerFrames;
46
- export const successFrames = ['OK'];
47
- export const pulseFrames = ['▓', '▒', '░', '▒', '▓', '█', '▓', '▒'];
48
- export const INPUT_PLACEHOLDER = 'Ask about Ju Jingyi or type / for commands';
49
- export const UI_ICONS = Object.freeze({
50
- help: '📖',
51
- music: '🎵',
52
- sponsor: '📢',
53
- });
54
-
55
- const fanNames = ['Mìjú / 蜜橘'];
56
- export function randomFanName() {
57
- return fanNames[Math.floor(Math.random() * fanNames.length)];
58
- }
59
-
60
-
61
-
62
- const graphemeSegmenter = typeof Intl?.Segmenter === 'function'
63
- ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
64
- : null;
65
-
66
- function fallbackGraphemeSegments(str) {
67
- const segments = [];
68
- let index = 0;
69
-
70
- for (const char of Array.from(str)) {
71
- const isContinuation = segments.length > 0 && (
72
- /^\p{Mark}$/u.test(char) ||
73
- /^\p{Emoji_Modifier}$/u.test(char) ||
74
- char === '\uFE0F' ||
75
- char === '\u20E3' ||
76
- segments.at(-1).segment.endsWith('\u200D') ||
77
- char === '\u200D'
78
- );
79
-
80
- if (isContinuation) {
81
- segments.at(-1).segment += char;
82
- segments.at(-1).end += char.length;
83
- } else {
84
- segments.push({ segment: char, index, end: index + char.length });
85
- }
86
- index += char.length;
87
- }
88
-
89
- return segments;
90
- }
91
-
92
-
93
- export function getGraphemeSegments(str) {
94
- if (!str) return [];
95
- if (!graphemeSegmenter) return fallbackGraphemeSegments(str);
96
- return Array.from(graphemeSegmenter.segment(str), ({ segment, index }) => ({
97
- segment,
98
- index,
99
- end: index + segment.length,
100
- }));
101
- }
102
-
103
- export function previousGraphemeBoundary(str, cursor) {
104
- const safeCursor = Math.max(0, Math.min(str.length, cursor));
105
- let previous = 0;
106
- for (const item of getGraphemeSegments(str)) {
107
- if (item.end >= safeCursor) return item.index;
108
- previous = item.end;
109
- }
110
- return previous;
111
- }
112
-
113
- export function nextGraphemeBoundary(str, cursor) {
114
- const safeCursor = Math.max(0, Math.min(str.length, cursor));
115
- for (const item of getGraphemeSegments(str)) {
116
- if (item.end > safeCursor) return item.end;
117
- }
118
- return str.length;
119
- }
120
-
121
- function getGraphemeDisplayWidth(segment) {
122
- if (/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(segment) || /[\uFE0F\u20E3]/u.test(segment)) {
123
- return 2;
124
- }
125
-
126
- let width = 0;
127
- for (const char of Array.from(segment)) {
128
- const code = char.codePointAt(0);
129
- if (/^\p{Mark}$/u.test(char) || code === 0x200D || (code >= 0xFE00 && code <= 0xFE0F)) continue;
130
- if (
131
- (code >= 0x1100 && code <= 0x11FF) ||
132
- (code >= 0x2E80 && code <= 0x9FFF) ||
133
- (code >= 0xAC00 && code <= 0xD7A3) ||
134
- (code >= 0xF900 && code <= 0xFAFF) ||
135
- (code >= 0xFE30 && code <= 0xFE4F)
136
- ) {
137
- width += 2;
138
- } else {
139
- width += 1;
140
- }
141
- }
142
- return width;
143
- }
144
-
145
-
146
- export function getStringDisplayWidth(str) {
147
- if (!str) return 0;
148
- const clean = str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
149
- return getGraphemeSegments(clean).reduce((width, item) => width + getGraphemeDisplayWidth(item.segment), 0);
150
- }
151
-
152
-
153
- export function sliceLineForDisplay(str, cursor, maxWidth) {
154
- const segments = getGraphemeSegments(str).map(item => ({
155
- ...item,
156
- width: getGraphemeDisplayWidth(item.segment),
157
- }));
158
- const safeMaxWidth = Math.max(1, maxWidth);
159
- const totalWidth = segments.reduce((sum, item) => sum + item.width, 0);
160
- const safeCursor = Math.max(0, Math.min(str.length, cursor));
161
-
162
- let cursorIndex = segments.findIndex(item => item.index >= safeCursor);
163
- if (cursorIndex === -1) cursorIndex = segments.length;
164
-
165
- if (totalWidth <= safeMaxWidth) {
166
- return {
167
- displayLine: str,
168
- cursorVisualOffset: segments.slice(0, cursorIndex).reduce((sum, item) => sum + item.width, 0),
169
- };
170
- }
171
-
172
- let start = cursorIndex;
173
- let end = cursorIndex;
174
- let contentWidth = 0;
175
- let leftWidth = 0;
176
- let rightWidth = 0;
177
-
178
- while (true) {
179
- const candidates = [];
180
- if (start > 0) {
181
- const nextStart = start - 1;
182
- const markerWidth = (nextStart > 0 ? 1 : 0) + (end < segments.length ? 1 : 0);
183
- const width = segments[nextStart].width;
184
- if (contentWidth + width + markerWidth <= safeMaxWidth) {
185
- candidates.push({ side: 'left', width, balance: leftWidth });
186
- }
187
- }
188
- if (end < segments.length) {
189
- const nextEnd = end + 1;
190
- const markerWidth = (start > 0 ? 1 : 0) + (nextEnd < segments.length ? 1 : 0);
191
- const width = segments[end].width;
192
- if (contentWidth + width + markerWidth <= safeMaxWidth) {
193
- candidates.push({ side: 'right', width, balance: rightWidth });
194
- }
195
- }
196
- if (candidates.length === 0) break;
197
-
198
- candidates.sort((a, b) => a.balance - b.balance || a.width - b.width);
199
- const selected = candidates[0];
200
- if (selected.side === 'left') {
201
- start--;
202
- leftWidth += selected.width;
203
- } else {
204
- rightWidth += selected.width;
205
- end++;
206
- }
207
- contentWidth += selected.width;
208
- }
209
-
210
- const hasLeftEllipsis = start > 0;
211
- const hasRightEllipsis = end < segments.length;
212
- const displayLine = `${hasLeftEllipsis ? '…' : ''}${segments.slice(start, end).map(item => item.segment).join('')}${hasRightEllipsis ? '…' : ''}`;
213
- const cursorVisualOffset = (hasLeftEllipsis ? 1 : 0) + segments
214
- .slice(start, cursorIndex)
215
- .reduce((sum, item) => sum + item.width, 0);
216
-
217
- return { displayLine, cursorVisualOffset };
218
- }
219
-
220
- export function getFullWidth() {
221
- return process.stdout.columns || 80;
222
- }
223
-
224
-
225
-
226
- export function wrapText(text, maxWidth = 72) {
227
- if (!text) return [];
228
- const lines = [];
229
- const rawParagraphs = text.split('\n');
230
-
231
- for (const para of rawParagraphs) {
232
- if (!para.trim()) {
233
- lines.push('');
234
- continue;
235
- }
236
-
237
- const words = para.split(/\s+/);
238
- let currentLine = '';
239
-
240
- for (const word of words) {
241
- if (!word) continue;
242
- const testLine = currentLine ? (currentLine + ' ' + word) : word;
243
- const testWidth = getStringDisplayWidth(testLine);
244
-
245
- if (testWidth <= maxWidth) {
246
- currentLine = testLine;
247
- } else {
248
- if (currentLine) {
249
- lines.push(currentLine);
250
- currentLine = word;
251
- } else {
252
- let chunk = '';
253
- for (const ch of word) {
254
- if (getStringDisplayWidth(chunk + ch) <= maxWidth) {
255
- chunk += ch;
256
- } else {
257
- lines.push(chunk);
258
- chunk = ch;
259
- }
260
- }
261
- currentLine = chunk;
262
- }
263
- }
264
- }
265
- if (currentLine) {
266
- lines.push(currentLine);
267
- }
268
- }
269
- return lines;
270
- }
271
-
272
-
273
-
274
- export function stripAnsi(str) {
275
- return str.replace(/\x1b\[[0-9;]*m/g, '');
276
- }
277
-
278
-
279
- export function fitStyledText(styledText, maxWidth) {
280
- const safeWidth = Math.max(0, maxWidth);
281
- let result = '';
282
- let usedWidth = 0;
283
- const tokens = String(styledText || '').split(/(\x1b\[[0-9;]*[a-zA-Z])/g);
284
-
285
- for (const token of tokens) {
286
- if (!token) continue;
287
- if (/^\x1b\[[0-9;]*[a-zA-Z]$/.test(token)) {
288
- result += token;
289
- continue;
290
- }
291
- for (const item of getGraphemeSegments(token)) {
292
- const itemWidth = getGraphemeDisplayWidth(item.segment);
293
- if (usedWidth + itemWidth > safeWidth) return result;
294
- result += item.segment;
295
- usedWidth += itemWidth;
296
- }
297
- }
298
- return result;
299
- }
300
-
301
-
302
-
303
- export function printSolidBannerRow(styledText, targetWidth, requestedBg = C.bgPanel, indent = '') {
304
- const terminalWidth = getFullWidth();
305
- const rowWidth = Math.max(1, Math.min(targetWidth || terminalWidth, terminalWidth) - 1);
306
- const contentWidth = Math.max(0, rowWidth - getStringDisplayWidth(indent));
307
- const fitted = fitStyledText(styledText, contentWidth);
308
- const background = requestedBg === C.bgPanelHeader ? C.bgPanelHeader : C.bgPanel;
309
- const fixedStyled = fitted.replace(/\x1b\[0m/g, C.reset + background + C.white);
310
-
311
- console.log(indent + background + C.white + fixedStyled + '\x1b[0K' + C.reset);
312
- }
313
-
314
- export function printSolidPanel({ title, lines, width, indent = '' }) {
315
- const w = width || getFullWidth();
316
- if (title) {
317
- printSolidBannerRow(`${indent} ${C.bold}${C.roseGlow}${title}${C.reset}`, w, C.bgPanelHeader);
318
- }
319
- for (const raw of lines) {
320
- const text = typeof raw === 'string' ? raw : raw.text;
321
- printSolidBannerRow(`${indent} ${text}`, w, C.bgPanel);
322
- }
323
- console.log('');
324
- }
325
-
326
-
327
-
328
- export function formatTerminalMarkdown(text) {
329
- if (!text) return '';
330
- return text
331
- .replace(/^###\s*(.+)$/gm, `${C.bold}${C.gold}✦ $1${C.reset}`)
332
- .replace(/^##\s*(.+)$/gm, `\n${C.bold}${C.roseGlow}$1${C.reset}`)
333
- .replace(/^#\s*(.+)$/gm, `\n${C.bold}${C.sakura}━━━ $1 ━━━${C.reset}`)
334
- .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, `${C.bold}${C.cyberCyan}$1${C.reset} ➔ ${C.underline}${C.roseGlow}$2${C.reset}`)
335
- .replace(/\*{3}([^*]+)\*{3}/g, `${C.bold}${C.italic}${C.white}$1${C.reset}`)
336
- .replace(/\*\*([^*]+)\*\*/g, `${C.bold}${C.white}$1${C.reset}`)
337
- .replace(/(?<!\*)\*([^\*\n]+)\*(?!\*)/g, `${C.italic}${C.white}$1${C.reset}`)
338
- .replace(/`([^`]+)`/g, `${C.cyberCyan}$1${C.reset}`)
339
- .replace(/^(\s*)[*•-]\s+/gm, `$1 ${C.sakura}•${C.reset} `)
340
- .replace(/^\s*[-*_]{3,}\s*$/gm, ` ${C.slate}${'─'.repeat(Math.max(20, getFullWidth() - 6))}${C.reset}`);
341
- }
342
-
343
-
344
- export function parseMarkdownTableRow(text) {
345
- const trimmed = String(text || '').trim();
346
- if (!trimmed.includes('|')) return null;
347
-
348
- const content = trimmed
349
- .replace(/^\|/, '')
350
- .replace(/\|$/, '');
351
- const cells = content
352
- .split(/(?<!\\)\|/)
353
- .map(cell => cell.trim().replace(/\\\|/g, '|'));
354
-
355
- return cells.length >= 2 ? cells : null;
356
- }
357
-
358
-
359
- export function isMarkdownTableSeparator(cells) {
360
- return Array.isArray(cells)
361
- && cells.length > 0
362
- && cells.every(cell => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, '')));
363
- }
364
-
365
-
366
- export function expandMarkdownLinks(text) {
367
- return String(text || '').replace(
368
- /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
369
- '$1: $2'
370
- );
371
- }
372
-
373
-
374
-
375
- export function printWelcomeHero() {
376
- console.log(`\n${C.sakura} ██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${C.reset}\n${C.sakura} ██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║${C.reset}\n${C.roseGlow} █████═╝ ██║█████═╝ ██║ ██║ ███████║██║${C.reset}\n${C.roseGlow} ██╔═██╗ ██║██╔═██╗ ██║ ██║ ██╔══██║██║${C.reset}\n${C.sakura} ██║ ╚██╗██║██║ ╚██╗╚██████╔╝ ██║ ██║██║${C.reset}\n${C.sakura} ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝${C.reset}\n\n${C.bold}${C.roseGlow}Example Questions:${C.reset}\n ${C.dim}•${C.reset} ${C.white}"What are Ju Jingyi's 2026 upcoming drama series?"${C.reset}\n ${C.dim}•${C.reset} ${C.white}"Tell me about her Global Brand Ambassador roles in 2026."${C.reset}\n ${C.dim}•${C.reset} ${C.white}"Search the internet for Ju Jingyi's latest news."${C.reset}\n ${C.dim}•${C.reset} ${C.white}"What OST songs are from The Blooms at Ruyi Pavilion?"${C.reset}\n${C.lavender}Quick Commands:${C.reset} ${C.cyberCyan}/help${C.reset} ${C.dim}•${C.reset} ${C.emerald}/history${C.reset} ${C.dim}•${C.reset} ${C.emerald}/lang${C.reset} ${C.dim}•${C.reset} ${C.emerald}/session${C.reset} ${C.dim}•${C.reset} ${C.emerald}/tool${C.reset} ${C.dim}•${C.reset} ${C.emerald}/mem${C.reset} ${C.dim}•${C.reset} ${C.emerald}/play${C.reset} ${C.dim}•${C.reset} ${C.emerald}/pause${C.reset} ${C.dim}•${C.reset} ${C.emerald}/next${C.reset} ${C.dim}•${C.reset} ${C.emerald}/search${C.reset} ${C.dim}•${C.reset} ${C.emerald}/current${C.reset} ${C.dim}•${C.reset} ${C.emerald}/ads${C.reset} ${C.dim}(Tab or ↑/↓ to navigate / commands)${C.reset}\n`);
377
- }
378
-
379
-
380
-
381
-
382
- export function setupOutputInterception(tuiPrompt, _isProcessingFn) {
383
- let isTuiWriting = false;
384
- let isReplaying = false;
385
- let resizeTimer = null;
386
- const transcript = [];
387
- const MAX_TRANSCRIPT_ENTRIES = 2500;
388
-
389
- const remember = (entry) => {
390
- if (isReplaying) return;
391
- transcript.push(entry);
392
- if (transcript.length > MAX_TRANSCRIPT_ENTRIES) {
393
- transcript.splice(0, transcript.length - MAX_TRANSCRIPT_ENTRIES);
394
- }
395
- };
396
-
397
- process.stdout.write = function (chunk, encoding, callback) {
398
- if (!isTuiWriting && !isReplaying) remember({ type: 'write', chunk: String(chunk) });
399
- if (tuiPrompt && tuiPrompt.activeResolve && !isTuiWriting) {
400
- isTuiWriting = true;
401
- try {
402
- tuiPrompt.clearPrompt();
403
- return _rawStdoutWrite(chunk, encoding, callback);
404
- } finally {
405
- tuiPrompt.render();
406
- isTuiWriting = false;
407
- }
408
- }
409
- return _rawStdoutWrite(chunk, encoding, callback);
410
- };
411
-
412
- console.log = function (...args) {
413
- if (!isReplaying) remember({ type: 'log', args: [...args] });
414
- if (tuiPrompt && tuiPrompt.activeResolve && !isTuiWriting) {
415
- isTuiWriting = true;
416
- try {
417
- tuiPrompt.clearPrompt();
418
- _rawConsoleLog(...args);
419
- } finally {
420
- tuiPrompt.render();
421
- isTuiWriting = false;
422
- }
423
- } else {
424
- const wasTuiWriting = isTuiWriting;
425
- isTuiWriting = true;
426
- try { _rawConsoleLog(...args); }
427
- finally { isTuiWriting = wasTuiWriting; }
428
- }
429
- };
430
-
431
- console.clear = function () {
432
- transcript.length = 0;
433
- _rawConsoleClear();
434
- };
435
-
436
- process.stdout.on('resize', () => {
437
- if (resizeTimer) clearTimeout(resizeTimer);
438
- resizeTimer = setTimeout(() => {
439
- resizeTimer = null;
440
- if (!tuiPrompt || !tuiPrompt.activeResolve) return;
441
-
442
- isTuiWriting = true;
443
- isReplaying = true;
444
- try {
445
- tuiPrompt.lastRenderedLines = 0;
446
- tuiPrompt.hasTopPadding = false;
447
- _rawStdoutWrite('\x1b[?25l\x1b[2J\x1b[H');
448
- printWelcomeHero();
449
- for (const entry of transcript) {
450
- if (entry.type === 'log') _rawConsoleLog(...entry.args);
451
- else _rawStdoutWrite(entry.chunk);
452
- }
453
- tuiPrompt.render();
454
- } finally {
455
- _rawStdoutWrite('\x1b[?25h');
456
- isReplaying = false;
457
- isTuiWriting = false;
458
- }
459
- }, 120);
460
- });
461
- }
462
-
463
- export function restoreOutput() {
464
- process.stdout.write = _rawStdoutWrite;
465
- console.log = _rawConsoleLog;
466
- console.clear = _rawConsoleClear;
467
- }
468
-
469
- export { _rawStdoutWrite as rawWrite, _rawConsoleLog as rawLog };
1
+ import readline from 'node:readline';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+ import { eventBus, Events } from './eventBus.js';
4
+ import { AgentStates } from './agentState.js';
5
+ import { ansiBackground, ansiForeground, getThemePalette } from './theme.js';
6
+
7
+
8
+ export const C = Object.freeze({
9
+ reset: '\x1b[0m',
10
+ bold: '\x1b[1m',
11
+ dim: '\x1b[2m',
12
+ italic: '\x1b[3m',
13
+ underline: '\x1b[4m',
14
+
15
+ get sakura() { return ansiForeground(getThemePalette().primary); },
16
+ get roseGlow() { return ansiForeground(getThemePalette().secondary); },
17
+ get gold() { return ansiForeground(getThemePalette().highlight); },
18
+ get amber() { return ansiForeground(getThemePalette().warning); },
19
+ get cyberCyan() { return ansiForeground(getThemePalette().info); },
20
+ get emerald() { return ansiForeground(getThemePalette().success); },
21
+ get lavender() { return ansiForeground(getThemePalette().tertiary); },
22
+ get slate() { return ansiForeground(getThemePalette().muted); },
23
+ get darkGray() { return ansiForeground(getThemePalette().subdued); },
24
+ get white() { return ansiForeground(getThemePalette().text); },
25
+ get red() { return ansiForeground(getThemePalette().error); },
26
+ get bgPanel() { return ansiBackground(getThemePalette().panel); },
27
+ get bgPanelHeader() { return ansiBackground(getThemePalette().panelHeader); },
28
+ get bgDark() { return ansiBackground(getThemePalette().panel); },
29
+ get bgTool() { return ansiBackground(getThemePalette().panel); },
30
+ get bgToolResult() { return ansiBackground(getThemePalette().panel); },
31
+ get bgToolError() { return ansiBackground(getThemePalette().panel); },
32
+ get bgThinking() { return ansiBackground(getThemePalette().panel); },
33
+ get bgResponse() { return ansiBackground(getThemePalette().panel); },
34
+ get bgError() { return ansiBackground(getThemePalette().panel); },
35
+ });
36
+
37
+ const _rawStdoutWrite = process.stdout.write.bind(process.stdout);
38
+ const _rawConsoleLog = console.log.bind(console);
39
+ const _rawConsoleClear = console.clear.bind(console);
40
+
41
+
42
+
43
+ export const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
44
+ export const processingFrames = spinnerFrames;
45
+ export const thinkingFrames = spinnerFrames;
46
+ export const successFrames = ['OK'];
47
+ export const pulseFrames = ['▓', '▒', '░', '▒', '▓', '█', '▓', '▒'];
48
+ export const INPUT_PLACEHOLDER = 'Ask about Ju Jingyi or type / for commands';
49
+ export const UI_ICONS = Object.freeze({
50
+ help: '📖',
51
+ music: '🎵',
52
+ sponsor: '📢',
53
+ });
54
+
55
+ const fanNames = ['Mìjú / 蜜橘'];
56
+ export function randomFanName() {
57
+ return fanNames[Math.floor(Math.random() * fanNames.length)];
58
+ }
59
+
60
+
61
+
62
+ const graphemeSegmenter = typeof Intl?.Segmenter === 'function'
63
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
64
+ : null;
65
+
66
+ function fallbackGraphemeSegments(str) {
67
+ const segments = [];
68
+ let index = 0;
69
+
70
+ for (const char of Array.from(str)) {
71
+ const isContinuation = segments.length > 0 && (
72
+ /^\p{Mark}$/u.test(char) ||
73
+ /^\p{Emoji_Modifier}$/u.test(char) ||
74
+ char === '\uFE0F' ||
75
+ char === '\u20E3' ||
76
+ segments.at(-1).segment.endsWith('\u200D') ||
77
+ char === '\u200D'
78
+ );
79
+
80
+ if (isContinuation) {
81
+ segments.at(-1).segment += char;
82
+ segments.at(-1).end += char.length;
83
+ } else {
84
+ segments.push({ segment: char, index, end: index + char.length });
85
+ }
86
+ index += char.length;
87
+ }
88
+
89
+ return segments;
90
+ }
91
+
92
+
93
+ export function getGraphemeSegments(str) {
94
+ if (!str) return [];
95
+ if (!graphemeSegmenter) return fallbackGraphemeSegments(str);
96
+ return Array.from(graphemeSegmenter.segment(str), ({ segment, index }) => ({
97
+ segment,
98
+ index,
99
+ end: index + segment.length,
100
+ }));
101
+ }
102
+
103
+ export function previousGraphemeBoundary(str, cursor) {
104
+ const safeCursor = Math.max(0, Math.min(str.length, cursor));
105
+ let previous = 0;
106
+ for (const item of getGraphemeSegments(str)) {
107
+ if (item.end >= safeCursor) return item.index;
108
+ previous = item.end;
109
+ }
110
+ return previous;
111
+ }
112
+
113
+ export function nextGraphemeBoundary(str, cursor) {
114
+ const safeCursor = Math.max(0, Math.min(str.length, cursor));
115
+ for (const item of getGraphemeSegments(str)) {
116
+ if (item.end > safeCursor) return item.end;
117
+ }
118
+ return str.length;
119
+ }
120
+
121
+ function getGraphemeDisplayWidth(segment) {
122
+ if (/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(segment) || /[\uFE0F\u20E3]/u.test(segment)) {
123
+ return 2;
124
+ }
125
+
126
+ let width = 0;
127
+ for (const char of Array.from(segment)) {
128
+ const code = char.codePointAt(0);
129
+ if (/^\p{Mark}$/u.test(char) || code === 0x200D || (code >= 0xFE00 && code <= 0xFE0F)) continue;
130
+ if (
131
+ (code >= 0x1100 && code <= 0x11FF) ||
132
+ (code >= 0x2E80 && code <= 0x9FFF) ||
133
+ (code >= 0xAC00 && code <= 0xD7A3) ||
134
+ (code >= 0xF900 && code <= 0xFAFF) ||
135
+ (code >= 0xFE30 && code <= 0xFE4F)
136
+ ) {
137
+ width += 2;
138
+ } else {
139
+ width += 1;
140
+ }
141
+ }
142
+ return width;
143
+ }
144
+
145
+
146
+ export function getStringDisplayWidth(str) {
147
+ if (!str) return 0;
148
+ const clean = str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
149
+ return getGraphemeSegments(clean).reduce((width, item) => width + getGraphemeDisplayWidth(item.segment), 0);
150
+ }
151
+
152
+
153
+ export function sliceLineForDisplay(str, cursor, maxWidth) {
154
+ const segments = getGraphemeSegments(str).map(item => ({
155
+ ...item,
156
+ width: getGraphemeDisplayWidth(item.segment),
157
+ }));
158
+ const safeMaxWidth = Math.max(1, maxWidth);
159
+ const totalWidth = segments.reduce((sum, item) => sum + item.width, 0);
160
+ const safeCursor = Math.max(0, Math.min(str.length, cursor));
161
+
162
+ let cursorIndex = segments.findIndex(item => item.index >= safeCursor);
163
+ if (cursorIndex === -1) cursorIndex = segments.length;
164
+
165
+ if (totalWidth <= safeMaxWidth) {
166
+ return {
167
+ displayLine: str,
168
+ cursorVisualOffset: segments.slice(0, cursorIndex).reduce((sum, item) => sum + item.width, 0),
169
+ };
170
+ }
171
+
172
+ let start = cursorIndex;
173
+ let end = cursorIndex;
174
+ let contentWidth = 0;
175
+ let leftWidth = 0;
176
+ let rightWidth = 0;
177
+
178
+ while (true) {
179
+ const candidates = [];
180
+ if (start > 0) {
181
+ const nextStart = start - 1;
182
+ const markerWidth = (nextStart > 0 ? 1 : 0) + (end < segments.length ? 1 : 0);
183
+ const width = segments[nextStart].width;
184
+ if (contentWidth + width + markerWidth <= safeMaxWidth) {
185
+ candidates.push({ side: 'left', width, balance: leftWidth });
186
+ }
187
+ }
188
+ if (end < segments.length) {
189
+ const nextEnd = end + 1;
190
+ const markerWidth = (start > 0 ? 1 : 0) + (nextEnd < segments.length ? 1 : 0);
191
+ const width = segments[end].width;
192
+ if (contentWidth + width + markerWidth <= safeMaxWidth) {
193
+ candidates.push({ side: 'right', width, balance: rightWidth });
194
+ }
195
+ }
196
+ if (candidates.length === 0) break;
197
+
198
+ candidates.sort((a, b) => a.balance - b.balance || a.width - b.width);
199
+ const selected = candidates[0];
200
+ if (selected.side === 'left') {
201
+ start--;
202
+ leftWidth += selected.width;
203
+ } else {
204
+ rightWidth += selected.width;
205
+ end++;
206
+ }
207
+ contentWidth += selected.width;
208
+ }
209
+
210
+ const hasLeftEllipsis = start > 0;
211
+ const hasRightEllipsis = end < segments.length;
212
+ const displayLine = `${hasLeftEllipsis ? '…' : ''}${segments.slice(start, end).map(item => item.segment).join('')}${hasRightEllipsis ? '…' : ''}`;
213
+ const cursorVisualOffset = (hasLeftEllipsis ? 1 : 0) + segments
214
+ .slice(start, cursorIndex)
215
+ .reduce((sum, item) => sum + item.width, 0);
216
+
217
+ return { displayLine, cursorVisualOffset };
218
+ }
219
+
220
+ export function getFullWidth() {
221
+ return process.stdout.columns || 80;
222
+ }
223
+
224
+
225
+
226
+ export function wrapText(text, maxWidth = 72) {
227
+ if (!text) return [];
228
+ const lines = [];
229
+ const rawParagraphs = text.split('\n');
230
+
231
+ for (const para of rawParagraphs) {
232
+ if (!para.trim()) {
233
+ lines.push('');
234
+ continue;
235
+ }
236
+
237
+ const words = para.split(/\s+/);
238
+ let currentLine = '';
239
+
240
+ for (const word of words) {
241
+ if (!word) continue;
242
+ const testLine = currentLine ? (currentLine + ' ' + word) : word;
243
+ const testWidth = getStringDisplayWidth(testLine);
244
+
245
+ if (testWidth <= maxWidth) {
246
+ currentLine = testLine;
247
+ } else {
248
+ if (currentLine) {
249
+ lines.push(currentLine);
250
+ currentLine = word;
251
+ } else {
252
+ let chunk = '';
253
+ for (const ch of word) {
254
+ if (getStringDisplayWidth(chunk + ch) <= maxWidth) {
255
+ chunk += ch;
256
+ } else {
257
+ lines.push(chunk);
258
+ chunk = ch;
259
+ }
260
+ }
261
+ currentLine = chunk;
262
+ }
263
+ }
264
+ }
265
+ if (currentLine) {
266
+ lines.push(currentLine);
267
+ }
268
+ }
269
+ return lines;
270
+ }
271
+
272
+
273
+
274
+ export function stripAnsi(str) {
275
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
276
+ }
277
+
278
+
279
+ export function fitStyledText(styledText, maxWidth) {
280
+ const safeWidth = Math.max(0, maxWidth);
281
+ let result = '';
282
+ let usedWidth = 0;
283
+ const tokens = String(styledText || '').split(/(\x1b\[[0-9;]*[a-zA-Z])/g);
284
+
285
+ for (const token of tokens) {
286
+ if (!token) continue;
287
+ if (/^\x1b\[[0-9;]*[a-zA-Z]$/.test(token)) {
288
+ result += token;
289
+ continue;
290
+ }
291
+ for (const item of getGraphemeSegments(token)) {
292
+ const itemWidth = getGraphemeDisplayWidth(item.segment);
293
+ if (usedWidth + itemWidth > safeWidth) return result;
294
+ result += item.segment;
295
+ usedWidth += itemWidth;
296
+ }
297
+ }
298
+ return result;
299
+ }
300
+
301
+
302
+
303
+ export function printSolidBannerRow(styledText, targetWidth, requestedBg = C.bgPanel, indent = '') {
304
+ const terminalWidth = getFullWidth();
305
+ const rowWidth = Math.max(1, Math.min(targetWidth || terminalWidth, terminalWidth) - 1);
306
+ const contentWidth = Math.max(0, rowWidth - getStringDisplayWidth(indent));
307
+ const fitted = fitStyledText(styledText, contentWidth);
308
+ const background = requestedBg === C.bgPanelHeader ? C.bgPanelHeader : C.bgPanel;
309
+ const fixedStyled = fitted.replace(/\x1b\[0m/g, C.reset + background + C.white);
310
+
311
+ console.log(indent + background + C.white + fixedStyled + '\x1b[0K' + C.reset);
312
+ }
313
+
314
+ export function createProgressRunner({
315
+ badge = 'RUN',
316
+ title = 'Processing...',
317
+ step = '',
318
+ badgeBg = '\x1b[48;2;14;165;233m\x1b[38;2;15;23;42m',
319
+ } = {}) {
320
+ const startTime = Date.now();
321
+ let currentStep = step;
322
+ let currentTitle = title;
323
+ let frameIdx = 0;
324
+ let timer = null;
325
+ let isStopped = false;
326
+
327
+ const renderFrame = () => {
328
+ if (isStopped) return;
329
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
330
+ const spinner = spinnerFrames[frameIdx % spinnerFrames.length];
331
+ frameIdx++;
332
+
333
+ const badgeStr = `${badgeBg}${C.bold} ${badge} ${C.reset}`;
334
+ const spinStr = `${C.sakura}${spinner}${C.reset}`;
335
+ const titleStr = `${C.bold}${C.white}${currentTitle}${C.reset}`;
336
+ const stepStr = currentStep ? ` ${C.dim}▸ ${currentStep}${C.reset}` : '';
337
+ const timeStr = ` ${C.gold}(${elapsed}s)${C.reset}`;
338
+
339
+ const line = ` ${badgeStr} ${spinStr} ${titleStr}${stepStr}${timeStr}`;
340
+ const termWidth = getFullWidth();
341
+ const fitted = fitStyledText(line, termWidth - 1);
342
+
343
+ _rawStdoutWrite(`\r\x1b[2K${fitted}`);
344
+ };
345
+
346
+ timer = setInterval(renderFrame, 80);
347
+ if (timer.unref) timer.unref();
348
+ renderFrame();
349
+
350
+ return {
351
+ update({ title: newTitle, step: newStep } = {}) {
352
+ if (newTitle !== undefined) currentTitle = newTitle;
353
+ if (newStep !== undefined) currentStep = newStep;
354
+ renderFrame();
355
+ },
356
+ stop() {
357
+ if (isStopped) return;
358
+ isStopped = true;
359
+ if (timer) {
360
+ clearInterval(timer);
361
+ timer = null;
362
+ }
363
+ _rawStdoutWrite('\r\x1b[2K');
364
+ },
365
+ done(options = {}) {
366
+ this.stop();
367
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
368
+ if (typeof options === 'string') {
369
+ console.log(`\n \x1b[48;2;16;185;129m\x1b[38;2;15;23;42m${C.bold} OK ${C.reset} ${C.emerald}${options}${C.reset} ${C.dim}(${elapsed}s)${C.reset}\n`);
370
+ } else if (options && options.panel) {
371
+ console.log('');
372
+ printSolidPanel(options.panel);
373
+ } else if (options && options.title) {
374
+ console.log('');
375
+ const lines = options.lines || [];
376
+ printSolidPanel({
377
+ title: `${options.title} • ${elapsed}s`,
378
+ lines,
379
+ headerBg: options.headerBg,
380
+ bg: options.bg,
381
+ });
382
+ } else if (options && options.message) {
383
+ console.log(`\n \x1b[48;2;16;185;129m\x1b[38;2;15;23;42m${C.bold} OK ${C.reset} ${C.emerald}${options.message}${C.reset} ${C.dim}(${elapsed}s)${C.reset}\n`);
384
+ }
385
+ },
386
+ fail(error) {
387
+ this.stop();
388
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
389
+ const errMsg = error?.message || String(error || 'Failed');
390
+ console.log(`\n \x1b[48;2;239;68;68m\x1b[38;2;255;255;255m${C.bold} FAILED ${C.reset} ${C.red}${errMsg}${C.reset} ${C.dim}(${elapsed}s)${C.reset}\n`);
391
+ }
392
+ };
393
+ }
394
+
395
+ export function formatSolidPanelLines({ title, lines, width, indent = '', headerBg, bg }) {
396
+ const w = width || getFullWidth();
397
+ const rows = [];
398
+ if (title) {
399
+ const rawTitle = `${indent} ${C.bold}${C.roseGlow}${title}${C.reset}`;
400
+ const fitted = fitStyledText(rawTitle, w);
401
+ const pad = Math.max(0, w - getStringDisplayWidth(fitted));
402
+ const hBg = headerBg || C.bgPanelHeader;
403
+ const themed = fitted.replace(/\x1b\[0m/g, C.reset + hBg + C.white);
404
+ rows.push(`${hBg}${C.white}${themed}${' '.repeat(pad)}${C.reset}`);
405
+ }
406
+ const bodyBg = bg || C.bgPanel;
407
+ for (const raw of (lines || [])) {
408
+ const text = typeof raw === 'string' ? raw : (raw?.text ?? '');
409
+ const sublines = String(text).split('\n');
410
+ for (const subline of sublines) {
411
+ const rowText = `${indent} ${subline}`;
412
+ const fitted = fitStyledText(rowText, w);
413
+ const pad = Math.max(0, w - getStringDisplayWidth(fitted));
414
+ const themed = fitted.replace(/\x1b\[0m/g, C.reset + bodyBg + C.white);
415
+ rows.push(`${bodyBg}${C.white}${themed}${' '.repeat(pad)}${C.reset}`);
416
+ }
417
+ }
418
+ return rows;
419
+ }
420
+
421
+ export function printSolidPanel(options) {
422
+ const rows = formatSolidPanelLines(options);
423
+ for (const row of rows) {
424
+ console.log(row);
425
+ }
426
+ console.log('');
427
+ }
428
+
429
+
430
+
431
+ export function formatTerminalMarkdown(text) {
432
+ if (!text) return '';
433
+ return text
434
+ .replace(/^###\s*(.+)$/gm, `${C.bold}${C.gold}✦ $1${C.reset}`)
435
+ .replace(/^##\s*(.+)$/gm, `\n${C.bold}${C.roseGlow}$1${C.reset}`)
436
+ .replace(/^#\s*(.+)$/gm, `\n${C.bold}${C.sakura}━━━ $1 ━━━${C.reset}`)
437
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, `${C.bold}${C.cyberCyan}$1${C.reset} ➔ ${C.underline}${C.roseGlow}$2${C.reset}`)
438
+ .replace(/\*{3}([^*]+)\*{3}/g, `${C.bold}${C.italic}${C.white}$1${C.reset}`)
439
+ .replace(/\*\*([^*]+)\*\*/g, `${C.bold}${C.white}$1${C.reset}`)
440
+ .replace(/(?<!\*)\*([^\*\n]+)\*(?!\*)/g, `${C.italic}${C.white}$1${C.reset}`)
441
+ .replace(/`([^`]+)`/g, `${C.cyberCyan}$1${C.reset}`)
442
+ .replace(/^(\s*)[*•-]\s+/gm, `$1 ${C.sakura}•${C.reset} `)
443
+ .replace(/^\s*[-*_]{3,}\s*$/gm, ` ${C.slate}${'─'.repeat(Math.max(20, getFullWidth() - 6))}${C.reset}`);
444
+ }
445
+
446
+
447
+ export function parseMarkdownTableRow(text) {
448
+ const trimmed = String(text || '').trim();
449
+ if (!trimmed.includes('|')) return null;
450
+
451
+ const content = trimmed
452
+ .replace(/^\|/, '')
453
+ .replace(/\|$/, '');
454
+ const cells = content
455
+ .split(/(?<!\\)\|/)
456
+ .map(cell => cell.trim().replace(/\\\|/g, '|'));
457
+
458
+ return cells.length >= 2 ? cells : null;
459
+ }
460
+
461
+
462
+ export function isMarkdownTableSeparator(cells) {
463
+ return Array.isArray(cells)
464
+ && cells.length > 0
465
+ && cells.every(cell => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, '')));
466
+ }
467
+
468
+ export function expandMarkdownLinks(text) {
469
+ return String(text || '').replace(
470
+ /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
471
+ '$1: $2'
472
+ );
473
+ }
474
+
475
+ export function getWelcomeHeroLines() {
476
+ return [
477
+ '',
478
+ ` ${C.sakura}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${C.reset} ${C.bold}${C.white}Kiku AI Terminal Agent${C.reset}`,
479
+ ` ${C.sakura}██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║${C.reset} ${C.gold}v1.0.3${C.reset} ${C.slate}• Intelligent Fan Portal${C.reset}`,
480
+ ` ${C.roseGlow}█████═╝ ██║█████═╝ ██║ ██║ ███████║██║${C.reset} ${C.cyberCyan}SSOT • DAG • Dynamic Tools${C.reset}`,
481
+ ` ${C.roseGlow}██╔═██╗ ██║██╔═██╗ ██║ ██║ ██╔══██╗██║${C.reset} ${C.emerald}LM Studio / Cloud Providers${C.reset}`,
482
+ ` ${C.sakura}██║ ╚██╗██║██║ ╚██╗╚██████╔╝ ██║ ██║██║${C.reset}`,
483
+ ` ${C.sakura}╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝${C.reset}`,
484
+ '',
485
+ ` ${C.bold}${C.gold}Suggested Queries & Topics:${C.reset}`,
486
+ ` ${C.sakura}• 2026 Works${C.reset} ${C.white}"What are Ju Jingyi's 2026 upcoming dramas?"${C.reset}`,
487
+ ` ${C.cyberCyan}• Music & OST${C.reset} ${C.white}"OSTs from In Blossom & Ruyi Pavilion, /play Ginkgo"${C.reset}`,
488
+ ` ${C.emerald}• Endorsements${C.reset} ${C.white}"Versace Global Ambassador & fashion partnerships"${C.reset}`,
489
+ ` ${C.lavender}• Tools / MCP${C.reset} ${C.white}"@search_works 2026, /mcp list, /stats, /help"${C.reset}`,
490
+ '',
491
+ ` ${C.slate}Shortcuts: [Tab] Autocomplete • [↑/↓] History • [PgUp/PgDn] Scroll • [F2] Details • [Ctrl+C] Exit${C.reset}`,
492
+ '',
493
+ ];
494
+ }
495
+
496
+ export function printWelcomeHero() {
497
+ const rows = getWelcomeHeroLines();
498
+ console.log(rows.join('\n'));
499
+ }
500
+
501
+
502
+
503
+
504
+ export function setupOutputInterception(tuiPrompt, _isProcessingFn) {
505
+ let isTuiWriting = false;
506
+ let isReplaying = false;
507
+ let resizeTimer = null;
508
+ const transcript = [];
509
+ const MAX_TRANSCRIPT_ENTRIES = 2500;
510
+
511
+ const remember = (entry) => {
512
+ if (isReplaying) return;
513
+ transcript.push(entry);
514
+ if (transcript.length > MAX_TRANSCRIPT_ENTRIES) {
515
+ transcript.splice(0, transcript.length - MAX_TRANSCRIPT_ENTRIES);
516
+ }
517
+ };
518
+
519
+ const onResize = () => {
520
+ if (resizeTimer) clearTimeout(resizeTimer);
521
+ resizeTimer = setTimeout(() => {
522
+ resizeTimer = null;
523
+ if (tuiPrompt && !tuiPrompt.isDisposed && tuiPrompt.screenBuffer) {
524
+ tuiPrompt.render();
525
+ }
526
+ }, 80);
527
+ };
528
+
529
+ process.stdout.on('resize', onResize);
530
+
531
+ process.stdout.write = function (chunk, encoding, callback) {
532
+ if (tuiPrompt && tuiPrompt.screenBuffer && !tuiPrompt.isDisposed && !isTuiWriting) {
533
+ const str = String(chunk);
534
+ if (
535
+ str.startsWith('\x1b[H') ||
536
+ str.startsWith('\x1b[?1049') ||
537
+ str.startsWith('\x1b[?25') ||
538
+ str.startsWith('\x1b[?1000') ||
539
+ str.startsWith('\x1b[?1002') ||
540
+ str.startsWith('\x1b[?1006')
541
+ ) {
542
+ return _rawStdoutWrite(chunk, encoding, callback);
543
+ }
544
+ isTuiWriting = true;
545
+ try {
546
+ tuiPrompt.addViewportLines(str);
547
+ } finally {
548
+ isTuiWriting = false;
549
+ }
550
+ if (typeof callback === 'function') callback();
551
+ return true;
552
+ }
553
+ if (!isTuiWriting && !isReplaying) remember({ type: 'write', chunk: String(chunk) });
554
+ return _rawStdoutWrite(chunk, encoding, callback);
555
+ };
556
+
557
+ console.log = function (...args) {
558
+ if (tuiPrompt && tuiPrompt.screenBuffer && !tuiPrompt.isDisposed && !isTuiWriting) {
559
+ isTuiWriting = true;
560
+ try {
561
+ const text = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ');
562
+ tuiPrompt.addViewportLines(text);
563
+ } finally {
564
+ isTuiWriting = false;
565
+ }
566
+ return;
567
+ }
568
+ if (!isReplaying) remember({ type: 'log', args: [...args] });
569
+ _rawConsoleLog(...args);
570
+ };
571
+
572
+ console.clear = function () {
573
+ if (tuiPrompt && tuiPrompt.screenBuffer && !tuiPrompt.isDisposed) {
574
+ tuiPrompt.clearViewport();
575
+ return;
576
+ }
577
+ transcript.length = 0;
578
+ _rawConsoleClear();
579
+ };
580
+
581
+ return () => {
582
+ process.stdout.removeListener('resize', onResize);
583
+ process.stdout.write = _rawStdoutWrite;
584
+ console.log = _rawConsoleLog;
585
+ console.clear = _rawConsoleClear;
586
+ };
587
+ }
588
+
589
+ export function formatTaskPlanBanner(plan) {
590
+ if (!plan || !plan.steps || plan.steps.length === 0) return '';
591
+ const completed = plan.steps.filter(s => s.state === 'complete').length;
592
+ const total = plan.steps.length;
593
+ const pct = Math.round((completed / total) * 100);
594
+ const barFilled = Math.round((completed / total) * 10);
595
+ const barEmpty = 10 - barFilled;
596
+ const progressBar = `[${'█'.repeat(barFilled)}${'░'.repeat(barEmpty)}] ${pct}%`;
597
+
598
+ const termWidth = Math.min(80, Math.max(20, getFullWidth() - 2));
599
+ const bg = C.bgPanel;
600
+ const bgHeader = C.bgPanelHeader;
601
+
602
+ const headerText = ` ${C.bold}${C.sakura}◆ Task Plan${C.reset} ${C.slate}•${C.reset} ${C.gold}${progressBar}${C.reset} ${C.slate}(${completed}/${total})${C.reset}`;
603
+ const rows = [];
604
+ rows.push(fitStyledText(headerText, termWidth));
605
+
606
+ if (plan.question) {
607
+ const goalText = ` ${C.slate}│${C.reset} ${C.slate}Goal: "${plan.question.slice(0, 48)}${plan.question.length > 48 ? '…' : ''}"${C.reset}`;
608
+ rows.push(fitStyledText(goalText, termWidth));
609
+ }
610
+
611
+ for (let i = 0; i < plan.steps.length; i++) {
612
+ const s = plan.steps[i];
613
+ const isLast = i === plan.steps.length - 1;
614
+ const branch = isLast ? '└─' : '├─';
615
+
616
+ let icon = `${C.slate}○${C.reset}`;
617
+ let stateLabel = `${C.slate}pending${C.reset}`;
618
+
619
+ if (s.state === 'complete') {
620
+ icon = `${C.emerald}✓${C.reset}`;
621
+ stateLabel = `${C.emerald}done${C.reset}`;
622
+ } else if (s.state === 'running') {
623
+ icon = `${C.cyberCyan}●${C.reset}`;
624
+ stateLabel = `${C.cyberCyan}active${C.reset}`;
625
+ } else if (s.state === 'failed') {
626
+ icon = `${C.red}✕${C.reset}`;
627
+ stateLabel = `${C.red}failed${C.reset}`;
628
+ }
629
+
630
+ const stepNum = `${C.slate}Step ${i + 1}:${C.reset}`;
631
+ const stepTitle = `${C.white}${s.label || s.id}${C.reset}`;
632
+ const maxObjLen = Math.max(10, termWidth - getStringDisplayWidth(s.label || s.id) - 36);
633
+ const objText = s.objective && maxObjLen > 6 ? ` ${C.slate}— ${s.objective.slice(0, maxObjLen)}${s.objective.length > maxObjLen ? '…' : ''}${C.reset}` : '';
634
+
635
+ const lineText = ` ${C.slate}${branch}${C.reset} ${icon} ${stepNum} ${stepTitle}${objText} ${C.slate}[${C.reset}${stateLabel}${C.slate}]${C.reset}`;
636
+ rows.push(fitStyledText(lineText, termWidth));
637
+ }
638
+
639
+ return rows.map((r, idx) => {
640
+ const currentBg = idx === 0 ? bgHeader : bg;
641
+ const padding = Math.max(0, termWidth - getStringDisplayWidth(r));
642
+ const styled = r.replace(/\x1b\[0m/g, C.reset + currentBg + C.white);
643
+ return `${currentBg}${C.white}${styled}${' '.repeat(padding)}${C.reset}`;
644
+ }).join('\n');
645
+ }
646
+
647
+ export function enterAlternateScreen() {
648
+ if (process.stdout.isTTY) {
649
+ _rawStdoutWrite('\x1b[?1049h\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?25l\x1b[H\x1b[2J');
650
+ }
651
+ }
652
+
653
+ export function exitAlternateScreen() {
654
+ if (process.stdout.isTTY) {
655
+ _rawStdoutWrite('\x1b[?1006l\x1b[?1005l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1049l\x1b[?25h\x1b[0m\r\n');
656
+ }
657
+ }
658
+
659
+ export class VirtualScreenBuffer {
660
+ constructor(width = 80, height = 24, writer = null) {
661
+ this.width = Math.max(10, width);
662
+ this.height = Math.max(5, height);
663
+ this.writer = writer || _rawStdoutWrite;
664
+ this.rows = new Array(this.height).fill('');
665
+ this.prevRows = new Array(this.height).fill('');
666
+ this.cursorRow = 0;
667
+ this.cursorCol = 0;
668
+ this.showCursor = true;
669
+ this.prevCursorRow = -1;
670
+ this.prevCursorCol = -1;
671
+ this.prevShowCursor = false;
672
+ this.forceFullRedraw = true;
673
+ }
674
+
675
+ resize(width, height) {
676
+ const newWidth = Math.max(10, width);
677
+ const newHeight = Math.max(5, height);
678
+ if (newWidth !== this.width || newHeight !== this.height) {
679
+ this.width = newWidth;
680
+ this.height = newHeight;
681
+ this.rows = new Array(this.height).fill('');
682
+ this.prevRows = new Array(this.height).fill('');
683
+ this.forceFullRedraw = true;
684
+ }
685
+ }
686
+
687
+ clear(bg = '') {
688
+ const safeW = Math.max(1, this.width - 1);
689
+ const blank = bg ? `${bg}${' '.repeat(safeW)}${C.reset}` : ' '.repeat(safeW);
690
+ this.rows.fill(blank);
691
+ }
692
+
693
+ setRow(y, styledText, bg = '') {
694
+ if (y < 0 || y >= this.height) return;
695
+ const safeW = Math.max(1, this.width - 1);
696
+ const fitted = fitStyledText(styledText, safeW);
697
+ const textWidth = getStringDisplayWidth(fitted);
698
+ const pad = Math.max(0, safeW - textWidth);
699
+ if (bg) {
700
+ const themed = fitted.replace(/\x1b\[0m/g, C.reset + bg + C.white);
701
+ this.rows[y] = `${bg}${C.white}${themed}${' '.repeat(pad)}${C.reset}`;
702
+ } else {
703
+ this.rows[y] = `${fitted}${' '.repeat(pad)}${C.reset}`;
704
+ }
705
+ }
706
+
707
+ setCursor(row, col, visible = true) {
708
+ this.cursorRow = Math.max(0, Math.min(this.height - 1, row));
709
+ this.cursorCol = Math.max(0, Math.min(this.width - 1, col));
710
+ this.showCursor = visible;
711
+ }
712
+
713
+ render() {
714
+ let output = '';
715
+ let hasChanges = false;
716
+
717
+ if (this.forceFullRedraw) {
718
+ output += '\x1b[?25l';
719
+ for (let i = 0; i < this.height; i++) {
720
+ output += `\x1b[${i + 1};1H${this.rows[i] || ''}`;
721
+ this.prevRows[i] = this.rows[i];
722
+ }
723
+ this.forceFullRedraw = false;
724
+ hasChanges = true;
725
+ } else {
726
+ for (let i = 0; i < this.height; i++) {
727
+ if (this.rows[i] !== this.prevRows[i]) {
728
+ if (!hasChanges) {
729
+ output += '\x1b[?25l';
730
+ hasChanges = true;
731
+ }
732
+ output += `\x1b[${i + 1};1H${this.rows[i] || ''}`;
733
+ this.prevRows[i] = this.rows[i];
734
+ }
735
+ }
736
+ }
737
+
738
+ const cursorChanged =
739
+ this.cursorRow !== this.prevCursorRow ||
740
+ this.cursorCol !== this.prevCursorCol ||
741
+ this.showCursor !== this.prevShowCursor;
742
+
743
+ if (hasChanges || cursorChanged) {
744
+ if (this.showCursor) {
745
+ output += `\x1b[${this.cursorRow + 1};${this.cursorCol + 1}H\x1b[?25h`;
746
+ } else if (!this.showCursor && hasChanges) {
747
+ output += '\x1b[?25l';
748
+ }
749
+ this.prevCursorRow = this.cursorRow;
750
+ this.prevCursorCol = this.cursorCol;
751
+ this.prevShowCursor = this.showCursor;
752
+ }
753
+
754
+ if (output) {
755
+ this.writer(output);
756
+ }
757
+ }
758
+ }
759
+
760
+ export { _rawStdoutWrite as rawWrite, _rawConsoleLog as rawLog };