@graphty/remote-logger 1.2.2 → 1.3.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/README.md +53 -2
- package/dist/bundle/browser-entry.d.ts +34 -0
- package/dist/bundle/browser-entry.d.ts.map +1 -0
- package/dist/bundle/browser-entry.js +129 -0
- package/dist/bundle/browser-entry.js.map +1 -0
- package/dist/client/RemoteLogClient.d.ts +2 -1
- package/dist/client/RemoteLogClient.d.ts.map +1 -1
- package/dist/client/RemoteLogClient.js +20 -10
- package/dist/client/RemoteLogClient.js.map +1 -1
- package/dist/client/types.d.ts +6 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +20 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/mcp-server.d.ts +5 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -1
- package/dist/mcp/mcp-server.js +54 -8
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +6 -6
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +1 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/logs-clear.d.ts +1 -2
- package/dist/mcp/tools/logs-clear.d.ts.map +1 -1
- package/dist/mcp/tools/logs-get-all.d.ts +1 -2
- package/dist/mcp/tools/logs-get-all.d.ts.map +1 -1
- package/dist/mcp/tools/logs-get-errors.d.ts +1 -2
- package/dist/mcp/tools/logs-get-errors.d.ts.map +1 -1
- package/dist/mcp/tools/logs-get-file-path.d.ts +8 -2
- package/dist/mcp/tools/logs-get-file-path.d.ts.map +1 -1
- package/dist/mcp/tools/logs-get-file-path.js +1 -1
- package/dist/mcp/tools/logs-get-file-path.js.map +1 -1
- package/dist/mcp/tools/logs-receive.d.ts +9 -10
- package/dist/mcp/tools/logs-receive.d.ts.map +1 -1
- package/dist/mcp/tools/logs-search.d.ts +1 -2
- package/dist/mcp/tools/logs-search.d.ts.map +1 -1
- package/dist/mcp/tools/logs-status.d.ts.map +1 -1
- package/dist/mcp/tools/logs-status.js +4 -1
- package/dist/mcp/tools/logs-status.js.map +1 -1
- package/dist/remote-logger.browser.js +166 -0
- package/dist/remote-logger.browser.js.map +1 -0
- package/dist/server/dual-server.d.ts +7 -4
- package/dist/server/dual-server.d.ts.map +1 -1
- package/dist/server/dual-server.js +124 -50
- package/dist/server/dual-server.js.map +1 -1
- package/dist/server/log-server.d.ts +12 -2
- package/dist/server/log-server.d.ts.map +1 -1
- package/dist/server/log-server.js +109 -63
- package/dist/server/log-server.js.map +1 -1
- package/dist/server/log-storage.d.ts +4 -0
- package/dist/server/log-storage.d.ts.map +1 -1
- package/dist/server/log-storage.js.map +1 -1
- package/dist/server/proxy.d.ts +39 -0
- package/dist/server/proxy.d.ts.map +1 -0
- package/dist/server/proxy.js +290 -0
- package/dist/server/proxy.js.map +1 -0
- package/dist/server/self-signed-cert.d.ts +9 -0
- package/dist/server/self-signed-cert.d.ts.map +1 -1
- package/dist/server/self-signed-cert.js +9 -0
- package/dist/server/self-signed-cert.js.map +1 -1
- package/dist/ui/ConsoleCaptureUI.d.ts.map +1 -1
- package/dist/ui/ConsoleCaptureUI.js +6 -1
- package/dist/ui/ConsoleCaptureUI.js.map +1 -1
- package/package.json +7 -2
- package/src/bundle/browser-entry.ts +157 -0
- package/src/client/RemoteLogClient.ts +22 -11
- package/src/client/types.ts +6 -0
- package/src/mcp/index.ts +42 -0
- package/src/mcp/mcp-server.ts +54 -8
- package/src/mcp/tools/index.ts +7 -0
- package/src/mcp/tools/logs-clear.ts +1 -1
- package/src/mcp/tools/logs-get-all.ts +1 -1
- package/src/mcp/tools/logs-get-errors.ts +1 -1
- package/src/mcp/tools/logs-get-file-path.ts +2 -2
- package/src/mcp/tools/logs-receive.ts +1 -1
- package/src/mcp/tools/logs-search.ts +1 -1
- package/src/mcp/tools/logs-status.ts +4 -1
- package/src/server/dual-server.ts +165 -57
- package/src/server/log-server.ts +126 -68
- package/src/server/log-storage.ts +4 -0
- package/src/server/proxy.ts +357 -0
- package/src/server/self-signed-cert.ts +9 -0
- package/src/ui/ConsoleCaptureUI.ts +6 -1
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
|
|
9
9
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
import
|
|
12
|
-
import
|
|
11
|
+
import * as http from "http";
|
|
12
|
+
import * as https from "https";
|
|
13
|
+
import { internalIpV4Sync } from "internal-ip";
|
|
13
14
|
import * as net from "net";
|
|
14
15
|
import * as os from "os";
|
|
15
16
|
import * as path from "path";
|
|
16
17
|
|
|
17
18
|
import { createMcpServer } from "../mcp/mcp-server.js";
|
|
18
19
|
import { JsonlWriter } from "./jsonl-writer.js";
|
|
19
|
-
import { createLogServer } from "./log-server.js";
|
|
20
|
+
import { createLogServer, setProxy } from "./log-server.js";
|
|
20
21
|
import { LogStorage, type ServerMode } from "./log-storage.js";
|
|
22
|
+
import { createProxy, type ProxyInstance } from "./proxy.js";
|
|
21
23
|
import { certFilesExist } from "./self-signed-cert.js";
|
|
22
24
|
|
|
23
25
|
// ANSI color codes for terminal output
|
|
@@ -35,6 +37,90 @@ const MAX_PORT_SCAN_ATTEMPTS = 100;
|
|
|
35
37
|
/** Maximum port number allowed (ports 9000-9099 per project guidelines) */
|
|
36
38
|
const MAX_PORT_NUMBER = 9099;
|
|
37
39
|
|
|
40
|
+
/** Maximum retries for binding after EADDRINUSE during listen */
|
|
41
|
+
const MAX_BIND_RETRIES = 10;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Attempt to bind an HTTP/HTTPS server to a port with retry on EADDRINUSE.
|
|
45
|
+
* This handles race conditions where a port becomes unavailable between
|
|
46
|
+
* port scanning and actual binding.
|
|
47
|
+
* @param serverFactory - Function to create a new server instance
|
|
48
|
+
* @param startPort - Starting port to try
|
|
49
|
+
* @param host - Host to bind to
|
|
50
|
+
* @param quiet - Suppress output messages
|
|
51
|
+
* @param maxPort - Maximum port number to scan up to (default: 9099)
|
|
52
|
+
* @returns Promise resolving to bound server and actual port
|
|
53
|
+
*/
|
|
54
|
+
async function tryBindWithRetry(
|
|
55
|
+
serverFactory: () => http.Server | https.Server,
|
|
56
|
+
startPort: number,
|
|
57
|
+
host: string,
|
|
58
|
+
quiet: boolean,
|
|
59
|
+
maxPort: number = MAX_PORT_NUMBER,
|
|
60
|
+
): Promise<{ server: http.Server | https.Server; port: number }> {
|
|
61
|
+
let port = startPort;
|
|
62
|
+
let attempts = 0;
|
|
63
|
+
|
|
64
|
+
while (attempts < MAX_BIND_RETRIES) {
|
|
65
|
+
if (port > maxPort) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const server = serverFactory();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await new Promise<void>((resolve, reject) => {
|
|
73
|
+
const errorHandler = (err: NodeJS.ErrnoException): void => {
|
|
74
|
+
if (err.code === "EADDRINUSE") {
|
|
75
|
+
if (!quiet) {
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.log(
|
|
78
|
+
`${colors.yellow}Port ${port} claimed during bind, trying ${port + 1}...${colors.reset}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
reject(err);
|
|
82
|
+
} else {
|
|
83
|
+
reject(err);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
server.once("error", errorHandler);
|
|
88
|
+
server.listen({ port, host, exclusive: false }, () => {
|
|
89
|
+
server.removeListener("error", errorHandler);
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Success - server is bound
|
|
95
|
+
if (!quiet) {
|
|
96
|
+
// eslint-disable-next-line no-console
|
|
97
|
+
console.log(
|
|
98
|
+
`${colors.green}HTTP server listening on ${host}:${port}${colors.reset}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return { server, port };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") {
|
|
104
|
+
port++;
|
|
105
|
+
attempts++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Re-throw non-EADDRINUSE errors
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Exhausted retries
|
|
114
|
+
const errorMsg = `Could not bind to any port after ${attempts} attempts. ` +
|
|
115
|
+
`Ports ${startPort}-${port - 1} are all in use or were claimed during bind.`;
|
|
116
|
+
|
|
117
|
+
if (!quiet) {
|
|
118
|
+
console.error(`${colors.red}${errorMsg}${colors.reset}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error(errorMsg);
|
|
122
|
+
}
|
|
123
|
+
|
|
38
124
|
/**
|
|
39
125
|
* Check if a port is available for binding.
|
|
40
126
|
* @param port - Port number to check
|
|
@@ -72,16 +158,22 @@ async function isPortAvailable(port: number, host: string): Promise<boolean> {
|
|
|
72
158
|
* @param basePort - Starting port number
|
|
73
159
|
* @param host - Host to bind to
|
|
74
160
|
* @param quiet - Suppress output messages
|
|
161
|
+
* @param maxPort - Maximum port number to scan up to (default: 9099)
|
|
75
162
|
* @returns Promise resolving to available port number
|
|
76
163
|
* @throws Error if no available port found within MAX_PORT_SCAN_ATTEMPTS
|
|
77
164
|
*/
|
|
78
|
-
export async function findAvailablePort(
|
|
165
|
+
export async function findAvailablePort(
|
|
166
|
+
basePort: number,
|
|
167
|
+
host: string,
|
|
168
|
+
quiet: boolean = false,
|
|
169
|
+
maxPort: number = MAX_PORT_NUMBER,
|
|
170
|
+
): Promise<number> {
|
|
79
171
|
let port = basePort;
|
|
80
172
|
let attempts = 0;
|
|
81
173
|
|
|
82
174
|
while (attempts < MAX_PORT_SCAN_ATTEMPTS) {
|
|
83
|
-
if (port >
|
|
84
|
-
//
|
|
175
|
+
if (port > maxPort) {
|
|
176
|
+
// Stop if we exceed max port
|
|
85
177
|
break;
|
|
86
178
|
}
|
|
87
179
|
|
|
@@ -107,12 +199,12 @@ export async function findAvailablePort(basePort: number, host: string, quiet: b
|
|
|
107
199
|
}
|
|
108
200
|
|
|
109
201
|
// No available port found
|
|
110
|
-
const errorMsg = `Could not find available port after ${
|
|
111
|
-
`Ports ${basePort}-${port - 1} are all in use. ` +
|
|
202
|
+
const errorMsg = `Could not find available port after ${attempts} attempts starting from ${basePort}. ` +
|
|
203
|
+
`Ports ${basePort}-${Math.min(port - 1, maxPort)} are all in use. ` +
|
|
112
204
|
`Try killing existing processes: pkill -f "remote-log-server"`;
|
|
113
205
|
|
|
114
206
|
if (!quiet) {
|
|
115
|
-
|
|
207
|
+
|
|
116
208
|
console.error(`${colors.red}${errorMsg}${colors.reset}`);
|
|
117
209
|
}
|
|
118
210
|
|
|
@@ -125,7 +217,7 @@ export async function findAvailablePort(basePort: number, host: string, quiet: b
|
|
|
125
217
|
export interface DualServerOptions {
|
|
126
218
|
/** Port for HTTP server (default: 9080) */
|
|
127
219
|
httpPort?: number;
|
|
128
|
-
/** Host for HTTP server (default:
|
|
220
|
+
/** Host for HTTP server (default: 0.0.0.0) */
|
|
129
221
|
httpHost?: string;
|
|
130
222
|
/** Enable MCP server (default: true) */
|
|
131
223
|
mcpEnabled?: boolean;
|
|
@@ -145,6 +237,8 @@ export interface DualServerOptions {
|
|
|
145
237
|
jsonlWriter?: JsonlWriter;
|
|
146
238
|
/** Only serve /log POST and /health GET endpoints (default: false) */
|
|
147
239
|
logReceiveOnly?: boolean;
|
|
240
|
+
/** Maximum port number to scan up to (default: 9099) */
|
|
241
|
+
maxPortNumber?: number;
|
|
148
242
|
}
|
|
149
243
|
|
|
150
244
|
/**
|
|
@@ -176,7 +270,7 @@ export interface DualServerResult {
|
|
|
176
270
|
export async function createDualServer(options: DualServerOptions = {}): Promise<DualServerResult> {
|
|
177
271
|
const {
|
|
178
272
|
httpPort: requestedPort = 9080,
|
|
179
|
-
httpHost = "
|
|
273
|
+
httpHost = "0.0.0.0",
|
|
180
274
|
mcpEnabled = true,
|
|
181
275
|
httpEnabled = true,
|
|
182
276
|
quiet = false,
|
|
@@ -185,6 +279,7 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
185
279
|
logReceiveOnly = false,
|
|
186
280
|
certPath,
|
|
187
281
|
keyPath,
|
|
282
|
+
maxPortNumber = MAX_PORT_NUMBER,
|
|
188
283
|
} = options;
|
|
189
284
|
|
|
190
285
|
// Create or use provided JSONL writer
|
|
@@ -197,6 +292,7 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
197
292
|
let httpServer: http.Server | https.Server | undefined;
|
|
198
293
|
let mcpServer: McpServer | undefined;
|
|
199
294
|
let actualHttpPort: number | undefined;
|
|
295
|
+
let proxyInstance: ProxyInstance | undefined;
|
|
200
296
|
|
|
201
297
|
// Track active connections for graceful shutdown
|
|
202
298
|
const activeConnections = new Set<net.Socket>();
|
|
@@ -204,18 +300,31 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
204
300
|
// Start HTTP server if enabled
|
|
205
301
|
if (httpEnabled) {
|
|
206
302
|
// Find an available port starting from the requested port
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
303
|
+
const startPort = await findAvailablePort(requestedPort, httpHost, quiet, maxPortNumber);
|
|
304
|
+
|
|
305
|
+
// Factory function to create server instances (for retry on bind failure)
|
|
306
|
+
const createServerInstance = (): http.Server | https.Server => {
|
|
307
|
+
return createLogServer({
|
|
308
|
+
port: startPort, // Port is set but not used until bind
|
|
309
|
+
host: httpHost,
|
|
310
|
+
storage,
|
|
311
|
+
quiet: true, // Quiet during factory, messages handled by tryBindWithRetry
|
|
312
|
+
logReceiveOnly,
|
|
313
|
+
certPath,
|
|
314
|
+
keyPath,
|
|
315
|
+
}).server;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Bind with retry logic to handle race conditions
|
|
319
|
+
const bindResult = await tryBindWithRetry(
|
|
320
|
+
createServerInstance,
|
|
321
|
+
startPort,
|
|
322
|
+
httpHost,
|
|
213
323
|
quiet,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
httpServer = result.server;
|
|
324
|
+
maxPortNumber,
|
|
325
|
+
);
|
|
326
|
+
httpServer = bindResult.server;
|
|
327
|
+
actualHttpPort = bindResult.port;
|
|
219
328
|
|
|
220
329
|
// Track connections for graceful shutdown
|
|
221
330
|
httpServer.on("connection", (socket: net.Socket) => {
|
|
@@ -225,40 +334,8 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
225
334
|
});
|
|
226
335
|
});
|
|
227
336
|
|
|
228
|
-
|
|
229
|
-
httpServer.
|
|
230
|
-
httpServer.headersTimeout = 2000;
|
|
231
|
-
|
|
232
|
-
// Wait for HTTP server to be listening
|
|
233
|
-
const serverToStart = httpServer;
|
|
234
|
-
const portToUse = actualHttpPort;
|
|
235
|
-
await new Promise<void>((resolve, reject) => {
|
|
236
|
-
serverToStart.on("error", (err: NodeJS.ErrnoException) => {
|
|
237
|
-
// Provide helpful error message for port conflicts
|
|
238
|
-
if (err.code === "EADDRINUSE") {
|
|
239
|
-
const errorMsg = `Port ${portToUse} is already in use. ` +
|
|
240
|
-
`This shouldn't happen after port scanning - there may be a race condition. ` +
|
|
241
|
-
`Try again or kill existing processes: pkill -f "remote-log-server"`;
|
|
242
|
-
if (!quiet) {
|
|
243
|
-
|
|
244
|
-
console.error(`${colors.red}${errorMsg}${colors.reset}`);
|
|
245
|
-
}
|
|
246
|
-
reject(new Error(errorMsg));
|
|
247
|
-
} else {
|
|
248
|
-
reject(err);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
serverToStart.listen({ port: portToUse, host: httpHost, exclusive: false }, () => {
|
|
252
|
-
serverToStart.removeListener("error", reject);
|
|
253
|
-
if (!quiet) {
|
|
254
|
-
// eslint-disable-next-line no-console
|
|
255
|
-
console.log(
|
|
256
|
-
`${colors.green}HTTP server listening on ${httpHost}:${portToUse}${colors.reset}`,
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
resolve();
|
|
260
|
-
});
|
|
261
|
-
});
|
|
337
|
+
httpServer.keepAliveTimeout = 30000;
|
|
338
|
+
httpServer.headersTimeout = 35000;
|
|
262
339
|
|
|
263
340
|
// Set server config in storage so MCP tools can report it
|
|
264
341
|
// Determine protocol based on whether valid cert files were provided
|
|
@@ -273,13 +350,39 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
273
350
|
} else {
|
|
274
351
|
mode = "dual";
|
|
275
352
|
}
|
|
353
|
+
// When bound to all interfaces (0.0.0.0), detect the machine's IP address
|
|
354
|
+
// for the endpoint URL since 0.0.0.0 is not a routable address for clients
|
|
355
|
+
let endpointHost = httpHost;
|
|
356
|
+
if (httpHost === "0.0.0.0") {
|
|
357
|
+
const internalIp = internalIpV4Sync();
|
|
358
|
+
if (internalIp) {
|
|
359
|
+
endpointHost = internalIp;
|
|
360
|
+
} else {
|
|
361
|
+
// Fallback for local-only use - don't use hostname as it may not resolve
|
|
362
|
+
endpointHost = "127.0.0.1";
|
|
363
|
+
if (!quiet) {
|
|
364
|
+
console.warn(
|
|
365
|
+
`${colors.yellow}Could not detect LAN IP. Endpoint URL will only work locally.${colors.reset}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const scriptUrl = `${protocol}://${endpointHost}:${actualHttpPort}/remote-logger.js`;
|
|
371
|
+
const proxyBaseUrl = `/proxy/`;
|
|
372
|
+
|
|
276
373
|
storage.setServerConfig({
|
|
277
374
|
httpPort: actualHttpPort,
|
|
278
375
|
httpHost,
|
|
279
376
|
protocol,
|
|
280
|
-
httpEndpoint: `${protocol}://${
|
|
377
|
+
httpEndpoint: `${protocol}://${endpointHost}:${actualHttpPort}/log`,
|
|
378
|
+
scriptUrl,
|
|
379
|
+
proxyBaseUrl: `${protocol}://${endpointHost}:${actualHttpPort}${proxyBaseUrl}`,
|
|
281
380
|
mode,
|
|
282
381
|
});
|
|
382
|
+
|
|
383
|
+
// Create and register the proxy instance
|
|
384
|
+
proxyInstance = createProxy(proxyBaseUrl, quiet);
|
|
385
|
+
setProxy(proxyInstance);
|
|
283
386
|
}
|
|
284
387
|
|
|
285
388
|
// Create and connect MCP server if enabled
|
|
@@ -307,6 +410,11 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
|
|
|
307
410
|
});
|
|
308
411
|
}
|
|
309
412
|
|
|
413
|
+
// Close proxy
|
|
414
|
+
if (proxyInstance) {
|
|
415
|
+
proxyInstance.close();
|
|
416
|
+
}
|
|
417
|
+
|
|
310
418
|
// Close JSONL writer only if we created it internally
|
|
311
419
|
if (ownsJsonlWriter) {
|
|
312
420
|
await jsonlWriter.close();
|
package/src/server/log-server.ts
CHANGED
|
@@ -18,10 +18,11 @@ import * as http from "http";
|
|
|
18
18
|
import * as https from "https";
|
|
19
19
|
import * as os from "os";
|
|
20
20
|
import * as path from "path";
|
|
21
|
-
import { URL } from "url";
|
|
21
|
+
import { fileURLToPath,URL } from "url";
|
|
22
22
|
|
|
23
23
|
import { JsonlWriter } from "./jsonl-writer.js";
|
|
24
24
|
import { type LogEntry, LogStorage } from "./log-storage.js";
|
|
25
|
+
import type { ProxyInstance } from "./proxy.js";
|
|
25
26
|
import { certFilesExist, readCertFiles } from "./self-signed-cert.js";
|
|
26
27
|
|
|
27
28
|
// Shared log storage instance
|
|
@@ -62,6 +63,55 @@ export function setLogStorage(storage: LogStorage): void {
|
|
|
62
63
|
sharedStorage = storage;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// Shared proxy instance
|
|
67
|
+
let sharedProxy: ProxyInstance | null = null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set the shared proxy instance.
|
|
71
|
+
* @param proxy - The proxy instance to use
|
|
72
|
+
*/
|
|
73
|
+
export function setProxy(proxy: ProxyInstance): void {
|
|
74
|
+
sharedProxy = proxy;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
// Browser bundle cache (loaded on first request)
|
|
79
|
+
let browserBundleCache: string | null | undefined;
|
|
80
|
+
|
|
81
|
+
function loadBrowserBundle(): string | null {
|
|
82
|
+
if (browserBundleCache !== undefined) {
|
|
83
|
+
return browserBundleCache;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
87
|
+
|
|
88
|
+
// Try multiple locations: dist/ (when running from built output)
|
|
89
|
+
// and the package root dist/ (when running from source during tests)
|
|
90
|
+
const candidates = [
|
|
91
|
+
path.resolve(thisDir, "..", "remote-logger.browser.js"),
|
|
92
|
+
path.resolve(thisDir, "..", "..", "dist", "remote-logger.browser.js"),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const candidate of candidates) {
|
|
96
|
+
try {
|
|
97
|
+
browserBundleCache = fs.readFileSync(candidate, "utf-8");
|
|
98
|
+
return browserBundleCache;
|
|
99
|
+
} catch {
|
|
100
|
+
// Try next candidate
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
browserBundleCache = null;
|
|
105
|
+
return browserBundleCache;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reset the browser bundle cache. Used for testing.
|
|
110
|
+
*/
|
|
111
|
+
export function resetBrowserBundleCache(): void {
|
|
112
|
+
browserBundleCache = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
65
115
|
// ANSI color codes for terminal output
|
|
66
116
|
const colors = {
|
|
67
117
|
reset: "\x1b[0m",
|
|
@@ -81,7 +131,7 @@ const colors = {
|
|
|
81
131
|
export interface LogServerOptions {
|
|
82
132
|
/** Port to listen on (default: 9080) */
|
|
83
133
|
port?: number;
|
|
84
|
-
/** Hostname to bind to (default:
|
|
134
|
+
/** Hostname to bind to (default: 0.0.0.0) */
|
|
85
135
|
host?: string;
|
|
86
136
|
/** Path to SSL certificate file (HTTPS only used if both certPath and keyPath provided) */
|
|
87
137
|
certPath?: string;
|
|
@@ -269,7 +319,36 @@ function handleRequest(
|
|
|
269
319
|
return;
|
|
270
320
|
}
|
|
271
321
|
|
|
272
|
-
//
|
|
322
|
+
// Serve browser-ready script bundle (available in all modes including logReceiveOnly)
|
|
323
|
+
if ((url === "/remote-logger.js" || url.startsWith("/remote-logger.js?")) && req.method === "GET") {
|
|
324
|
+
const bundle = loadBrowserBundle();
|
|
325
|
+
if (!bundle) {
|
|
326
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
327
|
+
res.end(JSON.stringify({ error: "Browser bundle not found. Run 'npm run build' first." }));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const hostHeader = req.headers.host ?? `${host}:${port}`;
|
|
332
|
+
const serverUrl = `${protocol}://${hostHeader}`;
|
|
333
|
+
const configPrefix = `window.__REMOTE_LOG_SERVER_URL__="${serverUrl}";\n`;
|
|
334
|
+
|
|
335
|
+
res.writeHead(200, {
|
|
336
|
+
"Content-Type": "application/javascript",
|
|
337
|
+
"Cache-Control": "no-cache",
|
|
338
|
+
});
|
|
339
|
+
res.end(configPrefix + bundle);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Reverse proxy with auto-injection (available in all modes including logReceiveOnly)
|
|
344
|
+
if (url.startsWith("/proxy/") && sharedProxy) {
|
|
345
|
+
const targetUrl = decodeURIComponent(url.substring("/proxy/".length));
|
|
346
|
+
const hostHeader = req.headers.host ?? `${host}:${port}`;
|
|
347
|
+
sharedProxy.handleRequest(req, res, targetUrl, protocol, hostHeader);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// In logReceiveOnly mode, only /log, /health, /remote-logger.js, and /proxy/ are available
|
|
273
352
|
if (logReceiveOnly) {
|
|
274
353
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
275
354
|
res.end(JSON.stringify({ error: "Not found (log receive only mode)" }));
|
|
@@ -377,17 +456,21 @@ function printBanner(host: string, port: number, useHttps: boolean): void {
|
|
|
377
456
|
// eslint-disable-next-line no-console
|
|
378
457
|
console.log(`${colors.yellow}API Endpoints:${colors.reset}`);
|
|
379
458
|
// eslint-disable-next-line no-console
|
|
380
|
-
console.log(` ${colors.cyan}POST /log
|
|
459
|
+
console.log(` ${colors.cyan}POST /log ${colors.reset} - Receive logs from browser`);
|
|
460
|
+
// eslint-disable-next-line no-console
|
|
461
|
+
console.log(` ${colors.cyan}GET /remote-logger.js ${colors.reset} - Browser-ready auto-config script`);
|
|
381
462
|
// eslint-disable-next-line no-console
|
|
382
|
-
console.log(` ${colors.cyan}
|
|
463
|
+
console.log(` ${colors.cyan}* /proxy/<url> ${colors.reset} - Reverse proxy with script injection`);
|
|
383
464
|
// eslint-disable-next-line no-console
|
|
384
|
-
console.log(` ${colors.cyan}GET /logs
|
|
465
|
+
console.log(` ${colors.cyan}GET /logs ${colors.reset} - Get all logs as JSON`);
|
|
385
466
|
// eslint-disable-next-line no-console
|
|
386
|
-
console.log(` ${colors.cyan}GET /logs/
|
|
467
|
+
console.log(` ${colors.cyan}GET /logs/recent ${colors.reset} - Get last 50 logs (?n=100 for more)`);
|
|
387
468
|
// eslint-disable-next-line no-console
|
|
388
|
-
console.log(` ${colors.cyan}
|
|
469
|
+
console.log(` ${colors.cyan}GET /logs/errors ${colors.reset} - Get only error logs`);
|
|
389
470
|
// eslint-disable-next-line no-console
|
|
390
|
-
console.log(` ${colors.cyan}
|
|
471
|
+
console.log(` ${colors.cyan}POST /logs/clear ${colors.reset} - Clear all logs`);
|
|
472
|
+
// eslint-disable-next-line no-console
|
|
473
|
+
console.log(` ${colors.cyan}GET /health ${colors.reset} - Health check`);
|
|
391
474
|
// eslint-disable-next-line no-console
|
|
392
475
|
console.log("");
|
|
393
476
|
// eslint-disable-next-line no-console
|
|
@@ -477,7 +560,7 @@ export function createLogServer(options: CreateLogServerOptions): CreateLogServe
|
|
|
477
560
|
*/
|
|
478
561
|
export function startLogServer(options: LogServerOptions = {}): http.Server | https.Server {
|
|
479
562
|
const port = options.port ?? 9080;
|
|
480
|
-
const host = options.host ?? "
|
|
563
|
+
const host = options.host ?? "0.0.0.0";
|
|
481
564
|
const quiet = options.quiet ?? false;
|
|
482
565
|
|
|
483
566
|
// Set up log file if specified
|
|
@@ -555,7 +638,7 @@ Usage:
|
|
|
555
638
|
|
|
556
639
|
Options:
|
|
557
640
|
--port, -p <port> Port to listen on (default: 9080)
|
|
558
|
-
--host, -h <host> Hostname to bind to (default:
|
|
641
|
+
--host, -h <host> Hostname to bind to (default: 0.0.0.0)
|
|
559
642
|
--cert, -c <path> Path to SSL certificate file (enables HTTPS)
|
|
560
643
|
--key, -k <path> Path to SSL private key file (enables HTTPS)
|
|
561
644
|
--log-file, -l <path> Write logs to file
|
|
@@ -677,87 +760,62 @@ export async function main(): Promise<void> {
|
|
|
677
760
|
|
|
678
761
|
const { options } = result;
|
|
679
762
|
|
|
763
|
+
// Common server options shared across all modes
|
|
764
|
+
const baseOptions = {
|
|
765
|
+
httpPort: options.port ?? 9080,
|
|
766
|
+
httpHost: options.host ?? "0.0.0.0",
|
|
767
|
+
quiet: options.quiet ?? false,
|
|
768
|
+
certPath: options.certPath,
|
|
769
|
+
keyPath: options.keyPath,
|
|
770
|
+
};
|
|
771
|
+
|
|
680
772
|
// Determine mode: mcp-only, http-only, or dual (default)
|
|
681
773
|
// All modes now use createDualServer with different options
|
|
682
774
|
const { createDualServer } = await import("./dual-server.js");
|
|
683
775
|
|
|
776
|
+
let dualServer;
|
|
777
|
+
let modeMessage: string;
|
|
778
|
+
|
|
684
779
|
if (options.mcpOnly) {
|
|
685
780
|
// MCP-only mode: HTTP only serves /log endpoint, MCP enabled
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
httpHost: options.host ?? "localhost",
|
|
781
|
+
dualServer = await createDualServer({
|
|
782
|
+
...baseOptions,
|
|
689
783
|
httpEnabled: true,
|
|
690
784
|
mcpEnabled: true,
|
|
691
|
-
quiet: options.quiet ?? false,
|
|
692
785
|
logReceiveOnly: true, // Only serve /log and /health endpoints
|
|
693
|
-
certPath: options.certPath,
|
|
694
|
-
keyPath: options.keyPath,
|
|
695
786
|
});
|
|
696
|
-
|
|
697
|
-
// Handle graceful shutdown
|
|
698
|
-
process.on("SIGINT", () => {
|
|
699
|
-
// eslint-disable-next-line no-console
|
|
700
|
-
console.log("\nShutting down...");
|
|
701
|
-
void dualServer.shutdown().then(() => {
|
|
702
|
-
process.exit(0);
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
if (!options.quiet) {
|
|
707
|
-
// eslint-disable-next-line no-console
|
|
708
|
-
console.log("MCP mode: Log receive endpoint and MCP tools running");
|
|
709
|
-
}
|
|
787
|
+
modeMessage = "MCP mode: Log receive endpoint and MCP tools running";
|
|
710
788
|
} else if (options.httpOnly) {
|
|
711
789
|
// HTTP-only mode: All HTTP endpoints, no MCP
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
httpHost: options.host ?? "localhost",
|
|
790
|
+
dualServer = await createDualServer({
|
|
791
|
+
...baseOptions,
|
|
715
792
|
httpEnabled: true,
|
|
716
793
|
mcpEnabled: false,
|
|
717
|
-
quiet: options.quiet ?? false,
|
|
718
|
-
certPath: options.certPath,
|
|
719
|
-
keyPath: options.keyPath,
|
|
720
794
|
logFile: options.logFile,
|
|
721
795
|
});
|
|
722
|
-
|
|
723
|
-
// Handle graceful shutdown
|
|
724
|
-
process.on("SIGINT", () => {
|
|
725
|
-
// eslint-disable-next-line no-console
|
|
726
|
-
console.log("\nShutting down...");
|
|
727
|
-
void dualServer.shutdown().then(() => {
|
|
728
|
-
process.exit(0);
|
|
729
|
-
});
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
if (!options.quiet) {
|
|
733
|
-
// eslint-disable-next-line no-console
|
|
734
|
-
console.log("HTTP-only mode: All HTTP endpoints running");
|
|
735
|
-
}
|
|
796
|
+
modeMessage = "HTTP-only mode: All HTTP endpoints running";
|
|
736
797
|
} else {
|
|
737
798
|
// Dual mode (default): All HTTP endpoints and MCP
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
httpHost: options.host ?? "localhost",
|
|
799
|
+
dualServer = await createDualServer({
|
|
800
|
+
...baseOptions,
|
|
741
801
|
httpEnabled: true,
|
|
742
802
|
mcpEnabled: true,
|
|
743
|
-
quiet: options.quiet ?? false,
|
|
744
|
-
certPath: options.certPath,
|
|
745
|
-
keyPath: options.keyPath,
|
|
746
803
|
logFile: options.logFile,
|
|
747
804
|
});
|
|
805
|
+
modeMessage = "Dual mode: HTTP and MCP servers running";
|
|
806
|
+
}
|
|
748
807
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
});
|
|
808
|
+
// Single SIGINT handler for all modes
|
|
809
|
+
process.on("SIGINT", () => {
|
|
810
|
+
// eslint-disable-next-line no-console
|
|
811
|
+
console.log("\nShutting down...");
|
|
812
|
+
void dualServer.shutdown().then(() => {
|
|
813
|
+
process.exit(0);
|
|
756
814
|
});
|
|
815
|
+
});
|
|
757
816
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
817
|
+
if (!options.quiet) {
|
|
818
|
+
// eslint-disable-next-line no-console
|
|
819
|
+
console.log(modeMessage);
|
|
762
820
|
}
|
|
763
821
|
}
|
|
@@ -141,6 +141,10 @@ export interface ServerConfig {
|
|
|
141
141
|
protocol: "http" | "https";
|
|
142
142
|
/** Full URL for browser clients to send logs to */
|
|
143
143
|
httpEndpoint: string;
|
|
144
|
+
/** URL to the browser-ready auto-config script */
|
|
145
|
+
scriptUrl?: string;
|
|
146
|
+
/** Base URL for the reverse proxy (e.g., "http://host:port/proxy/") */
|
|
147
|
+
proxyBaseUrl?: string;
|
|
144
148
|
/** Server mode (mcp-only, http-only, or dual) */
|
|
145
149
|
mode: ServerMode;
|
|
146
150
|
}
|