@0xmaxma/claude-gateway 1.3.4 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent/runner.d.ts +20 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +94 -1
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/gateway-router.d.ts +10 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +261 -7
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/session/process.d.ts +8 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +28 -0
- package/dist/session/process.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +211 -4
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/emitter.d.ts +13 -0
- package/dist/shell/emitter.d.ts.map +1 -1
- package/dist/shell/emitter.js +16 -0
- package/dist/shell/emitter.js.map +1 -1
- package/dist/shell/menu-cancel.d.ts +50 -0
- package/dist/shell/menu-cancel.d.ts.map +1 -0
- package/dist/shell/menu-cancel.js +62 -0
- package/dist/shell/menu-cancel.js.map +1 -0
- package/dist/shell/pty-serialize.d.ts +23 -0
- package/dist/shell/pty-serialize.d.ts.map +1 -0
- package/dist/shell/pty-serialize.js +131 -0
- package/dist/shell/pty-serialize.js.map +1 -0
- package/dist/shell/pty-stream-registry.d.ts +59 -0
- package/dist/shell/pty-stream-registry.d.ts.map +1 -0
- package/dist/shell/pty-stream-registry.js +279 -0
- package/dist/shell/pty-stream-registry.js.map +1 -0
- package/dist/shell/screen.d.ts +43 -0
- package/dist/shell/screen.d.ts.map +1 -1
- package/dist/shell/screen.js +91 -1
- package/dist/shell/screen.js.map +1 -1
- package/dist/ui/web-ui.d.ts +2 -2
- package/dist/ui/web-ui.d.ts.map +1 -1
- package/dist/ui/web-ui.js +615 -114
- package/dist/ui/web-ui.js.map +1 -1
- package/mcp/tools/discord/module.ts +88 -1
- package/mcp/tools/discord/outbound.ts +36 -0
- package/mcp/tools/discord/types.ts +2 -0
- package/mcp/tools/telegram/menu-parser.ts +37 -0
- package/mcp/tools/telegram/receiver-server.ts +83 -0
- package/mcp/tools/telegram/typing.ts +7 -1
- package/package.json +4 -2
package/dist/ui/web-ui.js
CHANGED
|
@@ -3,15 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.generateDashboardHtml = generateDashboardHtml;
|
|
4
4
|
/**
|
|
5
5
|
* Generates a self-contained HTML dashboard page for the gateway status UI.
|
|
6
|
-
* No external dependencies
|
|
6
|
+
* No external dependencies except xterm.js CDN for PTY viewer.
|
|
7
7
|
*/
|
|
8
|
-
function generateDashboardHtml() {
|
|
8
|
+
function generateDashboardHtml(dashToken = '') {
|
|
9
|
+
const safeToken = dashToken.replace(/&/g, '&').replace(/"/g, '"');
|
|
9
10
|
return `<!DOCTYPE html>
|
|
10
11
|
<html lang="en">
|
|
11
12
|
<head>
|
|
12
13
|
<meta charset="UTF-8">
|
|
13
14
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
14
|
-
<
|
|
15
|
+
<meta name="dash-token" content="${safeToken}">
|
|
16
|
+
<title>Claude Gateway</title>
|
|
17
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/css/xterm.min.css"
|
|
18
|
+
integrity="sha384-eDYu/eBZQNhtqTaA7Wl3XighXKxm/9VYF+Chh3hQS+UUlKQIJ14hK2imKu4n99aR" crossorigin="anonymous"/>
|
|
15
19
|
<style>
|
|
16
20
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
17
21
|
body {
|
|
@@ -20,8 +24,17 @@ function generateDashboardHtml() {
|
|
|
20
24
|
color: #e2e8f0;
|
|
21
25
|
padding: 24px;
|
|
22
26
|
}
|
|
23
|
-
h1 {
|
|
24
|
-
.
|
|
27
|
+
h1 { font-size: 1.5rem; margin-bottom: 8px; }
|
|
28
|
+
.rainbow {
|
|
29
|
+
background: linear-gradient(90deg, #ff0080, #ff8c00, #ffe600, #00d26a, #00b4ff, #a855f7, #ff0080);
|
|
30
|
+
background-size: 200% auto;
|
|
31
|
+
-webkit-background-clip: text;
|
|
32
|
+
-webkit-text-fill-color: transparent;
|
|
33
|
+
background-clip: text;
|
|
34
|
+
animation: rainbow-shift 3s linear infinite;
|
|
35
|
+
}
|
|
36
|
+
@keyframes rainbow-shift { to { background-position: 200% center; } }
|
|
37
|
+
.meta { color: #718096; font-size: 0.85rem; margin-bottom: 16px; }
|
|
25
38
|
.meta span { color: #a0aec0; }
|
|
26
39
|
h2 { color: #90cdf4; font-size: 1.1rem; margin: 20px 0 10px; }
|
|
27
40
|
table {
|
|
@@ -45,7 +58,8 @@ function generateDashboardHtml() {
|
|
|
45
58
|
padding: 8px 12px;
|
|
46
59
|
border-bottom: 1px solid #1a202c;
|
|
47
60
|
}
|
|
48
|
-
tr
|
|
61
|
+
tr.session-row td { background: #0f1117; color: #cbd5e0; font-size: 0.82rem; }
|
|
62
|
+
tr.session-row:hover td { background: #1a202c; }
|
|
49
63
|
.badge {
|
|
50
64
|
display: inline-block;
|
|
51
65
|
padding: 2px 8px;
|
|
@@ -56,72 +70,216 @@ function generateDashboardHtml() {
|
|
|
56
70
|
.badge-green { background: #22543d; color: #68d391; }
|
|
57
71
|
.badge-red { background: #742a2a; color: #fc8181; }
|
|
58
72
|
.badge-gray { background: #2d3748; color: #a0aec0; }
|
|
73
|
+
.badge-blue { background: #1a365d; color: #63b3ed; }
|
|
74
|
+
.badge-purple { background: #44337a; color: #b794f4; }
|
|
75
|
+
/* Per-model badge colors — each model family gets a distinct hue. */
|
|
76
|
+
.badge-opus { background: #5a3a1a; color: #f6ad55; }
|
|
77
|
+
.badge-sonnet { background: #1a4a52; color: #4fd1c5; }
|
|
78
|
+
.badge-haiku { background: #22543d; color: #68d391; }
|
|
79
|
+
.badge-fable { background: #553052; color: #f687b3; }
|
|
80
|
+
.badge-model { background: #2d3748; color: #cbd5e0; }
|
|
59
81
|
.ts { color: #718096; font-size: 0.8rem; }
|
|
60
|
-
#refresh-indicator {
|
|
61
|
-
|
|
82
|
+
#refresh-indicator { float: right; font-size: 0.75rem; color: #4a5568; }
|
|
83
|
+
.error { color: #fc8181; font-size: 0.85rem; margin-top: 8px; }
|
|
84
|
+
.btn-stream {
|
|
85
|
+
background: #44337a;
|
|
86
|
+
color: #d6bcfa;
|
|
87
|
+
border: 1px solid #6b46c1;
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
padding: 2px 8px;
|
|
62
90
|
font-size: 0.75rem;
|
|
63
|
-
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
.btn-stream:hover { background: #6b46c1; color: #faf5ff; }
|
|
94
|
+
.pty-viewer {
|
|
95
|
+
display: none;
|
|
96
|
+
margin-top: 24px;
|
|
97
|
+
border: 1px solid #2d3748;
|
|
98
|
+
border-radius: 6px;
|
|
99
|
+
overflow-x: hidden;
|
|
100
|
+
}
|
|
101
|
+
.pty-viewer-header {
|
|
102
|
+
background: #1a202c;
|
|
103
|
+
padding: 8px 12px;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
font-size: 0.85rem;
|
|
108
|
+
color: #a0aec0;
|
|
109
|
+
}
|
|
110
|
+
.pty-viewer-header .agent-label { color: #63b3ed; font-weight: 600; }
|
|
111
|
+
.pty-viewer-header .session-label { color: #718096; font-family: monospace; font-size: 0.78rem; }
|
|
112
|
+
.pty-close, .pty-refresh {
|
|
113
|
+
background: none;
|
|
114
|
+
border: none;
|
|
115
|
+
color: #718096;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-size: 1rem;
|
|
118
|
+
padding: 0 4px;
|
|
119
|
+
}
|
|
120
|
+
.pty-close:hover { color: #fc8181; }
|
|
121
|
+
.pty-refresh:hover { color: #63b3ed; }
|
|
122
|
+
/* Fixed-size terminal viewport — the server PTY runs at 200x50, so the
|
|
123
|
+
viewer must NOT resize to the panel (that mismatch is what garbles the
|
|
124
|
+
output). We render at the native size and pan horizontally if the 200-col
|
|
125
|
+
width overflows the panel. The Claude TUI uses the alternate screen buffer
|
|
126
|
+
(\x1b[?1049h), which has no scrollback by design — so there is nothing to
|
|
127
|
+
scroll vertically and we hide the (non-functional) vertical scrollbar. */
|
|
128
|
+
#pty-terminal {
|
|
129
|
+
padding: 8px;
|
|
130
|
+
background: #0d1117;
|
|
131
|
+
overflow-x: auto;
|
|
132
|
+
overflow-y: hidden;
|
|
133
|
+
border-radius: 6px;
|
|
134
|
+
}
|
|
135
|
+
/* No scrollback in alt-screen mode → suppress xterm's vertical scrollbar. */
|
|
136
|
+
#pty-terminal .xterm-viewport { overflow-y: hidden !important; }
|
|
137
|
+
.proc-tree {
|
|
138
|
+
font-family: monospace;
|
|
139
|
+
font-size: 0.82rem;
|
|
140
|
+
background: #0d1117;
|
|
141
|
+
border: 1px solid #2d3748;
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
padding: 12px 16px;
|
|
144
|
+
white-space: pre-wrap;
|
|
145
|
+
word-break: break-word;
|
|
146
|
+
color: #a0aec0;
|
|
147
|
+
}
|
|
148
|
+
.proc-tree .proc-orchestrator { color: #63b3ed; }
|
|
149
|
+
.proc-tree .proc-pty { color: #68d391; }
|
|
150
|
+
.proc-tree .proc-claude { color: #f6e05e; }
|
|
151
|
+
.proc-tree .proc-mcp { color: #b794f4; }
|
|
152
|
+
.proc-tree .proc-receiver { color: #76e4f7; }
|
|
153
|
+
.proc-tree .proc-orphan { color: #fc8181; }
|
|
154
|
+
.proc-tree .proc-label { color: #718096; }
|
|
155
|
+
.proc-tree .proc-summary { color: #f6e05e; font-weight: 600; }
|
|
156
|
+
.session-id {
|
|
157
|
+
font-family: monospace;
|
|
158
|
+
font-size: 0.75rem;
|
|
159
|
+
color: #a0aec0;
|
|
160
|
+
word-break: break-all;
|
|
161
|
+
}
|
|
162
|
+
/* Agent status badges bar */
|
|
163
|
+
.agents-bar {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-wrap: wrap;
|
|
166
|
+
gap: 8px;
|
|
167
|
+
margin-bottom: 20px;
|
|
168
|
+
}
|
|
169
|
+
.agent-badge {
|
|
170
|
+
display: inline-flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 6px;
|
|
173
|
+
background: #1a202c;
|
|
174
|
+
border: 1px solid #2d3748;
|
|
175
|
+
border-radius: 6px;
|
|
176
|
+
padding: 4px 12px;
|
|
177
|
+
font-size: 0.8rem;
|
|
178
|
+
}
|
|
179
|
+
.agent-badge .agent-name { color: #90cdf4; font-weight: 600; }
|
|
180
|
+
.agent-badge .dot-green { color: #68d391; }
|
|
181
|
+
.agent-badge .dot-red { color: #fc8181; }
|
|
182
|
+
|
|
183
|
+
/* Top row: Processes 70% | Agents 30%. Collapses to a single column on
|
|
184
|
+
narrow screens (see media query below). */
|
|
185
|
+
.top-grid {
|
|
186
|
+
display: grid;
|
|
187
|
+
grid-template-columns: 7fr 3fr;
|
|
188
|
+
gap: 24px;
|
|
189
|
+
align-items: start;
|
|
190
|
+
}
|
|
191
|
+
/* The Sessions table has 10 columns — too wide for phones. Wrap it so it
|
|
192
|
+
scrolls horizontally instead of breaking the layout. */
|
|
193
|
+
.table-wrap { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
194
|
+
.table-wrap table { min-width: 820px; }
|
|
195
|
+
|
|
196
|
+
/* ── Responsive breakpoints ──────────────────────────────────────────── */
|
|
197
|
+
@media (max-width: 900px) {
|
|
198
|
+
.top-grid { grid-template-columns: 1fr; gap: 16px; }
|
|
199
|
+
}
|
|
200
|
+
@media (max-width: 640px) {
|
|
201
|
+
body { padding: 14px; }
|
|
202
|
+
h1 { font-size: 1.2rem; }
|
|
203
|
+
h2 { font-size: 1rem; }
|
|
204
|
+
.meta { font-size: 0.78rem; }
|
|
205
|
+
#refresh-indicator { float: none; display: block; margin-top: 4px; }
|
|
206
|
+
.proc-tree { font-size: 0.72rem; padding: 10px 12px; }
|
|
207
|
+
/* On phones allow horizontal pan only — no vertical clipping. */
|
|
64
208
|
}
|
|
65
|
-
.error { color: #fc8181; font-size: 0.85rem; margin-top: 8px; }
|
|
66
209
|
</style>
|
|
67
210
|
</head>
|
|
68
211
|
<body>
|
|
69
|
-
<h1>Claude Gateway <span id="refresh-indicator">refreshing...</span></h1>
|
|
212
|
+
<h1><span class="rainbow">Claude Gateway</span> <span id="gateway-version" style="font-size:0.75rem;color:#718096;"></span> <span id="refresh-indicator">refreshing...</span></h1>
|
|
70
213
|
<div class="meta">
|
|
71
|
-
Uptime: <span id="uptime"
|
|
72
|
-
Started: <span id="started-at"
|
|
73
|
-
Last updated: <span id="last-updated"
|
|
214
|
+
Uptime: <span id="uptime">—</span> |
|
|
215
|
+
Started: <span id="started-at">—</span> |
|
|
216
|
+
Last updated: <span id="last-updated">—</span>
|
|
74
217
|
</div>
|
|
75
218
|
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
219
|
+
<!-- Row: Processes 70% | Agent badges 30% (collapses on narrow screens) -->
|
|
220
|
+
<div class="top-grid">
|
|
221
|
+
<div>
|
|
222
|
+
<h2>Processes</h2>
|
|
223
|
+
<div class="proc-tree" id="proc-tree">Loading...</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div>
|
|
226
|
+
<h2>Agents</h2>
|
|
227
|
+
<div class="agents-bar" id="agents-bar"></div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- PTY viewer — full width so the native 200-col terminal has room.
|
|
232
|
+
Placed above the Sessions table so the live mirror is the first thing
|
|
233
|
+
in view when streaming. -->
|
|
234
|
+
<div class="pty-viewer" id="pty-viewer">
|
|
235
|
+
<div class="pty-viewer-header">
|
|
236
|
+
<span>Shell Process Viewer — <span class="agent-label" id="pty-agent-label"></span><span class="session-label" id="pty-session-label"></span></span>
|
|
237
|
+
<span>
|
|
238
|
+
<button class="pty-refresh" id="pty-refresh-btn" title="Refresh (reconnect & redraw)">↺</button>
|
|
239
|
+
<button class="pty-close" id="pty-close-btn" title="Close">✕</button>
|
|
240
|
+
</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div id="pty-terminal"></div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Sessions — full width (session-centric, flat list) -->
|
|
246
|
+
<h2>Sessions</h2>
|
|
247
|
+
<div class="table-wrap">
|
|
248
|
+
<table id="sessions-table">
|
|
249
|
+
<thead>
|
|
250
|
+
<tr>
|
|
251
|
+
<th>Agent</th>
|
|
252
|
+
<th>Session ID</th>
|
|
253
|
+
<th>Chat ID</th>
|
|
254
|
+
<th>Source</th>
|
|
255
|
+
<th>Mode</th>
|
|
256
|
+
<th>Model</th>
|
|
257
|
+
<th>Tokens</th>
|
|
258
|
+
<th>Status</th>
|
|
259
|
+
<th>Uptime</th>
|
|
260
|
+
<th>Spawned</th>
|
|
261
|
+
<th>Shell</th>
|
|
262
|
+
</tr>
|
|
263
|
+
</thead>
|
|
264
|
+
<tbody id="sessions-tbody">
|
|
265
|
+
<tr><td colspan="11" class="ts">Loading...</td></tr>
|
|
266
|
+
</tbody>
|
|
267
|
+
</table>
|
|
268
|
+
</div>
|
|
121
269
|
|
|
122
270
|
<div id="error-msg" class="error" style="display:none;"></div>
|
|
123
271
|
|
|
272
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/lib/xterm.min.js"
|
|
273
|
+
integrity="sha384-pELe6ZHtFxFcuYBq3gMkqvmnNIqUWnAYjBG5gThqQQCjWp8PJ/65MLK4lMIfEK1e" crossorigin="anonymous"></script>
|
|
124
274
|
<script>
|
|
275
|
+
// Read short-lived dashboard token from meta tag (10 min, server-issued at page load).
|
|
276
|
+
// The raw API key is never embedded in HTML — only this scoped, expiring token is.
|
|
277
|
+
const DASHBOARD_API_KEY = document.querySelector('meta[name="dash-token"]') ? document.querySelector('meta[name="dash-token"]').getAttribute('content') : '';
|
|
278
|
+
|
|
279
|
+
// Must match the server PTY size (src/shell/screen.ts ScreenModel defaults).
|
|
280
|
+
const PTY_COLS = 200;
|
|
281
|
+
const PTY_ROWS = 50;
|
|
282
|
+
|
|
125
283
|
function fmtUptime(seconds) {
|
|
126
284
|
const h = Math.floor(seconds / 3600);
|
|
127
285
|
const m = Math.floor((seconds % 3600) / 60);
|
|
@@ -131,86 +289,282 @@ function generateDashboardHtml() {
|
|
|
131
289
|
return s + 's';
|
|
132
290
|
}
|
|
133
291
|
|
|
292
|
+
// Spawned timestamp, formatted like "6/14/2026, 10:32:31 PM" (en-US, 12h).
|
|
134
293
|
function fmtTs(ts) {
|
|
135
|
-
if (!ts) return '<span class="ts"
|
|
294
|
+
if (!ts) return '<span class="ts">—</span>';
|
|
136
295
|
try {
|
|
137
296
|
const d = new Date(ts);
|
|
138
|
-
return '<span class="ts">' + d.
|
|
297
|
+
return '<span class="ts">' + d.toLocaleString('en-US', {
|
|
298
|
+
year: 'numeric', month: 'numeric', day: 'numeric',
|
|
299
|
+
hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true
|
|
300
|
+
}) + '</span>';
|
|
139
301
|
} catch(e) { return ts; }
|
|
140
302
|
}
|
|
141
303
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
304
|
+
function basePath() {
|
|
305
|
+
const p = window.location.pathname;
|
|
306
|
+
if (p.endsWith('/dashboard')) return p.slice(0, -10);
|
|
307
|
+
if (p.endsWith('/dashboard/')) return p.slice(0, -11);
|
|
308
|
+
return p.endsWith('/') ? p.slice(0, -1) : p;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function apiUrl(path) {
|
|
312
|
+
return basePath() + path;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function wsPtyUrl(agentId, sessionId) {
|
|
316
|
+
// Exchange the API key for a short-lived ticket so the key never appears in
|
|
317
|
+
// the WS URL (which would expose it in server logs and browser history).
|
|
318
|
+
// Streams are per-session, so the session id is always part of the request.
|
|
319
|
+
const base = basePath() + '/api/v1/agents/' + encodeURIComponent(agentId) + '/pty-stream';
|
|
320
|
+
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
321
|
+
if (!DASHBOARD_API_KEY) {
|
|
322
|
+
return proto + '//' + window.location.host + base + '?session=' + encodeURIComponent(sessionId);
|
|
323
|
+
}
|
|
324
|
+
const r = await fetch(apiUrl('/api/v1/pty-stream-ticket'), {
|
|
325
|
+
method: 'POST',
|
|
326
|
+
headers: { 'Content-Type': 'application/json', 'X-Dash-Token': DASHBOARD_API_KEY },
|
|
327
|
+
body: JSON.stringify({ agentId, sessionId }),
|
|
328
|
+
});
|
|
329
|
+
const { ticket } = await r.json();
|
|
330
|
+
return proto + '//' + window.location.host + base + '?ticket=' + ticket;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── PTY Viewer ───────────────────────────────────────────────────────────
|
|
334
|
+
let term = null;
|
|
335
|
+
let ptyWs = null;
|
|
336
|
+
let currentPtyAgent = null;
|
|
337
|
+
let currentPtySession = null;
|
|
338
|
+
// Streaming UTF-8 decoder. The PTY stream carries raw UTF-8 bytes (box-drawing
|
|
339
|
+
// chars, spinner braille, emoji). Decoding them as latin1 mangles every
|
|
340
|
+
// multi-byte char into noise — decode as UTF-8 with {stream:true} so sequences
|
|
341
|
+
// split across WebSocket frames are reassembled instead of corrupted.
|
|
342
|
+
let utf8Decoder = null;
|
|
343
|
+
|
|
344
|
+
// The agent's TUI enables mouse tracking (DECSET 1000/1002/1003/1006...).
|
|
345
|
+
// While those modes are active, xterm.js forwards wheel/click events to the
|
|
346
|
+
// app as mouse escapes, which can leave stray report bytes in the view. This
|
|
347
|
+
// is a view-only mirror (disableStdin) so mouse reporting is useless here —
|
|
348
|
+
// strip the set/reset sequences to keep the mirror clean.
|
|
349
|
+
function stripMouseModes(s) {
|
|
350
|
+
return s.replace(/\\x1b\\[\\?(1000|1001|1002|1003|1004|1005|1006|1015|1016)[hl]/g, '');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function openPtyViewer(agentId, sessionId) {
|
|
354
|
+
// Compare the SESSION, not the agent: one agent can have several sessions,
|
|
355
|
+
// each its own stream. Guarding on agent alone made switching between two
|
|
356
|
+
// sessions of the same agent a no-op (the viewer never reconnected).
|
|
357
|
+
if (currentPtySession === sessionId && ptyWs && ptyWs.readyState === WebSocket.OPEN) return;
|
|
358
|
+
closePtyViewer();
|
|
359
|
+
|
|
360
|
+
currentPtyAgent = agentId;
|
|
361
|
+
currentPtySession = sessionId;
|
|
362
|
+
document.getElementById('pty-agent-label').textContent = agentId;
|
|
363
|
+
// Append the session id after the agent name, e.g. "claude-founder · 3c01897c…".
|
|
364
|
+
document.getElementById('pty-session-label').textContent = sessionId ? ' \\u00b7 ' + sessionId : '';
|
|
365
|
+
document.getElementById('pty-viewer').style.display = 'block';
|
|
366
|
+
|
|
367
|
+
if (!term) {
|
|
368
|
+
term = new Terminal({
|
|
369
|
+
theme: { background: '#0d1117', foreground: '#e2e8f0', cursor: '#63b3ed' },
|
|
370
|
+
fontSize: 11,
|
|
371
|
+
lineHeight: 1.0,
|
|
372
|
+
letterSpacing: 0,
|
|
373
|
+
fontFamily: '"JetBrains Mono", "Cascadia Code", Menlo, Monaco, Consolas, "Courier New", monospace',
|
|
374
|
+
fontWeight: 400,
|
|
375
|
+
fontWeightBold: 600,
|
|
376
|
+
// Fixed dimensions matching the server PTY — do NOT auto-fit, the
|
|
377
|
+
// size mismatch is what makes the output unreadable.
|
|
378
|
+
cols: PTY_COLS,
|
|
379
|
+
rows: PTY_ROWS,
|
|
380
|
+
// Alt-screen TUI has no scrollback (the live mirror only shows the
|
|
381
|
+
// current screen), so don't retain any — this also removes the
|
|
382
|
+
// non-functional vertical scrollbar.
|
|
383
|
+
scrollback: 0,
|
|
384
|
+
// View-only mirror of the agent's TUI.
|
|
385
|
+
disableStdin: true,
|
|
386
|
+
cursorBlink: false,
|
|
387
|
+
convertEol: false,
|
|
388
|
+
});
|
|
389
|
+
term.open(document.getElementById('pty-terminal'));
|
|
390
|
+
// After open(), xterm has measured cell height. Pin the container height
|
|
391
|
+
// so all PTY_ROWS are always visible — prevents the alt-screen bottom rows
|
|
392
|
+
// (cost bar, status line) from being clipped by parent overflow or viewport.
|
|
393
|
+
requestAnimationFrame(function() {
|
|
394
|
+
const screen = document.querySelector('#pty-terminal .xterm-screen');
|
|
395
|
+
if (screen) {
|
|
396
|
+
const h = screen.offsetHeight;
|
|
397
|
+
if (h > 0) document.getElementById('pty-terminal').style.minHeight = h + 'px';
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
} else {
|
|
401
|
+
term.reset();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Fresh decoder per session so a leftover partial byte from a previous
|
|
405
|
+
// viewing can't corrupt the first character of this stream.
|
|
406
|
+
utf8Decoder = new TextDecoder('utf-8');
|
|
407
|
+
|
|
408
|
+
const url = await wsPtyUrl(agentId, sessionId);
|
|
409
|
+
ptyWs = new WebSocket(url);
|
|
410
|
+
ptyWs.binaryType = 'arraybuffer';
|
|
411
|
+
|
|
412
|
+
ptyWs.onmessage = function(ev) {
|
|
413
|
+
const data = ev.data instanceof ArrayBuffer
|
|
414
|
+
? utf8Decoder.decode(ev.data, { stream: true })
|
|
415
|
+
: ev.data;
|
|
416
|
+
term.write(stripMouseModes(data));
|
|
417
|
+
};
|
|
418
|
+
ptyWs.onclose = function(ev) {
|
|
419
|
+
if (term) term.writeln('\\r\\n\\x1b[33m[disconnected: ' + (ev.reason || 'closed') + ']\\x1b[0m');
|
|
420
|
+
};
|
|
421
|
+
ptyWs.onerror = function() { if (term) term.writeln('\\r\\n\\x1b[31m[connection error]\\x1b[0m'); };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function closePtyViewer() {
|
|
425
|
+
if (ptyWs) { ptyWs.close(); ptyWs = null; }
|
|
426
|
+
currentPtyAgent = null;
|
|
427
|
+
currentPtySession = null;
|
|
428
|
+
document.getElementById('pty-viewer').style.display = 'none';
|
|
146
429
|
}
|
|
147
430
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
431
|
+
// Refresh: force a clean reconnect of the CURRENT session. The server replays
|
|
432
|
+
// a freshly-serialized screen frame on subscribe, so this redraws from a clean
|
|
433
|
+
// xterm state — a manual escape hatch if the live stream ever drifts.
|
|
434
|
+
async function refreshPtyViewer() {
|
|
435
|
+
const agentId = currentPtyAgent;
|
|
436
|
+
const sessionId = currentPtySession;
|
|
437
|
+
if (!agentId) return;
|
|
438
|
+
closePtyViewer();
|
|
439
|
+
await openPtyViewer(agentId, sessionId);
|
|
152
440
|
}
|
|
153
441
|
|
|
442
|
+
document.getElementById('pty-close-btn').addEventListener('click', closePtyViewer);
|
|
443
|
+
document.getElementById('pty-refresh-btn').addEventListener('click', function() { void refreshPtyViewer(); });
|
|
444
|
+
|
|
445
|
+
// Event delegation for Live buttons (avoids inline onclick + HTML injection)
|
|
446
|
+
document.getElementById('sessions-tbody').addEventListener('click', function(e) {
|
|
447
|
+
const btn = e.target.closest('.btn-stream');
|
|
448
|
+
if (btn) void openPtyViewer(btn.getAttribute('data-agent-id'), btn.getAttribute('data-session-id'));
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
function escHtml(s) {
|
|
452
|
+
return String(s)
|
|
453
|
+
.replace(/&/g, '&')
|
|
454
|
+
.replace(/</g, '<')
|
|
455
|
+
.replace(/>/g, '>')
|
|
456
|
+
.replace(/"/g, '"');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function modeBadge(mode) {
|
|
460
|
+
if (mode === 'pty-shell') return '<span class="badge badge-blue">wrap-shell</span>';
|
|
461
|
+
if (mode === 'headless') return '<span class="badge badge-purple">headless</span>';
|
|
462
|
+
return '<span class="ts">' + escHtml(mode || '?') + '</span>';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Source badge with a per-channel color: telegram=blue, discord=purple,
|
|
466
|
+
// api=gray. Unknown sources fall back to gray.
|
|
467
|
+
function sourceBadge(source) {
|
|
468
|
+
const s = String(source || '?').toLowerCase();
|
|
469
|
+
if (s === 'telegram') return '<span class="badge badge-blue">telegram</span>';
|
|
470
|
+
if (s === 'discord') return '<span class="badge badge-purple">discord</span>';
|
|
471
|
+
if (s === 'api') return '<span class="badge badge-gray">api</span>';
|
|
472
|
+
return '<span class="badge badge-gray">' + escHtml(source || '?') + '</span>';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Prettify a model id for display: drop the "claude-" prefix and any
|
|
476
|
+
// trailing date stamp, e.g. claude-haiku-4-5-20251001 -> haiku-4-5.
|
|
477
|
+
// Each model family gets a distinct badge color; full id kept in the tooltip.
|
|
478
|
+
function fmtModel(m) {
|
|
479
|
+
if (!m) return '<span class="ts">—</span>';
|
|
480
|
+
const id = String(m);
|
|
481
|
+
const label = id.replace(/^claude-/, '').replace(/-\\d{8}$/, '');
|
|
482
|
+
let cls = 'badge-model';
|
|
483
|
+
if (/opus/i.test(id)) cls = 'badge-opus';
|
|
484
|
+
else if (/sonnet/i.test(id)) cls = 'badge-sonnet';
|
|
485
|
+
else if (/haiku/i.test(id)) cls = 'badge-haiku';
|
|
486
|
+
else if (/fable/i.test(id)) cls = 'badge-fable';
|
|
487
|
+
return '<span class="badge ' + cls + '" title="' + escHtml(id) + '">' + escHtml(label) + '</span>';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Format a context-window token count compactly: 1234 -> "1.2k", 45000 -> "45k".
|
|
491
|
+
// Full value is kept in the tooltip. 0/unknown renders as a dash.
|
|
492
|
+
function fmtTokens(n) {
|
|
493
|
+
const v = Number(n) || 0;
|
|
494
|
+
if (v <= 0) return '<span class="ts">—</span>';
|
|
495
|
+
const label = v >= 1000 ? (v / 1000).toFixed(v >= 10000 ? 0 : 1) + 'k' : String(v);
|
|
496
|
+
return '<span title="' + v.toLocaleString() + ' tokens">' + label + '</span>';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ── Status Refresh ────────────────────────────────────────────────────────
|
|
154
500
|
async function refresh() {
|
|
155
501
|
document.getElementById('refresh-indicator').textContent = 'refreshing...';
|
|
156
502
|
try {
|
|
157
|
-
const res = await fetch('/status');
|
|
503
|
+
const res = await fetch(apiUrl('/status'));
|
|
158
504
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
159
505
|
const data = await res.json();
|
|
160
506
|
|
|
161
507
|
document.getElementById('uptime').textContent = fmtUptime(data.uptime || 0);
|
|
162
508
|
document.getElementById('started-at').textContent = data.startedAt
|
|
163
|
-
? new Date(data.startedAt).toLocaleString() : '
|
|
509
|
+
? new Date(data.startedAt).toLocaleString() : '\\u2014';
|
|
164
510
|
document.getElementById('last-updated').textContent = new Date().toLocaleTimeString();
|
|
511
|
+
if (data.version) document.getElementById('gateway-version').textContent = 'v' + data.version;
|
|
165
512
|
document.getElementById('error-msg').style.display = 'none';
|
|
166
513
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
agentRows.length ? agentRows.join('') : '<tr><td colspan="5" class="ts">No agents</td></tr>';
|
|
179
|
-
|
|
180
|
-
// Heartbeat table
|
|
181
|
-
const hbRows = [];
|
|
182
|
-
(data.agents || []).forEach(function(a) {
|
|
183
|
-
const lastResults = (a.heartbeat && a.heartbeat.lastResults) || [];
|
|
184
|
-
lastResults.forEach(function(r) {
|
|
185
|
-
if (!r) return;
|
|
186
|
-
hbRows.push('<tr>' +
|
|
187
|
-
'<td>' + a.id + '</td>' +
|
|
188
|
-
'<td>' + r.taskName + '</td>' +
|
|
189
|
-
'<td>' + fmtTs(r.ts) + '</td>' +
|
|
190
|
-
'<td>' + resultBadge(r.suppressed, r.rateLimited) + '</td>' +
|
|
191
|
-
'</tr>');
|
|
192
|
-
});
|
|
514
|
+
const agents = data.agents || [];
|
|
515
|
+
|
|
516
|
+
// Agent badges bar.
|
|
517
|
+
// Green = available. An agent with a channel receiver (telegram/discord)
|
|
518
|
+
// is green only while its receiver is running; API-only agents have no
|
|
519
|
+
// receiver, so they are always green as long as the gateway loaded them.
|
|
520
|
+
// Red = a channel agent whose receiver is down (genuinely stopped).
|
|
521
|
+
const badges = agents.map(function(a) {
|
|
522
|
+
const ok = a.hasChannel ? a.isRunning : true;
|
|
523
|
+
const dot = ok ? '<span class="dot-green">●</span>' : '<span class="dot-red">●</span>';
|
|
524
|
+
return '<span class="agent-badge">' + dot + ' <span class="agent-name">' + escHtml(a.id) + '</span></span>';
|
|
193
525
|
});
|
|
194
|
-
document.getElementById('
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
'<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'<
|
|
207
|
-
'
|
|
526
|
+
document.getElementById('agents-bar').innerHTML = badges.join('') || '<span class="ts">No agents</span>';
|
|
527
|
+
|
|
528
|
+
// Sessions table — flat, session-centric. One row per real session across
|
|
529
|
+
// all agents; agents with no session do not produce a row.
|
|
530
|
+
const rows = [];
|
|
531
|
+
agents.forEach(function(a) {
|
|
532
|
+
(a.sessions || []).forEach(function(s) {
|
|
533
|
+
const statusBadge = s.isRunning
|
|
534
|
+
? '<span class="badge badge-green">running</span>'
|
|
535
|
+
: '<span class="badge badge-gray">stopped</span>';
|
|
536
|
+
const uptime = s.isRunning ? fmtUptime(s.uptimeSec || 0) : '<span class="ts">—</span>';
|
|
537
|
+
const sessId = s.sessionId
|
|
538
|
+
? '<span class="session-id">' + escHtml(s.sessionId) + '</span>'
|
|
539
|
+
: '<span class="ts">—</span>';
|
|
540
|
+
const chatCell = s.chatId
|
|
541
|
+
? '<span class="session-id">' + escHtml(String(s.chatId)) + '</span>'
|
|
542
|
+
: '<span class="ts">—</span>';
|
|
543
|
+
const liveBtn = (s.hasPtyStream && s.isRunning && s.mode === 'pty-shell')
|
|
544
|
+
? '<button class="btn-stream" data-agent-id="' + escHtml(a.id) + '" data-session-id="' + escHtml(s.sessionId || '') + '">💻 View</button>'
|
|
545
|
+
: '<span class="ts">—</span>';
|
|
546
|
+
rows.push(
|
|
547
|
+
'<tr class="session-row">' +
|
|
548
|
+
'<td><span style="color:#90cdf4;font-weight:600;">' + escHtml(a.id) + '</span></td>' +
|
|
549
|
+
'<td>' + sessId + '</td>' +
|
|
550
|
+
'<td>' + chatCell + '</td>' +
|
|
551
|
+
'<td>' + sourceBadge(s.source) + '</td>' +
|
|
552
|
+
'<td>' + modeBadge(s.mode) + '</td>' +
|
|
553
|
+
'<td>' + fmtModel(s.model) + '</td>' +
|
|
554
|
+
'<td>' + fmtTokens(s.tokens) + '</td>' +
|
|
555
|
+
'<td>' + statusBadge + '</td>' +
|
|
556
|
+
'<td>' + uptime + '</td>' +
|
|
557
|
+
'<td>' + fmtTs(s.spawnedAt ? new Date(s.spawnedAt).toISOString() : null) + '</td>' +
|
|
558
|
+
'<td>' + liveBtn + '</td>' +
|
|
559
|
+
'</tr>'
|
|
560
|
+
);
|
|
208
561
|
});
|
|
209
562
|
});
|
|
563
|
+
|
|
210
564
|
document.getElementById('sessions-tbody').innerHTML =
|
|
211
|
-
|
|
565
|
+
rows.length ? rows.join('') : '<tr><td colspan="11" class="ts">No active sessions</td></tr>';
|
|
212
566
|
|
|
213
|
-
document.getElementById('refresh-indicator').textContent = 'auto-refresh
|
|
567
|
+
document.getElementById('refresh-indicator').textContent = 'auto-refresh 3s';
|
|
214
568
|
} catch(e) {
|
|
215
569
|
document.getElementById('error-msg').textContent = 'Error fetching status: ' + e.message;
|
|
216
570
|
document.getElementById('error-msg').style.display = 'block';
|
|
@@ -218,8 +572,155 @@ function generateDashboardHtml() {
|
|
|
218
572
|
}
|
|
219
573
|
}
|
|
220
574
|
|
|
575
|
+
// ── Process Tree ─────────────────────────────────────────────────────────
|
|
576
|
+
async function refreshProcesses() {
|
|
577
|
+
try {
|
|
578
|
+
const res = await fetch(apiUrl('/processes'));
|
|
579
|
+
if (!res.ok) return;
|
|
580
|
+
const data = await res.json();
|
|
581
|
+
renderProcessTree(data.processes || []);
|
|
582
|
+
} catch(e) {
|
|
583
|
+
document.getElementById('proc-tree').textContent = 'Error: ' + e.message;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function renderProcessTree(procs) {
|
|
588
|
+
if (!procs.length) {
|
|
589
|
+
document.getElementById('proc-tree').textContent = '— no gateway processes found —';
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const pidMap = {};
|
|
594
|
+
procs.forEach(function(p) { pidMap[p.pid] = p; });
|
|
595
|
+
|
|
596
|
+
// Aggregate resource usage across the whole gateway process tree.
|
|
597
|
+
// %CPU is ps's lifetime average per process (summed → total load share);
|
|
598
|
+
// RSS is summed and may slightly over-count shared pages, but is a good
|
|
599
|
+
// proxy for "memory used by the gateway".
|
|
600
|
+
let totalCpu = 0, totalRssKb = 0;
|
|
601
|
+
procs.forEach(function(p) {
|
|
602
|
+
totalCpu += Number(p.cpu) || 0;
|
|
603
|
+
totalRssKb += Number(p.rssKb) || 0;
|
|
604
|
+
});
|
|
605
|
+
const totalMemMb = totalRssKb / 1024;
|
|
606
|
+
const memStr = totalMemMb >= 1024
|
|
607
|
+
? (totalMemMb / 1024).toFixed(2) + ' GB'
|
|
608
|
+
: totalMemMb.toFixed(0) + ' MB';
|
|
609
|
+
|
|
610
|
+
function cat(p) {
|
|
611
|
+
const a = p.args;
|
|
612
|
+
if (a.includes('node') && a.includes('dist/index')) return 'orchestrator';
|
|
613
|
+
if (a.includes('claude-pty-shell')) return 'pty';
|
|
614
|
+
if (a.includes('bun') && a.includes('mcp/server')) return 'mcp';
|
|
615
|
+
if (a.includes('bun') && a.includes('telegram') && a.includes('receiver')) return 'telegram';
|
|
616
|
+
if (a.includes('bun') && a.includes('discord') && a.includes('receiver')) return 'discord';
|
|
617
|
+
if (a.includes('--mcp-config') && (a.includes('--session-id') || a.includes('--print'))) {
|
|
618
|
+
if (a.includes('--session-id')) {
|
|
619
|
+
const parent = pidMap[p.ppid];
|
|
620
|
+
return (parent && cat(parent) === 'pty') ? 'claude-pty' : 'claude-headless';
|
|
621
|
+
}
|
|
622
|
+
return 'claude-headless';
|
|
623
|
+
}
|
|
624
|
+
return 'other';
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Show full command lines (no truncation) — text wraps inside the box.
|
|
628
|
+
function full(args) {
|
|
629
|
+
return escHtml(args);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function sessionId(args) {
|
|
633
|
+
const m = args.match(/--session-id\\s+(\\S+)/);
|
|
634
|
+
return m ? escHtml(m[1]) : '?';
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function agentName(args) {
|
|
638
|
+
const m = args.match(/agents\\/([^/]+)\\/workspace/);
|
|
639
|
+
return m ? m[1] : '?';
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const lines = [];
|
|
643
|
+
// First line: total resource usage across the whole gateway tree.
|
|
644
|
+
lines.push(
|
|
645
|
+
'<span class="proc-summary">' +
|
|
646
|
+
'\\u2211 ' + procs.length + ' procs' +
|
|
647
|
+
' \\u00b7 CPU ' + totalCpu.toFixed(1) + '%' +
|
|
648
|
+
' \\u00b7 MEM ' + memStr +
|
|
649
|
+
'</span>'
|
|
650
|
+
);
|
|
651
|
+
lines.push('');
|
|
652
|
+
const orchestrator = procs.find(function(p) { return cat(p) === 'orchestrator'; });
|
|
653
|
+
const ptys = procs.filter(function(p) { return cat(p) === 'pty'; });
|
|
654
|
+
const headless = procs.filter(function(p) { return cat(p) === 'claude-headless'; });
|
|
655
|
+
const telegramReceivers = procs.filter(function(p) { return cat(p) === 'telegram'; });
|
|
656
|
+
const discordReceivers = procs.filter(function(p) { return cat(p) === 'discord'; });
|
|
657
|
+
const mcpServers = procs.filter(function(p) { return cat(p) === 'mcp'; });
|
|
658
|
+
|
|
659
|
+
const gatewayPids = new Set(procs.map(function(p) { return p.pid; }));
|
|
660
|
+
const orphans = procs.filter(function(p) {
|
|
661
|
+
const c = cat(p);
|
|
662
|
+
return (c === 'claude-pty' || c === 'claude-headless' || c === 'pty' || c === 'mcp')
|
|
663
|
+
&& !gatewayPids.has(p.ppid)
|
|
664
|
+
&& p.pid !== (orchestrator && orchestrator.pid);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
if (orchestrator) {
|
|
668
|
+
lines.push('<span class="proc-orchestrator">Orchestrator</span>');
|
|
669
|
+
lines.push(' PID ' + orchestrator.pid + ' <span class="proc-orchestrator">' + full(orchestrator.args) + '</span>');
|
|
670
|
+
lines.push('');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const sessionCount = ptys.length + headless.length;
|
|
674
|
+
lines.push('<span class="proc-label">Sessions (' + sessionCount + ')</span>');
|
|
675
|
+
|
|
676
|
+
ptys.forEach(function(pty) {
|
|
677
|
+
const agent = agentName(pty.args);
|
|
678
|
+
lines.push(' PID ' + pty.pid + ' <span class="proc-pty">wrap-shell</span> [' + agent + ']');
|
|
679
|
+
const claudeChild = procs.find(function(p) { return p.ppid === pty.pid && cat(p) === 'claude-pty'; });
|
|
680
|
+
if (claudeChild) {
|
|
681
|
+
lines.push(' \\u2514\\u2500 PID ' + claudeChild.pid + ' <span class="proc-claude">claude ' + sessionId(claudeChild.args) + '</span>');
|
|
682
|
+
const mcp = mcpServers.find(function(p) { return p.ppid === claudeChild.pid; });
|
|
683
|
+
if (mcp) {
|
|
684
|
+
lines.push(' \\u2514\\u2500 PID ' + mcp.pid + ' <span class="proc-mcp">mcp</span>');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
headless.forEach(function(cl) {
|
|
690
|
+
const agent = agentName(cl.args);
|
|
691
|
+
lines.push(' PID ' + cl.pid + ' <span class="proc-claude">claude --print</span>' + (agent !== '?' ? ' [' + agent + ']' : ''));
|
|
692
|
+
const mcp = mcpServers.find(function(p) { return p.ppid === cl.pid; });
|
|
693
|
+
if (mcp) {
|
|
694
|
+
lines.push(' \\u2514\\u2500 PID ' + mcp.pid + ' <span class="proc-mcp">mcp</span>');
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
if (sessionCount === 0) lines.push(' <span class="ts">\\u2014 none \\u2014</span>');
|
|
699
|
+
lines.push('');
|
|
700
|
+
|
|
701
|
+
lines.push('<span class="proc-label">Receivers</span>');
|
|
702
|
+
if (telegramReceivers.length) lines.push(' Telegram \\u00d7' + telegramReceivers.length);
|
|
703
|
+
if (discordReceivers.length) lines.push(' Discord \\u00d7' + discordReceivers.length);
|
|
704
|
+
if (!telegramReceivers.length && !discordReceivers.length) lines.push(' <span class="ts">\\u2014 none \\u2014</span>');
|
|
705
|
+
lines.push('');
|
|
706
|
+
|
|
707
|
+
lines.push('<span class="proc-label">Orphans</span>');
|
|
708
|
+
if (orphans.length) {
|
|
709
|
+
orphans.forEach(function(p) {
|
|
710
|
+
lines.push(' \\u26a0 PID ' + p.pid + ' <span class="proc-orphan">' + full(p.args) + '</span>');
|
|
711
|
+
});
|
|
712
|
+
} else {
|
|
713
|
+
lines.push(' <span class="ts">none \\u2705</span>');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
document.getElementById('proc-tree').innerHTML = lines.join('\\n');
|
|
717
|
+
}
|
|
718
|
+
|
|
221
719
|
refresh();
|
|
222
|
-
|
|
720
|
+
refreshProcesses();
|
|
721
|
+
setInterval(refresh, 3000);
|
|
722
|
+
// Process tree (with CPU/mem) is heavier (spawns ps) — refresh a bit slower.
|
|
723
|
+
setInterval(refreshProcesses, 6000);
|
|
223
724
|
</script>
|
|
224
725
|
</body>
|
|
225
726
|
</html>`;
|