@graphty/remote-logger 0.0.1 → 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.
Files changed (138) hide show
  1. package/README.md +944 -28
  2. package/bin/remote-log-server.js +3 -0
  3. package/dist/client/RemoteLogClient.d.ts +116 -0
  4. package/dist/client/RemoteLogClient.d.ts.map +1 -0
  5. package/dist/client/RemoteLogClient.js +269 -0
  6. package/dist/client/RemoteLogClient.js.map +1 -0
  7. package/dist/client/index.d.ts +7 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +6 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/types.d.ts +60 -0
  12. package/dist/client/types.d.ts.map +1 -0
  13. package/dist/client/types.js +6 -0
  14. package/dist/client/types.js.map +1 -0
  15. package/dist/index.d.ts +22 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +23 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/mcp/index.d.ts +9 -0
  20. package/dist/mcp/index.d.ts.map +1 -0
  21. package/dist/mcp/index.js +9 -0
  22. package/dist/mcp/index.js.map +1 -0
  23. package/dist/mcp/mcp-server.d.ts +32 -0
  24. package/dist/mcp/mcp-server.d.ts.map +1 -0
  25. package/dist/mcp/mcp-server.js +270 -0
  26. package/dist/mcp/mcp-server.js.map +1 -0
  27. package/dist/mcp/tools/index.d.ts +14 -0
  28. package/dist/mcp/tools/index.d.ts.map +1 -0
  29. package/dist/mcp/tools/index.js +14 -0
  30. package/dist/mcp/tools/index.js.map +1 -0
  31. package/dist/mcp/tools/logs-clear.d.ts +76 -0
  32. package/dist/mcp/tools/logs-clear.d.ts.map +1 -0
  33. package/dist/mcp/tools/logs-clear.js +58 -0
  34. package/dist/mcp/tools/logs-clear.js.map +1 -0
  35. package/dist/mcp/tools/logs-get-all.d.ts +60 -0
  36. package/dist/mcp/tools/logs-get-all.d.ts.map +1 -0
  37. package/dist/mcp/tools/logs-get-all.js +50 -0
  38. package/dist/mcp/tools/logs-get-all.js.map +1 -0
  39. package/dist/mcp/tools/logs-get-errors.d.ts +65 -0
  40. package/dist/mcp/tools/logs-get-errors.d.ts.map +1 -0
  41. package/dist/mcp/tools/logs-get-errors.js +46 -0
  42. package/dist/mcp/tools/logs-get-errors.js.map +1 -0
  43. package/dist/mcp/tools/logs-get-file-path.d.ts +75 -0
  44. package/dist/mcp/tools/logs-get-file-path.d.ts.map +1 -0
  45. package/dist/mcp/tools/logs-get-file-path.js +90 -0
  46. package/dist/mcp/tools/logs-get-file-path.js.map +1 -0
  47. package/dist/mcp/tools/logs-get-recent.d.ts +89 -0
  48. package/dist/mcp/tools/logs-get-recent.d.ts.map +1 -0
  49. package/dist/mcp/tools/logs-get-recent.js +74 -0
  50. package/dist/mcp/tools/logs-get-recent.js.map +1 -0
  51. package/dist/mcp/tools/logs-list-sessions.d.ts +64 -0
  52. package/dist/mcp/tools/logs-list-sessions.d.ts.map +1 -0
  53. package/dist/mcp/tools/logs-list-sessions.js +48 -0
  54. package/dist/mcp/tools/logs-list-sessions.js.map +1 -0
  55. package/dist/mcp/tools/logs-receive.d.ts +150 -0
  56. package/dist/mcp/tools/logs-receive.d.ts.map +1 -0
  57. package/dist/mcp/tools/logs-receive.js +68 -0
  58. package/dist/mcp/tools/logs-receive.js.map +1 -0
  59. package/dist/mcp/tools/logs-search.d.ts +91 -0
  60. package/dist/mcp/tools/logs-search.d.ts.map +1 -0
  61. package/dist/mcp/tools/logs-search.js +68 -0
  62. package/dist/mcp/tools/logs-search.js.map +1 -0
  63. package/dist/mcp/tools/logs-status.d.ts +45 -0
  64. package/dist/mcp/tools/logs-status.d.ts.map +1 -0
  65. package/dist/mcp/tools/logs-status.js +45 -0
  66. package/dist/mcp/tools/logs-status.js.map +1 -0
  67. package/dist/server/dual-server.d.ts +76 -0
  68. package/dist/server/dual-server.d.ts.map +1 -0
  69. package/dist/server/dual-server.js +214 -0
  70. package/dist/server/dual-server.js.map +1 -0
  71. package/dist/server/index.d.ts +12 -0
  72. package/dist/server/index.d.ts.map +1 -0
  73. package/dist/server/index.js +12 -0
  74. package/dist/server/index.js.map +1 -0
  75. package/dist/server/jsonl-writer.d.ts +93 -0
  76. package/dist/server/jsonl-writer.d.ts.map +1 -0
  77. package/dist/server/jsonl-writer.js +205 -0
  78. package/dist/server/jsonl-writer.js.map +1 -0
  79. package/dist/server/log-server.d.ts +126 -0
  80. package/dist/server/log-server.d.ts.map +1 -0
  81. package/dist/server/log-server.js +589 -0
  82. package/dist/server/log-server.js.map +1 -0
  83. package/dist/server/log-storage.d.ts +301 -0
  84. package/dist/server/log-storage.d.ts.map +1 -0
  85. package/dist/server/log-storage.js +408 -0
  86. package/dist/server/log-storage.js.map +1 -0
  87. package/dist/server/marker-utils.d.ts +69 -0
  88. package/dist/server/marker-utils.d.ts.map +1 -0
  89. package/dist/server/marker-utils.js +118 -0
  90. package/dist/server/marker-utils.js.map +1 -0
  91. package/dist/server/self-signed-cert.d.ts +30 -0
  92. package/dist/server/self-signed-cert.d.ts.map +1 -0
  93. package/dist/server/self-signed-cert.js +83 -0
  94. package/dist/server/self-signed-cert.js.map +1 -0
  95. package/dist/ui/ConsoleCaptureUI.d.ts +118 -0
  96. package/dist/ui/ConsoleCaptureUI.d.ts.map +1 -0
  97. package/dist/ui/ConsoleCaptureUI.js +571 -0
  98. package/dist/ui/ConsoleCaptureUI.js.map +1 -0
  99. package/dist/ui/index.d.ts +15 -0
  100. package/dist/ui/index.d.ts.map +1 -0
  101. package/dist/ui/index.js +15 -0
  102. package/dist/ui/index.js.map +1 -0
  103. package/dist/vite/index.d.ts +8 -0
  104. package/dist/vite/index.d.ts.map +1 -0
  105. package/dist/vite/index.js +8 -0
  106. package/dist/vite/index.js.map +1 -0
  107. package/dist/vite/plugin.d.ts +42 -0
  108. package/dist/vite/plugin.d.ts.map +1 -0
  109. package/dist/vite/plugin.js +46 -0
  110. package/dist/vite/plugin.js.map +1 -0
  111. package/package.json +90 -7
  112. package/src/client/RemoteLogClient.ts +328 -0
  113. package/src/client/index.ts +7 -0
  114. package/src/client/types.ts +62 -0
  115. package/src/index.ts +28 -0
  116. package/src/mcp/index.ts +25 -0
  117. package/src/mcp/mcp-server.ts +364 -0
  118. package/src/mcp/tools/index.ts +69 -0
  119. package/src/mcp/tools/logs-clear.ts +86 -0
  120. package/src/mcp/tools/logs-get-all.ts +78 -0
  121. package/src/mcp/tools/logs-get-errors.ts +71 -0
  122. package/src/mcp/tools/logs-get-file-path.ts +121 -0
  123. package/src/mcp/tools/logs-get-recent.ts +104 -0
  124. package/src/mcp/tools/logs-list-sessions.ts +71 -0
  125. package/src/mcp/tools/logs-receive.ts +96 -0
  126. package/src/mcp/tools/logs-search.ts +95 -0
  127. package/src/mcp/tools/logs-status.ts +69 -0
  128. package/src/server/dual-server.ts +308 -0
  129. package/src/server/index.ts +54 -0
  130. package/src/server/jsonl-writer.ts +277 -0
  131. package/src/server/log-server.ts +763 -0
  132. package/src/server/log-storage.ts +651 -0
  133. package/src/server/marker-utils.ts +144 -0
  134. package/src/server/self-signed-cert.ts +93 -0
  135. package/src/ui/ConsoleCaptureUI.ts +649 -0
  136. package/src/ui/index.ts +15 -0
  137. package/src/vite/index.ts +8 -0
  138. package/src/vite/plugin.ts +59 -0
