@arvoretech/runtime-lens-mcp 1.1.0 → 1.2.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/agent/index.ts +281 -212
- package/dist/agent/index.js +92 -36
- package/dist/agent/index.js.map +2 -2
- package/dist/agent-bridge.d.ts +20 -0
- package/dist/agent-bridge.d.ts.map +1 -0
- package/dist/agent-bridge.js +185 -0
- package/dist/agent-bridge.js.map +1 -0
- package/dist/extension/extension.js +58 -32
- package/dist/extension/extension.js.map +2 -2
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +115 -100
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-bridge.ts +218 -0
- package/src/server.ts +405 -358
package/agent/index.ts
CHANGED
|
@@ -1,263 +1,332 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
4
|
import type { Socket } from "node:net";
|
|
5
5
|
|
|
6
6
|
const PORT = parseInt(process.env.RUNTIME_LENS_PORT || "9500", 10);
|
|
7
7
|
|
|
8
8
|
interface LogMessage {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
type: "log" | "error" | "warn" | "info" | "debug" | "result";
|
|
10
|
+
file: string;
|
|
11
|
+
line: number;
|
|
12
|
+
column: number;
|
|
13
|
+
values: string[];
|
|
14
|
+
timestamp: number;
|
|
15
|
+
expression?: string;
|
|
16
|
+
count?: number;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
const clients: Set<Socket> = new Set();
|
|
19
20
|
const buffer: LogMessage[] = [];
|
|
20
|
-
const MAX_BUFFER =
|
|
21
|
+
const MAX_BUFFER = 1000;
|
|
21
22
|
|
|
22
23
|
function serialize(value: unknown, depth = 0): string {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
24
|
+
if (depth > 3) return "[...]";
|
|
25
|
+
if (value === null) return "null";
|
|
26
|
+
if (value === undefined) return "undefined";
|
|
27
|
+
if (typeof value === "string")
|
|
28
|
+
return value.length > 100 ? `"${value.slice(0, 100)}..."` : `"${value}"`;
|
|
29
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
30
|
+
return String(value);
|
|
31
|
+
if (typeof value === "function") return `fn ${value.name || "anonymous"}()`;
|
|
32
|
+
if (typeof value === "symbol") return value.toString();
|
|
33
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
34
|
+
if (value instanceof Error)
|
|
35
|
+
return `${value.name}: ${value.message}${value.stack ? "\n" + value.stack : ""}`;
|
|
36
|
+
if (value instanceof Date) return value.toISOString();
|
|
37
|
+
if (value instanceof RegExp) return value.toString();
|
|
38
|
+
if (value instanceof Map) return `Map(${value.size})`;
|
|
39
|
+
if (value instanceof Set) return `Set(${value.size})`;
|
|
40
|
+
if (value instanceof Promise) return "Promise";
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
if (value.length === 0) return "[]";
|
|
43
|
+
const items = value.slice(0, 5).map((v) => serialize(v, depth + 1));
|
|
44
|
+
const suffix = value.length > 5 ? `, ...+${value.length - 5}` : "";
|
|
45
|
+
return `[${items.join(", ")}${suffix}]`;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === "object") {
|
|
48
|
+
const keys = Object.keys(value);
|
|
49
|
+
if (keys.length === 0) return "{}";
|
|
50
|
+
const entries = keys
|
|
51
|
+
.slice(0, 5)
|
|
52
|
+
.map(
|
|
53
|
+
(k) =>
|
|
54
|
+
`${k}: ${serialize((value as Record<string, unknown>)[k], depth + 1)}`,
|
|
55
|
+
);
|
|
56
|
+
const suffix = keys.length > 5 ? `, ...+${keys.length - 5}` : "";
|
|
57
|
+
return `{${entries.join(", ")}${suffix}}`;
|
|
58
|
+
}
|
|
59
|
+
return typeof value === "object"
|
|
60
|
+
? Object.prototype.toString.call(value)
|
|
61
|
+
: String(value);
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
function broadcast(msg: LogMessage): void {
|
|
65
|
+
const payload = JSON.stringify(msg);
|
|
66
|
+
const buf = Buffer.from(payload, "utf-8");
|
|
67
|
+
const frame = buildWsFrame(buf);
|
|
68
|
+
|
|
69
|
+
for (const client of clients) {
|
|
70
|
+
if (!client.destroyed) {
|
|
71
|
+
client.write(frame);
|
|
72
|
+
} else {
|
|
73
|
+
clients.delete(client);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Always buffer for late-connecting clients (MCP server, extension)
|
|
78
|
+
buffer.push(msg);
|
|
79
|
+
if (buffer.length > MAX_BUFFER) buffer.shift();
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
function flushBuffer(): void {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
client.write(buildWsFrame(buf));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
82
|
+
function flushBuffer(socket: Socket): void {
|
|
83
|
+
for (const msg of buffer) {
|
|
84
|
+
const buf = Buffer.from(JSON.stringify(msg), "utf-8");
|
|
85
|
+
socket.write(buildWsFrame(buf));
|
|
86
|
+
}
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
function buildWsFrame(data: Buffer): Buffer {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
const len = data.length;
|
|
91
|
+
let header: Buffer;
|
|
92
|
+
if (len < 126) {
|
|
93
|
+
header = Buffer.alloc(2);
|
|
94
|
+
header[0] = 0x81;
|
|
95
|
+
header[1] = len;
|
|
96
|
+
} else if (len < 65536) {
|
|
97
|
+
header = Buffer.alloc(4);
|
|
98
|
+
header[0] = 0x81;
|
|
99
|
+
header[1] = 126;
|
|
100
|
+
header.writeUInt16BE(len, 2);
|
|
101
|
+
} else {
|
|
102
|
+
header = Buffer.alloc(10);
|
|
103
|
+
header[0] = 0x81;
|
|
104
|
+
header[1] = 127;
|
|
105
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
106
|
+
}
|
|
107
|
+
return Buffer.concat([header, data]);
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
function parseWsFrame(data: Buffer): string | null {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
111
|
+
if (data.length < 2) return null;
|
|
112
|
+
const masked = (data[1] & 0x80) !== 0;
|
|
113
|
+
let payloadLen = data[1] & 0x7f;
|
|
114
|
+
let offset = 2;
|
|
115
|
+
|
|
116
|
+
if (payloadLen === 126) {
|
|
117
|
+
payloadLen = data.readUInt16BE(2);
|
|
118
|
+
offset = 4;
|
|
119
|
+
} else if (payloadLen === 127) {
|
|
120
|
+
payloadLen = Number(data.readBigUInt64BE(2));
|
|
121
|
+
offset = 10;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let maskKey: Buffer | null = null;
|
|
125
|
+
if (masked) {
|
|
126
|
+
maskKey = data.subarray(offset, offset + 4);
|
|
127
|
+
offset += 4;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const payload = data.subarray(offset, offset + payloadLen);
|
|
131
|
+
if (maskKey) {
|
|
132
|
+
for (let i = 0; i < payload.length; i++) {
|
|
133
|
+
payload[i] ^= maskKey[i % 4];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return payload.toString("utf-8");
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
function extractCallSite(): { file: string; line: number; column: number } {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
const stack = new Error().stack || "";
|
|
141
|
+
const lines = stack.split("\n");
|
|
142
|
+
for (let i = 2; i < lines.length; i++) {
|
|
143
|
+
const line = lines[i];
|
|
144
|
+
if (line.includes("node:") || line.includes("dist/agent/index.js"))
|
|
145
|
+
continue;
|
|
146
|
+
const match =
|
|
147
|
+
/\((.+):(\d+):(\d+)\)/.exec(line) || /at (.+):(\d+):(\d+)/.exec(line);
|
|
148
|
+
if (match) {
|
|
149
|
+
return {
|
|
150
|
+
file: match[1],
|
|
151
|
+
line: parseInt(match[2], 10),
|
|
152
|
+
column: parseInt(match[3], 10),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { file: "unknown", line: 0, column: 0 };
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
type ConsoleMethod = "log" | "info" | "warn" | "error" | "debug";
|
|
140
160
|
const originalConsole: Record<ConsoleMethod, (...args: unknown[]) => void> = {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
log: console.log.bind(console),
|
|
162
|
+
info: console.info.bind(console),
|
|
163
|
+
warn: console.warn.bind(console),
|
|
164
|
+
error: console.error.bind(console),
|
|
165
|
+
debug: console.debug.bind(console),
|
|
146
166
|
};
|
|
147
167
|
|
|
148
168
|
function patchConsole(): void {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
const methods: ConsoleMethod[] = ["log", "info", "warn", "error", "debug"];
|
|
170
|
+
for (const method of methods) {
|
|
171
|
+
console[method] = (...args: unknown[]) => {
|
|
172
|
+
const site = extractCallSite();
|
|
173
|
+
broadcast({
|
|
174
|
+
type: method,
|
|
175
|
+
file: site.file,
|
|
176
|
+
line: site.line,
|
|
177
|
+
column: site.column,
|
|
178
|
+
values: args.map((a) => serialize(a)),
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
});
|
|
181
|
+
originalConsole[method](...args);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
164
184
|
}
|
|
165
185
|
|
|
166
186
|
const server = createServer((_req, res) => {
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
188
|
+
res.end(
|
|
189
|
+
JSON.stringify({
|
|
190
|
+
status: "ok",
|
|
191
|
+
pid: process.pid,
|
|
192
|
+
uptime: process.uptime(),
|
|
193
|
+
clients: clients.size,
|
|
194
|
+
buffered: buffer.length,
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
169
197
|
});
|
|
170
198
|
|
|
171
199
|
server.on("upgrade", (req: IncomingMessage, socket: Socket) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
200
|
+
const key = req.headers["sec-websocket-key"];
|
|
201
|
+
if (!key) {
|
|
202
|
+
socket.destroy();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const acceptKey = createHash("sha1")
|
|
207
|
+
.update(key + "258EAFA5-E914-47DA-95CA-5AB5DC11650B")
|
|
208
|
+
.digest("base64");
|
|
209
|
+
|
|
210
|
+
socket.write(
|
|
211
|
+
"HTTP/1.1 101 Switching Protocols\r\n" +
|
|
212
|
+
"Upgrade: websocket\r\n" +
|
|
213
|
+
"Connection: Upgrade\r\n" +
|
|
214
|
+
`Sec-WebSocket-Accept: ${acceptKey}\r\n` +
|
|
215
|
+
"\r\n",
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
clients.add(socket);
|
|
219
|
+
originalConsole.log(
|
|
220
|
+
`[runtime-lens] client connected (${clients.size} total)`,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
flushBuffer(socket);
|
|
224
|
+
|
|
225
|
+
socket.on("data", (data: Buffer) => {
|
|
226
|
+
const text = parseWsFrame(data);
|
|
227
|
+
if (!text) return;
|
|
228
|
+
try {
|
|
229
|
+
const msg = JSON.parse(text);
|
|
230
|
+
if (msg.type === "eval") {
|
|
231
|
+
try {
|
|
232
|
+
const fn = new Function(`return (${msg.expression})`);
|
|
233
|
+
const result = fn();
|
|
234
|
+
broadcast({
|
|
235
|
+
type: "result",
|
|
236
|
+
file: msg.file || "eval",
|
|
237
|
+
line: msg.line || 0,
|
|
238
|
+
column: msg.column || 0,
|
|
239
|
+
values: [serialize(result)],
|
|
240
|
+
timestamp: Date.now(),
|
|
241
|
+
expression: msg.expression,
|
|
242
|
+
});
|
|
243
|
+
} catch (err: unknown) {
|
|
244
|
+
broadcast({
|
|
245
|
+
type: "error",
|
|
246
|
+
file: msg.file || "eval",
|
|
247
|
+
line: msg.line || 0,
|
|
248
|
+
column: msg.column || 0,
|
|
249
|
+
values: [err instanceof Error ? err.message : String(err)],
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
expression: msg.expression,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
} else if (msg.type === "get_buffer") {
|
|
255
|
+
const payload = JSON.stringify({ type: "buffer", logs: buffer });
|
|
256
|
+
const buf = Buffer.from(payload, "utf-8");
|
|
257
|
+
socket.write(buildWsFrame(buf));
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// invalid JSON
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
socket.on("close", () => {
|
|
265
|
+
clients.delete(socket);
|
|
266
|
+
originalConsole.log(
|
|
267
|
+
`[runtime-lens] client disconnected (${clients.size} total)`,
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
socket.on("error", () => {
|
|
272
|
+
clients.delete(socket);
|
|
273
|
+
});
|
|
235
274
|
});
|
|
236
275
|
|
|
237
276
|
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
277
|
+
if (err.code === "EADDRINUSE") {
|
|
278
|
+
originalConsole.log(
|
|
279
|
+
`[runtime-lens] port ${PORT} in use, skipping agent server`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
241
282
|
});
|
|
242
283
|
|
|
243
284
|
server.listen(PORT, () => {
|
|
244
|
-
|
|
285
|
+
originalConsole.log(
|
|
286
|
+
`[runtime-lens] agent listening on ws://localhost:${PORT}`,
|
|
287
|
+
);
|
|
245
288
|
});
|
|
246
289
|
|
|
247
|
-
|
|
290
|
+
let keepAliveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
291
|
+
const KEEP_ALIVE_MS = parseInt(
|
|
292
|
+
process.env.RUNTIME_LENS_KEEP_ALIVE || "30000",
|
|
293
|
+
10,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
function scheduleShutdown(): void {
|
|
297
|
+
if (keepAliveTimer) return;
|
|
298
|
+
keepAliveTimer = setTimeout(() => {
|
|
299
|
+
if (clients.size === 0) {
|
|
300
|
+
originalConsole.log(
|
|
301
|
+
`[runtime-lens] no clients connected, shutting down agent`,
|
|
302
|
+
);
|
|
303
|
+
server.close();
|
|
304
|
+
process.exit(0);
|
|
305
|
+
} else {
|
|
306
|
+
keepAliveTimer = null;
|
|
307
|
+
scheduleShutdown();
|
|
308
|
+
}
|
|
309
|
+
}, KEEP_ALIVE_MS);
|
|
310
|
+
keepAliveTimer.unref();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
process.on("beforeExit", () => {
|
|
314
|
+
server.ref();
|
|
315
|
+
scheduleShutdown();
|
|
316
|
+
});
|
|
248
317
|
|
|
249
318
|
patchConsole();
|
|
250
319
|
|
|
251
320
|
(globalThis as Record<string, unknown>).__runtimeLens = {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
321
|
+
log: (...args: unknown[]) => {
|
|
322
|
+
const site = extractCallSite();
|
|
323
|
+
broadcast({
|
|
324
|
+
type: "result",
|
|
325
|
+
file: site.file,
|
|
326
|
+
line: site.line,
|
|
327
|
+
column: site.column,
|
|
328
|
+
values: args.map((a) => serialize(a)),
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
});
|
|
331
|
+
},
|
|
263
332
|
};
|