kiku-agent 1.0.6 → 1.0.8

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/package.json CHANGED
@@ -1,93 +1,96 @@
1
- {
2
- "name": "kiku-agent",
3
- "version": "1.0.6",
4
- "description": "Kiku AI terminal agent and MCP server for Ju Jingyi knowledge, media, and career data.",
5
- "type": "module",
6
- "bin": {
7
- "kiku-agent": "server/agent/cli.js",
8
- "kiku-mcp": "server/mcp/stdio.js"
9
- },
10
- "exports": {
11
- ".": "./server/agent/agentCore.js",
12
- "./mcp": "./server/mcp/server.js",
13
- "./tools": "./server/agent/tools.js"
14
- },
15
- "files": [
16
- "server/agent/*.js",
17
- "server/mcp/*.js",
18
- "server/mcp/data/*.js",
19
- "server/mcp/**/*.json",
20
- "src/data/**/*.js",
21
- "README.md"
22
- ],
23
- "engines": {
24
- "node": ">=20.0.0"
25
- },
26
- "kikuUpdate": {
27
- "channel": "stable",
28
- "releaseSummary": [
29
- "AI research suggestion engine and batch subagent execution",
30
- "Enhanced web search fallbacks and missing catalog tool hints",
31
- "Updated brand endorsements (Versace) and dual catalog sync",
32
- "Secure npm-backed updates and terminal markdown renderer"
33
- ],
34
- "releaseUrl": "https://www.npmjs.com/package/kiku-agent"
35
- },
36
- "keywords": [
37
- "kiku",
38
- "ju-jingyi",
39
- "cli",
40
- "ai-agent",
41
- "mcp",
42
- "model-context-protocol",
43
- "lm-studio"
44
- ],
45
- "repository": {
46
- "type": "git",
47
- "url": "git+https://github.com/ThemeHackers/jujingyi_kikuuu.git"
48
- },
49
- "homepage": "https://jujingyi-kikuuu.vercel.app/",
50
- "bugs": {
51
- "url": "https://github.com/ThemeHackers/jujingyi_kikuuu/issues"
52
- },
53
- "license": "UNLICENSED",
54
- "publishConfig": {
55
- "access": "public"
56
- },
57
- "scripts": {
58
- "dev": "vite",
59
- "build": "vite build",
60
- "preview": "vite preview",
61
- "kiku-agent": "node server/agent/cli.js",
62
- "kiku-mcp": "node server/mcp/stdio.js",
63
- "kiku-ink": "npx tsx server/agent/ink/main.jsx",
64
- "test": "node --test server/agent/tests/*.test.js server/mcp/tests/*.test.js"
65
- },
66
- "dependencies": {
67
- "@modelcontextprotocol/sdk": "^1.30.0",
68
- "clsx": "^2.1.1",
69
- "framer-motion": "^12.4.7",
70
- "lucide-react": "^1.16.0",
71
- "react": "^19.0.0",
72
- "react-dom": "^19.0.0",
73
- "tailwind-merge": "^3.0.2",
74
- "zod": "^4.4.3"
75
- },
76
- "devDependencies": {
77
- "@tailwindcss/vite": "^4.0.9",
78
- "@types/react": "^19.0.10",
79
- "@types/react-dom": "^19.0.4",
80
- "@vitejs/plugin-react": "^4.3.4",
81
- "ink": "^7.1.1",
82
- "ink-select-input": "^6.2.0",
83
- "ink-spinner": "^5.0.0",
84
- "ink-text-input": "^6.0.0",
85
- "tailwindcss": "^4.0.9",
86
- "tsx": "^4.23.12",
87
- "vite": "^6.2.0"
88
- },
89
- "allowScripts": {
90
- "esbuild@0.25.12": true,
91
- "esbuild@0.28.2": true
92
- }
93
- }
1
+ {
2
+ "name": "kiku-agent",
3
+ "version": "1.0.8",
4
+ "description": "Kiku AI terminal agent and MCP server for Ju Jingyi knowledge, media, and career data.",
5
+ "license": "GPL-3.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "kiku-agent": "server/agent/cli.js",
9
+ "kiku-mcp": "server/mcp/stdio.js"
10
+ },
11
+ "exports": {
12
+ ".": "./server/agent/agentCore.js",
13
+ "./mcp": "./server/mcp/server.js",
14
+ "./tools": "./server/agent/tools.js"
15
+ },
16
+ "files": [
17
+ "server/agent/*.js",
18
+ "server/mcp/*.js",
19
+ "server/mcp/data/*.js",
20
+ "server/mcp/**/*.json",
21
+ "src/data/**/*.js",
22
+ "src/assets/images/**/*",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20.0.0"
28
+ },
29
+ "kikuUpdate": {
30
+ "channel": "stable",
31
+ "releaseSummary": [
32
+ "Updated production model defaults and live fallback resolution across cloud providers",
33
+ "Hardened REST API key validation and isolated rate-limit probes",
34
+ "Fixed discography filtering, MCP configuration isolation, and environment loading order",
35
+ "Secured YouTube iframe messaging and expanded provider and API regression coverage"
36
+ ],
37
+ "releaseUrl": "https://www.npmjs.com/package/kiku-agent"
38
+ },
39
+ "keywords": [
40
+ "kiku",
41
+ "ju-jingyi",
42
+ "cli",
43
+ "ai-agent",
44
+ "mcp",
45
+ "model-context-protocol",
46
+ "lm-studio"
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/ThemeHackers/jujingyi_kikuuu.git"
51
+ },
52
+ "homepage": "https://jujingyi-kikuuu.vercel.app/",
53
+ "bugs": {
54
+ "url": "https://github.com/ThemeHackers/jujingyi_kikuuu/issues"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "scripts": {
60
+ "dev": "vite",
61
+ "build": "vite build",
62
+ "preview": "vite preview",
63
+ "kiku-agent": "node server/agent/cli.js",
64
+ "kiku-mcp": "node server/mcp/stdio.js",
65
+ "kiku-ink": "npx tsx server/agent/ink/main.jsx",
66
+ "test": "node --test server/agent/tests/*.test.js server/mcp/tests/*.test.js"
67
+ },
68
+ "dependencies": {
69
+ "@modelcontextprotocol/sdk": "^1.30.0",
70
+ "clsx": "^2.1.1",
71
+ "framer-motion": "^12.4.7",
72
+ "jpeg-js": "^0.4.4",
73
+ "lucide-react": "^1.16.0",
74
+ "react": "^19.0.0",
75
+ "react-dom": "^19.0.0",
76
+ "tailwind-merge": "^3.0.2",
77
+ "zod": "^4.4.3"
78
+ },
79
+ "devDependencies": {
80
+ "@tailwindcss/vite": "^4.0.9",
81
+ "@types/react": "^19.0.10",
82
+ "@types/react-dom": "^19.0.4",
83
+ "@vitejs/plugin-react": "^4.3.4",
84
+ "ink": "^7.1.1",
85
+ "ink-select-input": "^6.2.0",
86
+ "ink-spinner": "^5.0.0",
87
+ "ink-text-input": "^6.0.0",
88
+ "tailwindcss": "^4.0.9",
89
+ "tsx": "^4.23.12",
90
+ "vite": "^6.2.0"
91
+ },
92
+ "allowScripts": {
93
+ "esbuild@0.25.12": true,
94
+ "esbuild@0.28.2": true
95
+ }
96
+ }
@@ -12,7 +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';
15
+ import { contextCompactor } from './contextCompactor.js';
16
+ import { AgentJobManager } from './jobManager.js';
16
17
 
