mcp-pentester-cli 1.0.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 +75 -0
- package/README.md +327 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +23 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +163 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/transport/base.d.ts +18 -0
- package/dist/transport/base.d.ts.map +1 -0
- package/dist/transport/base.js +64 -0
- package/dist/transport/base.js.map +1 -0
- package/dist/transport/http.d.ts +14 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +137 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/stdio.d.ts +15 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +89 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport/websocket.d.ts +15 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +109 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/tui.d.ts +43 -0
- package/dist/ui/tui.d.ts.map +1 -0
- package/dist/ui/tui.js +872 -0
- package/dist/ui/tui.js.map +1 -0
- package/examples/http-burp-config.json +9 -0
- package/examples/https-burp-config.json +13 -0
- package/examples/stdio-config.json +10 -0
- package/examples/tor-config.json +9 -0
- package/examples/websocket-config.json +9 -0
- package/package.json +44 -0
package/dist/ui/tui.js
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TUI = void 0;
|
|
7
|
+
const blessed_1 = __importDefault(require("blessed"));
|
|
8
|
+
class TUI {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.currentView = 'tools';
|
|
11
|
+
this.trafficLines = [];
|
|
12
|
+
this.trafficPairs = [];
|
|
13
|
+
this.activePanel = 'sidebar';
|
|
14
|
+
this.pendingRequests = new Map(); // Track requests waiting for responses
|
|
15
|
+
this.currentPopupContent = null;
|
|
16
|
+
this.showRawInPopup = false;
|
|
17
|
+
this.screen = blessed_1.default.screen({
|
|
18
|
+
smartCSR: true,
|
|
19
|
+
title: 'MCP Pentester CLI - IntegSec',
|
|
20
|
+
});
|
|
21
|
+
this.layout = this.createLayout();
|
|
22
|
+
this.setupKeyBindings();
|
|
23
|
+
}
|
|
24
|
+
showSplashScreen() {
|
|
25
|
+
const splash = blessed_1.default.box({
|
|
26
|
+
parent: this.screen,
|
|
27
|
+
top: 'center',
|
|
28
|
+
left: 'center',
|
|
29
|
+
width: 70,
|
|
30
|
+
height: 18,
|
|
31
|
+
border: { type: 'line' },
|
|
32
|
+
style: {
|
|
33
|
+
border: { fg: 'cyan', bold: true },
|
|
34
|
+
},
|
|
35
|
+
content: `{center}{cyan-fg}{bold}
|
|
36
|
+
██▓ ███▄ █ ▄▄▄█████▓▓█████ ▄████ ██████ ▓█████ ▄████▄
|
|
37
|
+
▓██▒ ██ ▀█ █ ▓ ██▒ ▓▒▓█ ▀ ██▒ ▀█▒▒██ ▒ ▓█ ▀ ▒██▀ ▀█
|
|
38
|
+
▒██▒▓██ ▀█ ██▒▒ ▓██░ ▒░▒███ ▒██░▄▄▄░░ ▓██▄ ▒███ ▒▓█ ▄
|
|
39
|
+
░██░▓██▒ ▐▌██▒░ ▓██▓ ░ ▒▓█ ▄ ░▓█ ██▓ ▒ ██▒▒▓█ ▄ ▒▓▓▄ ▄██▒
|
|
40
|
+
░██░▒██░ ▓██░ ▒██▒ ░ ░▒████▒░▒▓███▀▒▒██████▒▒░▒████▒▒ ▓███▀ ░
|
|
41
|
+
░▓ ░ ▒░ ▒ ▒ ▒ ░░ ░░ ▒░ ░ ░▒ ▒ ▒ ▒▓▒ ▒ ░░░ ▒░ ░░ ░▒ ▒ ░
|
|
42
|
+
▒ ░░ ░░ ░ ▒░ ░ ░ ░ ░ ░ ░ ░ ░▒ ░ ░ ░ ░ ░ ░ ▒
|
|
43
|
+
▒ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
|
44
|
+
░ ░ ░ ░ ░ ░ ░ ░░ ░
|
|
45
|
+
{/bold}{/cyan-fg}
|
|
46
|
+
{bold}{yellow-fg}MCP Pentester CLI v1.0.0{/yellow-fg}{/bold}
|
|
47
|
+
{green-fg}{bold}integsec.com{/bold}{/green-fg} {gray-fg}|{/gray-fg} {white-fg}Security Testing{/white-fg}
|
|
48
|
+
{gray-fg}© 2025 IntegSec - All Rights Reserved{/gray-fg}
|
|
49
|
+
{green-fg}Press any key (auto-closes in 2s)...{/green-fg}{/center}`,
|
|
50
|
+
tags: true,
|
|
51
|
+
});
|
|
52
|
+
this.screen.render();
|
|
53
|
+
const closeHandler = () => {
|
|
54
|
+
if (!splash.detached) {
|
|
55
|
+
splash.destroy();
|
|
56
|
+
this.screen.render();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
// Auto-close after 2 seconds
|
|
60
|
+
setTimeout(closeHandler, 2000);
|
|
61
|
+
// Or close on any keypress
|
|
62
|
+
this.screen.once('keypress', closeHandler);
|
|
63
|
+
}
|
|
64
|
+
createLayout() {
|
|
65
|
+
// IntegSec Logo (top right)
|
|
66
|
+
const logo = blessed_1.default.box({
|
|
67
|
+
parent: this.screen,
|
|
68
|
+
top: 0,
|
|
69
|
+
right: 0,
|
|
70
|
+
width: 20,
|
|
71
|
+
height: 5,
|
|
72
|
+
tags: true,
|
|
73
|
+
content: '{right}{cyan-fg}{bold}IntegSec{/bold}\n{gray-fg}Security\nTesting{/gray-fg}{/cyan-fg}{/right}',
|
|
74
|
+
style: {
|
|
75
|
+
fg: 'cyan',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
// Sidebar (left panel) - Navigation
|
|
79
|
+
const sidebar = blessed_1.default.list({
|
|
80
|
+
parent: this.screen,
|
|
81
|
+
label: ' Navigation ',
|
|
82
|
+
tags: true,
|
|
83
|
+
top: 0,
|
|
84
|
+
left: 0,
|
|
85
|
+
width: '20%',
|
|
86
|
+
height: '70%',
|
|
87
|
+
border: { type: 'line' },
|
|
88
|
+
style: {
|
|
89
|
+
border: { fg: 'cyan' },
|
|
90
|
+
selected: { bg: 'blue', fg: 'white' },
|
|
91
|
+
item: { fg: 'white' },
|
|
92
|
+
},
|
|
93
|
+
keys: true,
|
|
94
|
+
vi: true,
|
|
95
|
+
mouse: true,
|
|
96
|
+
items: [
|
|
97
|
+
'{cyan-fg}Tools{/cyan-fg}',
|
|
98
|
+
'{cyan-fg}Resources{/cyan-fg}',
|
|
99
|
+
'{cyan-fg}Prompts{/cyan-fg}',
|
|
100
|
+
'{cyan-fg}Traffic Log{/cyan-fg}',
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
// Main panel (center) - Content display (using list for interactivity)
|
|
104
|
+
const main = blessed_1.default.list({
|
|
105
|
+
parent: this.screen,
|
|
106
|
+
label: ' Content ',
|
|
107
|
+
tags: true,
|
|
108
|
+
top: 0,
|
|
109
|
+
left: '20%',
|
|
110
|
+
width: '80%',
|
|
111
|
+
height: '70%',
|
|
112
|
+
border: { type: 'line' },
|
|
113
|
+
style: {
|
|
114
|
+
border: { fg: 'cyan' },
|
|
115
|
+
selected: { bg: 'blue', fg: 'white' },
|
|
116
|
+
item: { fg: 'white' },
|
|
117
|
+
},
|
|
118
|
+
scrollable: true,
|
|
119
|
+
alwaysScroll: true,
|
|
120
|
+
keys: true,
|
|
121
|
+
vi: true,
|
|
122
|
+
mouse: true,
|
|
123
|
+
scrollbar: {
|
|
124
|
+
ch: ' ',
|
|
125
|
+
style: { bg: 'cyan' },
|
|
126
|
+
},
|
|
127
|
+
interactive: true,
|
|
128
|
+
});
|
|
129
|
+
// Status bar
|
|
130
|
+
const status = blessed_1.default.box({
|
|
131
|
+
parent: this.screen,
|
|
132
|
+
top: '70%',
|
|
133
|
+
left: 0,
|
|
134
|
+
width: '100%',
|
|
135
|
+
height: 3,
|
|
136
|
+
tags: true,
|
|
137
|
+
border: { type: 'line' },
|
|
138
|
+
style: {
|
|
139
|
+
border: { fg: 'yellow' },
|
|
140
|
+
},
|
|
141
|
+
content: '{yellow-fg}Status:{/yellow-fg} Disconnected',
|
|
142
|
+
});
|
|
143
|
+
// Traffic log (bottom panel)
|
|
144
|
+
const traffic = blessed_1.default.box({
|
|
145
|
+
parent: this.screen,
|
|
146
|
+
label: ' Traffic Log ',
|
|
147
|
+
tags: true,
|
|
148
|
+
top: '73%',
|
|
149
|
+
left: 0,
|
|
150
|
+
width: '100%',
|
|
151
|
+
height: '24%',
|
|
152
|
+
border: { type: 'line' },
|
|
153
|
+
style: {
|
|
154
|
+
border: { fg: 'green' },
|
|
155
|
+
},
|
|
156
|
+
scrollable: true,
|
|
157
|
+
alwaysScroll: true,
|
|
158
|
+
keys: true,
|
|
159
|
+
vi: true,
|
|
160
|
+
mouse: true,
|
|
161
|
+
scrollbar: {
|
|
162
|
+
ch: ' ',
|
|
163
|
+
style: { bg: 'green' },
|
|
164
|
+
},
|
|
165
|
+
content: '',
|
|
166
|
+
});
|
|
167
|
+
// Input box (hidden by default)
|
|
168
|
+
const input = blessed_1.default.textbox({
|
|
169
|
+
parent: this.screen,
|
|
170
|
+
label: ' Input ',
|
|
171
|
+
top: 'center',
|
|
172
|
+
left: 'center',
|
|
173
|
+
width: '60%',
|
|
174
|
+
height: 3,
|
|
175
|
+
border: { type: 'line' },
|
|
176
|
+
style: {
|
|
177
|
+
border: { fg: 'magenta' },
|
|
178
|
+
focus: { border: { fg: 'blue' } },
|
|
179
|
+
},
|
|
180
|
+
hidden: true,
|
|
181
|
+
inputOnFocus: true,
|
|
182
|
+
keys: true,
|
|
183
|
+
mouse: true,
|
|
184
|
+
});
|
|
185
|
+
return { sidebar, main, status, traffic, input };
|
|
186
|
+
}
|
|
187
|
+
setupKeyBindings() {
|
|
188
|
+
// F10 - Quit application
|
|
189
|
+
this.screen.key(['f10', 'C-c'], () => {
|
|
190
|
+
return process.exit(0);
|
|
191
|
+
});
|
|
192
|
+
// Navigate sidebar
|
|
193
|
+
this.layout.sidebar.on('select', (item, index) => {
|
|
194
|
+
switch (index) {
|
|
195
|
+
case 0:
|
|
196
|
+
this.showTools();
|
|
197
|
+
break;
|
|
198
|
+
case 1:
|
|
199
|
+
this.showResources();
|
|
200
|
+
break;
|
|
201
|
+
case 2:
|
|
202
|
+
this.showPrompts();
|
|
203
|
+
break;
|
|
204
|
+
case 3:
|
|
205
|
+
this.showTrafficLog();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
// Auto-focus the content pane after selecting from sidebar
|
|
209
|
+
this.activePanel = 'main';
|
|
210
|
+
this.updatePanelHighlight();
|
|
211
|
+
this.layout.main.focus();
|
|
212
|
+
this.screen.render();
|
|
213
|
+
});
|
|
214
|
+
// F5 - Refresh current view
|
|
215
|
+
this.screen.key(['f5'], async () => {
|
|
216
|
+
if (this.client) {
|
|
217
|
+
await this.client.refreshAll();
|
|
218
|
+
this.updateCurrentView();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
// F1 - Focus sidebar
|
|
222
|
+
this.screen.key(['f1'], () => {
|
|
223
|
+
this.activePanel = 'sidebar';
|
|
224
|
+
this.updatePanelHighlight();
|
|
225
|
+
this.layout.sidebar.focus();
|
|
226
|
+
this.screen.render();
|
|
227
|
+
});
|
|
228
|
+
// F2 - Focus main panel
|
|
229
|
+
this.screen.key(['f2'], () => {
|
|
230
|
+
this.activePanel = 'main';
|
|
231
|
+
this.updatePanelHighlight();
|
|
232
|
+
this.layout.main.focus();
|
|
233
|
+
this.screen.render();
|
|
234
|
+
});
|
|
235
|
+
// F3 - Focus traffic panel
|
|
236
|
+
this.screen.key(['f3'], () => {
|
|
237
|
+
this.activePanel = 'traffic';
|
|
238
|
+
this.updatePanelHighlight();
|
|
239
|
+
this.layout.traffic.focus();
|
|
240
|
+
this.screen.render();
|
|
241
|
+
});
|
|
242
|
+
// Handle selection in main panel
|
|
243
|
+
this.layout.main.key(['enter'], async () => {
|
|
244
|
+
await this.handleMainSelection();
|
|
245
|
+
});
|
|
246
|
+
// Clear traffic log with 'c' when focused
|
|
247
|
+
this.layout.traffic.key(['c'], () => {
|
|
248
|
+
if (this.client) {
|
|
249
|
+
this.client.clearTrafficLog();
|
|
250
|
+
this.trafficLines = [];
|
|
251
|
+
this.layout.traffic.setContent('');
|
|
252
|
+
this.screen.render();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
setClient(client) {
|
|
257
|
+
this.client = client;
|
|
258
|
+
// Set up event handlers
|
|
259
|
+
client.on('connected', (result) => {
|
|
260
|
+
const serverName = result.serverInfo?.name || 'Unknown';
|
|
261
|
+
const serverVersion = result.serverInfo?.version || '?';
|
|
262
|
+
this.layout.status.setContent(`{green-fg}Connected{/green-fg} to ${this.escapeBlessedTags(serverName)} v${serverVersion} | ` +
|
|
263
|
+
`{cyan-fg}F1{/cyan-fg}=Nav {cyan-fg}F2{/cyan-fg}=Content {cyan-fg}F3{/cyan-fg}=Traffic {cyan-fg}F4{/cyan-fg}=Close {cyan-fg}F5{/cyan-fg}=Refresh {cyan-fg}F10{/cyan-fg}=Quit | ` +
|
|
264
|
+
`{bold}{cyan-fg}IntegSec{/cyan-fg}{/bold} {gray-fg}({green-fg}integsec.com{/green-fg}) - Need pentesting? Contact us!{/gray-fg}`);
|
|
265
|
+
this.updateCurrentView();
|
|
266
|
+
this.screen.render();
|
|
267
|
+
});
|
|
268
|
+
client.on('disconnected', () => {
|
|
269
|
+
this.layout.status.setContent('{red-fg}Status:{/red-fg} Disconnected');
|
|
270
|
+
this.screen.render();
|
|
271
|
+
});
|
|
272
|
+
client.on('error', (error) => {
|
|
273
|
+
this.addTrafficLine(`{red-fg}ERROR:{/red-fg} ${error.message}`);
|
|
274
|
+
this.screen.render();
|
|
275
|
+
});
|
|
276
|
+
client.on('traffic', ({ direction, data }) => {
|
|
277
|
+
const timestamp = new Date().toISOString().substr(11, 12);
|
|
278
|
+
if (direction === 'sent' && 'method' in data) {
|
|
279
|
+
// This is a request
|
|
280
|
+
const method = data.method;
|
|
281
|
+
let details = '';
|
|
282
|
+
if (data.method === 'tools/call' && data.params) {
|
|
283
|
+
details = ` tool=${data.params.name}`;
|
|
284
|
+
if (data.params.arguments) {
|
|
285
|
+
const argKeys = Object.keys(data.params.arguments);
|
|
286
|
+
if (argKeys.length > 0) {
|
|
287
|
+
details += ` args=${argKeys.join(',')}`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (data.method === 'resources/read' && data.params) {
|
|
292
|
+
details = ` uri=${data.params.uri}`;
|
|
293
|
+
}
|
|
294
|
+
else if (data.method === 'prompts/get' && data.params) {
|
|
295
|
+
details = ` prompt=${data.params.name}`;
|
|
296
|
+
}
|
|
297
|
+
const requestLine = `{cyan-fg}[${timestamp}]{/cyan-fg} {yellow-fg}>>>{/yellow-fg} ${this.escapeBlessedTags(method + details)}`;
|
|
298
|
+
if ('id' in data && data.id !== undefined) {
|
|
299
|
+
// Store request, waiting for response - store full data for detail view
|
|
300
|
+
this.pendingRequests.set(data.id, { line: requestLine, data, timestamp: new Date() });
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Notification (no response expected)
|
|
304
|
+
this.addTrafficLine(requestLine);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (direction === 'received') {
|
|
308
|
+
// This is a response
|
|
309
|
+
const responseId = ('id' in data) ? data.id : null;
|
|
310
|
+
let responseLine = '';
|
|
311
|
+
if ('error' in data && data.error) {
|
|
312
|
+
const errorMsg = this.escapeBlessedTags(String(data.error.message || 'Unknown error'));
|
|
313
|
+
responseLine = `{cyan-fg}[${timestamp}]{/cyan-fg} {red-fg}<<<{/red-fg} ERROR: ${errorMsg}`;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const resultPreview = ('result' in data && data.result)
|
|
317
|
+
? this.formatResultPreview(data.result)
|
|
318
|
+
: 'OK';
|
|
319
|
+
responseLine = `{cyan-fg}[${timestamp}]{/cyan-fg} {green-fg}<<<{/green-fg} ${resultPreview}`;
|
|
320
|
+
}
|
|
321
|
+
if (responseId !== null && this.pendingRequests.has(responseId)) {
|
|
322
|
+
// Found matching request - show summary in log, store full data for detail view
|
|
323
|
+
const { line: requestLine, data: requestData, timestamp: reqTime } = this.pendingRequests.get(responseId);
|
|
324
|
+
this.pendingRequests.delete(responseId);
|
|
325
|
+
// Add summary to traffic log
|
|
326
|
+
this.addTrafficPair(requestLine, responseLine);
|
|
327
|
+
// Store full data for detail view
|
|
328
|
+
this.trafficPairs.unshift({ request: requestData, response: data, timestamp: reqTime });
|
|
329
|
+
if (this.trafficPairs.length > 50) {
|
|
330
|
+
this.trafficPairs = this.trafficPairs.slice(0, 50);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// No matching request or notification
|
|
335
|
+
this.addTrafficLine(responseLine);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
this.screen.render();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
updatePanelHighlight() {
|
|
342
|
+
// Reset all borders to normal
|
|
343
|
+
this.layout.sidebar.style.border = { fg: 'cyan' };
|
|
344
|
+
this.layout.main.style.border = { fg: 'cyan' };
|
|
345
|
+
this.layout.traffic.style.border = { fg: 'green' };
|
|
346
|
+
// Highlight active panel with bold yellow border
|
|
347
|
+
switch (this.activePanel) {
|
|
348
|
+
case 'sidebar':
|
|
349
|
+
this.layout.sidebar.style.border = { fg: 'yellow', bold: true };
|
|
350
|
+
break;
|
|
351
|
+
case 'main':
|
|
352
|
+
this.layout.main.style.border = { fg: 'yellow', bold: true };
|
|
353
|
+
break;
|
|
354
|
+
case 'traffic':
|
|
355
|
+
this.layout.traffic.style.border = { fg: 'yellow', bold: true };
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
showTools() {
|
|
360
|
+
this.currentView = 'tools';
|
|
361
|
+
this.updateCurrentView();
|
|
362
|
+
}
|
|
363
|
+
showResources() {
|
|
364
|
+
this.currentView = 'resources';
|
|
365
|
+
this.updateCurrentView();
|
|
366
|
+
}
|
|
367
|
+
showPrompts() {
|
|
368
|
+
this.currentView = 'prompts';
|
|
369
|
+
this.updateCurrentView();
|
|
370
|
+
}
|
|
371
|
+
showTrafficLog() {
|
|
372
|
+
this.currentView = 'traffic';
|
|
373
|
+
this.updateCurrentView();
|
|
374
|
+
}
|
|
375
|
+
updateCurrentView() {
|
|
376
|
+
if (!this.client)
|
|
377
|
+
return;
|
|
378
|
+
const state = this.client.getState();
|
|
379
|
+
switch (this.currentView) {
|
|
380
|
+
case 'tools':
|
|
381
|
+
this.displayTools(state.tools);
|
|
382
|
+
break;
|
|
383
|
+
case 'resources':
|
|
384
|
+
this.displayResources(state.resources);
|
|
385
|
+
break;
|
|
386
|
+
case 'prompts':
|
|
387
|
+
this.displayPrompts(state.prompts);
|
|
388
|
+
break;
|
|
389
|
+
case 'traffic':
|
|
390
|
+
this.displayTraffic();
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
displayTools(tools) {
|
|
395
|
+
this.layout.main.setLabel(` Tools (${tools.length}) - Press Enter to execute `);
|
|
396
|
+
if (tools.length === 0) {
|
|
397
|
+
this.layout.main.setItems(['{yellow-fg}No tools available - Press F5 to refresh{/yellow-fg}']);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const items = tools.map((tool, idx) => {
|
|
401
|
+
let label = `${idx + 1}. {bold}${tool.name}{/bold}`;
|
|
402
|
+
if (tool.description) {
|
|
403
|
+
label += ` - ${tool.description}`;
|
|
404
|
+
}
|
|
405
|
+
return label;
|
|
406
|
+
});
|
|
407
|
+
this.layout.main.setItems(items);
|
|
408
|
+
}
|
|
409
|
+
this.screen.render();
|
|
410
|
+
}
|
|
411
|
+
displayResources(resources) {
|
|
412
|
+
this.layout.main.setLabel(` Resources (${resources.length}) - Press Enter to read `);
|
|
413
|
+
if (resources.length === 0) {
|
|
414
|
+
this.layout.main.setItems(['{yellow-fg}No resources available - Press F5 to refresh{/yellow-fg}']);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
const items = resources.map((resource, idx) => {
|
|
418
|
+
let label = `${idx + 1}. {bold}${resource.name}{/bold} - {gray-fg}${resource.uri}{/gray-fg}`;
|
|
419
|
+
if (resource.description) {
|
|
420
|
+
label += ` - ${resource.description}`;
|
|
421
|
+
}
|
|
422
|
+
return label;
|
|
423
|
+
});
|
|
424
|
+
this.layout.main.setItems(items);
|
|
425
|
+
}
|
|
426
|
+
this.screen.render();
|
|
427
|
+
}
|
|
428
|
+
displayPrompts(prompts) {
|
|
429
|
+
this.layout.main.setLabel(` Prompts (${prompts.length}) - Press Enter to use `);
|
|
430
|
+
if (prompts.length === 0) {
|
|
431
|
+
this.layout.main.setItems(['{yellow-fg}No prompts available - Press F5 to refresh{/yellow-fg}']);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
const items = prompts.map((prompt, idx) => {
|
|
435
|
+
let label = `${idx + 1}. {bold}${prompt.name}{/bold}`;
|
|
436
|
+
if (prompt.description) {
|
|
437
|
+
label += ` - ${prompt.description}`;
|
|
438
|
+
}
|
|
439
|
+
return label;
|
|
440
|
+
});
|
|
441
|
+
this.layout.main.setItems(items);
|
|
442
|
+
}
|
|
443
|
+
this.screen.render();
|
|
444
|
+
}
|
|
445
|
+
displayTraffic() {
|
|
446
|
+
if (!this.client)
|
|
447
|
+
return;
|
|
448
|
+
const logs = this.client.getTrafficLog();
|
|
449
|
+
this.layout.main.setLabel(` Traffic Details (${logs.length}) - Full Request/Response `);
|
|
450
|
+
if (logs.length === 0) {
|
|
451
|
+
this.layout.main.setItems(['{yellow-fg}No traffic logged yet{/yellow-fg}']);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
// Build full request/response pairs with details
|
|
455
|
+
const items = [];
|
|
456
|
+
const processedIds = new Set();
|
|
457
|
+
// Process in reverse order (most recent first)
|
|
458
|
+
for (let i = 0; i < Math.min(logs.length, 20); i++) {
|
|
459
|
+
const log = logs[i];
|
|
460
|
+
// Skip if we already processed this request/response pair
|
|
461
|
+
if ('id' in log.data) {
|
|
462
|
+
const logId = log.data.id;
|
|
463
|
+
if (processedIds.has(logId))
|
|
464
|
+
continue;
|
|
465
|
+
processedIds.add(logId);
|
|
466
|
+
}
|
|
467
|
+
// Find matching request/response pair
|
|
468
|
+
let requestLog = log;
|
|
469
|
+
let responseLog = null;
|
|
470
|
+
if (log.direction === 'received' && 'id' in log.data) {
|
|
471
|
+
const requestId = log.data.id;
|
|
472
|
+
// Find the request
|
|
473
|
+
for (let j = i + 1; j < logs.length; j++) {
|
|
474
|
+
if (logs[j].direction === 'sent' && 'id' in logs[j].data &&
|
|
475
|
+
logs[j].data.id === requestId) {
|
|
476
|
+
requestLog = logs[j];
|
|
477
|
+
responseLog = log;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (log.direction === 'sent' && 'id' in log.data) {
|
|
483
|
+
const requestId = log.data.id;
|
|
484
|
+
// Find the response
|
|
485
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
486
|
+
if (logs[j].direction === 'received' && 'id' in logs[j].data &&
|
|
487
|
+
logs[j].data.id === requestId) {
|
|
488
|
+
responseLog = logs[j];
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Format as compact single-line entry with timestamp and method
|
|
494
|
+
const timestamp = requestLog.timestamp.toISOString().substr(11, 12);
|
|
495
|
+
const method = ('method' in requestLog.data) ? requestLog.data.method : 'unknown';
|
|
496
|
+
const respStatus = responseLog
|
|
497
|
+
? ('error' in responseLog.data ? '{red-fg}ERROR{/red-fg}' : '{green-fg}OK{/green-fg}')
|
|
498
|
+
: '{gray-fg}pending{/gray-fg}';
|
|
499
|
+
items.push(`{gray-fg}[${timestamp}]{/gray-fg} {yellow-fg}${this.escapeBlessedTags(method)}{/yellow-fg} → ${respStatus}`);
|
|
500
|
+
}
|
|
501
|
+
this.layout.main.setItems(items);
|
|
502
|
+
}
|
|
503
|
+
this.screen.render();
|
|
504
|
+
}
|
|
505
|
+
escapeBlessedTags(text) {
|
|
506
|
+
// Escape curly braces so blessed doesn't try to parse them as tags
|
|
507
|
+
return text.replace(/{/g, '\\{').replace(/}/g, '\\}');
|
|
508
|
+
}
|
|
509
|
+
formatResultPreview(result) {
|
|
510
|
+
let preview;
|
|
511
|
+
if (typeof result === 'string') {
|
|
512
|
+
preview = result.length > 50 ? result.substring(0, 47) + '...' : result;
|
|
513
|
+
}
|
|
514
|
+
else if (Array.isArray(result)) {
|
|
515
|
+
preview = `Array[${result.length}]`;
|
|
516
|
+
}
|
|
517
|
+
else if (typeof result === 'object' && result !== null) {
|
|
518
|
+
const keys = Object.keys(result);
|
|
519
|
+
if (keys.length === 0)
|
|
520
|
+
return '{}';
|
|
521
|
+
preview = `{${keys.slice(0, 3).join(',')}}`;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
preview = String(result);
|
|
525
|
+
}
|
|
526
|
+
return this.escapeBlessedTags(preview);
|
|
527
|
+
}
|
|
528
|
+
addTrafficPair(requestLine, responseLine) {
|
|
529
|
+
// Add simple summary lines to the small traffic log
|
|
530
|
+
this.trafficLines.unshift(responseLine);
|
|
531
|
+
this.trafficLines.unshift(requestLine);
|
|
532
|
+
// Keep only last 100 lines
|
|
533
|
+
if (this.trafficLines.length > 100) {
|
|
534
|
+
this.trafficLines = this.trafficLines.slice(0, 100);
|
|
535
|
+
}
|
|
536
|
+
// Update the traffic box content
|
|
537
|
+
this.layout.traffic.setContent(this.trafficLines.join('\n'));
|
|
538
|
+
this.layout.traffic.setScrollPerc(0);
|
|
539
|
+
}
|
|
540
|
+
addTrafficLine(line) {
|
|
541
|
+
this.trafficLines.unshift(line);
|
|
542
|
+
if (this.trafficLines.length > 100) {
|
|
543
|
+
this.trafficLines = this.trafficLines.slice(0, 100);
|
|
544
|
+
}
|
|
545
|
+
this.layout.traffic.setContent(this.trafficLines.join('\n'));
|
|
546
|
+
}
|
|
547
|
+
render() {
|
|
548
|
+
// Set up the main UI first
|
|
549
|
+
this.activePanel = 'sidebar';
|
|
550
|
+
this.updatePanelHighlight();
|
|
551
|
+
this.layout.sidebar.focus();
|
|
552
|
+
this.screen.render();
|
|
553
|
+
// Show splash screen as overlay (auto-closes)
|
|
554
|
+
this.showSplashScreen();
|
|
555
|
+
}
|
|
556
|
+
async prompt(label) {
|
|
557
|
+
return new Promise((resolve) => {
|
|
558
|
+
this.layout.input.setLabel(` ${label} `);
|
|
559
|
+
this.layout.input.show();
|
|
560
|
+
this.layout.input.focus();
|
|
561
|
+
this.layout.input.on('submit', (value) => {
|
|
562
|
+
this.layout.input.hide();
|
|
563
|
+
this.layout.input.clearValue();
|
|
564
|
+
this.layout.sidebar.focus();
|
|
565
|
+
this.screen.render();
|
|
566
|
+
resolve(value || '');
|
|
567
|
+
});
|
|
568
|
+
this.layout.input.on('cancel', () => {
|
|
569
|
+
this.layout.input.hide();
|
|
570
|
+
this.layout.input.clearValue();
|
|
571
|
+
this.layout.sidebar.focus();
|
|
572
|
+
this.screen.render();
|
|
573
|
+
resolve('');
|
|
574
|
+
});
|
|
575
|
+
this.screen.render();
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
async handleMainSelection() {
|
|
579
|
+
if (!this.client)
|
|
580
|
+
return;
|
|
581
|
+
const selectedIndex = this.layout.main.selected || 0;
|
|
582
|
+
const state = this.client.getState();
|
|
583
|
+
try {
|
|
584
|
+
switch (this.currentView) {
|
|
585
|
+
case 'tools':
|
|
586
|
+
if (selectedIndex < state.tools.length) {
|
|
587
|
+
await this.executeTool(state.tools[selectedIndex]);
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
case 'resources':
|
|
591
|
+
if (selectedIndex < state.resources.length) {
|
|
592
|
+
await this.readResource(state.resources[selectedIndex]);
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
case 'prompts':
|
|
596
|
+
if (selectedIndex < state.prompts.length) {
|
|
597
|
+
await this.usePrompt(state.prompts[selectedIndex]);
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
case 'traffic':
|
|
601
|
+
const logs = this.client.getTrafficLog().slice(-50);
|
|
602
|
+
if (selectedIndex < logs.length) {
|
|
603
|
+
await this.showTrafficDetail(logs[selectedIndex]);
|
|
604
|
+
}
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
this.showMessage('Error', error.message || String(error), 'red');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async executeTool(tool) {
|
|
613
|
+
// Build arguments
|
|
614
|
+
const args = {};
|
|
615
|
+
if (tool.inputSchema?.properties) {
|
|
616
|
+
const props = tool.inputSchema.properties;
|
|
617
|
+
const required = tool.inputSchema.required || [];
|
|
618
|
+
for (const paramName of Object.keys(props)) {
|
|
619
|
+
const isRequired = required.includes(paramName);
|
|
620
|
+
const prompt = `${paramName}${isRequired ? ' (required)' : ' (optional)'}:`;
|
|
621
|
+
const value = await this.prompt(prompt);
|
|
622
|
+
if (value || isRequired) {
|
|
623
|
+
// Try to parse as JSON for complex types
|
|
624
|
+
try {
|
|
625
|
+
args[paramName] = JSON.parse(value);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
args[paramName] = value;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const result = await this.client.callTool(tool.name, args);
|
|
634
|
+
const formatted = this.formatForDisplay(result);
|
|
635
|
+
this.showMessage(`Tool Result: ${tool.name}`, formatted, 'green');
|
|
636
|
+
}
|
|
637
|
+
async readResource(resource) {
|
|
638
|
+
let uri = resource.uri;
|
|
639
|
+
// Check if URI contains parameters like {id}, {path}, etc.
|
|
640
|
+
const paramMatches = uri.match(/\{([^}]+)\}/g);
|
|
641
|
+
if (paramMatches) {
|
|
642
|
+
// Extract parameter names and prompt for values
|
|
643
|
+
for (const paramMatch of paramMatches) {
|
|
644
|
+
const paramName = paramMatch.slice(1, -1); // Remove { and }
|
|
645
|
+
const value = await this.prompt(`Enter value for {${paramName}}:`);
|
|
646
|
+
if (!value) {
|
|
647
|
+
this.showMessage('Error', `Parameter {${paramName}} is required`, 'red');
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// Replace the parameter in the URI
|
|
651
|
+
uri = uri.replace(paramMatch, value);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const result = await this.client.readResource(uri);
|
|
655
|
+
const formatted = this.formatForDisplay(result);
|
|
656
|
+
this.showMessage(`Resource: ${resource.name}`, formatted, 'cyan');
|
|
657
|
+
}
|
|
658
|
+
async usePrompt(prompt) {
|
|
659
|
+
const args = {};
|
|
660
|
+
if (prompt.arguments && prompt.arguments.length > 0) {
|
|
661
|
+
for (const arg of prompt.arguments) {
|
|
662
|
+
const isRequired = arg.required || false;
|
|
663
|
+
const promptText = `${arg.name}${isRequired ? ' (required)' : ' (optional)'}:`;
|
|
664
|
+
const value = await this.prompt(promptText);
|
|
665
|
+
if (value || isRequired) {
|
|
666
|
+
args[arg.name] = value;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const result = await this.client.getPrompt(prompt.name, args);
|
|
671
|
+
const formatted = this.formatForDisplay(result);
|
|
672
|
+
this.showMessage(`Prompt: ${prompt.name}`, formatted, 'magenta');
|
|
673
|
+
}
|
|
674
|
+
async showTrafficDetail(log) {
|
|
675
|
+
// Find matching request/response pair
|
|
676
|
+
const logs = this.client.getTrafficLog();
|
|
677
|
+
const logIndex = logs.indexOf(log);
|
|
678
|
+
let requestLog = log;
|
|
679
|
+
let responseLog = null;
|
|
680
|
+
// If this is a response, find its request
|
|
681
|
+
if (log.direction === 'received' && 'id' in log.data) {
|
|
682
|
+
const requestId = log.data.id;
|
|
683
|
+
if (requestId !== undefined && requestId !== null) {
|
|
684
|
+
// Look backwards for the request with matching ID
|
|
685
|
+
for (let i = logIndex + 1; i < logs.length; i++) {
|
|
686
|
+
const logData = logs[i].data;
|
|
687
|
+
if (logs[i].direction === 'sent' && 'id' in logs[i].data &&
|
|
688
|
+
logData.id !== undefined && logData.id === requestId) {
|
|
689
|
+
requestLog = logs[i];
|
|
690
|
+
responseLog = log;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// If this is a request, find its response
|
|
697
|
+
else if (log.direction === 'sent' && 'id' in log.data) {
|
|
698
|
+
const requestId = log.data.id;
|
|
699
|
+
if (requestId !== undefined && requestId !== null) {
|
|
700
|
+
// Look backwards for response with matching ID
|
|
701
|
+
for (let i = logIndex - 1; i >= 0; i--) {
|
|
702
|
+
const logData = logs[i].data;
|
|
703
|
+
if (logs[i].direction === 'received' && 'id' in logs[i].data &&
|
|
704
|
+
logData.id !== undefined && logData.id === requestId) {
|
|
705
|
+
responseLog = logs[i];
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Format request and response JSON (already formatted with indentation)
|
|
712
|
+
const requestJson = this.formatForDisplay(requestLog.data);
|
|
713
|
+
const responseJson = responseLog
|
|
714
|
+
? this.formatForDisplay(responseLog.data)
|
|
715
|
+
: '(pending - no response yet)';
|
|
716
|
+
// Split into lines for side-by-side display
|
|
717
|
+
const requestLines = requestJson.split('\n');
|
|
718
|
+
const responseLines = responseJson.split('\n');
|
|
719
|
+
const maxLines = Math.max(requestLines.length, responseLines.length);
|
|
720
|
+
// Calculate column width (half of 90% screen width, minus separator)
|
|
721
|
+
const colWidth = 60;
|
|
722
|
+
// Build side-by-side display
|
|
723
|
+
const lines = [];
|
|
724
|
+
lines.push(`{bold}{yellow-fg}REQUEST{/yellow-fg}{/bold} - ${requestLog.timestamp.toISOString()}`.padEnd(colWidth) +
|
|
725
|
+
' {gray-fg}│{/gray-fg} ' +
|
|
726
|
+
`{bold}{green-fg}RESPONSE{/green-fg}{/bold}${responseLog ? ' - ' + responseLog.timestamp.toISOString() : ''}`);
|
|
727
|
+
lines.push(`{gray-fg}${'─'.repeat(colWidth)}{/gray-fg} {gray-fg}┼{/gray-fg} {gray-fg}${'─'.repeat(colWidth)}{/gray-fg}`);
|
|
728
|
+
for (let i = 0; i < maxLines; i++) {
|
|
729
|
+
const reqLine = requestLines[i] || '';
|
|
730
|
+
const respLine = responseLines[i] || '';
|
|
731
|
+
// Pad request line to column width
|
|
732
|
+
const paddedReq = reqLine.padEnd(colWidth).substring(0, colWidth);
|
|
733
|
+
lines.push(paddedReq + ' {gray-fg}│{/gray-fg} ' + respLine);
|
|
734
|
+
}
|
|
735
|
+
this.showMessage('Traffic Detail - Request/Response Pair', lines.join('\n'), 'cyan');
|
|
736
|
+
}
|
|
737
|
+
formatXML(xml) {
|
|
738
|
+
// Simple XML formatter with indentation
|
|
739
|
+
let formatted = '';
|
|
740
|
+
let indent = 0;
|
|
741
|
+
const tab = ' ';
|
|
742
|
+
xml.split(/>\s*</).forEach((node, index) => {
|
|
743
|
+
// Add back the angle brackets
|
|
744
|
+
if (index > 0)
|
|
745
|
+
node = '<' + node;
|
|
746
|
+
if (index < xml.split(/>\s*</).length - 1)
|
|
747
|
+
node = node + '>';
|
|
748
|
+
// Check if it's a closing tag
|
|
749
|
+
if (node.match(/^<\/\w/)) {
|
|
750
|
+
indent--;
|
|
751
|
+
}
|
|
752
|
+
// Add the indented line
|
|
753
|
+
formatted += tab.repeat(Math.max(0, indent)) + node + '\n';
|
|
754
|
+
// Check if it's an opening tag (not self-closing and not closing)
|
|
755
|
+
if (node.match(/^<\w[^>]*[^\/]>$/)) {
|
|
756
|
+
indent++;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
// Add syntax highlighting for XML
|
|
760
|
+
return formatted
|
|
761
|
+
.replace(/<(\/?[\w:]+)/g, '{cyan-fg}<$1{/cyan-fg}') // Tag names
|
|
762
|
+
.replace(/([\w:]+)=/g, '{yellow-fg}$1{/yellow-fg}=') // Attributes
|
|
763
|
+
.replace(/="([^"]*)"/g, '="{green-fg}$1{/green-fg}"') // Attribute values
|
|
764
|
+
.replace(/>/g, '{cyan-fg}>{/cyan-fg}'); // Closing brackets
|
|
765
|
+
}
|
|
766
|
+
formatForDisplay(data) {
|
|
767
|
+
// Smart formatter for better readability
|
|
768
|
+
if (typeof data === 'string') {
|
|
769
|
+
// Check if it's XML
|
|
770
|
+
if (data.trim().startsWith('<') && data.trim().includes('</')) {
|
|
771
|
+
try {
|
|
772
|
+
return this.formatXML(data);
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
// If XML formatting fails, continue to JSON attempt
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Try to parse as JSON for better formatting
|
|
779
|
+
try {
|
|
780
|
+
const parsed = JSON.parse(data);
|
|
781
|
+
const json = JSON.stringify(parsed, null, 2);
|
|
782
|
+
// Add color hints for better readability
|
|
783
|
+
return json
|
|
784
|
+
.replace(/"([^"]+)":/g, '{cyan-fg}"$1"{/cyan-fg}:') // Property names
|
|
785
|
+
.replace(/: "([^"]*?)"/g, ': {green-fg}"$1"{/green-fg}') // String values
|
|
786
|
+
.replace(/: (\d+)/g, ': {yellow-fg}$1{/yellow-fg}') // Numbers
|
|
787
|
+
.replace(/: (true|false|null)/g, ': {magenta-fg}$1{/magenta-fg}'); // Keywords
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
// Not JSON, return as-is
|
|
791
|
+
return data;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (typeof data === 'object' && data !== null) {
|
|
795
|
+
// Format objects with syntax highlighting hints
|
|
796
|
+
const json = JSON.stringify(data, null, 2);
|
|
797
|
+
// Add color hints for better readability
|
|
798
|
+
return json
|
|
799
|
+
.replace(/"([^"]+)":/g, '{cyan-fg}"$1"{/cyan-fg}:') // Property names
|
|
800
|
+
.replace(/: "([^"]*?)"/g, ': {green-fg}"$1"{/green-fg}') // String values
|
|
801
|
+
.replace(/: (\d+)/g, ': {yellow-fg}$1{/yellow-fg}') // Numbers
|
|
802
|
+
.replace(/: (true|false|null)/g, ': {magenta-fg}$1{/magenta-fg}'); // Keywords
|
|
803
|
+
}
|
|
804
|
+
return String(data);
|
|
805
|
+
}
|
|
806
|
+
showMessage(title, content, color) {
|
|
807
|
+
// Store both raw and rendered versions
|
|
808
|
+
const rawContent = content;
|
|
809
|
+
const renderedContent = content.replace(/\\n/g, '\n').replace(/\\t/g, ' ');
|
|
810
|
+
// Store for toggling
|
|
811
|
+
this.currentPopupContent = { raw: rawContent, rendered: renderedContent, title, color };
|
|
812
|
+
this.showRawInPopup = false;
|
|
813
|
+
const msg = blessed_1.default.box({
|
|
814
|
+
parent: this.screen,
|
|
815
|
+
top: 'center',
|
|
816
|
+
left: 'center',
|
|
817
|
+
width: '90%',
|
|
818
|
+
height: '85%',
|
|
819
|
+
label: ` ${title} - F4/ESC=close W=toggle PgUp/PgDn=scroll `,
|
|
820
|
+
tags: true,
|
|
821
|
+
border: { type: 'line' },
|
|
822
|
+
style: {
|
|
823
|
+
border: { fg: color, bold: true },
|
|
824
|
+
},
|
|
825
|
+
scrollable: true,
|
|
826
|
+
alwaysScroll: true,
|
|
827
|
+
keys: true,
|
|
828
|
+
vi: true,
|
|
829
|
+
mouse: true,
|
|
830
|
+
scrollbar: {
|
|
831
|
+
ch: '█',
|
|
832
|
+
track: {
|
|
833
|
+
ch: '░',
|
|
834
|
+
},
|
|
835
|
+
style: {
|
|
836
|
+
fg: color,
|
|
837
|
+
bg: 'black',
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
content: this.showRawInPopup ? rawContent : renderedContent,
|
|
841
|
+
});
|
|
842
|
+
// Page Up/Page Down for scrolling
|
|
843
|
+
msg.key(['pageup'], () => {
|
|
844
|
+
msg.scroll(-10);
|
|
845
|
+
this.screen.render();
|
|
846
|
+
});
|
|
847
|
+
msg.key(['pagedown'], () => {
|
|
848
|
+
msg.scroll(10);
|
|
849
|
+
this.screen.render();
|
|
850
|
+
});
|
|
851
|
+
// W to toggle between raw and rendered
|
|
852
|
+
msg.key(['w', 'W'], () => {
|
|
853
|
+
this.showRawInPopup = !this.showRawInPopup;
|
|
854
|
+
if (this.currentPopupContent) {
|
|
855
|
+
msg.setContent(this.showRawInPopup ? this.currentPopupContent.raw : this.currentPopupContent.rendered);
|
|
856
|
+
msg.setLabel(` ${this.currentPopupContent.title} - Press F4/ESC to close, W to toggle raw/rendered ${this.showRawInPopup ? '(RAW)' : '(RENDERED)'} `);
|
|
857
|
+
this.screen.render();
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
// F4 or ESC to close
|
|
861
|
+
msg.key(['f4', 'escape', 'enter'], () => {
|
|
862
|
+
this.currentPopupContent = null;
|
|
863
|
+
this.showRawInPopup = false;
|
|
864
|
+
msg.destroy();
|
|
865
|
+
this.screen.render();
|
|
866
|
+
});
|
|
867
|
+
msg.focus();
|
|
868
|
+
this.screen.render();
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
exports.TUI = TUI;
|
|
872
|
+
//# sourceMappingURL=tui.js.map
|