neuro-cli 4.1.0
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/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - MCP Apps: Interactive Tool UI Extensions
|
|
3
|
+
// GAP-32: Allow MCP tools to return interactive HTML/JS
|
|
4
|
+
// UI components, rendered as rich terminal output in CLI.
|
|
5
|
+
// ============================================================
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
// ---- State Store for Components ----
|
|
10
|
+
class ComponentStateStore {
|
|
11
|
+
states = new Map();
|
|
12
|
+
getState(componentId) {
|
|
13
|
+
if (!this.states.has(componentId)) {
|
|
14
|
+
this.states.set(componentId, new Map());
|
|
15
|
+
}
|
|
16
|
+
return this.states.get(componentId);
|
|
17
|
+
}
|
|
18
|
+
setState(componentId, key, value) {
|
|
19
|
+
const state = this.getState(componentId);
|
|
20
|
+
state.set(key, value);
|
|
21
|
+
}
|
|
22
|
+
clearState(componentId) {
|
|
23
|
+
this.states.delete(componentId);
|
|
24
|
+
}
|
|
25
|
+
clearAll() {
|
|
26
|
+
this.states.clear();
|
|
27
|
+
}
|
|
28
|
+
hasState(componentId) {
|
|
29
|
+
return this.states.has(componentId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ============================================================
|
|
33
|
+
// Terminal Renderers
|
|
34
|
+
// ============================================================
|
|
35
|
+
// ---- Form Renderer ----
|
|
36
|
+
async function renderForm(component, style) {
|
|
37
|
+
const fields = Array.isArray(component.data?.fields)
|
|
38
|
+
? component.data.fields
|
|
39
|
+
: [];
|
|
40
|
+
const lines = [];
|
|
41
|
+
if (component.title) {
|
|
42
|
+
lines.push(chalk.bold.cyan(` ${component.title}`));
|
|
43
|
+
lines.push(chalk.dim(' ' + '─'.repeat(Math.min(component.title.length + 2, 50))));
|
|
44
|
+
}
|
|
45
|
+
if (fields.length === 0) {
|
|
46
|
+
lines.push(chalk.dim(' (empty form)'));
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
for (let i = 0; i < fields.length; i++) {
|
|
50
|
+
const field = fields[i];
|
|
51
|
+
const num = chalk.cyan(` ${i + 1}.`);
|
|
52
|
+
const required = field.required ? chalk.red(' *') : '';
|
|
53
|
+
const label = chalk.white(`${field.label}${required}`);
|
|
54
|
+
lines.push(`${num} ${label}`);
|
|
55
|
+
if (field.type === 'select' && field.options) {
|
|
56
|
+
for (const opt of field.options) {
|
|
57
|
+
const marker = field.default === opt.value ? chalk.green(' ◉') : chalk.dim(' ○');
|
|
58
|
+
lines.push(` ${marker} ${chalk.gray(opt.label)}${field.default === opt.value ? chalk.green(' (default)') : ''}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (field.type === 'checkbox' && field.options) {
|
|
62
|
+
const defaults = Array.isArray(field.default) ? field.default : (field.default ? [field.default] : []);
|
|
63
|
+
for (const opt of field.options) {
|
|
64
|
+
const checked = defaults.includes(opt.value);
|
|
65
|
+
const marker = checked ? chalk.green(' ☑') : chalk.dim(' ☐');
|
|
66
|
+
lines.push(` ${marker} ${chalk.gray(opt.label)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (field.type === 'toggle') {
|
|
70
|
+
const val = field.default ?? false;
|
|
71
|
+
lines.push(` ${val ? chalk.green('● ON') : chalk.red('● OFF')}`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const displayDefault = field.default !== undefined
|
|
75
|
+
? chalk.gray(` [${String(field.default)}]`)
|
|
76
|
+
: '';
|
|
77
|
+
const placeholder = field.placeholder
|
|
78
|
+
? chalk.dim(` (${field.placeholder})`)
|
|
79
|
+
: '';
|
|
80
|
+
lines.push(` ${chalk.gray(field.type)}${displayDefault}${placeholder}`);
|
|
81
|
+
}
|
|
82
|
+
if (field.validation) {
|
|
83
|
+
lines.push(` ${chalk.dim(`validation: ${field.validation}`)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (component.actions && component.actions.length > 0) {
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push(chalk.dim(' Actions:'));
|
|
89
|
+
for (const action of component.actions) {
|
|
90
|
+
const icon = action.type === 'submit' ? '↵' : action.type === 'cancel' ? '✕' : '→';
|
|
91
|
+
lines.push(` ${chalk.yellow(icon)} ${chalk.white(action.label)} ${chalk.dim(`[${action.type}]`)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
// ---- Table Renderer ----
|
|
97
|
+
async function renderTable(component, style) {
|
|
98
|
+
const data = component.data || {};
|
|
99
|
+
const headers = data.headers || [];
|
|
100
|
+
const rows = data.rows || [];
|
|
101
|
+
const showRowNumbers = data.showRowNumbers ?? false;
|
|
102
|
+
const lines = [];
|
|
103
|
+
if (component.title) {
|
|
104
|
+
lines.push(chalk.bold.cyan(` ${component.title}`));
|
|
105
|
+
}
|
|
106
|
+
if (headers.length === 0 && rows.length === 0) {
|
|
107
|
+
lines.push(chalk.dim(' (empty table)'));
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
const colCount = headers.length || (rows.length > 0 ? Object.keys(rows[0]).length : 0);
|
|
111
|
+
if (colCount === 0) {
|
|
112
|
+
lines.push(chalk.dim(' (no columns)'));
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
const effectiveHeaders = headers.length > 0 ? headers : Object.keys(rows[0] || {});
|
|
116
|
+
const colWidths = data.colWidths || effectiveHeaders.map((h) => {
|
|
117
|
+
let max = h.length;
|
|
118
|
+
for (const row of rows) {
|
|
119
|
+
const val = String(row[h] ?? '');
|
|
120
|
+
max = Math.max(max, val.length);
|
|
121
|
+
}
|
|
122
|
+
return Math.min(max + 2, 40);
|
|
123
|
+
});
|
|
124
|
+
const colAlign = data.colAlign || effectiveHeaders.map(() => 'left');
|
|
125
|
+
const rowNumWidth = showRowNumbers ? String(rows.length).length + 2 : 0;
|
|
126
|
+
const padCell = (text, width, align) => {
|
|
127
|
+
const str = String(text);
|
|
128
|
+
if (str.length >= width)
|
|
129
|
+
return str.slice(0, width - 1) + '…';
|
|
130
|
+
const gap = width - str.length;
|
|
131
|
+
if (align === 'right')
|
|
132
|
+
return ' '.repeat(gap) + str;
|
|
133
|
+
if (align === 'center') {
|
|
134
|
+
const left = Math.floor(gap / 2);
|
|
135
|
+
return ' '.repeat(left) + str + ' '.repeat(gap - left);
|
|
136
|
+
}
|
|
137
|
+
return str + ' '.repeat(gap);
|
|
138
|
+
};
|
|
139
|
+
const horizontalLine = (left, mid, right, fill) => {
|
|
140
|
+
const parts = colWidths.map((w) => fill.repeat(w + 2));
|
|
141
|
+
const prefix = showRowNumbers ? left + fill.repeat(rowNumWidth + 2) + mid : '';
|
|
142
|
+
return chalk.dim(prefix + parts.join(mid) + right);
|
|
143
|
+
};
|
|
144
|
+
lines.push(horizontalLine('┌', '┬', '┐', '─'));
|
|
145
|
+
const headerCells = effectiveHeaders.map((h, i) => chalk.bold(padCell(h, colWidths[i], colAlign[i])));
|
|
146
|
+
const headerPrefix = showRowNumbers ? chalk.dim('│ ') + ' '.repeat(rowNumWidth) + chalk.dim(' │') : '';
|
|
147
|
+
lines.push(`${chalk.dim('│')} ${headerCells.join(chalk.dim(' │ '))} ${chalk.dim('│')}`.replace(/^│/, headerPrefix ? headerPrefix : chalk.dim('│')));
|
|
148
|
+
lines.push(horizontalLine('├', '┼', '┤', '─'));
|
|
149
|
+
for (let r = 0; r < rows.length; r++) {
|
|
150
|
+
const row = rows[r];
|
|
151
|
+
const cells = effectiveHeaders.map((h, i) => {
|
|
152
|
+
const val = row[h];
|
|
153
|
+
const str = val === null ? chalk.dim('null') : val === undefined ? chalk.dim('—') : String(val);
|
|
154
|
+
return padCell(str, colWidths[i], colAlign[i]);
|
|
155
|
+
});
|
|
156
|
+
const rowPrefix = showRowNumbers
|
|
157
|
+
? chalk.dim('│ ') + chalk.cyan(padCell(String(r + 1), rowNumWidth, 'right')) + chalk.dim(' │')
|
|
158
|
+
: '';
|
|
159
|
+
const rowLine = rowPrefix
|
|
160
|
+
? `${rowPrefix} ${cells.join(chalk.dim(' │ '))} ${chalk.dim('│')}`
|
|
161
|
+
: `${chalk.dim('│')} ${cells.join(chalk.dim(' │ '))} ${chalk.dim('│')}`;
|
|
162
|
+
lines.push(rowLine);
|
|
163
|
+
}
|
|
164
|
+
lines.push(horizontalLine('└', '┴', '┘', '─'));
|
|
165
|
+
if (rows.length > 0) {
|
|
166
|
+
lines.push(chalk.dim(` ${rows.length} row${rows.length !== 1 ? 's' : ''}`));
|
|
167
|
+
}
|
|
168
|
+
if (component.actions && component.actions.length > 0) {
|
|
169
|
+
lines.push('');
|
|
170
|
+
for (const action of component.actions) {
|
|
171
|
+
lines.push(` ${chalk.yellow('→')} ${chalk.white(action.label)} ${chalk.dim(`[${action.type}]`)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
// ---- Chart Renderer ----
|
|
177
|
+
async function renderChart(component, style) {
|
|
178
|
+
const data = component.data || {};
|
|
179
|
+
const chartType = data.chartType || 'bar';
|
|
180
|
+
const labels = data.labels || [];
|
|
181
|
+
const values = data.values || [];
|
|
182
|
+
const maxValue = data.maxValue ?? Math.max(...values, 1);
|
|
183
|
+
const unit = data.unit || '';
|
|
184
|
+
const showValues = data.showValues ?? true;
|
|
185
|
+
const lines = [];
|
|
186
|
+
if (component.title) {
|
|
187
|
+
lines.push(chalk.bold.cyan(` ${component.title}`));
|
|
188
|
+
}
|
|
189
|
+
if (labels.length === 0 || values.length === 0) {
|
|
190
|
+
lines.push(chalk.dim(' (no chart data)'));
|
|
191
|
+
return lines.join('\n');
|
|
192
|
+
}
|
|
193
|
+
const maxBarWidth = 30;
|
|
194
|
+
const maxLabelLen = Math.max(...labels.map((l) => l.length), 5);
|
|
195
|
+
if (chartType === 'bar' || chartType === 'horizontal-bar') {
|
|
196
|
+
for (let i = 0; i < labels.length; i++) {
|
|
197
|
+
const ratio = values[i] / maxValue;
|
|
198
|
+
const barWidth = Math.round(ratio * maxBarWidth);
|
|
199
|
+
const bar = '█'.repeat(Math.max(barWidth, 1));
|
|
200
|
+
const valueStr = showValues ? ` ${values[i]}${unit}` : '';
|
|
201
|
+
const colorFn = ratio > 0.75 ? chalk.green : ratio > 0.5 ? chalk.cyan : ratio > 0.25 ? chalk.yellow : chalk.red;
|
|
202
|
+
lines.push(` ${chalk.white(padRight(labels[i], maxLabelLen))} ${colorFn(bar)}${chalk.gray(valueStr)}`);
|
|
203
|
+
}
|
|
204
|
+
const scaleLine = chalk.dim(` ${' '.repeat(maxLabelLen)} 0${' '.repeat(maxBarWidth - 4)}${maxValue}${unit}`);
|
|
205
|
+
lines.push(scaleLine);
|
|
206
|
+
}
|
|
207
|
+
else if (chartType === 'sparkline') {
|
|
208
|
+
const sparkChars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
209
|
+
const normalizedValues = values.map((v) => {
|
|
210
|
+
const ratio = v / maxValue;
|
|
211
|
+
const idx = Math.min(Math.floor(ratio * (sparkChars.length - 1)), sparkChars.length - 1);
|
|
212
|
+
return Math.max(idx, 0);
|
|
213
|
+
});
|
|
214
|
+
const sparkline = normalizedValues.map((idx) => {
|
|
215
|
+
const colorFn = idx > 5 ? chalk.green : idx > 3 ? chalk.cyan : idx > 1 ? chalk.yellow : chalk.red;
|
|
216
|
+
return colorFn(sparkChars[idx]);
|
|
217
|
+
}).join('');
|
|
218
|
+
lines.push(` ${sparkline}`);
|
|
219
|
+
if (showValues && labels.length > 0) {
|
|
220
|
+
lines.push(` ${chalk.dim(labels[0])} → ${chalk.dim(labels[labels.length - 1])} ${chalk.gray(`min:${Math.min(...values)} max:${Math.max(...values)}${unit}`)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (chartType === 'pie') {
|
|
224
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
225
|
+
if (total === 0) {
|
|
226
|
+
lines.push(chalk.dim(' (no data for pie chart)'));
|
|
227
|
+
return lines.join('\n');
|
|
228
|
+
}
|
|
229
|
+
const pieColors = [chalk.cyan, chalk.green, chalk.yellow, chalk.magenta, chalk.red, chalk.blue, chalk.white];
|
|
230
|
+
const segments = [];
|
|
231
|
+
for (let i = 0; i < values.length; i++) {
|
|
232
|
+
const pct = ((values[i] / total) * 100).toFixed(1);
|
|
233
|
+
const colorFn = pieColors[i % pieColors.length];
|
|
234
|
+
const filled = Math.round((values[i] / total) * 20);
|
|
235
|
+
const bar = '●'.repeat(Math.max(filled, 1));
|
|
236
|
+
segments.push(` ${colorFn(bar)} ${chalk.white(labels[i])} ${chalk.gray(`${pct}%`)} ${chalk.dim(`(${values[i]}${unit})`)}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push(...segments);
|
|
239
|
+
lines.push(` ${chalk.dim(`Total: ${total}${unit}`)}`);
|
|
240
|
+
}
|
|
241
|
+
if (component.actions && component.actions.length > 0) {
|
|
242
|
+
lines.push('');
|
|
243
|
+
for (const action of component.actions) {
|
|
244
|
+
lines.push(` ${chalk.yellow('→')} ${chalk.white(action.label)} ${chalk.dim(`[${action.type}]`)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return lines.join('\n');
|
|
248
|
+
}
|
|
249
|
+
// ---- Button Group Renderer ----
|
|
250
|
+
async function renderButtonGroup(component, style) {
|
|
251
|
+
const data = component.data || {};
|
|
252
|
+
const buttons = data.buttons || [];
|
|
253
|
+
const layout = data.layout || 'horizontal';
|
|
254
|
+
const lines = [];
|
|
255
|
+
if (component.title) {
|
|
256
|
+
lines.push(chalk.bold.cyan(` ${component.title}`));
|
|
257
|
+
}
|
|
258
|
+
if (data.description) {
|
|
259
|
+
lines.push(chalk.dim(` ${data.description}`));
|
|
260
|
+
}
|
|
261
|
+
if (buttons.length === 0) {
|
|
262
|
+
lines.push(chalk.dim(' (no actions available)'));
|
|
263
|
+
return lines.join('\n');
|
|
264
|
+
}
|
|
265
|
+
const variantStyles = {
|
|
266
|
+
primary: (t) => chalk.bgCyan.black(` ${t} `),
|
|
267
|
+
secondary: (t) => chalk.bgGray.white(` ${t} `),
|
|
268
|
+
danger: (t) => chalk.bgRed.white(` ${t} `),
|
|
269
|
+
ghost: (t) => chalk.dim(`[ ${t} ]`),
|
|
270
|
+
};
|
|
271
|
+
if (layout === 'vertical') {
|
|
272
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
273
|
+
const btn = buttons[i];
|
|
274
|
+
const num = chalk.cyan(`${i + 1}.`);
|
|
275
|
+
const styleFn = variantStyles[btn.variant || 'primary'] || variantStyles.primary;
|
|
276
|
+
const icon = btn.icon ? `${btn.icon} ` : '';
|
|
277
|
+
const disabled = btn.disabled ? chalk.dim(' (disabled)') : '';
|
|
278
|
+
lines.push(` ${num} ${styleFn(`${icon}${btn.label}`)}${disabled}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const parts = [];
|
|
283
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
284
|
+
const btn = buttons[i];
|
|
285
|
+
const num = chalk.cyan(`[${i + 1}]`);
|
|
286
|
+
const styleFn = variantStyles[btn.variant || 'primary'] || variantStyles.primary;
|
|
287
|
+
const icon = btn.icon ? `${btn.icon} ` : '';
|
|
288
|
+
const disabled = btn.disabled ? chalk.dim('(disabled)') : '';
|
|
289
|
+
parts.push(`${num} ${styleFn(`${icon}${btn.label}`)} ${disabled}`);
|
|
290
|
+
}
|
|
291
|
+
lines.push(` ${parts.join(' ')}`);
|
|
292
|
+
}
|
|
293
|
+
if (component.actions && component.actions.length > 0) {
|
|
294
|
+
lines.push('');
|
|
295
|
+
for (const action of component.actions) {
|
|
296
|
+
lines.push(` ${chalk.yellow('→')} ${chalk.white(action.label)} ${chalk.dim(`[${action.type}]`)}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return lines.join('\n');
|
|
300
|
+
}
|
|
301
|
+
// ---- Progress Renderer ----
|
|
302
|
+
async function renderProgress(component, style) {
|
|
303
|
+
const data = component.data || {};
|
|
304
|
+
const percent = Math.max(0, Math.min(100, data.percent ?? 0));
|
|
305
|
+
const current = data.current;
|
|
306
|
+
const total = data.total;
|
|
307
|
+
const label = data.label || '';
|
|
308
|
+
const status = data.status || 'active';
|
|
309
|
+
const eta = data.eta;
|
|
310
|
+
const lines = [];
|
|
311
|
+
if (component.title) {
|
|
312
|
+
lines.push(chalk.bold.cyan(` ${component.title}`));
|
|
313
|
+
}
|
|
314
|
+
const barWidth = 30;
|
|
315
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
316
|
+
const empty = barWidth - filled;
|
|
317
|
+
const statusIcons = {
|
|
318
|
+
active: chalk.cyan('◉'),
|
|
319
|
+
succeeded: chalk.green('✓'),
|
|
320
|
+
failed: chalk.red('✕'),
|
|
321
|
+
paused: chalk.yellow('ǁ'),
|
|
322
|
+
indeterminate: chalk.blue('◎'),
|
|
323
|
+
};
|
|
324
|
+
const barColors = {
|
|
325
|
+
active: (b) => chalk.cyan(b),
|
|
326
|
+
succeeded: (b) => chalk.green(b),
|
|
327
|
+
failed: (b) => chalk.red(b),
|
|
328
|
+
paused: (b) => chalk.yellow(b),
|
|
329
|
+
indeterminate: (b) => chalk.blue(b),
|
|
330
|
+
};
|
|
331
|
+
const colorFn = barColors[status] || barColors.active;
|
|
332
|
+
const icon = statusIcons[status] || statusIcons.active;
|
|
333
|
+
if (status === 'indeterminate') {
|
|
334
|
+
const animBar = '≋'.repeat(barWidth);
|
|
335
|
+
lines.push(` ${icon} ${label} ${chalk.blue(animBar)}`);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const filledBar = colorFn('█'.repeat(filled));
|
|
339
|
+
const emptyBar = chalk.dim('░'.repeat(empty));
|
|
340
|
+
const pctStr = `${percent.toFixed(1)}%`.padStart(7);
|
|
341
|
+
let detail = '';
|
|
342
|
+
if (current !== undefined && total !== undefined) {
|
|
343
|
+
detail = chalk.gray(` (${current}/${total})`);
|
|
344
|
+
}
|
|
345
|
+
let etaStr = '';
|
|
346
|
+
if (eta) {
|
|
347
|
+
etaStr = chalk.gray(` ETA: ${eta}`);
|
|
348
|
+
}
|
|
349
|
+
lines.push(` ${icon} ${chalk.white(label.padEnd(20))} ${filledBar}${emptyBar} ${chalk.bold(pctStr)}${detail}${etaStr}`);
|
|
350
|
+
}
|
|
351
|
+
if (component.actions && component.actions.length > 0) {
|
|
352
|
+
lines.push('');
|
|
353
|
+
for (const action of component.actions) {
|
|
354
|
+
lines.push(` ${chalk.yellow('→')} ${chalk.white(action.label)} ${chalk.dim(`[${action.type}]`)}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return lines.join('\n');
|
|
358
|
+
}
|
|
359
|
+
// ---- Diff Renderer ----
|
|
360
|
+
async function renderDiff(component, style) {
|
|
361
|
+
const data = component.data || {};
|
|
362
|
+
const fileName = data.fileName || 'unknown';
|
|
363
|
+
const hunks = data.hunks || [];
|
|
364
|
+
const additions = data.additions || 0;
|
|
365
|
+
const deletions = data.deletions || 0;
|
|
366
|
+
const lines = [];
|
|
367
|
+
const fileLabel = chalk.bold.white(fileName);
|
|
368
|
+
const addLabel = chalk.green(`+${additions}`);
|
|
369
|
+
const delLabel = chalk.red(`-${deletions}`);
|
|
370
|
+
lines.push(` ${fileLabel} ${addLabel} ${delLabel}`);
|
|
371
|
+
if (data.isBinary) {
|
|
372
|
+
lines.push(chalk.dim(' (binary file)'));
|
|
373
|
+
return lines.join('\n');
|
|
374
|
+
}
|
|
375
|
+
for (const hunk of hunks) {
|
|
376
|
+
const header = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
377
|
+
lines.push(chalk.magenta(` ${header}`));
|
|
378
|
+
const contentLines = hunk.content.split('\n');
|
|
379
|
+
for (const line of contentLines) {
|
|
380
|
+
if (line.startsWith('+')) {
|
|
381
|
+
lines.push(chalk.green(` ${line}`));
|
|
382
|
+
}
|
|
383
|
+
else if (line.startsWith('-')) {
|
|
384
|
+
lines.push(chalk.red(` ${line}`));
|
|
385
|
+
}
|
|
386
|
+
else if (line.startsWith('@@')) {
|
|
387
|
+
lines.push(chalk.magenta(` ${line}`));
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
lines.push(chalk.dim(` ${line}`));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (component.actions && component.actions.length > 0) {
|
|
395
|
+
lines.push('');
|
|
396
|
+
for (const action of component.actions) {
|
|
397
|
+
const icon = action.type === 'submit' ? '✓' : action.type === 'cancel' ? '✕' : '→';
|
|
398
|
+
lines.push(` ${chalk.yellow(icon)} ${chalk.white(action.label)} ${chalk.dim(`[${action.type}]`)}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return lines.join('\n');
|
|
402
|
+
}
|
|
403
|
+
// ---- Custom Renderer (fallback) ----
|
|
404
|
+
async function renderCustom(component, style) {
|
|
405
|
+
const lines = [];
|
|
406
|
+
if (component.title) {
|
|
407
|
+
lines.push(chalk.bold.cyan(` ${component.title}`));
|
|
408
|
+
}
|
|
409
|
+
if (typeof component.data === 'string') {
|
|
410
|
+
lines.push(` ${component.data}`);
|
|
411
|
+
}
|
|
412
|
+
else if (component.data && typeof component.data === 'object') {
|
|
413
|
+
const json = JSON.stringify(component.data, null, 2);
|
|
414
|
+
const indented = json.split('\n').map((l) => ` ${l}`).join('\n');
|
|
415
|
+
lines.push(chalk.dim(indented));
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
lines.push(chalk.dim(' (custom component)'));
|
|
419
|
+
}
|
|
420
|
+
if (component.actions && component.actions.length > 0) {
|
|
421
|
+
lines.push('');
|
|
422
|
+
for (const action of component.actions) {
|
|
423
|
+
lines.push(` ${chalk.yellow('→')} ${chalk.white(action.label)} ${chalk.dim(`[${action.type}]`)}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return lines.join('\n');
|
|
427
|
+
}
|
|
428
|
+
// ---- Utility Functions ----
|
|
429
|
+
function padRight(str, len) {
|
|
430
|
+
if (str.length >= len)
|
|
431
|
+
return str.slice(0, len);
|
|
432
|
+
return str + ' '.repeat(len - str.length);
|
|
433
|
+
}
|
|
434
|
+
function resolveStyle(accent, fallback) {
|
|
435
|
+
if (!accent)
|
|
436
|
+
return chalk.hex(fallback);
|
|
437
|
+
try {
|
|
438
|
+
return chalk.hex(accent);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return chalk.hex(fallback);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// ============================================================
|
|
445
|
+
// MCPAppManager - Main Manager Class
|
|
446
|
+
// ============================================================
|
|
447
|
+
export class MCPAppManager {
|
|
448
|
+
mcpClient;
|
|
449
|
+
renderers = new Map();
|
|
450
|
+
stateStore = new ComponentStateStore();
|
|
451
|
+
knownApps = new Map();
|
|
452
|
+
currentStyle;
|
|
453
|
+
constructor(mcpClient) {
|
|
454
|
+
this.mcpClient = mcpClient;
|
|
455
|
+
this.currentStyle = { theme: 'dark' };
|
|
456
|
+
this.registerBuiltinRenderers();
|
|
457
|
+
}
|
|
458
|
+
// ---- Renderer Registration ----
|
|
459
|
+
registerComponentType(type, renderer) {
|
|
460
|
+
this.renderers.set(type, renderer);
|
|
461
|
+
}
|
|
462
|
+
registerBuiltinRenderers() {
|
|
463
|
+
this.renderers.set('form', renderForm);
|
|
464
|
+
this.renderers.set('table', renderTable);
|
|
465
|
+
this.renderers.set('chart', renderChart);
|
|
466
|
+
this.renderers.set('button-group', renderButtonGroup);
|
|
467
|
+
this.renderers.set('progress', renderProgress);
|
|
468
|
+
this.renderers.set('diff', renderDiff);
|
|
469
|
+
this.renderers.set('custom', renderCustom);
|
|
470
|
+
}
|
|
471
|
+
// ---- Style Configuration ----
|
|
472
|
+
setStyle(style) {
|
|
473
|
+
this.currentStyle = { ...this.currentStyle, ...style };
|
|
474
|
+
}
|
|
475
|
+
getStyle() {
|
|
476
|
+
return { ...this.currentStyle };
|
|
477
|
+
}
|
|
478
|
+
// ---- Component Rendering ----
|
|
479
|
+
async renderComponent(component) {
|
|
480
|
+
const renderer = this.renderers.get(component.type);
|
|
481
|
+
const mergedStyle = {
|
|
482
|
+
...this.currentStyle,
|
|
483
|
+
...component.style,
|
|
484
|
+
};
|
|
485
|
+
if (!renderer) {
|
|
486
|
+
return renderCustom(component, mergedStyle);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const rendered = await renderer(component, mergedStyle);
|
|
490
|
+
return rendered;
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
494
|
+
const fallback = [
|
|
495
|
+
chalk.red(` ⚠ Failed to render ${component.type} component (${component.id}): ${errMsg}`),
|
|
496
|
+
'',
|
|
497
|
+
await renderCustom(component, mergedStyle),
|
|
498
|
+
].join('\n');
|
|
499
|
+
return fallback;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async renderAppResult(appResult) {
|
|
503
|
+
const parts = [];
|
|
504
|
+
if (appResult.text) {
|
|
505
|
+
parts.push(appResult.text);
|
|
506
|
+
}
|
|
507
|
+
if (appResult.components.length > 0) {
|
|
508
|
+
if (appResult.text) {
|
|
509
|
+
parts.push('');
|
|
510
|
+
}
|
|
511
|
+
for (let i = 0; i < appResult.components.length; i++) {
|
|
512
|
+
const component = appResult.components[i];
|
|
513
|
+
const rendered = await this.renderComponent(component);
|
|
514
|
+
if (rendered.trim()) {
|
|
515
|
+
parts.push(rendered);
|
|
516
|
+
if (i < appResult.components.length - 1) {
|
|
517
|
+
parts.push('');
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (appResult.metadata && Object.keys(appResult.metadata).length > 0) {
|
|
523
|
+
parts.push('');
|
|
524
|
+
const metaEntries = Object.entries(appResult.metadata);
|
|
525
|
+
for (const [key, value] of metaEntries) {
|
|
526
|
+
parts.push(chalk.dim(` ${key}: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return parts.join('\n');
|
|
530
|
+
}
|
|
531
|
+
// ---- Action Handling ----
|
|
532
|
+
async handleAction(action) {
|
|
533
|
+
switch (action.type) {
|
|
534
|
+
case 'tool-call': {
|
|
535
|
+
if (!action.toolCall) {
|
|
536
|
+
return {
|
|
537
|
+
text: `Action "${action.label}" has no tool call defined.`,
|
|
538
|
+
components: [],
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const { tool, args } = action.toolCall;
|
|
542
|
+
const parsed = this.mcpClient.parseMCPToolName(tool);
|
|
543
|
+
if (!parsed) {
|
|
544
|
+
return {
|
|
545
|
+
text: `Invalid MCP tool name: "${tool}". Expected format: mcp_<server>__<tool>`,
|
|
546
|
+
components: [],
|
|
547
|
+
metadata: { error: true, actionId: action.id },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const result = await this.mcpClient.callTool(parsed.serverName, parsed.toolName, args);
|
|
552
|
+
return this.parseAppResult(result);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
556
|
+
return {
|
|
557
|
+
text: `Tool call failed: ${errMsg}`,
|
|
558
|
+
components: [],
|
|
559
|
+
metadata: { error: true, actionId: action.id, tool, args },
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
case 'submit': {
|
|
564
|
+
return {
|
|
565
|
+
text: `Submitted: ${action.label}`,
|
|
566
|
+
components: [],
|
|
567
|
+
metadata: { actionId: action.id, actionType: 'submit' },
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
case 'cancel': {
|
|
571
|
+
return {
|
|
572
|
+
text: `Cancelled: ${action.label}`,
|
|
573
|
+
components: [],
|
|
574
|
+
metadata: { actionId: action.id, actionType: 'cancel' },
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
case 'navigate': {
|
|
578
|
+
return {
|
|
579
|
+
text: `Navigate: ${action.label}`,
|
|
580
|
+
components: [],
|
|
581
|
+
metadata: { actionId: action.id, actionType: 'navigate' },
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
default: {
|
|
585
|
+
return {
|
|
586
|
+
text: `Unknown action type: ${action.type}`,
|
|
587
|
+
components: [],
|
|
588
|
+
metadata: { actionId: action.id },
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// ---- Result Parsing ----
|
|
594
|
+
parseAppResult(rawResult) {
|
|
595
|
+
if (!rawResult) {
|
|
596
|
+
return { text: '(no result)', components: [] };
|
|
597
|
+
}
|
|
598
|
+
if (typeof rawResult === 'string') {
|
|
599
|
+
try {
|
|
600
|
+
const parsed = JSON.parse(rawResult);
|
|
601
|
+
return this.parseAppResult(parsed);
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
return { text: rawResult, components: [] };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (typeof rawResult === 'object') {
|
|
608
|
+
const content = rawResult.content;
|
|
609
|
+
if (Array.isArray(content)) {
|
|
610
|
+
const textParts = [];
|
|
611
|
+
const components = [];
|
|
612
|
+
for (const item of content) {
|
|
613
|
+
if (item.type === 'text') {
|
|
614
|
+
textParts.push(item.text || '');
|
|
615
|
+
}
|
|
616
|
+
else if (item.type === 'resource') {
|
|
617
|
+
const resource = item.resource;
|
|
618
|
+
if (resource) {
|
|
619
|
+
textParts.push(resource.text || resource.uri || '');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else if (item.type === 'mcp-app' || item.type === 'component' || item.type === 'ui-component') {
|
|
623
|
+
const component = this.parseComponent(item);
|
|
624
|
+
if (component) {
|
|
625
|
+
components.push(component);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else if (item.type === 'image') {
|
|
629
|
+
textParts.push(chalk.dim(`[Image: ${item.mimeType || 'unknown'}]`));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const metadata = rawResult.meta || rawResult.metadata;
|
|
633
|
+
return {
|
|
634
|
+
text: textParts.join('\n'),
|
|
635
|
+
components,
|
|
636
|
+
metadata: metadata && typeof metadata === 'object' ? metadata : undefined,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
if (rawResult.text !== undefined || rawResult.components !== undefined) {
|
|
640
|
+
const components = Array.isArray(rawResult.components)
|
|
641
|
+
? rawResult.components.map((c) => this.parseComponent(c)).filter(Boolean)
|
|
642
|
+
: [];
|
|
643
|
+
return {
|
|
644
|
+
text: rawResult.text || '',
|
|
645
|
+
components,
|
|
646
|
+
metadata: rawResult.metadata || rawResult.meta,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (rawResult.type && rawResult.id) {
|
|
650
|
+
const component = this.parseComponent(rawResult);
|
|
651
|
+
if (component) {
|
|
652
|
+
return {
|
|
653
|
+
text: component.title || component.type,
|
|
654
|
+
components: [component],
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
return { text: JSON.stringify(rawResult, null, 2), components: [] };
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return { text: String(rawResult), components: [] };
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return { text: String(rawResult), components: [] };
|
|
666
|
+
}
|
|
667
|
+
parseComponent(raw) {
|
|
668
|
+
if (!raw || typeof raw !== 'object')
|
|
669
|
+
return null;
|
|
670
|
+
const validTypes = ['form', 'table', 'chart', 'button-group', 'progress', 'diff', 'custom'];
|
|
671
|
+
const type = validTypes.includes(raw.type) ? raw.type : 'custom';
|
|
672
|
+
const component = {
|
|
673
|
+
type,
|
|
674
|
+
id: raw.id || `comp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
675
|
+
data: raw.data || raw.fields || raw.rows || raw.content || {},
|
|
676
|
+
};
|
|
677
|
+
if (raw.title)
|
|
678
|
+
component.title = raw.title;
|
|
679
|
+
if (raw.actions && Array.isArray(raw.actions)) {
|
|
680
|
+
component.actions = raw.actions.map((a) => this.parseAction(a)).filter(Boolean);
|
|
681
|
+
}
|
|
682
|
+
if (raw.style)
|
|
683
|
+
component.style = raw.style;
|
|
684
|
+
return component;
|
|
685
|
+
}
|
|
686
|
+
parseAction(raw) {
|
|
687
|
+
if (!raw || typeof raw !== 'object')
|
|
688
|
+
return null;
|
|
689
|
+
const validTypes = ['submit', 'cancel', 'navigate', 'tool-call'];
|
|
690
|
+
const type = validTypes.includes(raw.type) ? raw.type : 'submit';
|
|
691
|
+
const action = {
|
|
692
|
+
id: raw.id || `action_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
693
|
+
label: raw.label || 'Action',
|
|
694
|
+
type,
|
|
695
|
+
};
|
|
696
|
+
if (raw.toolCall && typeof raw.toolCall === 'object') {
|
|
697
|
+
action.toolCall = {
|
|
698
|
+
tool: raw.toolCall.tool || '',
|
|
699
|
+
args: raw.toolCall.args || {},
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
else if (raw.tool_call && typeof raw.tool_call === 'object') {
|
|
703
|
+
action.toolCall = {
|
|
704
|
+
tool: raw.tool_call.tool || '',
|
|
705
|
+
args: raw.tool_call.args || {},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
else if (raw.tool && typeof raw.tool === 'string') {
|
|
709
|
+
action.toolCall = {
|
|
710
|
+
tool: raw.tool,
|
|
711
|
+
args: raw.args || raw.arguments || {},
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
return action;
|
|
715
|
+
}
|
|
716
|
+
// ---- App Discovery ----
|
|
717
|
+
async listAvailableApps() {
|
|
718
|
+
const tools = this.mcpClient.getAllTools();
|
|
719
|
+
const apps = [];
|
|
720
|
+
for (const { serverName, tool } of tools) {
|
|
721
|
+
const description = tool.description || '';
|
|
722
|
+
const isApp = this.detectAppCapability(tool);
|
|
723
|
+
if (isApp) {
|
|
724
|
+
const info = {
|
|
725
|
+
serverName,
|
|
726
|
+
toolName: tool.name,
|
|
727
|
+
description,
|
|
728
|
+
componentTypes: this.detectComponentTypes(tool),
|
|
729
|
+
hasActions: this.detectActions(tool),
|
|
730
|
+
};
|
|
731
|
+
apps.push(info);
|
|
732
|
+
this.knownApps.set(`${serverName}__${tool.name}`, info);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return apps;
|
|
736
|
+
}
|
|
737
|
+
detectAppCapability(tool) {
|
|
738
|
+
const desc = (tool.description || '').toLowerCase();
|
|
739
|
+
const appKeywords = ['interactive', 'ui', 'form', 'table', 'chart', 'component', 'app', 'widget', 'dashboard', 'visual'];
|
|
740
|
+
if (appKeywords.some((kw) => desc.includes(kw)))
|
|
741
|
+
return true;
|
|
742
|
+
const schema = tool.inputSchema || tool.parameters;
|
|
743
|
+
if (schema && schema.properties) {
|
|
744
|
+
const props = Object.keys(schema.properties);
|
|
745
|
+
if (props.includes('componentType') || props.includes('ui_type') || props.includes('format')) {
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
const hasComponentType = props.some((p) => {
|
|
749
|
+
const prop = schema.properties[p];
|
|
750
|
+
return prop && prop.enum && Array.isArray(prop.enum) &&
|
|
751
|
+
prop.enum.some((v) => ['form', 'table', 'chart', 'html', 'component'].includes(v));
|
|
752
|
+
});
|
|
753
|
+
if (hasComponentType)
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
if (tool.annotations && typeof tool.annotations === 'object') {
|
|
757
|
+
if (tool.annotations.mcpApp === true || tool.annotations.ui === true)
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
detectComponentTypes(tool) {
|
|
763
|
+
const types = [];
|
|
764
|
+
const schema = tool.inputSchema || tool.parameters;
|
|
765
|
+
if (schema && schema.properties) {
|
|
766
|
+
const componentTypeProp = schema.properties.componentType || schema.properties.ui_type || schema.properties.format;
|
|
767
|
+
if (componentTypeProp && componentTypeProp.enum) {
|
|
768
|
+
types.push(...componentTypeProp.enum);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const desc = (tool.description || '').toLowerCase();
|
|
772
|
+
const knownTypes = ['form', 'table', 'chart', 'button-group', 'progress', 'diff', 'custom'];
|
|
773
|
+
for (const t of knownTypes) {
|
|
774
|
+
if (desc.includes(t) && !types.includes(t))
|
|
775
|
+
types.push(t);
|
|
776
|
+
}
|
|
777
|
+
return types.length > 0 ? types : ['custom'];
|
|
778
|
+
}
|
|
779
|
+
detectActions(tool) {
|
|
780
|
+
const desc = (tool.description || '').toLowerCase();
|
|
781
|
+
if (desc.includes('action') || desc.includes('submit') || desc.includes('button'))
|
|
782
|
+
return true;
|
|
783
|
+
const schema = tool.inputSchema || tool.parameters;
|
|
784
|
+
if (schema && schema.properties) {
|
|
785
|
+
return 'action' in schema.properties || 'actions' in schema.properties;
|
|
786
|
+
}
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
// ---- Interactive Prompt Rendering ----
|
|
790
|
+
async renderInteractivePrompt(component) {
|
|
791
|
+
const rendered = await this.renderComponent(component);
|
|
792
|
+
console.log(rendered);
|
|
793
|
+
const actions = component.actions;
|
|
794
|
+
if (!actions || actions.length === 0)
|
|
795
|
+
return null;
|
|
796
|
+
if (component.type === 'button-group') {
|
|
797
|
+
const data = component.data || {};
|
|
798
|
+
const buttons = data.buttons || [];
|
|
799
|
+
if (buttons.length > 0) {
|
|
800
|
+
const choices = buttons.map((btn, i) => ({
|
|
801
|
+
name: `${i + 1}. ${btn.label}${btn.disabled ? ' (disabled)' : ''}`,
|
|
802
|
+
value: btn.id,
|
|
803
|
+
disabled: btn.disabled || false,
|
|
804
|
+
}));
|
|
805
|
+
try {
|
|
806
|
+
// @ts-ignore
|
|
807
|
+
const { default: inquirer } = await import('inquirer');
|
|
808
|
+
const answer = await inquirer.prompt([{
|
|
809
|
+
type: 'list',
|
|
810
|
+
name: 'action',
|
|
811
|
+
message: 'Choose an action:',
|
|
812
|
+
choices,
|
|
813
|
+
}]);
|
|
814
|
+
const selectedBtn = buttons.find((b) => b.id === answer.action);
|
|
815
|
+
if (selectedBtn) {
|
|
816
|
+
const matchingAction = actions.find((a) => a.label === selectedBtn.label) || actions[0];
|
|
817
|
+
if (matchingAction && matchingAction.type === 'tool-call' && matchingAction.toolCall) {
|
|
818
|
+
return matchingAction;
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
id: selectedBtn.id,
|
|
822
|
+
label: selectedBtn.label,
|
|
823
|
+
type: 'submit',
|
|
824
|
+
toolCall: matchingAction?.toolCall,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (component.type === 'form') {
|
|
834
|
+
const fields = component.data?.fields || [];
|
|
835
|
+
if (fields.length > 0) {
|
|
836
|
+
try {
|
|
837
|
+
// @ts-ignore
|
|
838
|
+
const { default: inquirer } = await import('inquirer');
|
|
839
|
+
const questions = fields.map((field) => {
|
|
840
|
+
const base = {
|
|
841
|
+
name: field.name,
|
|
842
|
+
message: field.label,
|
|
843
|
+
default: field.default,
|
|
844
|
+
};
|
|
845
|
+
if (field.type === 'select' && field.options) {
|
|
846
|
+
base.type = 'list';
|
|
847
|
+
base.choices = field.options.map((o) => ({ name: o.label, value: o.value }));
|
|
848
|
+
}
|
|
849
|
+
else if (field.type === 'checkbox' && field.options) {
|
|
850
|
+
base.type = 'checkbox';
|
|
851
|
+
base.choices = field.options.map((o) => ({ name: o.label, value: o.value }));
|
|
852
|
+
}
|
|
853
|
+
else if (field.type === 'toggle') {
|
|
854
|
+
base.type = 'confirm';
|
|
855
|
+
}
|
|
856
|
+
else if (field.type === 'password') {
|
|
857
|
+
base.type = 'password';
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
base.type = 'input';
|
|
861
|
+
}
|
|
862
|
+
return base;
|
|
863
|
+
});
|
|
864
|
+
const answers = await inquirer.prompt(questions);
|
|
865
|
+
const submitAction = actions.find((a) => a.type === 'submit') || actions[0];
|
|
866
|
+
if (submitAction) {
|
|
867
|
+
if (submitAction.toolCall) {
|
|
868
|
+
return {
|
|
869
|
+
...submitAction,
|
|
870
|
+
toolCall: {
|
|
871
|
+
tool: submitAction.toolCall.tool,
|
|
872
|
+
args: { ...submitAction.toolCall.args, ...answers },
|
|
873
|
+
},
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
return submitAction;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (actions.length > 0) {
|
|
885
|
+
const choices = actions.map((a) => ({
|
|
886
|
+
name: `${a.label} [${a.type}]`,
|
|
887
|
+
value: a.id,
|
|
888
|
+
}));
|
|
889
|
+
try {
|
|
890
|
+
// @ts-ignore
|
|
891
|
+
const { default: inquirer } = await import('inquirer');
|
|
892
|
+
const answer = await inquirer.prompt([{
|
|
893
|
+
type: 'list',
|
|
894
|
+
name: 'action',
|
|
895
|
+
message: 'Choose an action:',
|
|
896
|
+
choices,
|
|
897
|
+
}]);
|
|
898
|
+
return actions.find((a) => a.id === answer.action) || null;
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
// ---- Progress Spinner ----
|
|
907
|
+
createProgressSpinner(component) {
|
|
908
|
+
const data = component.data || {};
|
|
909
|
+
const label = data.label || component.title || 'Loading...';
|
|
910
|
+
const spinner = ora({
|
|
911
|
+
text: label,
|
|
912
|
+
spinner: 'dots',
|
|
913
|
+
});
|
|
914
|
+
return spinner;
|
|
915
|
+
}
|
|
916
|
+
async updateProgressSpinner(spinner, component) {
|
|
917
|
+
const data = component.data || {};
|
|
918
|
+
const label = data.label || component.title || '';
|
|
919
|
+
const percent = data.percent ?? 0;
|
|
920
|
+
const status = data.status || 'active';
|
|
921
|
+
switch (status) {
|
|
922
|
+
case 'succeeded':
|
|
923
|
+
spinner.succeed(`${label} - Complete (${percent.toFixed(1)}%)`);
|
|
924
|
+
break;
|
|
925
|
+
case 'failed':
|
|
926
|
+
spinner.fail(`${label} - Failed`);
|
|
927
|
+
break;
|
|
928
|
+
case 'paused':
|
|
929
|
+
spinner.warn(`${label} - Paused (${percent.toFixed(1)}%)`);
|
|
930
|
+
break;
|
|
931
|
+
case 'indeterminate':
|
|
932
|
+
spinner.start(label);
|
|
933
|
+
break;
|
|
934
|
+
default: {
|
|
935
|
+
const current = data.current !== undefined ? ` (${data.current}/${data.total})` : '';
|
|
936
|
+
spinner.text = `${label} ${percent.toFixed(1)}%${current}`;
|
|
937
|
+
if (!spinner.isSpinning)
|
|
938
|
+
spinner.start();
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// ---- State Management ----
|
|
944
|
+
getComponentState(componentId) {
|
|
945
|
+
return this.stateStore.getState(componentId);
|
|
946
|
+
}
|
|
947
|
+
setComponentState(componentId, key, value) {
|
|
948
|
+
this.stateStore.setState(componentId, key, value);
|
|
949
|
+
}
|
|
950
|
+
clearComponentState(componentId) {
|
|
951
|
+
this.stateStore.clearState(componentId);
|
|
952
|
+
}
|
|
953
|
+
clearAllState() {
|
|
954
|
+
this.stateStore.clearAll();
|
|
955
|
+
}
|
|
956
|
+
// ---- App Execution ----
|
|
957
|
+
async executeApp(serverName, toolName, args) {
|
|
958
|
+
try {
|
|
959
|
+
const rawResult = await this.mcpClient.callTool(serverName, toolName, args);
|
|
960
|
+
const appResult = this.parseAppResult(rawResult);
|
|
961
|
+
const rendered = await this.renderAppResult(appResult);
|
|
962
|
+
return rendered;
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
966
|
+
return chalk.red(` ⚠ MCP App execution failed: ${errMsg}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async executeAppInteractive(serverName, toolName, args) {
|
|
970
|
+
let rawResult;
|
|
971
|
+
try {
|
|
972
|
+
rawResult = await this.mcpClient.callTool(serverName, toolName, args);
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
976
|
+
console.log(chalk.red(` ⚠ MCP App execution failed: ${errMsg}`));
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const appResult = this.parseAppResult(rawResult);
|
|
980
|
+
if (appResult.text) {
|
|
981
|
+
console.log(appResult.text);
|
|
982
|
+
}
|
|
983
|
+
for (const component of appResult.components) {
|
|
984
|
+
const hasInteractiveActions = component.actions && component.actions.length > 0;
|
|
985
|
+
if (hasInteractiveActions) {
|
|
986
|
+
const action = await this.renderInteractivePrompt(component);
|
|
987
|
+
if (action) {
|
|
988
|
+
const actionResult = await this.handleAction(action);
|
|
989
|
+
if (actionResult.text) {
|
|
990
|
+
console.log(actionResult.text);
|
|
991
|
+
}
|
|
992
|
+
if (actionResult.components.length > 0) {
|
|
993
|
+
for (const subComponent of actionResult.components) {
|
|
994
|
+
const rendered = await this.renderComponent(subComponent);
|
|
995
|
+
console.log(rendered);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
const rendered = await this.renderComponent(component);
|
|
1002
|
+
console.log(rendered);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
//# sourceMappingURL=mcp-apps.js.map
|