17
18
  export function detectDominantLanguage(text) {
18
19
  if (!text || typeof text !== 'string') return 'th';
@@ -63,7 +64,8 @@ export class KikuAgent {
63
64
  this.mcpManager = options.mcpManager || new McpClientManager();
64
65
  this.router = new DynamicToolRouter(this.getAllToolsSchema());
65
66
  this.research = new ResearchOrchestrator({ client: this.client });
66
- this.suggestions = new SuggestionEngine({ client: this.client, memory: this.memory });
67
+ this.suggestions = new SuggestionEngine({ client: this.client, memory: this.memory });
68
+ this.jobs = new AgentJobManager();
67
69
  this.systemPrompt = this.buildSystemPrompt();
68
70
  this.initHistory();
69
71
  this.restoredMessageCount = 0;
@@ -101,33 +103,39 @@ export class KikuAgent {
101
103
  return this._ready;
102
104
  }
103
105
 
104
- async runSubagent(task, options = {}) {
105
- await this._ready;
106
- const subagent = new ResearchSubagent({
107
- client: this.client,
108
- maxIterations: options.maxIterations || 3,
109
- });
110
- return subagent.run(task, {
111
- signal: options.signal,
112
- onProgress: options.onProgress,
113
- skillContext: this.memory.buildUserSkillContext(),
114
- });
115
- }
116
-
117
- async runResearch(question, options = {}) {
118
- await this._ready;
119
- return this.research.run(question, {
120
- ...options,
121
- skillContext: this.memory.buildUserSkillContext(),
122
- });
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);
123
129
  }
124
130
 
125
131
  getResearchStatus() {
126
132
  return this.research.getStatus();
127
133
  }
128
134
 
129
- cancelResearch() {
130
- return this.research.cancel();
135
+ cancelResearch() {
136
+ const direct = this.research.cancel();
137
+ const tracked = this.jobs.cancelType('research') || this.jobs.cancelType('verify');
138
+ return direct || tracked;
131
139
  }
132
140
 
133
141
  async generateSuggestions(options = {}) {
@@ -135,14 +143,33 @@ export class KikuAgent {
135
143
  return this.suggestions.generateSuggestions(options);
136
144
  }
137
145
 
138
- async runSuggestionBatch(targets, options = {}) {
139
- await this._ready;
140
- return this.suggestions.runBatchSubagents(targets, options);
141
- }
142
-
143
- getLastSuggestions() {
144
- return this.suggestions.getLastSuggestions();
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
173
 
147
174
  initHistory() {
148
175
  this.conversationHistory = [
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import readline from 'node:readline';
4
- import { stdin as input } from 'node:process';
5
- import { existsSync } from 'node:fs';
6
- import { spawn } from 'node:child_process';
7
- import { KikuAgent } from './agentCore.js';
3
+ import readline from 'node:readline';
4
+ import { stdin as input } from 'node:process';
5
+ import { existsSync } from 'node:fs';
6
+ import { spawn } from 'node:child_process';
7
+ import './youtubeAuth.js';
8
+ import { KikuAgent } from './agentCore.js';
8
9
  import { LLMClient, LMStudioClient } from './llmClient.js';
9
10
  import { toolsSchema, toolHandlers, executeTool } from './tools.js';
10
11
  import { config, CLOUD_PROVIDERS } from './config.js';
@@ -16,8 +17,9 @@ import { sanitizeQuery } from './sanitizer.js';
16
17
  import { isAbortError } from './errors.js';
17
18
  import { COMMAND_DEFINITIONS, SUB_COMMANDS, findCommandDefinition, getCommandDetails } from './commandRegistry.js';
18
19
  import { ModelManager } from './modelManager.js';
19
- import { PACKAGE_VERSION } from './version.js';
20
- import { getTheme, getThemeName } from './theme.js';
20
+ import { PACKAGE_VERSION } from './version.js';
21
+ import { getTheme, getThemeName } from './theme.js';
22
+ import { createTerminalImageGallery } from './terminalImage.js';
21
23
  import {
22
24
  classifyToolResult,
23
25
  formatDetailDuration,
@@ -37,7 +39,7 @@ import {
37
39
  printWelcomeHero, getWelcomeHeroLines, randomFanName, setupOutputInterception,
38
40
  rawWrite, INPUT_PLACEHOLDER, UI_ICONS,
39
41
  spinnerFrames, thinkingFrames, createProgressRunner,
40
- enterAlternateScreen, exitAlternateScreen, getTuiViewportLayout, VirtualScreenBuffer,
42
+ enterAlternateScreen, exitAlternateScreen, getTuiViewportLayout, getNewOutputScrollOffset, VirtualScreenBuffer,
41
43
  } from './ui.js';
42
44
 
43
45
  let activeAbortController = null;
@@ -489,7 +491,11 @@ function printAd() {
489
491
 
490
492
  class InteractiveTuiPrompt {
491
493
  constructor(options = {}) {
492
- this.promptText = options.prompt || 'Kiku ❯ ';
494
+ this.defaultPromptText = options.prompt || 'Kiku ❯ ';
495
+ this.promptText = this.defaultPromptText;
496
+ this.inputPlaceholder = INPUT_PLACEHOLDER;
497
+ this.recordInputHistory = true;
498
+ this.cancelValue = null;
493
499
  this.agent = options.agent || null;
494
500
  this.modelName = options.modelName || 'gemma-4-e4b';
495
501
  this.provider = options.provider || 'lmstudio';
@@ -520,27 +526,28 @@ class InteractiveTuiPrompt {
520
526
  );
521
527
  }
522
528
 
523
- addViewportLines(lines) {
524
- if (lines === null || lines === undefined) return;
525
- if (!Array.isArray(lines)) lines = [lines];
526
- const wasLive = this.scrollOffset === 0;
527
- for (const l of lines) {
528
- if (typeof l === 'string') {
529
- const sublines = l.split('\n');
530
- for (const sl of sublines) {
531
- this.viewportLines.push(sl);
532
- }
533
- } else if (l != null) {
534
- this.viewportLines.push(String(l));
535
- }
529
+ addViewportLines(lines) {
530
+ if (lines === null || lines === undefined) return;
531
+ if (!Array.isArray(lines)) lines = [lines];
532
+ const wasLive = this.scrollOffset === 0;
533
+ let addedLineCount = 0;
534
+ for (const l of lines) {
535
+ if (typeof l === 'string') {
536
+ const sublines = l.split('\n');
537
+ for (const sl of sublines) {
538
+ this.viewportLines.push(sl);
539
+ addedLineCount += 1;
540
+ }
541
+ } else if (l != null) {
542
+ this.viewportLines.push(String(l));
543
+ addedLineCount += 1;
544
+ }
536
545
  }
537
546
  if (this.viewportLines.length > 5000) {
538
547
  const removedCount = this.viewportLines.length - 5000;
539
548
  this.viewportLines.splice(0, removedCount);
540
549
  }
541
- if (wasLive) {
542
- this.scrollOffset = 0;
543
- }
550
+ this.scrollOffset = getNewOutputScrollOffset(addedLineCount, process.stdout.rows || 24, wasLive ? 0 : this.scrollOffset);
544
551
  this.render();
545
552
  }
546
553
 
@@ -577,10 +584,15 @@ class InteractiveTuiPrompt {
577
584
  try { if (input.isTTY) input.setRawMode(false); } catch { }
578
585
  }
579
586
 
580
- async readLine() {
581
- return new Promise((resolve, reject) => {
582
- this._cleanupInput();
583
- this.line = '';
587
+ async readLine(options = {}) {
588
+ return new Promise((resolve, reject) => {
589
+ this._cleanupInput();
590
+ const inputOptions = typeof options === 'string' ? { prompt: options } : options;
591
+ this.promptText = inputOptions.prompt || this.defaultPromptText;
592
+ this.inputPlaceholder = inputOptions.placeholder || INPUT_PLACEHOLDER;
593
+ this.recordInputHistory = inputOptions.history !== false;
594
+ this.cancelValue = inputOptions.cancelValue ?? null;
595
+ this.line = '';
584
596
  this.cursor = 0;
585
597
  this.historyIndex = -1;
586
598
  this.popoverIndex = 0;
@@ -614,7 +626,7 @@ class InteractiveTuiPrompt {
614
626
  this.cursor = 0;
615
627
  this.popoverActive = false;
616
628
  this.popoverItems = [];
617
- if (result && result.trim()) this.history.push(result);
629
+ if (this.recordInputHistory && result && result.trim()) this.history.push(result);
618
630
  const resolve = this.activeResolve;
619
631
  this.activeResolve = null;
620
632
  this.activeReject = null;
@@ -783,7 +795,7 @@ class InteractiveTuiPrompt {
783
795
  trailingStyled = `${C.dim}${C.slate}${ghostDisplay}${C.reset}`;
784
796
  }
785
797
  } else if (!this.line) {
786
- const placeholder = sliceLineForDisplay(INPUT_PLACEHOLDER, 0, maxLineWidth).displayLine;
798
+ const placeholder = sliceLineForDisplay(this.inputPlaceholder, 0, maxLineWidth).displayLine;
787
799
  trailingStyled = `${C.dim}${C.slate}${placeholder}${C.reset}`;
788
800
  }
789
801
 
@@ -800,8 +812,12 @@ class InteractiveTuiPrompt {
800
812
  handleKeypress(str, key) {
801
813
  if (!key && !str) return;
802
814
 
803
- if ((key?.ctrl && key?.name === 'c') || str === '\x03') {
804
- if (isProcessing) {
815
+ if ((key?.ctrl && key?.name === 'c') || str === '\x03') {
816
+ if (this.cancelValue !== null) {
817
+ this.finish(this.cancelValue);
818
+ return;
819
+ }
820
+ if (isProcessing) {
805
821
  if (this.onCancel) this.onCancel();
806
822
  else if (this.onSigInt) this.onSigInt();
807
823
  this.addViewportLines(` ${C.amber}[✓ Cancelled by user]${C.reset}`);
@@ -826,8 +842,12 @@ class InteractiveTuiPrompt {
826
842
  return;
827
843
  }
828
844
 
829
- if (key?.name === 'escape' || str === '\x1b') {
830
- if (this.popoverActive) {
845
+ if (key?.name === 'escape' || str === '\x1b') {
846
+ if (this.cancelValue !== null) {
847
+ this.finish(this.cancelValue);
848
+ return;
849
+ }
850
+ if (this.popoverActive) {
831
851
  this.popoverActive = false;
832
852
  this.popoverItems = [];
833
853
  this.ghostText = '';
@@ -1540,15 +1560,23 @@ async function main() {
1540
1560
 
1541
1561
  if (opts.showStats) { printWelcomeHero(); const r = await executeTool('get_system_stats', {}); console.log(JSON.stringify(r, null, 2)); process.exit(0); }
1542
1562
 
1543
- if (opts.query) {
1544
- printWelcomeHero();
1545
- const client = new LLMClient({
1563
+ if (opts.query) {
1564
+ printWelcomeHero();
1565
+ const client = new LLMClient({
1546
1566
  provider: opts.provider || config.activeProvider,
1547
1567
  baseUrl: opts.url,
1548
1568
  apiKey: opts.apiKey,
1549
- model: opts.model
1550
- });
1551
- console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
1569
+ model: opts.model
1570
+ });
1571
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1572
+ const modelResolution = await modelManager.resolve({ requestedModel: opts.model || client.model });
1573
+ if (!modelResolution.connected || !modelResolution.selectedModel) {
1574
+ throw new Error(modelResolution.error || `No available chat model found for ${client.providerName}`);
1575
+ }
1576
+ if (modelResolution.usedFallback) {
1577
+ console.log(` ${C.amber}Model ${modelResolution.requestedModel} is unavailable; using ${modelResolution.selectedModel}.${C.reset}\n`);
1578
+ }
1579
+ console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${C.white}${opts.query}${C.reset}\n`);
1552
1580
  const agent = new KikuAgent({ client, thinkingMode: opts.thinking });
1553
1581
  await executeQueryWithUI(agent, opts.query, agent.session.thinkingMode === 'show', null, 1);
1554
1582
  process.exit(0);
@@ -1592,7 +1620,7 @@ async function main() {
1592
1620
  return false;
1593
1621
  }
1594
1622
 
1595
- const tuiPrompt = new InteractiveTuiPrompt({
1623
+ const tuiPrompt = new InteractiveTuiPrompt({
1596
1624
  prompt: 'Kiku ❯ ',
1597
1625
  agent,
1598
1626
  modelName: opts.model || client.model || 'gemma-4-e4b',
@@ -1612,7 +1640,8 @@ async function main() {
1612
1640
  process.exit(0);
1613
1641
  }
1614
1642
  }
1615
- });
1643
+ });
1644
+ const terminalGallery = createTerminalImageGallery();
1616
1645
 
1617
1646
  globalTuiPrompt = tuiPrompt;
1618
1647
  setupOutputInterception(tuiPrompt, () => isProcessing);
@@ -1665,7 +1694,7 @@ async function main() {
1665
1694
  }
1666
1695
  }
1667
1696
 
1668
- const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1697
+ const modelManager = new ModelManager(client, { requestedModel: opts.model || client.model });
1669
1698
  const modelResolution = catalogOnlyMode
1670
1699
  ? { connected: false, state: 'catalog_only' }
1671
1700
  : await modelManager.resolve({ requestedModel: opts.model || client.model });
@@ -1685,7 +1714,7 @@ async function main() {
1685
1714
  title: 'Connected & Ready',
1686
1715
  status: 'connected',
1687
1716
  lines: [
1688
- `• Provider: ${client.providerName} (Local) • Model: ${modelResolution.selectedModel}`,
1717
+ `• Provider: ${client.providerName}${client.provider === 'lmstudio' ? ' (Local)' : ''} • Model: ${modelResolution.selectedModel}`,
1689
1718
  `• Status: Chat & Tool DAG Active • Tools: ${toolsSchema.length} Loaded`,
1690
1719
  ],
1691
1720
  });
@@ -1734,9 +1763,17 @@ async function main() {
1734
1763
 
1735
1764
  const mcpStatus = agent.mcpManager ? agent.mcpManager.getStatus() : null;
1736
1765
  if (mcpStatus && mcpStatus.activeServersCount > 0 && !opts.mcp) {
1737
- const serverSummaries = mcpStatus.servers
1738
- .filter(s => s.status === 'connected')
1739
- .map(s => `• ${s.name} (${s.type}) — ${s.toolsCount} tools`);
1766
+ const serverSummaries = mcpStatus.servers
1767
+ .filter(s => s.status === 'connected')
1768
+ .map(s => {
1769
+ const sources = (s.sources || [])
1770
+ .map(source => String(source).split(/[\\/]/).pop())
1771
+ .filter(Boolean);
1772
+ const sourceLabel = sources.length > 0
1773
+ ? ` • source: ${[...new Set(sources)].join(', ')}`
1774
+ : '';
1775
+ return `• ${s.name} (${s.type}) — ${s.toolsCount} tools${sourceLabel}`;
1776
+ });
1740
1777
  printRichCard({
1741
1778
  title: 'Persisted MCP Connected',
1742
1779
  status: 'mcp',
@@ -1795,10 +1832,16 @@ async function main() {
1795
1832
  printHelp,
1796
1833
  printQueryDetails,
1797
1834
  copyQueryDetails,
1798
- printWelcome: printWelcomeHero,
1799
- printAd,
1800
- cancelActiveQuery,
1801
- enqueueQuery: async (prompt, isRetry) => {
1835
+ printWelcome: printWelcomeHero,
1836
+ printAd,
1837
+ showTerminalImage: () => {
1838
+ const imageLines = terminalGallery.maybeRender(queryCount, { force: true });
1839
+ if (imageLines.length > 0) tuiPrompt.addViewportLines(imageLines);
1840
+ return imageLines.length > 0;
1841
+ },
1842
+ cancelActiveQuery,
1843
+ readInput: options => tuiPrompt.readLine(options),
1844
+ enqueueQuery: async (prompt, isRetry) => {
1802
1845
  queryQueue.push({ id: 'q_' + Date.now(), number: nextQueryNumber++, prompt, addedAt: Date.now() });
1803
1846
  triggerQueueProcessing();
1804
1847
  },
@@ -1870,10 +1913,12 @@ async function main() {
1870
1913
  });
1871
1914
  }
1872
1915
  } finally {
1873
- activeAbortController = null;
1874
- queryCount++;
1875
- printAd();
1876
- activeQueryNumber = null;
1916
+ activeAbortController = null;
1917
+ queryCount++;
1918
+ printAd();
1919
+ const imageLines = terminalGallery.maybeRender(queryCount);
1920
+ if (imageLines.length > 0) tuiPrompt.addViewportLines(imageLines);
1921
+ activeQueryNumber = null;
1877
1922
  if (globalTuiPrompt) {
1878
1923
  globalTuiPrompt.agentState = '';
1879
1924
  globalTuiPrompt.render();