@@ -0,0 +1,763 @@
1
+ /**
2
+ * Remote Log Server - A standalone HTTP/HTTPS log server for remote debugging.
3
+ *
4
+ * Features:
5
+ * - HTTPS with auto-generated self-signed certs or custom certs
6
+ * - Receives logs from browser via POST /log
7
+ * - Pretty terminal output with colors
8
+ * - REST API for querying logs
9
+ * - Optional file logging for Claude Code to read
10
+ *
11
+ * Usage:
12
+ * npx remote-log-server --port 9080
13
+ * npx remote-log-server --cert /path/to/cert.crt --key /path/to/key.key
14
+ */
15
+
16
+ import * as fs from "fs";
17
+ import * as http from "http";
18
+ import * as https from "https";
19
+ import * as os from "os";
20
+ import * as path from "path";
21
+ import { URL } from "url";
22
+
23
+ import { JsonlWriter } from "./jsonl-writer.js";
24
+ import { type LogEntry, LogStorage } from "./log-storage.js";
25
+ import { certFilesExist, readCertFiles } from "./self-signed-cert.js";
26
+
27
+ // Shared log storage instance
28
+ let sharedStorage: LogStorage | null = null;
29
+ // Shared JSONL writer instance
30
+ let sharedJsonlWriter: JsonlWriter | null = null;
31
+
32
+ /**
33
+ * Get the shared LogStorage instance, creating it if needed.
34
+ * Creates a JsonlWriter for JSONL file streaming by default.
35
+ * @returns The shared LogStorage instance
36
+ */
37
+ export function getLogStorage(): LogStorage {
38
+ if (!sharedStorage) {
39
+ // Create JSONL writer for file streaming
40
+ const jsonlBaseDir = path.join(os.tmpdir(), "remote-logger");
41
+ sharedJsonlWriter = new JsonlWriter(jsonlBaseDir);
42
+
43
+ // Create storage with JSONL writer
44
+ sharedStorage = new LogStorage({ jsonlWriter: sharedJsonlWriter });
45
+ }
46
+ return sharedStorage;
47
+ }
48
+
49
+ /**
50
+ * Get the shared JsonlWriter instance.
51
+ * @returns The shared JsonlWriter instance or null if not initialized
52
+ */
53
+ export function getJsonlWriter(): JsonlWriter | null {
54
+ return sharedJsonlWriter;
55
+ }
56
+
57
+ /**
58
+ * Set the shared LogStorage instance (for testing or external injection).
59
+ * @param storage - The LogStorage instance to use
60
+ */
61
+ export function setLogStorage(storage: LogStorage): void {
62
+ sharedStorage = storage;
63
+ }
64
+
65
+ // ANSI color codes for terminal output
66
+ const colors = {
67
+ reset: "\x1b[0m",
68
+ bright: "\x1b[1m",
69
+ dim: "\x1b[2m",
70
+ red: "\x1b[31m",
71
+ green: "\x1b[32m",
72
+ yellow: "\x1b[33m",
73
+ blue: "\x1b[34m",
74
+ magenta: "\x1b[35m",
75
+ cyan: "\x1b[36m",
76
+ white: "\x1b[37m",
77
+ bgRed: "\x1b[41m",
78
+ bgYellow: "\x1b[43m",
79
+ };
80
+
81
+ export interface LogServerOptions {
82
+ /** Port to listen on (default: 9080) */
83
+ port?: number;
84
+ /** Hostname to bind to (default: localhost) */
85
+ host?: string;
86
+ /** Path to SSL certificate file (HTTPS only used if both certPath and keyPath provided) */
87
+ certPath?: string;
88
+ /** Path to SSL private key file (HTTPS only used if both certPath and keyPath provided) */
89
+ keyPath?: string;
90
+ /** Path to file for writing logs (optional) */
91
+ logFile?: string;
92
+ /** Start in MCP server mode (default: false) @deprecated Use mcpOnly instead */
93
+ mcp?: boolean;
94
+ /** Start only MCP server (no HTTP) */
95
+ mcpOnly?: boolean;
96
+ /** Start only HTTP server (no MCP) - legacy mode */
97
+ httpOnly?: boolean;
98
+ /** Suppress startup banner (default: false) */
99
+ quiet?: boolean;
100
+ }
101
+
102
+ // Re-export LogEntry from log-storage for backward compatibility
103
+ export type { LogEntry } from "./log-storage.js";
104
+
105
+ interface LogBatch {
106
+ sessionId: string;
107
+ logs: LogEntry[];
108
+ projectMarker?: string;
109
+ worktreePath?: string;
110
+ pageUrl?: string;
111
+ }
112
+
113
+ // File stream for log file
114
+ let logFileStream: fs.WriteStream | null = null;
115
+
116
+ /**
117
+ * Clear all stored logs.
118
+ * Useful for testing.
119
+ */
120
+ export function clearLogs(): void {
121
+ getLogStorage().clear();
122
+ }
123
+
124
+ /**
125
+ * Format log level for terminal output with colors.
126
+ * @param level - The log level string
127
+ * @returns Colored and formatted log level string
128
+ */
129
+ function formatLogLevel(level: string): string {
130
+ switch (level.toUpperCase()) {
131
+ case "ERROR":
132
+ return `${colors.bgRed}${colors.white} ERROR ${colors.reset}`;
133
+ case "WARN":
134
+ case "WARNING":
135
+ return `${colors.bgYellow}${colors.bright} WARN ${colors.reset}`;
136
+ case "INFO":
137
+ return `${colors.blue} INFO ${colors.reset}`;
138
+ case "DEBUG":
139
+ return `${colors.cyan} DEBUG ${colors.reset}`;
140
+ case "TRACE":
141
+ return `${colors.dim} TRACE ${colors.reset}`;
142
+ case "LOG":
143
+ default:
144
+ return `${colors.green} LOG ${colors.reset}`;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Display a log entry in the terminal.
150
+ * @param sessionId - The session ID for this log entry
151
+ * @param log - The log entry to display
152
+ * @param quiet - If true, suppress terminal output
153
+ */
154
+ function displayLog(sessionId: string, log: LogEntry, quiet: boolean): void {
155
+ if (!quiet) {
156
+ const time = new Date(log.time).toLocaleTimeString();
157
+ const level = formatLogLevel(log.level);
158
+ const session = `${colors.cyan}[${sessionId.substring(0, 12)}]${colors.reset}`;
159
+
160
+ // Truncate very long messages for display
161
+ let { message } = log;
162
+ if (message.length > 1000) {
163
+ message = `${message.substring(0, 1000)}... [truncated]`;
164
+ }
165
+
166
+ // eslint-disable-next-line no-console
167
+ console.log(`${time} ${session} ${level} ${message}`);
168
+ }
169
+
170
+ // Write to log file if configured
171
+ if (logFileStream) {
172
+ const logLine = JSON.stringify({
173
+ time: log.time,
174
+ sessionId,
175
+ level: log.level,
176
+ message: log.message,
177
+ });
178
+ logFileStream.write(`${logLine}\n`);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Handle incoming HTTP request.
184
+ * @param req - The incoming HTTP request
185
+ * @param res - The HTTP response object
186
+ * @param host - The server hostname
187
+ * @param port - The server port number
188
+ * @param useHttps - Whether HTTPS is being used
189
+ * @param quiet - If true, suppress terminal output
190
+ * @param logReceiveOnly - If true, only serve /log POST and /health GET endpoints
191
+ */
192
+ function handleRequest(
193
+ req: http.IncomingMessage,
194
+ res: http.ServerResponse,
195
+ host: string,
196
+ port: number,
197
+ useHttps: boolean,
198
+ quiet: boolean,
199
+ logReceiveOnly: boolean = false,
200
+ ): void {
201
+ // CORS headers
202
+ res.setHeader("Access-Control-Allow-Origin", "*");
203
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
204
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
205
+
206
+ if (req.method === "OPTIONS") {
207
+ res.writeHead(204);
208
+ res.end();
209
+ return;
210
+ }
211
+
212
+ const url = req.url ?? "/";
213
+ const protocol = useHttps ? "https" : "http";
214
+
215
+ // Handle log endpoint - receive logs from browser
216
+ if (url === "/log" && req.method === "POST") {
217
+ let body = "";
218
+ req.on("data", (chunk: Buffer) => {
219
+ body += chunk.toString();
220
+ });
221
+ req.on("end", () => {
222
+ try {
223
+ const data = JSON.parse(body) as LogBatch;
224
+ const { sessionId, logs, projectMarker, worktreePath, pageUrl } = data;
225
+
226
+ const storage = getLogStorage();
227
+ const isNewSession = !storage.hasSession(sessionId);
228
+
229
+ // Show new session banner
230
+ if (isNewSession && !quiet) {
231
+ // eslint-disable-next-line no-console
232
+ console.log(
233
+ `\n${colors.bright}${colors.magenta}═══════════════════════════════════════════════════════════${colors.reset}`,
234
+ );
235
+ // eslint-disable-next-line no-console
236
+ console.log(`${colors.bright}${colors.magenta} NEW SESSION: ${sessionId}${colors.reset}`);
237
+ // eslint-disable-next-line no-console
238
+ console.log(
239
+ `${colors.bright}${colors.magenta}═══════════════════════════════════════════════════════════${colors.reset}\n`,
240
+ );
241
+ }
242
+
243
+ // Add logs to storage
244
+ storage.addLogs(sessionId, logs, { projectMarker, worktreePath, pageUrl });
245
+
246
+ // Display each log in terminal
247
+ for (const log of logs) {
248
+ displayLog(sessionId, log, quiet);
249
+ }
250
+
251
+ res.writeHead(200, { "Content-Type": "application/json" });
252
+ res.end(JSON.stringify({ success: true }));
253
+ } catch (error) {
254
+ if (!quiet) {
255
+ console.error("Error parsing log data:", error);
256
+ }
257
+ res.writeHead(400, { "Content-Type": "application/json" });
258
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
259
+ }
260
+ });
261
+ return;
262
+ }
263
+
264
+ // Health check endpoint (always available)
265
+ if (url === "/health" && req.method === "GET") {
266
+ const health = getLogStorage().getHealth();
267
+ res.writeHead(200, { "Content-Type": "application/json" });
268
+ res.end(JSON.stringify({ status: health.status, sessions: health.sessionCount }));
269
+ return;
270
+ }
271
+
272
+ // In logReceiveOnly mode, only /log and /health are available
273
+ if (logReceiveOnly) {
274
+ res.writeHead(404, { "Content-Type": "application/json" });
275
+ res.end(JSON.stringify({ error: "Not found (log receive only mode)" }));
276
+ return;
277
+ }
278
+
279
+ // Handle logs viewer endpoint - GET all logs
280
+ if (url === "/logs" && req.method === "GET") {
281
+ const allLogs = getLogStorage().getAllLogsBySession();
282
+ res.writeHead(200, { "Content-Type": "application/json" });
283
+ res.end(JSON.stringify(allLogs, null, 2));
284
+ return;
285
+ }
286
+
287
+ // Handle recent logs endpoint - GET last N logs across all sessions
288
+ if (url.startsWith("/logs/recent") && req.method === "GET") {
289
+ const urlObj = new URL(url, `${protocol}://${host}:${port}`);
290
+ const count = parseInt(urlObj.searchParams.get("n") ?? "50", 10);
291
+ const errorsOnly = urlObj.searchParams.get("errors") === "true";
292
+
293
+ const storage = getLogStorage();
294
+ const filter = errorsOnly ? { level: "ERROR" } : {};
295
+ const recentLogs = storage.getRecentLogs(count, filter);
296
+ const totalLogs = storage.getLogs(filter).length;
297
+
298
+ res.writeHead(200, { "Content-Type": "application/json" });
299
+ res.end(
300
+ JSON.stringify(
301
+ {
302
+ total: totalLogs,
303
+ showing: recentLogs.length,
304
+ logs: recentLogs,
305
+ },
306
+ null,
307
+ 2,
308
+ ),
309
+ );
310
+ return;
311
+ }
312
+
313
+ // Handle errors-only endpoint
314
+ if (url === "/logs/errors" && req.method === "GET") {
315
+ const errorLogs = getLogStorage().getErrors();
316
+
317
+ res.writeHead(200, { "Content-Type": "application/json" });
318
+ res.end(
319
+ JSON.stringify(
320
+ {
321
+ total: errorLogs.length,
322
+ logs: errorLogs,
323
+ },
324
+ null,
325
+ 2,
326
+ ),
327
+ );
328
+ return;
329
+ }
330
+
331
+ // Handle clear logs endpoint
332
+ if (url === "/logs/clear" && req.method === "POST") {
333
+ getLogStorage().clear();
334
+ if (!quiet) {
335
+ // eslint-disable-next-line no-console
336
+ console.log(`\n${colors.yellow}Logs cleared${colors.reset}\n`);
337
+ }
338
+ res.writeHead(200, { "Content-Type": "application/json" });
339
+ res.end(JSON.stringify({ success: true }));
340
+ return;
341
+ }
342
+
343
+ // Default: 404
344
+ res.writeHead(404, { "Content-Type": "application/json" });
345
+ res.end(JSON.stringify({ error: "Not found" }));
346
+ }
347
+
348
+ /**
349
+ * Print startup banner.
350
+ * @param host - The server hostname
351
+ * @param port - The server port number
352
+ * @param useHttps - Whether HTTPS is being used
353
+ */
354
+ function printBanner(host: string, port: number, useHttps: boolean): void {
355
+ const protocol = useHttps ? "https" : "http";
356
+
357
+ // eslint-disable-next-line no-console
358
+ console.log("");
359
+ // eslint-disable-next-line no-console
360
+ console.log(
361
+ `${colors.bright}${colors.cyan}════════════════════════════════════════════════════════════${colors.reset}`,
362
+ );
363
+ // eslint-disable-next-line no-console
364
+ console.log(`${colors.bright}${colors.cyan} Remote Log Server${colors.reset}`);
365
+ // eslint-disable-next-line no-console
366
+ console.log(
367
+ `${colors.bright}${colors.cyan}════════════════════════════════════════════════════════════${colors.reset}`,
368
+ );
369
+ // eslint-disable-next-line no-console
370
+ console.log("");
371
+ // eslint-disable-next-line no-console
372
+ console.log(
373
+ `${colors.green}Server running at:${colors.reset} ${colors.bright}${protocol}://${host}:${port}/${colors.reset}`,
374
+ );
375
+ // eslint-disable-next-line no-console
376
+ console.log("");
377
+ // eslint-disable-next-line no-console
378
+ console.log(`${colors.yellow}API Endpoints:${colors.reset}`);
379
+ // eslint-disable-next-line no-console
380
+ console.log(` ${colors.cyan}POST /log ${colors.reset} - Receive logs from browser`);
381
+ // eslint-disable-next-line no-console
382
+ console.log(` ${colors.cyan}GET /logs ${colors.reset} - Get all logs as JSON`);
383
+ // eslint-disable-next-line no-console
384
+ console.log(` ${colors.cyan}GET /logs/recent ${colors.reset} - Get last 50 logs (?n=100 for more)`);
385
+ // eslint-disable-next-line no-console
386
+ console.log(` ${colors.cyan}GET /logs/errors ${colors.reset} - Get only error logs`);
387
+ // eslint-disable-next-line no-console
388
+ console.log(` ${colors.cyan}POST /logs/clear ${colors.reset} - Clear all logs`);
389
+ // eslint-disable-next-line no-console
390
+ console.log(` ${colors.cyan}GET /health ${colors.reset} - Health check`);
391
+ // eslint-disable-next-line no-console
392
+ console.log("");
393
+ // eslint-disable-next-line no-console
394
+ console.log(`${colors.dim}Remote logs will appear below:${colors.reset}`);
395
+ // eslint-disable-next-line no-console
396
+ console.log(`${colors.cyan}────────────────────────────────────────────────────────────${colors.reset}`);
397
+ }
398
+
399
+ /**
400
+ * Options for creating a log server without starting it.
401
+ */
402
+ export interface CreateLogServerOptions {
403
+ /** Port to listen on */
404
+ port: number;
405
+ /** Hostname to bind to */
406
+ host: string;
407
+ /** Shared log storage instance */
408
+ storage: LogStorage;
409
+ /** Suppress terminal output (default: false) */
410
+ quiet?: boolean;
411
+ /** Only serve /log POST and /health GET endpoints (default: false) */
412
+ logReceiveOnly?: boolean;
413
+ /** Path to SSL certificate file (HTTPS only used if both certPath and keyPath provided) */
414
+ certPath?: string;
415
+ /** Path to SSL private key file (HTTPS only used if both certPath and keyPath provided) */
416
+ keyPath?: string;
417
+ }
418
+
419
+ /**
420
+ * Result of creating a log server.
421
+ */
422
+ export interface CreateLogServerResult {
423
+ /** The HTTP or HTTPS server instance (not yet listening) */
424
+ server: http.Server | https.Server;
425
+ }
426
+
427
+ /**
428
+ * Create a log server with shared storage.
429
+ * The server is not started - call server.listen() to start it.
430
+ * @param options - Server configuration options
431
+ * @returns The server instance
432
+ */
433
+ export function createLogServer(options: CreateLogServerOptions): CreateLogServerResult {
434
+ const {
435
+ port,
436
+ host,
437
+ storage,
438
+ quiet = true,
439
+ logReceiveOnly = false,
440
+ certPath,
441
+ keyPath,
442
+ } = options;
443
+
444
+ // Set the shared storage
445
+ setLogStorage(storage);
446
+
447
+ let server: http.Server | https.Server;
448
+
449
+ // Use HTTPS only if valid certificate files are provided
450
+ const useHttps = certPath && keyPath && certFilesExist(certPath, keyPath);
451
+
452
+ if (useHttps) {
453
+ // HTTPS server with provided certificates
454
+ const { cert, key } = readCertFiles(certPath, keyPath);
455
+ if (!quiet) {
456
+ // eslint-disable-next-line no-console
457
+ console.log(`${colors.green}Using SSL certificates from: ${certPath}${colors.reset}`);
458
+ }
459
+
460
+ server = https.createServer({ cert, key }, (req, res) => {
461
+ handleRequest(req, res, host, port, true, quiet, logReceiveOnly);
462
+ });
463
+ } else {
464
+ // HTTP server (default - no self-signed certs as browsers reject them)
465
+ server = http.createServer((req, res) => {
466
+ handleRequest(req, res, host, port, false, quiet, logReceiveOnly);
467
+ });
468
+ }
469
+
470
+ return { server };
471
+ }
472
+
473
+ /**
474
+ * Start the log server.
475
+ * @param options - Server configuration options
476
+ * @returns The HTTP or HTTPS server instance
477
+ */
478
+ export function startLogServer(options: LogServerOptions = {}): http.Server | https.Server {
479
+ const port = options.port ?? 9080;
480
+ const host = options.host ?? "localhost";
481
+ const quiet = options.quiet ?? false;
482
+
483
+ // Set up log file if specified
484
+ if (options.logFile) {
485
+ logFileStream = fs.createWriteStream(options.logFile, { flags: "a" });
486
+ if (!quiet) {
487
+ // eslint-disable-next-line no-console
488
+ console.log(`${colors.green}Writing logs to: ${options.logFile}${colors.reset}`);
489
+ }
490
+ }
491
+
492
+ // Determine SSL configuration
493
+ // Use HTTPS only if valid certificate files are provided
494
+ let server: https.Server | http.Server;
495
+ let useHttps = false;
496
+ const { certPath, keyPath } = options;
497
+
498
+ if (certPath && keyPath && certFilesExist(certPath, keyPath)) {
499
+ // HTTPS server with provided certificates
500
+ useHttps = true;
501
+ const { cert, key } = readCertFiles(certPath, keyPath);
502
+ if (!quiet) {
503
+ // eslint-disable-next-line no-console
504
+ console.log(`${colors.green}Using SSL certificates from: ${certPath}${colors.reset}`);
505
+ }
506
+
507
+ server = https.createServer({ cert, key }, (req, res) => {
508
+ handleRequest(req, res, host, port, true, quiet);
509
+ });
510
+ } else {
511
+ // HTTP server (default - no self-signed certs as browsers reject them)
512
+ server = http.createServer((req, res) => {
513
+ handleRequest(req, res, host, port, false, quiet);
514
+ });
515
+ }
516
+
517
+ // Start listening
518
+ server.listen(port, host, () => {
519
+ if (!quiet) {
520
+ printBanner(host, port, useHttps);
521
+ }
522
+ });
523
+
524
+ // Handle graceful shutdown
525
+ process.on("SIGINT", () => {
526
+ // eslint-disable-next-line no-console
527
+ console.log(`\n${colors.yellow}Shutting down...${colors.reset}`);
528
+ if (logFileStream) {
529
+ logFileStream.end();
530
+ }
531
+
532
+ // Close JSONL writer to flush pending writes
533
+ const jsonlWriter = getJsonlWriter();
534
+ if (jsonlWriter) {
535
+ void jsonlWriter.close();
536
+ }
537
+
538
+ server.close(() => {
539
+ process.exit(0);
540
+ });
541
+ });
542
+
543
+ return server;
544
+ }
545
+
546
+ /**
547
+ * Help text displayed when --help is passed.
548
+ */
549
+ export const HELP_TEXT = `
550
+ Remote Log Server - Remote logging for browser debugging
551
+
552
+ Usage:
553
+ npx remote-log-server [options]
554
+ npx @graphty/remote-logger [options]
555
+
556
+ Options:
557
+ --port, -p <port> Port to listen on (default: 9080)
558
+ --host, -h <host> Hostname to bind to (default: localhost)
559
+ --cert, -c <path> Path to SSL certificate file (enables HTTPS)
560
+ --key, -k <path> Path to SSL private key file (enables HTTPS)
561
+ --log-file, -l <path> Write logs to file
562
+ --mcp-only Start only MCP server (no HTTP)
563
+ --http-only Start only HTTP server (legacy mode)
564
+ --mcp Alias for --mcp-only (deprecated)
565
+ --quiet, -q Suppress startup banner
566
+ --help Show this help message
567
+
568
+ Protocol:
569
+ HTTP is used by default. To use HTTPS, provide both --cert and --key.
570
+
571
+ Modes:
572
+ Default (no flags) Dual mode: HTTP + MCP running together
573
+ --mcp-only MCP only: For Claude Code integration
574
+ --http-only HTTP only: Legacy mode for browser debugging
575
+
576
+ Examples:
577
+ npx remote-log-server # Start dual mode (HTTP + MCP)
578
+ npx remote-log-server --port 9085 # Custom port
579
+ npx remote-log-server --mcp-only # MCP server only (for Claude Code)
580
+ npx remote-log-server --http-only # HTTP server only (legacy)
581
+ npx remote-log-server --cert cert.crt --key key.key # Use HTTPS with custom certs
582
+ npx remote-log-server --log-file ./tmp/logs.jsonl # Also write to file
583
+ `;
584
+
585
+ /**
586
+ * Result of parsing command line arguments.
587
+ */
588
+ export interface ParseArgsResult {
589
+ /** Parsed options for the log server */
590
+ options: LogServerOptions;
591
+ /** Whether --help was requested */
592
+ showHelp: boolean;
593
+ /** Error message if parsing failed */
594
+ error?: string;
595
+ }
596
+
597
+ /**
598
+ * Parse command line arguments into LogServerOptions.
599
+ * This is separated from main() to enable testing.
600
+ * @param args - Array of command line arguments (excluding node and script name)
601
+ * @returns ParseArgsResult with options, help flag, or error
602
+ */
603
+ export function parseArgs(args: string[]): ParseArgsResult {
604
+ const options: LogServerOptions = {};
605
+
606
+ for (let i = 0; i < args.length; i++) {
607
+ const arg = args[i];
608
+ const nextArg = args[i + 1];
609
+
610
+ switch (arg) {
611
+ case "--port":
612
+ case "-p":
613
+ options.port = parseInt(nextArg, 10);
614
+ i++;
615
+ break;
616
+ case "--host":
617
+ case "-h":
618
+ options.host = nextArg;
619
+ i++;
620
+ break;
621
+ case "--cert":
622
+ case "-c":
623
+ options.certPath = nextArg;
624
+ i++;
625
+ break;
626
+ case "--key":
627
+ case "-k":
628
+ options.keyPath = nextArg;
629
+ i++;
630
+ break;
631
+ case "--log-file":
632
+ case "-l":
633
+ options.logFile = nextArg;
634
+ i++;
635
+ break;
636
+ case "--mcp":
637
+ // Legacy alias for --mcp-only. Only set mcpOnly now.
638
+ options.mcpOnly = true;
639
+ break;
640
+ case "--mcp-only":
641
+ options.mcpOnly = true;
642
+ break;
643
+ case "--http-only":
644
+ options.httpOnly = true;
645
+ break;
646
+ case "--quiet":
647
+ case "-q":
648
+ options.quiet = true;
649
+ break;
650
+ case "--help":
651
+ return { options, showHelp: true };
652
+ default:
653
+ return { options, showHelp: false, error: `Unknown option: ${arg}` };
654
+ }
655
+ }
656
+
657
+ return { options, showHelp: false };
658
+ }
659
+
660
+ /**
661
+ * Parse command line arguments and start the server.
662
+ */
663
+ export async function main(): Promise<void> {
664
+ const args = process.argv.slice(2);
665
+ const result = parseArgs(args);
666
+
667
+ if (result.showHelp) {
668
+ // eslint-disable-next-line no-console
669
+ console.log(HELP_TEXT);
670
+ process.exit(0);
671
+ }
672
+
673
+ if (result.error) {
674
+ console.error(result.error);
675
+ process.exit(1);
676
+ }
677
+
678
+ const { options } = result;
679
+
680
+ // Determine mode: mcp-only, http-only, or dual (default)
681
+ // All modes now use createDualServer with different options
682
+ const { createDualServer } = await import("./dual-server.js");
683
+
684
+ if (options.mcpOnly) {
685
+ // MCP-only mode: HTTP only serves /log endpoint, MCP enabled
686
+ const dualServer = await createDualServer({
687
+ httpPort: options.port ?? 9080,
688
+ httpHost: options.host ?? "localhost",
689
+ httpEnabled: true,
690
+ mcpEnabled: true,
691
+ quiet: options.quiet ?? false,
692
+ logReceiveOnly: true, // Only serve /log and /health endpoints
693
+ certPath: options.certPath,
694
+ keyPath: options.keyPath,
695
+ });
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
+ }
710
+ } else if (options.httpOnly) {
711
+ // HTTP-only mode: All HTTP endpoints, no MCP
712
+ const dualServer = await createDualServer({
713
+ httpPort: options.port ?? 9080,
714
+ httpHost: options.host ?? "localhost",
715
+ httpEnabled: true,
716
+ mcpEnabled: false,
717
+ quiet: options.quiet ?? false,
718
+ certPath: options.certPath,
719
+ keyPath: options.keyPath,
720
+ logFile: options.logFile,
721
+ });
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
+ }
736
+ } else {
737
+ // Dual mode (default): All HTTP endpoints and MCP
738
+ const dualServer = await createDualServer({
739
+ httpPort: options.port ?? 9080,
740
+ httpHost: options.host ?? "localhost",
741
+ httpEnabled: true,
742
+ mcpEnabled: true,
743
+ quiet: options.quiet ?? false,
744
+ certPath: options.certPath,
745
+ keyPath: options.keyPath,
746
+ logFile: options.logFile,
747
+ });
748
+
749
+ // Handle graceful shutdown
750
+ process.on("SIGINT", () => {
751
+ // eslint-disable-next-line no-console
752
+ console.log("\nShutting down...");
753
+ void dualServer.shutdown().then(() => {
754
+ process.exit(0);
755
+ });
756
+ });
757
+
758
+ if (!options.quiet) {
759
+ // eslint-disable-next-line no-console
760
+ console.log("Dual mode: HTTP and MCP servers running");
761
+ }
762
+ }
763
+ }