kiku-agent 1.0.9 → 1.0.10

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 CHANGED
@@ -87,7 +87,7 @@ Automatic installation is available for global npm installs. Source checkouts, l
87
87
 
88
88
  After an update or rollback, restart `kiku-agent` and any application hosting `kiku-mcp`. Update settings and rollback history are stored in the normal Kiku user data directory as `update-state.json`; sessions, memory, `.env`, and YouTube cookies are not modified.
89
89
 
90
- For a local checkout before the package is published, use `npm pack` followed by `npm install -g ./kiku-agent-1.0.9.tgz`.
90
+ For a local checkout before the package is published, use `npm pack` followed by `npm install -g ./kiku-agent-1.0.10.tgz`.
91
91
 
92
92
  ### ✨ Features
93
93
  - **LM Studio Integration**: Connects seamlessly to LM Studio's OpenAI-compatible server at `http://127.0.0.1:1234/v1` (default model `google/gemma-4-e4b`, with auto-detection of loaded models).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiku-agent",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Kiku AI terminal agent and MCP server for Ju Jingyi knowledge, media, and career data.",
5
5
  "license": "GPL-3.0",
6
6
  "type": "module",
@@ -29,10 +29,10 @@
29
29
  "kikuUpdate": {
30
30
  "channel": "stable",
31
31
  "releaseSummary": [
32
- "Added graceful 404 error handling for unpublished channels and non-existent version lookups",
33
- "Enhanced CLI update diagnostics with active release channel switching guidance",
34
- "Updated production model defaults and live fallback resolution across cloud providers",
35
- "Hardened REST API key validation and isolated rate-limit probes"
32
+ "Added session restore and resume support",
33
+ "Fixed CJK fullwidth table wrapping in TUI",
34
+ "Enhanced React 19 rendering safety",
35
+ "Added comprehensive version bump tool"
36
36
  ],
37
37
  "releaseUrl": "https://www.npmjs.com/package/kiku-agent"
38
38
  },
@@ -63,7 +63,8 @@
63
63
  "kiku-agent": "node server/agent/cli.js",
64
64
  "kiku-mcp": "node server/mcp/stdio.js",
65
65
  "kiku-ink": "npx tsx server/agent/ink/main.jsx",
66
- "test": "node --test server/agent/tests/*.test.js server/mcp/tests/*.test.js"
66
+ "test": "node --test server/agent/tests/*.test.js server/mcp/tests/*.test.js",
67
+ "bump": "node bump_version.js"
67
68
  },
