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.
- package/README.md +293 -122
- package/package.json +60 -61
- package/server/agent/agentCore.js +892 -782
- package/server/agent/appPaths.js +21 -0
- package/server/agent/audioPlayer.js +739 -673
- package/server/agent/cli.js +1414 -1111
- package/server/agent/commandRegistry.js +74 -58
- package/server/agent/commands.js +1694 -788
- package/server/agent/config.js +264 -90
- package/server/agent/contextCompactor.js +79 -0
- package/server/agent/doctor.js +69 -0
- package/server/agent/eventBus.js +107 -103
- package/server/agent/gpuMetrics.js +189 -0
- package/server/agent/llmClient.js +369 -200
- package/server/agent/mcpClient.js +364 -0
- package/server/agent/memory.js +670 -566
- package/server/agent/modelManager.js +77 -0
- package/server/agent/research.js +202 -0
- package/server/agent/session.js +244 -239
- package/server/agent/subagent.js +116 -0
- package/server/agent/theme.js +125 -124
- package/server/agent/toolRouter.js +201 -0
- package/server/agent/tools.js +1679 -1528
- package/server/agent/ui.js +760 -469
- package/server/mcp/claude-desktop-config.example.json +1 -1
- package/server/mcp/data/discography.js +36 -36
- package/server/mcp/dataAdapter.js +102 -0
- package/server/mcp/examples/claude-code.mcp.example.json +19 -0
- package/server/mcp/examples/claude-desktop.example.json +11 -0
- package/server/mcp/examples/cline.example.json +13 -0
- package/server/mcp/examples/cursor.example.json +14 -0
- package/server/mcp/examples/gemini-cli.example.json +17 -0
- package/server/mcp/examples/opencode.example.json +21 -0
- package/server/mcp/examples/vscode.example.json +19 -0
- package/server/mcp/examples/windsurf.example.json +14 -0
- package/server/mcp/handler.js +85 -159
- package/server/mcp/security.js +140 -138
- package/server/mcp/server.js +129 -100
- package/server/mcp/stdio.js +4 -2
package/server/agent/ui.js
CHANGED
|
@@ -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
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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 };
|