50c 3.3.0 → 3.7.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/bin/50c.js +207 -2
- package/lib/invent-ui.js +717 -0
- package/lib/mcp-tv.js +1015 -0
- package/lib/team.js +47 -5
- package/package.json +1 -1
package/lib/mcp-tv.js
ADDED
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP-TV: Universal MCP Visualization Platform
|
|
4
|
+
* FREE - Any MCP can stream JSON events to display in browser
|
|
5
|
+
*
|
|
6
|
+
* Protocol: POST JSON to http://localhost:50888/stream
|
|
7
|
+
* {
|
|
8
|
+
* "channel": "my-mcp-name",
|
|
9
|
+
* "event": "stage|progress|result|error|log",
|
|
10
|
+
* "data": { ...any payload... }
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Or use Server-Sent Events: GET /events?channel=my-mcp
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* npx 50c tv # Start MCP-TV server
|
|
17
|
+
* npx 50c tv --port=3000 # Custom port
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const http = require('http');
|
|
21
|
+
const { spawn } = require('child_process');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
|
|
24
|
+
const DEFAULT_PORT = 50888;
|
|
25
|
+
|
|
26
|
+
// Channel state management
|
|
27
|
+
const channels = new Map();
|
|
28
|
+
const clients = new Map(); // channel -> [response streams]
|
|
29
|
+
|
|
30
|
+
function getChannel(name) {
|
|
31
|
+
if (!channels.has(name)) {
|
|
32
|
+
channels.set(name, {
|
|
33
|
+
name,
|
|
34
|
+
created: Date.now(),
|
|
35
|
+
events: [],
|
|
36
|
+
stages: [],
|
|
37
|
+
status: 'idle',
|
|
38
|
+
lastUpdate: Date.now()
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return channels.get(name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function broadcast(channel, event) {
|
|
45
|
+
const channelClients = clients.get(channel) || [];
|
|
46
|
+
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
47
|
+
channelClients.forEach(res => {
|
|
48
|
+
try { res.write(data); } catch (e) {}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Also broadcast to "all" channel listeners
|
|
52
|
+
const allClients = clients.get('__all__') || [];
|
|
53
|
+
allClients.forEach(res => {
|
|
54
|
+
try { res.write(data); } catch (e) {}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const HTML_PAGE = `<!DOCTYPE html>
|
|
59
|
+
<html lang="en">
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="UTF-8">
|
|
62
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
63
|
+
<meta name="theme-color" content="#141414">
|
|
64
|
+
<meta name="description" content="MCP-TV: Universal visualization for Model Context Protocol tools">
|
|
65
|
+
<link rel="manifest" href="/manifest.json">
|
|
66
|
+
<title>MCP-TV</title>
|
|
67
|
+
<style>
|
|
68
|
+
:root {
|
|
69
|
+
--bg-0: #0d0d0d;
|
|
70
|
+
--bg-1: #141414;
|
|
71
|
+
--bg-2: #1a1a1a;
|
|
72
|
+
--bg-3: #222222;
|
|
73
|
+
--bg-4: #2a2a2a;
|
|
74
|
+
--border: rgba(255,255,255,0.06);
|
|
75
|
+
--border-hover: rgba(255,255,255,0.1);
|
|
76
|
+
--text-0: #f5f5f5;
|
|
77
|
+
--text-1: #a0a0a0;
|
|
78
|
+
--text-2: #606060;
|
|
79
|
+
--accent: #4a9a8a;
|
|
80
|
+
--accent-dim: rgba(74,154,138,0.15);
|
|
81
|
+
--running: #8a8a4a;
|
|
82
|
+
--error: #8a4a4a;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
86
|
+
|
|
87
|
+
body {
|
|
88
|
+
background: var(--bg-0);
|
|
89
|
+
color: var(--text-1);
|
|
90
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
|
|
91
|
+
font-size: 13px;
|
|
92
|
+
min-height: 100vh;
|
|
93
|
+
line-height: 1.5;
|
|
94
|
+
-webkit-font-smoothing: antialiased;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.header {
|
|
98
|
+
background: var(--bg-1);
|
|
99
|
+
border-bottom: 1px solid var(--border);
|
|
100
|
+
padding: 0 20px;
|
|
101
|
+
height: 48px;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: space-between;
|
|
105
|
+
position: sticky;
|
|
106
|
+
top: 0;
|
|
107
|
+
z-index: 100;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.logo {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
font-weight: 500;
|
|
115
|
+
font-size: 14px;
|
|
116
|
+
color: var(--text-0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.logo-icon {
|
|
120
|
+
width: 24px;
|
|
121
|
+
height: 24px;
|
|
122
|
+
background: var(--bg-3);
|
|
123
|
+
border: 1px solid var(--border);
|
|
124
|
+
border-radius: 6px;
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
font-size: 12px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.header-actions {
|
|
132
|
+
display: flex;
|
|
133
|
+
gap: 12px;
|
|
134
|
+
align-items: center;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.channel-selector {
|
|
138
|
+
background: var(--bg-2);
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
padding: 5px 10px;
|
|
142
|
+
color: var(--text-1);
|
|
143
|
+
font-size: 12px;
|
|
144
|
+
outline: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.channel-selector:focus {
|
|
148
|
+
border-color: var(--border-hover);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.status-dot {
|
|
152
|
+
width: 6px;
|
|
153
|
+
height: 6px;
|
|
154
|
+
border-radius: 50%;
|
|
155
|
+
background: var(--accent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.status-dot.live {
|
|
159
|
+
animation: pulse 2.5s ease-in-out infinite;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@keyframes pulse {
|
|
163
|
+
0%, 100% { opacity: 0.4; }
|
|
164
|
+
50% { opacity: 1; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.main {
|
|
168
|
+
display: grid;
|
|
169
|
+
grid-template-columns: 220px 1fr;
|
|
170
|
+
min-height: calc(100vh - 48px);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.sidebar {
|
|
174
|
+
background: var(--bg-1);
|
|
175
|
+
border-right: 1px solid var(--border);
|
|
176
|
+
overflow-y: auto;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.sidebar-section {
|
|
180
|
+
padding: 16px 12px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.sidebar-title {
|
|
184
|
+
font-size: 10px;
|
|
185
|
+
text-transform: uppercase;
|
|
186
|
+
letter-spacing: 0.5px;
|
|
187
|
+
color: var(--text-2);
|
|
188
|
+
margin-bottom: 10px;
|
|
189
|
+
padding: 0 8px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.channel-list {
|
|
193
|
+
display: flex;
|
|
194
|
+
flex-direction: column;
|
|
195
|
+
gap: 2px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.channel-item {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 10px;
|
|
202
|
+
padding: 8px;
|
|
203
|
+
border-radius: 6px;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
transition: background 0.15s;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.channel-item:hover { background: var(--bg-2); }
|
|
209
|
+
.channel-item.active {
|
|
210
|
+
background: var(--accent-dim);
|
|
211
|
+
border-left: 2px solid var(--accent);
|
|
212
|
+
padding-left: 6px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.channel-icon {
|
|
216
|
+
width: 28px;
|
|
217
|
+
height: 28px;
|
|
218
|
+
background: var(--bg-3);
|
|
219
|
+
border-radius: 6px;
|
|
220
|
+
display: flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: center;
|
|
223
|
+
font-size: 14px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.channel-info { flex: 1; min-width: 0; }
|
|
227
|
+
.channel-name { font-size: 12px; color: var(--text-0); font-weight: 500; }
|
|
228
|
+
.channel-status { font-size: 10px; color: var(--text-2); }
|
|
229
|
+
|
|
230
|
+
.channel-badge {
|
|
231
|
+
font-size: 9px;
|
|
232
|
+
padding: 2px 5px;
|
|
233
|
+
border-radius: 3px;
|
|
234
|
+
background: var(--accent-dim);
|
|
235
|
+
color: var(--accent);
|
|
236
|
+
font-weight: 500;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.content {
|
|
240
|
+
padding: 20px 24px;
|
|
241
|
+
overflow-y: auto;
|
|
242
|
+
background: var(--bg-0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.empty-state {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
align-items: center;
|
|
249
|
+
justify-content: center;
|
|
250
|
+
height: 60vh;
|
|
251
|
+
text-align: center;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.empty-icon {
|
|
255
|
+
font-size: 48px;
|
|
256
|
+
margin-bottom: 16px;
|
|
257
|
+
opacity: 0.3;
|
|
258
|
+
filter: grayscale(1);
|
|
259
|
+
}
|
|
260
|
+
.empty-title {
|
|
261
|
+
font-size: 16px;
|
|
262
|
+
color: var(--text-0);
|
|
263
|
+
margin-bottom: 6px;
|
|
264
|
+
font-weight: 500;
|
|
265
|
+
}
|
|
266
|
+
.empty-subtitle {
|
|
267
|
+
max-width: 360px;
|
|
268
|
+
color: var(--text-2);
|
|
269
|
+
font-size: 12px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.code-block {
|
|
273
|
+
background: var(--bg-1);
|
|
274
|
+
border: 1px solid var(--border);
|
|
275
|
+
border-radius: 6px;
|
|
276
|
+
padding: 14px 16px;
|
|
277
|
+
margin-top: 20px;
|
|
278
|
+
text-align: left;
|
|
279
|
+
font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
|
|
280
|
+
font-size: 11px;
|
|
281
|
+
overflow-x: auto;
|
|
282
|
+
color: var(--text-1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.code-block code { color: var(--accent); }
|
|
286
|
+
|
|
287
|
+
.pipeline-header {
|
|
288
|
+
display: flex;
|
|
289
|
+
align-items: center;
|
|
290
|
+
justify-content: space-between;
|
|
291
|
+
margin-bottom: 20px;
|
|
292
|
+
padding-bottom: 12px;
|
|
293
|
+
border-bottom: 1px solid var(--border);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.pipeline-title {
|
|
297
|
+
font-size: 14px;
|
|
298
|
+
font-weight: 500;
|
|
299
|
+
color: var(--text-0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.pipeline-meta {
|
|
303
|
+
display: flex;
|
|
304
|
+
gap: 16px;
|
|
305
|
+
font-size: 11px;
|
|
306
|
+
color: var(--text-2);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.pipeline-stages {
|
|
310
|
+
display: flex;
|
|
311
|
+
gap: 6px;
|
|
312
|
+
flex-wrap: wrap;
|
|
313
|
+
margin-bottom: 24px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.stage {
|
|
317
|
+
flex: 1;
|
|
318
|
+
min-width: 80px;
|
|
319
|
+
max-width: 120px;
|
|
320
|
+
background: var(--bg-1);
|
|
321
|
+
border: 1px solid var(--border);
|
|
322
|
+
border-radius: 6px;
|
|
323
|
+
padding: 12px 10px;
|
|
324
|
+
text-align: center;
|
|
325
|
+
position: relative;
|
|
326
|
+
transition: all 0.2s;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.stage::after {
|
|
330
|
+
content: '';
|
|
331
|
+
position: absolute;
|
|
332
|
+
top: 50%;
|
|
333
|
+
right: -8px;
|
|
334
|
+
width: 6px;
|
|
335
|
+
height: 1px;
|
|
336
|
+
background: var(--border);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.stage:last-child::after { display: none; }
|
|
340
|
+
|
|
341
|
+
.stage.pending { opacity: 0.4; }
|
|
342
|
+
.stage.running {
|
|
343
|
+
border-color: var(--running);
|
|
344
|
+
background: rgba(138,138,74,0.08);
|
|
345
|
+
}
|
|
346
|
+
.stage.complete {
|
|
347
|
+
border-color: var(--accent);
|
|
348
|
+
border-color: rgba(74,154,138,0.4);
|
|
349
|
+
}
|
|
350
|
+
.stage.error {
|
|
351
|
+
border-color: var(--error);
|
|
352
|
+
background: rgba(138,74,74,0.08);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.stage-icon { font-size: 18px; margin-bottom: 4px; filter: grayscale(0.5); }
|
|
356
|
+
.stage-name { font-size: 9px; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.3px; }
|
|
357
|
+
.stage-time { font-size: 9px; color: var(--text-2); margin-top: 4px; }
|
|
358
|
+
|
|
359
|
+
.stage.running .stage-name { color: var(--running); }
|
|
360
|
+
.stage.complete .stage-name { color: var(--accent); }
|
|
361
|
+
|
|
362
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
363
|
+
|
|
364
|
+
.event-log {
|
|
365
|
+
background: var(--bg-1);
|
|
366
|
+
border: 1px solid var(--border);
|
|
367
|
+
border-radius: 6px;
|
|
368
|
+
overflow: hidden;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.event-log-header {
|
|
372
|
+
padding: 10px 14px;
|
|
373
|
+
border-bottom: 1px solid var(--border);
|
|
374
|
+
font-size: 11px;
|
|
375
|
+
font-weight: 500;
|
|
376
|
+
color: var(--text-1);
|
|
377
|
+
display: flex;
|
|
378
|
+
align-items: center;
|
|
379
|
+
gap: 6px;
|
|
380
|
+
background: var(--bg-2);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.event-log-content {
|
|
384
|
+
max-height: 320px;
|
|
385
|
+
overflow-y: auto;
|
|
386
|
+
font-family: 'SF Mono', Consolas, monospace;
|
|
387
|
+
font-size: 11px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.event-log-content::-webkit-scrollbar { width: 6px; }
|
|
391
|
+
.event-log-content::-webkit-scrollbar-track { background: transparent; }
|
|
392
|
+
.event-log-content::-webkit-scrollbar-thumb { background: var(--bg-4); border-radius: 3px; }
|
|
393
|
+
|
|
394
|
+
.event-item {
|
|
395
|
+
padding: 6px 14px;
|
|
396
|
+
border-bottom: 1px solid rgba(255,255,255,0.02);
|
|
397
|
+
display: flex;
|
|
398
|
+
gap: 12px;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.event-item:hover { background: var(--bg-2); }
|
|
402
|
+
|
|
403
|
+
.event-time { color: var(--text-2); min-width: 60px; }
|
|
404
|
+
.event-type { min-width: 50px; font-weight: 500; }
|
|
405
|
+
.event-type.stage { color: var(--accent); }
|
|
406
|
+
.event-type.progress { color: var(--running); }
|
|
407
|
+
.event-type.result { color: var(--accent); }
|
|
408
|
+
.event-type.error { color: var(--error); }
|
|
409
|
+
.event-type.log { color: var(--text-2); }
|
|
410
|
+
.event-data { flex: 1; color: var(--text-1); word-break: break-word; }
|
|
411
|
+
|
|
412
|
+
.result-card {
|
|
413
|
+
background: var(--bg-1);
|
|
414
|
+
border: 1px solid var(--accent);
|
|
415
|
+
border-color: rgba(74,154,138,0.3);
|
|
416
|
+
border-radius: 6px;
|
|
417
|
+
padding: 16px;
|
|
418
|
+
margin-top: 20px;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.result-card h3 {
|
|
422
|
+
color: var(--accent);
|
|
423
|
+
font-size: 11px;
|
|
424
|
+
font-weight: 500;
|
|
425
|
+
text-transform: uppercase;
|
|
426
|
+
letter-spacing: 0.5px;
|
|
427
|
+
margin-bottom: 12px;
|
|
428
|
+
display: flex;
|
|
429
|
+
align-items: center;
|
|
430
|
+
gap: 6px;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.result-content {
|
|
434
|
+
background: var(--bg-0);
|
|
435
|
+
border-radius: 4px;
|
|
436
|
+
padding: 14px;
|
|
437
|
+
max-height: 400px;
|
|
438
|
+
overflow-y: auto;
|
|
439
|
+
white-space: pre-wrap;
|
|
440
|
+
font-size: 12px;
|
|
441
|
+
line-height: 1.6;
|
|
442
|
+
color: var(--text-1);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.result-content::-webkit-scrollbar { width: 6px; }
|
|
446
|
+
.result-content::-webkit-scrollbar-track { background: transparent; }
|
|
447
|
+
.result-content::-webkit-scrollbar-thumb { background: var(--bg-4); border-radius: 3px; }
|
|
448
|
+
|
|
449
|
+
@media (max-width: 768px) {
|
|
450
|
+
.main { grid-template-columns: 1fr; }
|
|
451
|
+
.sidebar { display: none; }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.install-banner {
|
|
455
|
+
display: none;
|
|
456
|
+
background: var(--bg-2);
|
|
457
|
+
border-bottom: 1px solid var(--border);
|
|
458
|
+
color: var(--text-1);
|
|
459
|
+
padding: 8px 20px;
|
|
460
|
+
text-align: center;
|
|
461
|
+
font-size: 11px;
|
|
462
|
+
cursor: pointer;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.install-banner:hover { background: var(--bg-3); }
|
|
466
|
+
.install-banner.show { display: block; }
|
|
467
|
+
</style>
|
|
468
|
+
</head>
|
|
469
|
+
<body>
|
|
470
|
+
<div id="install-banner" class="install-banner" onclick="installPWA()">
|
|
471
|
+
📱 Install MCP-TV as an app for quick access
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<header class="header">
|
|
475
|
+
<div class="logo">
|
|
476
|
+
<div class="logo-icon">📺</div>
|
|
477
|
+
<span>MCP-TV</span>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="header-actions">
|
|
480
|
+
<select id="channel-select" class="channel-selector" onchange="switchChannel(this.value)">
|
|
481
|
+
<option value="__all__">All Channels</option>
|
|
482
|
+
</select>
|
|
483
|
+
<div class="status-dot" title="Connected"></div>
|
|
484
|
+
</div>
|
|
485
|
+
</header>
|
|
486
|
+
|
|
487
|
+
<main class="main">
|
|
488
|
+
<aside class="sidebar">
|
|
489
|
+
<div class="sidebar-section">
|
|
490
|
+
<div class="sidebar-title">Active Channels</div>
|
|
491
|
+
<div id="channel-list" class="channel-list"></div>
|
|
492
|
+
</div>
|
|
493
|
+
</aside>
|
|
494
|
+
|
|
495
|
+
<section class="content">
|
|
496
|
+
<div id="empty-state" class="empty-state">
|
|
497
|
+
<div class="empty-icon">📺</div>
|
|
498
|
+
<h2 class="empty-title">Waiting for MCP streams...</h2>
|
|
499
|
+
<p class="empty-subtitle">
|
|
500
|
+
Any MCP can stream events here. POST JSON to this server or connect via SSE.
|
|
501
|
+
</p>
|
|
502
|
+
<div class="code-block">
|
|
503
|
+
<code>// Stream from any MCP or script
|
|
504
|
+
fetch('http://localhost:' + PORT + '/stream', {
|
|
505
|
+
method: 'POST',
|
|
506
|
+
headers: { 'Content-Type': 'application/json' },
|
|
507
|
+
body: JSON.stringify({
|
|
508
|
+
channel: 'my-mcp',
|
|
509
|
+
event: 'stage',
|
|
510
|
+
data: { name: 'processing', status: 'running' }
|
|
511
|
+
})
|
|
512
|
+
});</code>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<div id="pipeline-view" style="display:none;">
|
|
517
|
+
<div class="pipeline-header">
|
|
518
|
+
<div class="pipeline-title" id="pipeline-title">Pipeline</div>
|
|
519
|
+
<div class="pipeline-meta">
|
|
520
|
+
<span id="pipeline-duration">0.0s</span>
|
|
521
|
+
<span id="pipeline-status">Running</span>
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
<div id="stages" class="pipeline-stages"></div>
|
|
526
|
+
|
|
527
|
+
<div class="event-log">
|
|
528
|
+
<div class="event-log-header">
|
|
529
|
+
<span>📋</span> Event Log
|
|
530
|
+
</div>
|
|
531
|
+
<div id="event-log" class="event-log-content"></div>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div id="result-card" class="result-card" style="display:none;">
|
|
535
|
+
<h3>✨ Result</h3>
|
|
536
|
+
<div id="result-content" class="result-content"></div>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</section>
|
|
540
|
+
</main>
|
|
541
|
+
|
|
542
|
+
<script>
|
|
543
|
+
const PORT = location.port || 50888;
|
|
544
|
+
let currentChannel = '__all__';
|
|
545
|
+
let channelData = new Map();
|
|
546
|
+
let startTime = Date.now();
|
|
547
|
+
|
|
548
|
+
// Connect to SSE
|
|
549
|
+
const eventSource = new EventSource('/events?channel=__all__');
|
|
550
|
+
|
|
551
|
+
eventSource.onmessage = (e) => {
|
|
552
|
+
const event = JSON.parse(e.data);
|
|
553
|
+
handleEvent(event);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
eventSource.onerror = () => {
|
|
557
|
+
document.querySelector('.status-dot').style.background = 'var(--accent-red)';
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
function handleEvent(event) {
|
|
561
|
+
const { channel, event: eventType, data, timestamp } = event;
|
|
562
|
+
|
|
563
|
+
// Update channel data
|
|
564
|
+
if (!channelData.has(channel)) {
|
|
565
|
+
channelData.set(channel, {
|
|
566
|
+
name: channel,
|
|
567
|
+
events: [],
|
|
568
|
+
stages: [],
|
|
569
|
+
status: 'active',
|
|
570
|
+
result: null
|
|
571
|
+
});
|
|
572
|
+
updateChannelList();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const ch = channelData.get(channel);
|
|
576
|
+
ch.events.push({ type: eventType, data, time: timestamp || Date.now() });
|
|
577
|
+
ch.lastUpdate = Date.now();
|
|
578
|
+
|
|
579
|
+
// Process event types
|
|
580
|
+
if (eventType === 'stage') {
|
|
581
|
+
updateStage(ch, data);
|
|
582
|
+
} else if (eventType === 'result') {
|
|
583
|
+
ch.result = data;
|
|
584
|
+
ch.status = 'complete';
|
|
585
|
+
} else if (eventType === 'error') {
|
|
586
|
+
ch.status = 'error';
|
|
587
|
+
} else if (eventType === 'init') {
|
|
588
|
+
ch.title = data.title || data.problem || channel;
|
|
589
|
+
ch.stages = (data.stages || []).map(s => ({ ...s, status: 'pending' }));
|
|
590
|
+
startTime = Date.now();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Update UI if viewing this channel or all
|
|
594
|
+
if (currentChannel === '__all__' || currentChannel === channel) {
|
|
595
|
+
renderChannel(ch);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
updateChannelList();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function updateStage(ch, stageData) {
|
|
602
|
+
const existing = ch.stages.find(s => s.name === stageData.name);
|
|
603
|
+
if (existing) {
|
|
604
|
+
Object.assign(existing, stageData);
|
|
605
|
+
} else {
|
|
606
|
+
ch.stages.push(stageData);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function updateChannelList() {
|
|
611
|
+
const list = document.getElementById('channel-list');
|
|
612
|
+
const select = document.getElementById('channel-select');
|
|
613
|
+
|
|
614
|
+
const channels = Array.from(channelData.entries());
|
|
615
|
+
|
|
616
|
+
list.innerHTML = channels.map(([name, ch]) => \`
|
|
617
|
+
<div class="channel-item \${currentChannel === name ? 'active' : ''}" onclick="switchChannel('\${name}')">
|
|
618
|
+
<div class="channel-icon" style="background: \${getChannelColor(name)}">\${getChannelIcon(ch)}</div>
|
|
619
|
+
<div class="channel-info">
|
|
620
|
+
<div class="channel-name">\${name}</div>
|
|
621
|
+
<div class="channel-status">\${ch.events.length} events</div>
|
|
622
|
+
</div>
|
|
623
|
+
\${ch.status === 'active' ? '<span class="channel-badge">LIVE</span>' : ''}
|
|
624
|
+
</div>
|
|
625
|
+
\`).join('');
|
|
626
|
+
|
|
627
|
+
// Update select
|
|
628
|
+
const currentOptions = new Set([...select.options].map(o => o.value));
|
|
629
|
+
channels.forEach(([name]) => {
|
|
630
|
+
if (!currentOptions.has(name)) {
|
|
631
|
+
const opt = document.createElement('option');
|
|
632
|
+
opt.value = name;
|
|
633
|
+
opt.textContent = name;
|
|
634
|
+
select.appendChild(opt);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function switchChannel(name) {
|
|
640
|
+
currentChannel = name;
|
|
641
|
+
document.getElementById('channel-select').value = name;
|
|
642
|
+
|
|
643
|
+
if (name === '__all__' && channelData.size === 0) {
|
|
644
|
+
document.getElementById('empty-state').style.display = 'flex';
|
|
645
|
+
document.getElementById('pipeline-view').style.display = 'none';
|
|
646
|
+
} else if (name === '__all__') {
|
|
647
|
+
// Show most recent channel
|
|
648
|
+
const latest = [...channelData.entries()].sort((a, b) => b[1].lastUpdate - a[1].lastUpdate)[0];
|
|
649
|
+
if (latest) renderChannel(latest[1]);
|
|
650
|
+
} else {
|
|
651
|
+
const ch = channelData.get(name);
|
|
652
|
+
if (ch) renderChannel(ch);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
updateChannelList();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function renderChannel(ch) {
|
|
659
|
+
document.getElementById('empty-state').style.display = 'none';
|
|
660
|
+
document.getElementById('pipeline-view').style.display = 'block';
|
|
661
|
+
|
|
662
|
+
// Title
|
|
663
|
+
document.getElementById('pipeline-title').textContent = ch.title || ch.name;
|
|
664
|
+
|
|
665
|
+
// Duration
|
|
666
|
+
const duration = ((ch.lastUpdate || Date.now()) - startTime) / 1000;
|
|
667
|
+
document.getElementById('pipeline-duration').textContent = duration.toFixed(1) + 's';
|
|
668
|
+
|
|
669
|
+
// Status
|
|
670
|
+
document.getElementById('pipeline-status').textContent = ch.status;
|
|
671
|
+
|
|
672
|
+
// Stages
|
|
673
|
+
const stagesEl = document.getElementById('stages');
|
|
674
|
+
stagesEl.innerHTML = ch.stages.map(s => \`
|
|
675
|
+
<div class="stage \${s.status || 'pending'}">
|
|
676
|
+
<div class="stage-icon">\${getStageIcon(s.name)}</div>
|
|
677
|
+
<div class="stage-name">\${s.name}</div>
|
|
678
|
+
\${s.duration_ms ? '<div class="stage-time">' + (s.duration_ms/1000).toFixed(1) + 's</div>' : ''}
|
|
679
|
+
</div>
|
|
680
|
+
\`).join('');
|
|
681
|
+
|
|
682
|
+
// Event log
|
|
683
|
+
const logEl = document.getElementById('event-log');
|
|
684
|
+
logEl.innerHTML = ch.events.slice(-50).map(e => \`
|
|
685
|
+
<div class="event-item">
|
|
686
|
+
<span class="event-time">\${formatTime(e.time)}</span>
|
|
687
|
+
<span class="event-type \${e.type}">\${e.type}</span>
|
|
688
|
+
<span class="event-data">\${formatData(e.data)}</span>
|
|
689
|
+
</div>
|
|
690
|
+
\`).join('');
|
|
691
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
692
|
+
|
|
693
|
+
// Result
|
|
694
|
+
if (ch.result) {
|
|
695
|
+
document.getElementById('result-card').style.display = 'block';
|
|
696
|
+
document.getElementById('result-content').textContent =
|
|
697
|
+
typeof ch.result === 'string' ? ch.result : JSON.stringify(ch.result, null, 2);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function getChannelColor(name) {
|
|
702
|
+
const colors = ['#00ff88', '#00d4ff', '#ff6b6b', '#ffd93d', '#6c5ce7', '#a29bfe'];
|
|
703
|
+
let hash = 0;
|
|
704
|
+
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
705
|
+
return colors[Math.abs(hash) % colors.length];
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function getChannelIcon(ch) {
|
|
709
|
+
if (ch.name.includes('invent')) return '🔬';
|
|
710
|
+
if (ch.name.includes('genius')) return '💡';
|
|
711
|
+
if (ch.name.includes('compute')) return '⚡';
|
|
712
|
+
if (ch.name.includes('search')) return '🔍';
|
|
713
|
+
return '📺';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function getStageIcon(name) {
|
|
717
|
+
const icons = {
|
|
718
|
+
'mind_opener': '🧠', 'idea_fold': '🔬', 'bcalc': '📊',
|
|
719
|
+
'web_search': '🔍', 'genius_plus': '💡', 'cvi_verify': '✓',
|
|
720
|
+
'roast': '🔥', 'compute': '⚡', 'init': '🚀'
|
|
721
|
+
};
|
|
722
|
+
return icons[name] || '⚙️';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function formatTime(ts) {
|
|
726
|
+
const d = new Date(ts);
|
|
727
|
+
return d.toLocaleTimeString('en-US', { hour12: false });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function formatData(data) {
|
|
731
|
+
if (typeof data === 'string') return data.slice(0, 100);
|
|
732
|
+
if (data && data.name) return data.name + (data.status ? ' → ' + data.status : '');
|
|
733
|
+
return JSON.stringify(data).slice(0, 100);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Update duration every 100ms
|
|
737
|
+
setInterval(() => {
|
|
738
|
+
if (channelData.size > 0) {
|
|
739
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
740
|
+
document.getElementById('pipeline-duration').textContent = duration.toFixed(1) + 's';
|
|
741
|
+
}
|
|
742
|
+
}, 100);
|
|
743
|
+
|
|
744
|
+
// PWA Install
|
|
745
|
+
let deferredPrompt;
|
|
746
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
747
|
+
e.preventDefault();
|
|
748
|
+
deferredPrompt = e;
|
|
749
|
+
document.getElementById('install-banner').classList.add('show');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
function installPWA() {
|
|
753
|
+
if (deferredPrompt) {
|
|
754
|
+
deferredPrompt.prompt();
|
|
755
|
+
deferredPrompt.userChoice.then(() => {
|
|
756
|
+
document.getElementById('install-banner').classList.remove('show');
|
|
757
|
+
deferredPrompt = null;
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Service Worker
|
|
763
|
+
if ('serviceWorker' in navigator) {
|
|
764
|
+
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
|
765
|
+
}
|
|
766
|
+
</script>
|
|
767
|
+
</body>
|
|
768
|
+
</html>`;
|
|
769
|
+
|
|
770
|
+
const MANIFEST = {
|
|
771
|
+
name: "MCP-TV",
|
|
772
|
+
short_name: "MCP-TV",
|
|
773
|
+
description: "Universal MCP Visualization Platform",
|
|
774
|
+
start_url: "/",
|
|
775
|
+
display: "standalone",
|
|
776
|
+
background_color: "#0a0a0f",
|
|
777
|
+
theme_color: "#00ff88",
|
|
778
|
+
icons: [
|
|
779
|
+
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
|
|
780
|
+
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" }
|
|
781
|
+
]
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const SW_JS = `
|
|
785
|
+
self.addEventListener('install', () => self.skipWaiting());
|
|
786
|
+
self.addEventListener('activate', (e) => e.waitUntil(clients.claim()));
|
|
787
|
+
self.addEventListener('fetch', (e) => {
|
|
788
|
+
if (e.request.method === 'GET' && !e.request.url.includes('/events')) {
|
|
789
|
+
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
`;
|
|
793
|
+
|
|
794
|
+
// Simple SVG icon as data URI
|
|
795
|
+
const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect fill="#0a0a0f" width="100" height="100" rx="20"/><text x="50" y="65" text-anchor="middle" font-size="50">📺</text></svg>`;
|
|
796
|
+
|
|
797
|
+
function createServer(port = DEFAULT_PORT) {
|
|
798
|
+
const server = http.createServer((req, res) => {
|
|
799
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
800
|
+
|
|
801
|
+
// CORS headers
|
|
802
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
803
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
804
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
805
|
+
|
|
806
|
+
if (req.method === 'OPTIONS') {
|
|
807
|
+
res.writeHead(204);
|
|
808
|
+
return res.end();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// SSE endpoint
|
|
812
|
+
if (url.pathname === '/events') {
|
|
813
|
+
const channel = url.searchParams.get('channel') || '__all__';
|
|
814
|
+
|
|
815
|
+
res.writeHead(200, {
|
|
816
|
+
'Content-Type': 'text/event-stream',
|
|
817
|
+
'Cache-Control': 'no-cache',
|
|
818
|
+
'Connection': 'keep-alive'
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
if (!clients.has(channel)) clients.set(channel, []);
|
|
822
|
+
clients.get(channel).push(res);
|
|
823
|
+
|
|
824
|
+
req.on('close', () => {
|
|
825
|
+
const arr = clients.get(channel) || [];
|
|
826
|
+
clients.set(channel, arr.filter(c => c !== res));
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Stream endpoint - receive events from MCPs
|
|
833
|
+
if (url.pathname === '/stream' && req.method === 'POST') {
|
|
834
|
+
let body = '';
|
|
835
|
+
req.on('data', chunk => body += chunk);
|
|
836
|
+
req.on('end', () => {
|
|
837
|
+
try {
|
|
838
|
+
const event = JSON.parse(body);
|
|
839
|
+
const channel = event.channel || 'default';
|
|
840
|
+
|
|
841
|
+
// Add timestamp
|
|
842
|
+
event.timestamp = Date.now();
|
|
843
|
+
|
|
844
|
+
// Store in channel
|
|
845
|
+
const ch = getChannel(channel);
|
|
846
|
+
ch.events.push(event);
|
|
847
|
+
ch.lastUpdate = Date.now();
|
|
848
|
+
|
|
849
|
+
// Broadcast to listeners
|
|
850
|
+
broadcast(channel, event);
|
|
851
|
+
|
|
852
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
853
|
+
res.end(JSON.stringify({ ok: true, channel }));
|
|
854
|
+
} catch (e) {
|
|
855
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
856
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// PWA assets
|
|
863
|
+
if (url.pathname === '/manifest.json') {
|
|
864
|
+
res.writeHead(200, { 'Content-Type': 'application/manifest+json' });
|
|
865
|
+
return res.end(JSON.stringify(MANIFEST));
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (url.pathname === '/sw.js') {
|
|
869
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
870
|
+
return res.end(SW_JS);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (url.pathname.includes('icon-')) {
|
|
874
|
+
res.writeHead(200, { 'Content-Type': 'image/svg+xml' });
|
|
875
|
+
return res.end(ICON_SVG);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// API: List channels
|
|
879
|
+
if (url.pathname === '/api/channels') {
|
|
880
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
881
|
+
return res.end(JSON.stringify([...channels.keys()]));
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// API: Get channel
|
|
885
|
+
if (url.pathname.startsWith('/api/channel/')) {
|
|
886
|
+
const name = url.pathname.split('/').pop();
|
|
887
|
+
const ch = channels.get(name);
|
|
888
|
+
res.writeHead(ch ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
889
|
+
return res.end(JSON.stringify(ch || { error: 'Not found' }));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Main page
|
|
893
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
894
|
+
res.end(HTML_PAGE.replace(/\$\{PORT\}/g, port));
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
server.listen(port, () => {
|
|
898
|
+
console.log(`
|
|
899
|
+
📺 MCP-TV is live!
|
|
900
|
+
|
|
901
|
+
Local: http://localhost:${port}
|
|
902
|
+
|
|
903
|
+
Stream events from any MCP:
|
|
904
|
+
POST http://localhost:${port}/stream
|
|
905
|
+
{
|
|
906
|
+
"channel": "my-mcp",
|
|
907
|
+
"event": "stage",
|
|
908
|
+
"data": { "name": "processing", "status": "running" }
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
Or connect via SSE:
|
|
912
|
+
GET http://localhost:${port}/events?channel=my-mcp
|
|
913
|
+
|
|
914
|
+
Press Ctrl+C to stop.
|
|
915
|
+
`);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
return server;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function openBrowser(url, width = 1200, height = 800) {
|
|
922
|
+
const platform = process.platform;
|
|
923
|
+
|
|
924
|
+
if (platform === 'win32') {
|
|
925
|
+
// Try Chrome first with app mode for clean window
|
|
926
|
+
const chromePaths = [
|
|
927
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
928
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
929
|
+
process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe'
|
|
930
|
+
];
|
|
931
|
+
const edgePath = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe';
|
|
932
|
+
|
|
933
|
+
let browserPath = null;
|
|
934
|
+
for (const p of chromePaths) {
|
|
935
|
+
try { if (require('fs').existsSync(p)) { browserPath = p; break; } } catch {}
|
|
936
|
+
}
|
|
937
|
+
if (!browserPath) {
|
|
938
|
+
try { if (require('fs').existsSync(edgePath)) browserPath = edgePath; } catch {}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (browserPath) {
|
|
942
|
+
spawn(browserPath, [
|
|
943
|
+
`--app=${url}`,
|
|
944
|
+
`--window-size=${width},${height}`,
|
|
945
|
+
'--window-position=100,50'
|
|
946
|
+
], { detached: true, stdio: 'ignore' }).unref();
|
|
947
|
+
} else {
|
|
948
|
+
spawn('cmd', ['/c', 'start', url], { detached: true, stdio: 'ignore' }).unref();
|
|
949
|
+
}
|
|
950
|
+
} else if (platform === 'darwin') {
|
|
951
|
+
// macOS - use osascript to set window bounds
|
|
952
|
+
spawn('open', ['-na', 'Google Chrome', '--args', `--app=${url}`, `--window-size=${width},${height}`],
|
|
953
|
+
{ detached: true, stdio: 'ignore' }).unref();
|
|
954
|
+
} else {
|
|
955
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Stream helper for other MCPs to use
|
|
960
|
+
function createMCPTVClient(channel, serverUrl = 'http://localhost:50888') {
|
|
961
|
+
return {
|
|
962
|
+
init: (data) => fetch(`${serverUrl}/stream`, {
|
|
963
|
+
method: 'POST',
|
|
964
|
+
headers: { 'Content-Type': 'application/json' },
|
|
965
|
+
body: JSON.stringify({ channel, event: 'init', data })
|
|
966
|
+
}),
|
|
967
|
+
|
|
968
|
+
stage: (name, status, duration_ms) => fetch(`${serverUrl}/stream`, {
|
|
969
|
+
method: 'POST',
|
|
970
|
+
headers: { 'Content-Type': 'application/json' },
|
|
971
|
+
body: JSON.stringify({ channel, event: 'stage', data: { name, status, duration_ms } })
|
|
972
|
+
}),
|
|
973
|
+
|
|
974
|
+
progress: (message, percent) => fetch(`${serverUrl}/stream`, {
|
|
975
|
+
method: 'POST',
|
|
976
|
+
headers: { 'Content-Type': 'application/json' },
|
|
977
|
+
body: JSON.stringify({ channel, event: 'progress', data: { message, percent } })
|
|
978
|
+
}),
|
|
979
|
+
|
|
980
|
+
log: (message) => fetch(`${serverUrl}/stream`, {
|
|
981
|
+
method: 'POST',
|
|
982
|
+
headers: { 'Content-Type': 'application/json' },
|
|
983
|
+
body: JSON.stringify({ channel, event: 'log', data: { message } })
|
|
984
|
+
}),
|
|
985
|
+
|
|
986
|
+
result: (data) => fetch(`${serverUrl}/stream`, {
|
|
987
|
+
method: 'POST',
|
|
988
|
+
headers: { 'Content-Type': 'application/json' },
|
|
989
|
+
body: JSON.stringify({ channel, event: 'result', data })
|
|
990
|
+
}),
|
|
991
|
+
|
|
992
|
+
error: (message) => fetch(`${serverUrl}/stream`, {
|
|
993
|
+
method: 'POST',
|
|
994
|
+
headers: { 'Content-Type': 'application/json' },
|
|
995
|
+
body: JSON.stringify({ channel, event: 'error', data: { message } })
|
|
996
|
+
})
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
module.exports = { createServer, createMCPTVClient, broadcast, getChannel };
|
|
1001
|
+
|
|
1002
|
+
// CLI
|
|
1003
|
+
if (require.main === module) {
|
|
1004
|
+
const args = process.argv.slice(2);
|
|
1005
|
+
let port = DEFAULT_PORT;
|
|
1006
|
+
let openOnStart = true;
|
|
1007
|
+
|
|
1008
|
+
for (const arg of args) {
|
|
1009
|
+
if (arg.startsWith('--port=')) port = parseInt(arg.split('=')[1]);
|
|
1010
|
+
if (arg === '--no-open') openOnStart = false;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
createServer(port);
|
|
1014
|
+
if (openOnStart) setTimeout(() => openBrowser(`http://localhost:${port}`), 500);
|
|
1015
|
+
}
|