68
69
  "dependencies": {
69
70
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -12,8 +12,8 @@ import { ResearchOrchestrator } from './research.js';
12
12
  import { SuggestionEngine } from './suggestions.js';
13
13
  import { McpClientManager } from './mcpClient.js';
14
14
  import { DynamicToolRouter } from './toolRouter.js';
15
- import { contextCompactor } from './contextCompactor.js';
16
- import { AgentJobManager } from './jobManager.js';
15
+ import { contextCompactor } from './contextCompactor.js';
16
+ import { AgentJobManager } from './jobManager.js';
17
17
 
18
18
  export function detectDominantLanguage(text) {
19
19
  if (!text || typeof text !== 'string') return 'th';
@@ -64,8 +64,8 @@ export class KikuAgent {
64
64
  this.mcpManager = options.mcpManager || new McpClientManager();
65
65
  this.router = new DynamicToolRouter(this.getAllToolsSchema());
66
66
  this.research = new ResearchOrchestrator({ client: this.client });
67
- this.suggestions = new SuggestionEngine({ client: this.client, memory: this.memory });
68
- this.jobs = new AgentJobManager();
67
+ this.suggestions = new SuggestionEngine({ client: this.client, memory: this.memory });
68
+ this.jobs = new AgentJobManager();
69
69
  this.systemPrompt = this.buildSystemPrompt();
70
70
  this.initHistory();
71
71
  this.restoredMessageCount = 0;
@@ -103,39 +103,39 @@ export class KikuAgent {
103
103
  return this._ready;
104
104
  }
105
105
 
106
- async runSubagent(task, options = {}) {
107
- await this._ready;
108
- return this.jobs.run('subagent', task, async ({ signal }) => {
109
- const subagent = new ResearchSubagent({
110
- client: this.client,
111
- maxIterations: options.maxIterations || 3,
112
- });
113
- return subagent.run(task, {
114
- signal,
115
- onProgress: options.onProgress,
116
- skillContext: this.memory.buildUserSkillContext(),
117
- });
118
- }, options);
119
- }
120
-
121
- async runResearch(question, options = {}) {
122
- await this._ready;
123
- const type = options.mode === 'verify' ? 'verify' : 'research';
124
- return this.jobs.run(type, question, ({ signal }) => this.research.run(question, {
125
- ...options,
126
- signal,
127
- skillContext: this.memory.buildUserSkillContext(),
128
- }), options);
106
+ async runSubagent(task, options = {}) {
107
+ await this._ready;
108
+ return this.jobs.run('subagent', task, async ({ signal }) => {
109
+ const subagent = new ResearchSubagent({
110
+ client: this.client,
111
+ maxIterations: options.maxIterations || 3,
112
+ });
113
+ return subagent.run(task, {
114
+ signal,
115
+ onProgress: options.onProgress,
116
+ skillContext: this.memory.buildUserSkillContext(),
117
+ });
118
+ }, options);
119
+ }
120
+
121
+ async runResearch(question, options = {}) {
122
+ await this._ready;
123
+ const type = options.mode === 'verify' ? 'verify' : 'research';
124
+ return this.jobs.run(type, question, ({ signal }) => this.research.run(question, {
125
+ ...options,
126
+ signal,
127
+ skillContext: this.memory.buildUserSkillContext(),
128
+ }), options);
129
129
  }
130
130
 
131
131
  getResearchStatus() {
132
132
  return this.research.getStatus();
133
133
  }
134
134
 
135
- cancelResearch() {
136
- const direct = this.research.cancel();
137
- const tracked = this.jobs.cancelType('research') || this.jobs.cancelType('verify');
138
- return direct || tracked;
135
+ cancelResearch() {
136
+ const direct = this.research.cancel();
137
+ const tracked = this.jobs.cancelType('research') || this.jobs.cancelType('verify');
138
+ return direct || tracked;
139
139
  }
140
140
 
141
141
  async generateSuggestions(options = {}) {
@@ -143,33 +143,33 @@ export class KikuAgent {
143
143
  return this.suggestions.generateSuggestions(options);
144
144
  }
145
145
 
146
- async runSuggestionBatch(targets, options = {}) {
147
- await this._ready;
148
- return this.jobs.run('suggestion', String(targets || 'all'), ({ signal }) => this.suggestions.runBatchSubagents(targets, {
149
- ...options,
150
- signal,
151
- }), options);
152
- }
153
-
154
- getLastSuggestions() {
155
- return this.suggestions.getLastSuggestions();
156
- }
157
-
158
- getJobs() {
159
- return this.jobs.list();
160
- }
161
-
162
- getJob(id) {
163
- return this.jobs.get(id);
164
- }
165
-
166
- cancelJob(id) {
167
- return this.jobs.cancel(id);
168
- }
169
-
170
- clearJobs() {
171
- return this.jobs.clear();
172
- }
146
+ async runSuggestionBatch(targets, options = {}) {
147
+ await this._ready;
148
+ return this.jobs.run('suggestion', String(targets || 'all'), ({ signal }) => this.suggestions.runBatchSubagents(targets, {
149
+ ...options,
150
+ signal,
151
+ }), options);
152
+ }
153
+
154
+ getLastSuggestions() {
155
+ return this.suggestions.getLastSuggestions();
156
+ }
157
+
158
+ getJobs() {
159
+ return this.jobs.list();
160
+ }
161
+
162
+ getJob(id) {
163
+ return this.jobs.get(id);
164
+ }
165
+
166
+ cancelJob(id) {
167
+ return this.jobs.cancel(id);
168
+ }
169
+
170
+ clearJobs() {
171
+ return this.jobs.clear();
172
+ }
173
173
 
174
174
  initHistory() {
175
175
  this.conversationHistory = [
@@ -223,6 +223,59 @@ export class KikuAgent {
223
223
  }
224
224
  }
225
225
 
226
+ async restoreSession(sessionId) {
227
+ await this._ready;
228
+ if (!sessionId) return null;
229
+
230
+ let targetSessionId = String(sessionId).trim();
231
+ const archives = this.memory.getArchivedSessions(50);
232
+ if (/^\d+$/.test(targetSessionId)) {
233
+ const idx = parseInt(targetSessionId, 10) - 1;
234
+ if (archives[idx]) {
235
+ targetSessionId = archives[idx].id;
236
+ }
237
+ } else {
238
+ const match = archives.find(s => s.id === targetSessionId || s.id.toLowerCase().includes(targetSessionId.toLowerCase()));
239
+ if (match) {
240
+ targetSessionId = match.id;
241
+ }
242
+ }
243
+
244
+ const archived = await this.memory.loadArchivedSession(targetSessionId);
245
+ if (!archived) return null;
246
+
247
+ if (this.session.messages.length > 0 && this.session.id !== targetSessionId) {
248
+ await this.memory.archiveSession(this.session.toSnapshot()).catch(() => {});
249
+ }
250
+
251
+ this.session.restoreFromSnapshot(archived);
252
+ await this.session.saveToDisk().catch(() => {});
253
+
254
+ this.initHistory();
255
+ for (const msg of this.session.messages) {
256
+ if (msg.role === 'user') {
257
+ const textPart = msg.parts?.find(p => p.type === 'text');
258
+ if (textPart?.content) {
259
+ this.conversationHistory.push({
260
+ role: 'user',
261
+ content: textPart.content
262
+ });
263
+ }
264
+ } else if (msg.role === 'assistant') {
265
+ const textPart = msg.parts?.find(p => p.type === 'text');
266
+ if (textPart?.content) {
267
+ this.conversationHistory.push({
268
+ role: 'assistant',
269
+ content: textPart.content
270
+ });
271
+ }
272
+ }
273
+ }
274
+ this.restoredMessageCount = this.session.messages.length;
275
+ eventBus.emit(Events.SESSION_SAVED, { sessionId: this.session.id });
276
+ return archived;
277
+ }
278
+
226
279
  getEnrichedSystemPrompt() {
227
280
  const memoryContext = this.memory.buildMemoryContext();
228
281
  return this.systemPrompt + memoryContext;
@@ -1,7 +1,7 @@
1
1
  export const COMMAND_DEFINITIONS = Object.freeze([
2
2
  { name: '/help', usage: '/help [command]', desc: 'Show all commands or details for one command', insert: '/help ' },
3
3
  { name: '/history', usage: '/history [n|search|summary|clear]', desc: 'View, search, or summarize chat history', insert: '/history ' },
4
- { name: '/session', usage: '/session [info|summary|list|export|clear]', desc: 'Manage or summarize the current session', insert: '/session ' },
4
+ { name: '/session', usage: '/session [info|summary|list|load <id>|export|clear]', desc: 'Manage, restore, or summarize conversation sessions', insert: '/session ' },
5
5
  { name: '/thinking', usage: '/thinking [show|hide|toggle]', desc: 'Control reasoning visibility', insert: '/thinking ' },
6
6
  { name: '/details', usage: '/details [Qn] [overview|answer|thinking|tools|tool n]', desc: 'Inspect query diagnostics (F2 opens latest overview)', insert: '/details ' },
7
7
  { name: '/copy', usage: '/copy [Qn] [answer|thinking|tools|tool n|all|markdown]', desc: 'Copy full untruncated model answer, thinking, or diagnostics to clipboard', insert: '/copy ' },
@@ -87,7 +87,7 @@ export const SUB_COMMANDS = Object.freeze({
87
87
  '/thinking': ['show', 'hide', 'toggle'],
88
88
  '/details': ['latest', 'overview', 'answer', 'thinking', 'tools', 'tool <n>', 'all', 'markdown', 'Q1 overview', 'Q1 tool 1'],
89
89
  '/copy': ['answer', 'thinking', 'tools', 'tool 1', 'overview', 'all', 'markdown', 'Q1 answer', 'Q1 thinking', 'Q1 all'],
90
- '/session': ['info', 'summary', 'list', 'export', 'clear'],
90
+ '/session': ['info', 'summary', 'list', 'load <id>', 'export', 'clear'],
91
91
  '/history': ['10', '20', '50', 'search <query>', 'summary', 'clear'],
92
92
  '/queue': ['clear'],
93
93
  '/update': ['check', 'install', 'install next', 'install canary', 'channel', 'channel latest', 'channel next', 'channel canary', 'rollback', 'help'],
@@ -408,12 +408,13 @@ export const COMMAND_DETAILS = Object.freeze({
408
408
  ],
409
409
  },
410
410
  '/session': {
411
- summary: 'Manage current conversation session, storage, and snapshots',
412
- usage: '/session [info | list | export | clear]',
411
+ summary: 'Manage, restore, or summarize conversation sessions and history snapshots',
412
+ usage: '/session [info | summary | list | load <id> | export | clear]',
413
413
  subcommands: [
414
414
  { cmd: 'info', desc: 'Display session ID, active duration, message count, and turn stats', example: '/session info' },
415
415
  { cmd: 'summary', desc: 'Synthesize an AI analytical summary of the active session', example: '/session summary' },
416
416
  { cmd: 'list', desc: 'List all saved conversation sessions on disk', example: '/session list' },
417
+ { cmd: 'load <id>', desc: 'Restore and resume an archived session by ID or list number', example: '/session load 1' },
417
418
  { cmd: 'export', desc: 'Export full conversation session history to JSON', example: '/session export' },
418
419
  { cmd: 'clear', desc: 'Archive current conversation session and reset history', example: '/session clear' },
419
420
  ],
@@ -421,6 +422,7 @@ export const COMMAND_DETAILS = Object.freeze({
421
422
  '/session info',
422
423
  '/session summary',
423
424
  '/session list',
425
+ '/session load 1',
424
426
  '/session clear',
425
427
  ],
426
428
  },
@@ -771,10 +771,14 @@ export const handlers = {
771
771
  },
772
772
 
773
773
  '/session': async (ctx) => {
774
- const { agent, sub } = ctx;
774
+ const { agent, sub, extra } = ctx;
775
775
  const sess = agent.session;
776
776
  const action = (sub || 'info').toLowerCase();
777
777
 
778
+ if (/^\d+$/.test(action) || action.startsWith('sess_')) {
779
+ return handlers['/session']({ ...ctx, sub: 'load', extra: sub });
780
+ }
781
+
778
782
  if (action === 'info' || !action) {
779
783
  const msgCount = sess.messages.length;
780
784
  const userMsgs = sess.messages.filter(m => m.role === 'user').length;
@@ -839,10 +843,10 @@ export const handlers = {
839
843
  title: `Archived Sessions (Total: ${sessions.length})`,
840
844
  lines: sessions.length === 0
841
845
  ? [`${C.dim}No archived sessions found.${C.reset}`]
842
- : sessions.map(s => {
846
+ : sessions.map((s, i) => {
843
847
  const date = new Date(s.createdAt).toLocaleString();
844
848
  const dur = ((s.stats?.totalDurationMs || 0) / 1000).toFixed(1);
845
- return `${C.gold}${s.id}${C.reset} ${C.dim}(${date})${C.reset} — ${s.messageCount} msgs, ${dur}s`;
849
+ return `${C.gold}[${i + 1}] ${s.id}${C.reset} ${C.dim}(${date})${C.reset} — ${s.messageCount} msgs, ${dur}s`;
846
850
  }),
847
851
  headerBg: '\x1b[48;2;48;54;70m',
848
852
  bg: '\x1b[48;2;36;40;52m'
@@ -850,6 +854,60 @@ export const handlers = {
850
854
  return 'HANDLED';
851
855
  }
852
856
 
857
+ if (action === 'load' || action === 'resume' || action === 'restore') {
858
+ const target = (extra || '').trim();
859
+ if (!target) {
860
+ const sessions = agent.memory.getArchivedSessions(10);
861
+ if (sessions.length === 0) {
862
+ console.log(`\n ${C.amber}No archived sessions available to load.${C.reset}\n`);
863
+ return 'HANDLED';
864
+ }
865
+ printSolidPanel({
866
+ title: 'Load Session Usage',
867
+ lines: [
868
+ `${C.dim}Specify a session ID or list number to resume:${C.reset}`,
869
+ '',
870
+ ...sessions.map((s, i) => {
871
+ const date = new Date(s.createdAt).toLocaleString();
872
+ return ` ${C.gold}[${i + 1}]${C.reset} ${C.bold}${C.white}${s.id}${C.reset} ${C.dim}(${date} — ${s.messageCount} msgs)${C.reset}`;
873
+ }),
874
+ '',
875
+ `${C.dim}Example: /session load 1 OR /session load ${sessions[0].id}${C.reset}`,
876
+ ],
877
+ headerBg: '\x1b[48;2;48;54;70m',
878
+ bg: '\x1b[48;2;36;40;52m',
879
+ });
880
+ return 'HANDLED';
881
+ }
882
+
883
+ const restored = await agent.restoreSession(target);
884
+ if (!restored) {
885
+ console.log(`\n ${C.amber}Session "${target}" not found. Use /session list to view available archives.${C.reset}\n`);
886
+ return 'HANDLED';
887
+ }
888
+
889
+ const date = restored.createdAt ? new Date(restored.createdAt).toLocaleString() : 'unknown';
890
+ const msgCount = restored.messages?.length || 0;
891
+ const totalDur = (((restored.stats?.totalDurationMs) || 0) / 1000).toFixed(1);
892
+
893
+ printSolidPanel({
894
+ title: 'Session Restored Successfully',
895
+ lines: [
896
+ `${C.gold}Session ID:${C.reset} ${C.bold}${C.white}${restored.id}${C.reset}`,
897
+ `${C.gold}Created At:${C.reset} ${C.white}${date}${C.reset}`,
898
+ `${C.gold}Restored Messages:${C.reset} ${C.cyberCyan}${msgCount}${C.reset} exchanges loaded into memory`,
899
+ `${C.gold}Total Queries:${C.reset} ${C.cyberCyan}${restored.stats?.totalQueries || 0}${C.reset}`,
900
+ `${C.gold}Tool Invocations:${C.reset} ${C.cyberCyan}${restored.stats?.totalToolCalls || 0}${C.reset}`,
901
+ `${C.gold}Total Duration:${C.reset} ${C.cyberCyan}${totalDur}s${C.reset}`,
902
+ '',
903
+ `${C.emerald}✓ Session conversation context reloaded. You can now continue chatting!${C.reset}`,
904
+ ],
905
+ headerBg: '\x1b[48;2;48;54;70m',
906
+ bg: '\x1b[48;2;36;40;52m',
907
+ });
908
+ return 'HANDLED';
909
+ }
910
+
853
911
  if (action === 'export') {
854
912
  const exported = await agent.session.exportToMarkdown();
855
913
  console.log(`\n ${C.emerald}[OK] Session transcript exported.${C.reset}`);
@@ -863,7 +921,7 @@ export const handlers = {
863
921
  return 'HANDLED';
864
922
  }
865
923
 
866
- console.log(`\n ${C.amber}Unknown session action: "${sub}". Use /session [info|list|export|clear]${C.reset}\n`);
924
+ console.log(`\n ${C.amber}Unknown session action: "${sub}". Use /session [info|summary|list|load|export|clear]${C.reset}\n`);
867
925
  return 'HANDLED';
868
926
  },
869
927
 
@@ -1168,7 +1226,16 @@ export const handlers = {
1168
1226
 
1169
1227
  '/stop': async () => { await audioPlayer.stop(); console.log(`\n ${C.emerald}[OK] Stopped${C.reset}\n`); return 'HANDLED'; },
1170
1228
  '/pause': async () => { const r = await audioPlayer.pause(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
1171
- '/resume': async () => { const r = await audioPlayer.resume(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
1229
+ '/resume': async (ctx) => {
1230
+ const { sub, extra } = ctx || {};
1231
+ if (sub) {
1232
+ const target = extra ? `${sub} ${extra}`.trim() : sub;
1233
+ return handlers['/session']({ ...ctx, sub: 'load', extra: target });
1234
+ }
1235
+ const r = await audioPlayer.resume();
1236
+ console.log(`\n ${C.emerald}${r}${C.reset}\n`);
1237
+ return 'HANDLED';
1238
+ },
1172
1239
  '/next': async () => { const r = await audioPlayer.next(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
1173
1240
  '/prev': async () => { const r = await audioPlayer.prev(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
1174
1241
  '/mute': async () => { const r = await audioPlayer.mute(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
@@ -2580,6 +2647,7 @@ const ALIASES = {
2580
2647
  '/?': '/help', '/commands': '/help', '/menu': '/help',
2581
2648
  '/hist': '/history', '/h': '/history',
2582
2649
  '/sess': '/session', '/sessions': '/session',
2650
+ '/load-session': '/session', '/load': '/session', '/restore': '/session',
2583
2651
  '/think': '/thinking', '/th': '/thinking',
2584
2652
  '/detail': '/details', '/d': '/details',
2585
2653
  '/language': '/lang',
@@ -160,6 +160,20 @@ export class AgentSession {
160
160
  };
161
161
  }
162
162
 
163
+ restoreFromSnapshot(sessionData) {
164
+ if (!sessionData) return false;
165
+ this.id = sessionData.id || `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
166
+ this.createdAt = sessionData.createdAt ? new Date(sessionData.createdAt) : new Date();
167
+ this.thinkingMode = sessionData.thinkingMode || 'hide';
168
+ this.messages = Array.isArray(sessionData.messages) ? sessionData.messages : [];
169
+ this.stats = sessionData.stats || {
170
+ totalQueries: 0,
171
+ totalToolCalls: 0,
172
+ totalDurationMs: 0
173
+ };
174
+ return true;
175
+ }
176
+
163
177
  reset() {
164
178
  this.id = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
165
179
  this.createdAt = new Date();
@@ -130,9 +130,13 @@ function getGraphemeDisplayWidth(segment) {
130
130
  if (
131
131
  (code >= 0x1100 && code <= 0x11FF) ||
132
132
  (code >= 0x2E80 && code <= 0x9FFF) ||
133
+ (code >= 0x3000 && code <= 0x303F) ||
133
134
  (code >= 0xAC00 && code <= 0xD7A3) ||
134
135
  (code >= 0xF900 && code <= 0xFAFF) ||
135
- (code >= 0xFE30 && code <= 0xFE4F)
136
+ (code >= 0xFE30 && code <= 0xFE4F) ||
137
+ (code >= 0xFF01 && code <= 0xFF60) ||
138
+ (code >= 0xFFE0 && code <= 0xFFE6) ||
139
+ (code >= 0x20000 && code <= 0x3FFFF)
136
140
  ) {
137
141
  width += 2;
138
142
  } else {
@@ -297,7 +301,7 @@ export function wrapText(text, maxWidth = 72) {
297
301
 
298
302
 
299
303
  export function stripAnsi(str) {
300
- return str.replace(/\x1b\[[0-9;]*m/g, '');
304
+ return String(str ?? '').replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g, '');
301
305
  }
302
306
 
303
307
  export function sanitizeTerminalText(value, maxLength = 2000) {
@@ -751,8 +755,8 @@ export function formatTerminalMarkdown(text) {
751
755
  if (!text) return '';
752
756
  return text
753
757
  .replace(/^###\s*(.+)$/gm, `${C.bold}${C.gold}✦ $1${C.reset}`)
754
- .replace(/^##\s*(.+)$/gm, `\n${C.bold}${C.roseGlow}■ $1${C.reset}`)
755
- .replace(/^#\s*(.+)$/gm, `\n${C.bold}${C.sakura}━━━ $1 ━━━${C.reset}`)
758
+ .replace(/^##\s*(.+)$/gm, `${C.bold}${C.roseGlow}■ $1${C.reset}`)
759
+ .replace(/^#\s*(.+)$/gm, `${C.bold}${C.sakura}━━━ $1 ━━━${C.reset}`)
756
760
  .replace(/^>\s*(.+)$/gm, ` ${C.slate}│${C.reset} ${C.italic}${C.white}$1${C.reset}`)
757
761
  .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, `${C.bold}${C.cyberCyan}$1${C.reset} ➔ ${C.underline}${C.roseGlow}$2${C.reset}`)
758
762
  .replace(/\*{3}([^*]+)\*{3}/g, `${C.bold}${C.italic}${C.white}$1${C.reset}`)
@@ -854,7 +858,7 @@ export function formatMarkdownLines(text, maxWidth = getFullWidth() - 8) {
854
858
  return result;
855
859
  }
856
860
 
857
- export function getWelcomeHeroLines() {
861
+ export function getWelcomeHeroLines() {
858
862
  const p = C.sakura;
859
863
  const r = C.roseGlow;
860
864
  const g = C.gold;
@@ -866,8 +870,8 @@ export function getWelcomeHeroLines() {
866
870
  const b = C.bold;
867
871
  const d = C.dim;
868
872
  const rst = C.reset;
869
- const boxWidth = Math.min(84, Math.max(50, getFullWidth() - 4));
870
- const compact = getFullWidth() < 120;
873
+ const boxWidth = Math.min(84, Math.max(50, getFullWidth() - 4));
874
+ const compact = getFullWidth() < 120;
871
875
  const headerTitle = 'Suggested Topics & Queries';
872
876
  const topDashCount = Math.max(2, boxWidth - getStringDisplayWidth(headerTitle) - 6);
873
877
  const innerWidth = boxWidth - 2;
@@ -880,28 +884,28 @@ export function getWelcomeHeroLines() {
880
884
  return ` ${s}│${rst} ${icon} ${tag}${rst}${' '.repeat(Math.max(0, tagWidth - getStringDisplayWidth(tag)))} ${w}${text}${rst}${' '.repeat(pad)}${s}│${rst}`;
881
885
  };
882
886
 
883
- const heroLines = compact ? [
884
- ` ${b}${p}◆ Ju Jingyi • 鞠婧祎 • Mìjú Terminal Portal ◆${rst}`,
885
- ` ${p}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${rst}`,
886
- ` ${r}█████═╝ ██║█████═╝ ██║ ██║ ███████║██║${rst}`,
887
- ] : [
888
- ` ${p}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${rst} ${b}${p}◆ Ju Jingyi • 鞠婧祎 • Mìjú Terminal Portal ◆${rst}`,
889
- ` ${p}██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║${rst} ${d}${s}Interactive Fan Knowledge Base & Autonomous Agent${rst}`,
890
- ` ${r}█████═╝ ██║█████═╝ ██║ ██║ ███████║██║${rst} ${d}${s}Filmography • Discography • Fashion & Awards${rst}`,
891
- ` ${r}██╔═██╗ ██║██╔═██╗ ██║ ██║ ██╔══██╗██║${rst} ${c}● Live Ready${rst} ${s}• Multilingual (CN / EN / TH)${rst}`,
892
- ` ${p}██║ ╚██╗██║██║ ╚██╗╚██████╔╝ ██║ ██║██║${rst}`,
893
- ` ${p}╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝${rst}`,
894
- ];
895
- const shortcutLines = compact ? [
896
- ` ${s}Shortcuts: ${b}${w}[Tab]${rst} ${s}Commands • ${b}${w}[↑/↓]${rst} ${s}History • ${b}${w}[F2]${rst} ${s}Details${rst}`,
897
- ` ${b}${w}[PgUp/PgDn]${rst} ${s}Scroll • ${b}${w}[Ctrl+C]${rst} ${s}Exit${rst}`,
898
- ] : [
899
- ` ${s}Shortcuts: ${b}${w}[Tab]${rst} ${s}Autocomplete • ${b}${w}[↑/↓]${rst} ${s}History • ${b}${w}[PgUp/PgDn]${rst} ${s}Scroll • ${b}${w}[F2]${rst} ${s}Details • ${b}${w}[Ctrl+C]${rst} ${s}Exit${rst}`,
900
- ];
901
-
902
- return [
903
- ...(compact ? [] : ['']),
904
- ...heroLines,
887
+ const heroLines = compact ? [
888
+ ` ${b}${p}◆ Ju Jingyi • 鞠婧祎 • Mìjú Terminal Portal ◆${rst}`,
889
+ ` ${p}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${rst}`,
890
+ ` ${r}█████═╝ ██║█████═╝ ██║ ██║ ███████║██║${rst}`,
891
+ ] : [
892
+ ` ${p}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${rst} ${b}${p}◆ Ju Jingyi • 鞠婧祎 • Mìjú Terminal Portal ◆${rst}`,
893
+ ` ${p}██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║${rst} ${d}${s}Interactive Fan Knowledge Base & Autonomous Agent${rst}`,
894
+ ` ${r}█████═╝ ██║█████═╝ ██║ ██║ ███████║██║${rst} ${d}${s}Filmography • Discography • Fashion & Awards${rst}`,
895
+ ` ${r}██╔═██╗ ██║██╔═██╗ ██║ ██║ ██╔══██╗██║${rst} ${c}● Live Ready${rst} ${s}• Multilingual (CN / EN / TH)${rst}`,
896
+ ` ${p}██║ ╚██╗██║██║ ╚██╗╚██████╔╝ ██║ ██║██║${rst}`,
897
+ ` ${p}╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝${rst}`,
898
+ ];
899
+ const shortcutLines = compact ? [
900
+ ` ${s}Shortcuts: ${b}${w}[Tab]${rst} ${s}Commands • ${b}${w}[↑/↓]${rst} ${s}History • ${b}${w}[F2]${rst} ${s}Details${rst}`,
901
+ ` ${b}${w}[PgUp/PgDn]${rst} ${s}Scroll • ${b}${w}[Ctrl+C]${rst} ${s}Exit${rst}`,
902
+ ] : [
903
+ ` ${s}Shortcuts: ${b}${w}[Tab]${rst} ${s}Autocomplete • ${b}${w}[↑/↓]${rst} ${s}History • ${b}${w}[PgUp/PgDn]${rst} ${s}Scroll • ${b}${w}[F2]${rst} ${s}Details • ${b}${w}[Ctrl+C]${rst} ${s}Exit${rst}`,
904
+ ];
905
+
906
+ return [
907
+ ...(compact ? [] : ['']),
908
+ ...heroLines,
905
909
  '',
906
910
  ` ${s}╭──${rst} ${b}${g}${headerTitle}${rst} ${s}${'─'.repeat(topDashCount)}╮${rst}`,
907
911
  padItem(p + '◆', '2026 Works', '"What are Ju Jingyi\'s 2026 upcoming dramas?"'),
@@ -910,7 +914,7 @@ export function getWelcomeHeroLines() {
910
914
  padItem(l + '◆', 'Tools / MCP', '"@search_works 2026, /mcp list, /stats, /help"'),
911
915
  ` ${s}╰${'─'.repeat(boxWidth)}╯${rst}`,
912
916
  '',
913
- ...shortcutLines,
917
+ ...shortcutLines,
914
918
  '',
915
919
  ];
916
920
  }
@@ -1080,7 +1084,7 @@ export function exitAlternateScreen() {
1080
1084
  }
1081
1085
  }
1082
1086
 
1083
- export function getTuiViewportLayout(termRows = 24) {
1087
+ export function getTuiViewportLayout(termRows = 24) {
1084
1088
  const parsedRows = Number(termRows);
1085
1089
  const rows = Math.max(10, Number.isFinite(parsedRows) ? Math.floor(parsedRows) : 24);
1086
1090
  const viewportStartRow = 1;
@@ -1095,16 +1099,16 @@ export function getTuiViewportLayout(termRows = 24) {
1095
1099
  statusBarRow,
1096
1100
  promptRow,
1097
1101
  separatorRow,
1098
- };
1099
- }
1100
-
1101
- export function getNewOutputScrollOffset(addedLineCount, termRows = 24, currentOffset = 0) {
1102
- const added = Math.max(0, Number.isFinite(Number(addedLineCount)) ? Math.floor(Number(addedLineCount)) : 0);
1103
- if (currentOffset > 0) return currentOffset + added;
1104
- return Math.max(0, added - getTuiViewportLayout(termRows).viewportHeight);
1105
- }
1106
-
1107
- export class VirtualScreenBuffer {
1102
+ };
1103
+ }
1104
+
1105
+ export function getNewOutputScrollOffset(addedLineCount, termRows = 24, currentOffset = 0) {
1106
+ const added = Math.max(0, Number.isFinite(Number(addedLineCount)) ? Math.floor(Number(addedLineCount)) : 0);
1107
+ if (currentOffset > 0) return currentOffset + added;
1108
+ return Math.max(0, added - getTuiViewportLayout(termRows).viewportHeight);
1109
+ }
1110
+
1111
+ export class VirtualScreenBuffer {
1108
1112
  constructor(width = 80, height = 24, writer = null) {
1109
1113
  this.width = Math.max(10, width);
1110
1114
  this.height = Math.max(5, height);