@graphty/remote-logger 1.1.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 (110) hide show
  1. package/README.md +318 -10
  2. package/dist/client/RemoteLogClient.d.ts +2 -0
  3. package/dist/client/RemoteLogClient.d.ts.map +1 -1
  4. package/dist/client/RemoteLogClient.js +35 -4
  5. package/dist/client/RemoteLogClient.js.map +1 -1
  6. package/dist/client/types.d.ts +13 -0
  7. package/dist/client/types.d.ts.map +1 -1
  8. package/dist/mcp/index.d.ts +9 -0
  9. package/dist/mcp/index.d.ts.map +1 -0
  10. package/dist/mcp/index.js +9 -0
  11. package/dist/mcp/index.js.map +1 -0
  12. package/dist/mcp/mcp-server.d.ts +32 -0
  13. package/dist/mcp/mcp-server.d.ts.map +1 -0
  14. package/dist/mcp/mcp-server.js +270 -0
  15. package/dist/mcp/mcp-server.js.map +1 -0
  16. package/dist/mcp/tools/index.d.ts +14 -0
  17. package/dist/mcp/tools/index.d.ts.map +1 -0
  18. package/dist/mcp/tools/index.js +14 -0
  19. package/dist/mcp/tools/index.js.map +1 -0
  20. package/dist/mcp/tools/logs-clear.d.ts +76 -0
  21. package/dist/mcp/tools/logs-clear.d.ts.map +1 -0
  22. package/dist/mcp/tools/logs-clear.js +58 -0
  23. package/dist/mcp/tools/logs-clear.js.map +1 -0
  24. package/dist/mcp/tools/logs-get-all.d.ts +60 -0
  25. package/dist/mcp/tools/logs-get-all.d.ts.map +1 -0
  26. package/dist/mcp/tools/logs-get-all.js +50 -0
  27. package/dist/mcp/tools/logs-get-all.js.map +1 -0
  28. package/dist/mcp/tools/logs-get-errors.d.ts +65 -0
  29. package/dist/mcp/tools/logs-get-errors.d.ts.map +1 -0
  30. package/dist/mcp/tools/logs-get-errors.js +46 -0
  31. package/dist/mcp/tools/logs-get-errors.js.map +1 -0
  32. package/dist/mcp/tools/logs-get-file-path.d.ts +75 -0
  33. package/dist/mcp/tools/logs-get-file-path.d.ts.map +1 -0
  34. package/dist/mcp/tools/logs-get-file-path.js +90 -0
  35. package/dist/mcp/tools/logs-get-file-path.js.map +1 -0
  36. package/dist/mcp/tools/logs-get-recent.d.ts +89 -0
  37. package/dist/mcp/tools/logs-get-recent.d.ts.map +1 -0
  38. package/dist/mcp/tools/logs-get-recent.js +74 -0
  39. package/dist/mcp/tools/logs-get-recent.js.map +1 -0
  40. package/dist/mcp/tools/logs-list-sessions.d.ts +64 -0
  41. package/dist/mcp/tools/logs-list-sessions.d.ts.map +1 -0
  42. package/dist/mcp/tools/logs-list-sessions.js +48 -0
  43. package/dist/mcp/tools/logs-list-sessions.js.map +1 -0
  44. package/dist/mcp/tools/logs-receive.d.ts +150 -0
  45. package/dist/mcp/tools/logs-receive.d.ts.map +1 -0
  46. package/dist/mcp/tools/logs-receive.js +68 -0
  47. package/dist/mcp/tools/logs-receive.js.map +1 -0
  48. package/dist/mcp/tools/logs-search.d.ts +91 -0
  49. package/dist/mcp/tools/logs-search.d.ts.map +1 -0
  50. package/dist/mcp/tools/logs-search.js +68 -0
  51. package/dist/mcp/tools/logs-search.js.map +1 -0
  52. package/dist/mcp/tools/logs-status.d.ts +45 -0
  53. package/dist/mcp/tools/logs-status.d.ts.map +1 -0
  54. package/dist/mcp/tools/logs-status.js +45 -0
  55. package/dist/mcp/tools/logs-status.js.map +1 -0
  56. package/dist/server/dual-server.d.ts +76 -0
  57. package/dist/server/dual-server.d.ts.map +1 -0
  58. package/dist/server/dual-server.js +214 -0
  59. package/dist/server/dual-server.js.map +1 -0
  60. package/dist/server/index.d.ts +5 -1
  61. package/dist/server/index.d.ts.map +1 -1
  62. package/dist/server/index.js +5 -1
  63. package/dist/server/index.js.map +1 -1
  64. package/dist/server/jsonl-writer.d.ts +93 -0
  65. package/dist/server/jsonl-writer.d.ts.map +1 -0
  66. package/dist/server/jsonl-writer.js +205 -0
  67. package/dist/server/jsonl-writer.js.map +1 -0
  68. package/dist/server/log-server.d.ts +62 -11
  69. package/dist/server/log-server.d.ts.map +1 -1
  70. package/dist/server/log-server.js +237 -101
  71. package/dist/server/log-server.js.map +1 -1
  72. package/dist/server/log-storage.d.ts +301 -0
  73. package/dist/server/log-storage.d.ts.map +1 -0
  74. package/dist/server/log-storage.js +408 -0
  75. package/dist/server/log-storage.js.map +1 -0
  76. package/dist/server/marker-utils.d.ts +69 -0
  77. package/dist/server/marker-utils.d.ts.map +1 -0
  78. package/dist/server/marker-utils.js +118 -0
  79. package/dist/server/marker-utils.js.map +1 -0
  80. package/dist/vite/index.d.ts +8 -0
  81. package/dist/vite/index.d.ts.map +1 -0
  82. package/dist/vite/index.js +8 -0
  83. package/dist/vite/index.js.map +1 -0
  84. package/dist/vite/plugin.d.ts +42 -0
  85. package/dist/vite/plugin.d.ts.map +1 -0
  86. package/dist/vite/plugin.js +46 -0
  87. package/dist/vite/plugin.js.map +1 -0
  88. package/package.json +12 -2
  89. package/src/client/RemoteLogClient.ts +52 -4
  90. package/src/client/types.ts +13 -0
  91. package/src/mcp/index.ts +25 -0
  92. package/src/mcp/mcp-server.ts +364 -0
  93. package/src/mcp/tools/index.ts +69 -0
  94. package/src/mcp/tools/logs-clear.ts +86 -0
  95. package/src/mcp/tools/logs-get-all.ts +78 -0
  96. package/src/mcp/tools/logs-get-errors.ts +71 -0
  97. package/src/mcp/tools/logs-get-file-path.ts +121 -0
  98. package/src/mcp/tools/logs-get-recent.ts +104 -0
  99. package/src/mcp/tools/logs-list-sessions.ts +71 -0
  100. package/src/mcp/tools/logs-receive.ts +96 -0
  101. package/src/mcp/tools/logs-search.ts +95 -0
  102. package/src/mcp/tools/logs-status.ts +69 -0
  103. package/src/server/dual-server.ts +308 -0
  104. package/src/server/index.ts +37 -0
  105. package/src/server/jsonl-writer.ts +277 -0
  106. package/src/server/log-server.ts +311 -119
  107. package/src/server/log-storage.ts +651 -0
  108. package/src/server/marker-utils.ts +144 -0
  109. package/src/vite/index.ts +8 -0
  110. package/src/vite/plugin.ts +59 -0
@@ -16,9 +16,51 @@
16
16
  import * as fs from "fs";
17
17
  import * as http from "http";
18
18
  import * as https from "https";
19
+ import * as os from "os";
20
+ import * as path from "path";
19
21
  import { URL } from "url";
20
22
 
21
- import { certFilesExist, generateSelfSignedCert, readCertFiles } from "./self-signed-cert.js";
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
+ }
22
64
 
23
65
  // ANSI color codes for terminal output
24
66
  const colors = {
@@ -41,32 +83,33 @@ export interface LogServerOptions {
41
83
  port?: number;
42
84
  /** Hostname to bind to (default: localhost) */
43
85
  host?: string;
44
- /** Path to SSL certificate file */
86
+ /** Path to SSL certificate file (HTTPS only used if both certPath and keyPath provided) */
45
87
  certPath?: string;
46
- /** Path to SSL private key file */
88
+ /** Path to SSL private key file (HTTPS only used if both certPath and keyPath provided) */
47
89
  keyPath?: string;
48
90
  /** Path to file for writing logs (optional) */
49
91
  logFile?: string;
50
- /** Use HTTP instead of HTTPS (default: false) */
51
- useHttp?: boolean;
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;
52
98
  /** Suppress startup banner (default: false) */
53
99
  quiet?: boolean;
54
100
  }
55
101
 
56
- export interface LogEntry {
57
- time: string;
58
- level: string;
59
- message: string;
60
- }
102
+ // Re-export LogEntry from log-storage for backward compatibility
103
+ export type { LogEntry } from "./log-storage.js";
61
104
 
62
105
  interface LogBatch {
63
106
  sessionId: string;
64
107
  logs: LogEntry[];
108
+ projectMarker?: string;
109
+ worktreePath?: string;
110
+ pageUrl?: string;
65
111
  }
66
112
 
67
- // Store for remote logs by session
68
- const remoteLogs = new Map<string, LogEntry[]>();
69
-
70
113
  // File stream for log file
71
114
  let logFileStream: fs.WriteStream | null = null;
72
115
 
@@ -75,7 +118,7 @@ let logFileStream: fs.WriteStream | null = null;
75
118
  * Useful for testing.
76
119
  */
77
120
  export function clearLogs(): void {
78
- remoteLogs.clear();
121
+ getLogStorage().clear();
79
122
  }
80
123
 
81
124
  /**
@@ -144,6 +187,7 @@ function displayLog(sessionId: string, log: LogEntry, quiet: boolean): void {
144
187
  * @param port - The server port number
145
188
  * @param useHttps - Whether HTTPS is being used
146
189
  * @param quiet - If true, suppress terminal output
190
+ * @param logReceiveOnly - If true, only serve /log POST and /health GET endpoints
147
191
  */
148
192
  function handleRequest(
149
193
  req: http.IncomingMessage,
@@ -152,6 +196,7 @@ function handleRequest(
152
196
  port: number,
153
197
  useHttps: boolean,
154
198
  quiet: boolean,
199
+ logReceiveOnly: boolean = false,
155
200
  ): void {
156
201
  // CORS headers
157
202
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -176,36 +221,30 @@ function handleRequest(
176
221
  req.on("end", () => {
177
222
  try {
178
223
  const data = JSON.parse(body) as LogBatch;
179
- const { sessionId, logs } = data;
180
-
181
- // Initialize session if new
182
- if (!remoteLogs.has(sessionId)) {
183
- remoteLogs.set(sessionId, []);
184
- if (!quiet) {
185
- // eslint-disable-next-line no-console
186
- console.log(
187
- `\n${colors.bright}${colors.magenta}═══════════════════════════════════════════════════════════${colors.reset}`,
188
- );
189
- // eslint-disable-next-line no-console
190
- console.log(`${colors.bright}${colors.magenta} NEW SESSION: ${sessionId}${colors.reset}`);
191
- // eslint-disable-next-line no-console
192
- console.log(
193
- `${colors.bright}${colors.magenta}═══════════════════════════════════════════════════════════${colors.reset}\n`,
194
- );
195
- }
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
+ );
196
241
  }
197
242
 
198
- const sessionLogs = remoteLogs.get(sessionId);
199
- if (!sessionLogs) {
200
- // Should not happen since we just set it above, but satisfy TypeScript
201
- res.writeHead(500, { "Content-Type": "application/json" });
202
- res.end(JSON.stringify({ error: "Internal error" }));
203
- return;
204
- }
243
+ // Add logs to storage
244
+ storage.addLogs(sessionId, logs, { projectMarker, worktreePath, pageUrl });
205
245
 
206
- // Display and store each log
246
+ // Display each log in terminal
207
247
  for (const log of logs) {
208
- sessionLogs.push(log);
209
248
  displayLog(sessionId, log, quiet);
210
249
  }
211
250
 
@@ -222,12 +261,24 @@ function handleRequest(
222
261
  return;
223
262
  }
224
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
+
225
279
  // Handle logs viewer endpoint - GET all logs
226
280
  if (url === "/logs" && req.method === "GET") {
227
- const allLogs: Record<string, LogEntry[]> = {};
228
- for (const [sessionId, logs] of remoteLogs) {
229
- allLogs[sessionId] = logs;
230
- }
281
+ const allLogs = getLogStorage().getAllLogsBySession();
231
282
  res.writeHead(200, { "Content-Type": "application/json" });
232
283
  res.end(JSON.stringify(allLogs, null, 2));
233
284
  return;
@@ -239,25 +290,16 @@ function handleRequest(
239
290
  const count = parseInt(urlObj.searchParams.get("n") ?? "50", 10);
240
291
  const errorsOnly = urlObj.searchParams.get("errors") === "true";
241
292
 
242
- // Collect all logs with session info
243
- const allLogs: (LogEntry & { sessionId: string })[] = [];
244
- for (const [sessionId, logs] of remoteLogs) {
245
- for (const log of logs) {
246
- if (!errorsOnly || log.level.toUpperCase() === "ERROR") {
247
- allLogs.push({ sessionId, ...log });
248
- }
249
- }
250
- }
251
-
252
- // Sort by time descending and take last N
253
- allLogs.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime());
254
- const recentLogs = allLogs.slice(0, count).reverse(); // Reverse to show oldest first
293
+ const storage = getLogStorage();
294
+ const filter = errorsOnly ? { level: "ERROR" } : {};
295
+ const recentLogs = storage.getRecentLogs(count, filter);
296
+ const totalLogs = storage.getLogs(filter).length;
255
297
 
256
298
  res.writeHead(200, { "Content-Type": "application/json" });
257
299
  res.end(
258
300
  JSON.stringify(
259
301
  {
260
- total: allLogs.length,
302
+ total: totalLogs,
261
303
  showing: recentLogs.length,
262
304
  logs: recentLogs,
263
305
  },
@@ -270,15 +312,7 @@ function handleRequest(
270
312
 
271
313
  // Handle errors-only endpoint
272
314
  if (url === "/logs/errors" && req.method === "GET") {
273
- const errorLogs: (LogEntry & { sessionId: string })[] = [];
274
- for (const [sessionId, logs] of remoteLogs) {
275
- for (const log of logs) {
276
- if (log.level.toUpperCase() === "ERROR") {
277
- errorLogs.push({ sessionId, ...log });
278
- }
279
- }
280
- }
281
- errorLogs.sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
315
+ const errorLogs = getLogStorage().getErrors();
282
316
 
283
317
  res.writeHead(200, { "Content-Type": "application/json" });
284
318
  res.end(
@@ -296,7 +330,7 @@ function handleRequest(
296
330
 
297
331
  // Handle clear logs endpoint
298
332
  if (url === "/logs/clear" && req.method === "POST") {
299
- remoteLogs.clear();
333
+ getLogStorage().clear();
300
334
  if (!quiet) {
301
335
  // eslint-disable-next-line no-console
302
336
  console.log(`\n${colors.yellow}Logs cleared${colors.reset}\n`);
@@ -306,13 +340,6 @@ function handleRequest(
306
340
  return;
307
341
  }
308
342
 
309
- // Health check endpoint
310
- if (url === "/health" && req.method === "GET") {
311
- res.writeHead(200, { "Content-Type": "application/json" });
312
- res.end(JSON.stringify({ status: "ok", sessions: remoteLogs.size }));
313
- return;
314
- }
315
-
316
343
  // Default: 404
317
344
  res.writeHead(404, { "Content-Type": "application/json" });
318
345
  res.end(JSON.stringify({ error: "Not found" }));
@@ -369,6 +396,80 @@ function printBanner(host: string, port: number, useHttps: boolean): void {
369
396
  console.log(`${colors.cyan}────────────────────────────────────────────────────────────${colors.reset}`);
370
397
  }
371
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
+
372
473
  /**
373
474
  * Start the log server.
374
475
  * @param options - Server configuration options
@@ -377,7 +478,6 @@ function printBanner(host: string, port: number, useHttps: boolean): void {
377
478
  export function startLogServer(options: LogServerOptions = {}): http.Server | https.Server {
378
479
  const port = options.port ?? 9080;
379
480
  const host = options.host ?? "localhost";
380
- const useHttp = options.useHttp ?? false;
381
481
  const quiet = options.quiet ?? false;
382
482
 
383
483
  // Set up log file if specified
@@ -390,50 +490,34 @@ export function startLogServer(options: LogServerOptions = {}): http.Server | ht
390
490
  }
391
491
 
392
492
  // Determine SSL configuration
493
+ // Use HTTPS only if valid certificate files are provided
393
494
  let server: https.Server | http.Server;
495
+ let useHttps = false;
496
+ const { certPath, keyPath } = options;
394
497
 
395
- if (useHttp) {
396
- // Plain HTTP server
397
- server = http.createServer((req, res) => {
398
- handleRequest(req, res, host, port, false, quiet);
399
- });
400
- } else {
401
- // HTTPS server
402
- let cert: string;
403
- let key: string;
404
-
405
- if (options.certPath && options.keyPath && certFilesExist(options.certPath, options.keyPath)) {
406
- // Use provided certificates
407
- ({ cert, key } = readCertFiles(options.certPath, options.keyPath));
408
- if (!quiet) {
409
- // eslint-disable-next-line no-console
410
- console.log(`${colors.green}Using SSL certificates from: ${options.certPath}${colors.reset}`);
411
- }
412
- } else {
413
- // Generate self-signed certificate
414
- if (!quiet) {
415
- // eslint-disable-next-line no-console
416
- console.log(`${colors.yellow}Generating self-signed certificate for ${host}...${colors.reset}`);
417
- }
418
-
419
- ({ cert, key } = generateSelfSignedCert(host));
420
- if (!quiet) {
421
- // eslint-disable-next-line no-console
422
- console.log(
423
- `${colors.yellow}Note: Browser will show certificate warning - this is expected for self-signed certs${colors.reset}`,
424
- );
425
- }
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}`);
426
505
  }
427
506
 
428
507
  server = https.createServer({ cert, key }, (req, res) => {
429
508
  handleRequest(req, res, host, port, true, quiet);
430
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
+ });
431
515
  }
432
516
 
433
517
  // Start listening
434
518
  server.listen(port, host, () => {
435
519
  if (!quiet) {
436
- printBanner(host, port, !useHttp);
520
+ printBanner(host, port, useHttps);
437
521
  }
438
522
  });
439
523
 
@@ -445,6 +529,12 @@ export function startLogServer(options: LogServerOptions = {}): http.Server | ht
445
529
  logFileStream.end();
446
530
  }
447
531
 
532
+ // Close JSONL writer to flush pending writes
533
+ const jsonlWriter = getJsonlWriter();
534
+ if (jsonlWriter) {
535
+ void jsonlWriter.close();
536
+ }
537
+
448
538
  server.close(() => {
449
539
  process.exit(0);
450
540
  });
@@ -466,18 +556,29 @@ Usage:
466
556
  Options:
467
557
  --port, -p <port> Port to listen on (default: 9080)
468
558
  --host, -h <host> Hostname to bind to (default: localhost)
469
- --cert, -c <path> Path to SSL certificate file
470
- --key, -k <path> Path to SSL private key file
559
+ --cert, -c <path> Path to SSL certificate file (enables HTTPS)
560
+ --key, -k <path> Path to SSL private key file (enables HTTPS)
471
561
  --log-file, -l <path> Write logs to file
472
- --http Use HTTP instead of HTTPS
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)
473
565
  --quiet, -q Suppress startup banner
474
566
  --help Show this help message
475
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
+
476
576
  Examples:
477
- npx remote-log-server # Start with defaults (port 9080, self-signed cert)
478
- npx remote-log-server --port 9085 # Custom port
479
- npx remote-log-server --http # Use HTTP instead of HTTPS
480
- npx remote-log-server --cert cert.crt --key key.key # Custom SSL certs
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
481
582
  npx remote-log-server --log-file ./tmp/logs.jsonl # Also write to file
482
583
  `;
483
584
 
@@ -532,8 +633,15 @@ export function parseArgs(args: string[]): ParseArgsResult {
532
633
  options.logFile = nextArg;
533
634
  i++;
534
635
  break;
535
- case "--http":
536
- options.useHttp = true;
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;
537
645
  break;
538
646
  case "--quiet":
539
647
  case "-q":
@@ -552,7 +660,7 @@ export function parseArgs(args: string[]): ParseArgsResult {
552
660
  /**
553
661
  * Parse command line arguments and start the server.
554
662
  */
555
- export function main(): void {
663
+ export async function main(): Promise<void> {
556
664
  const args = process.argv.slice(2);
557
665
  const result = parseArgs(args);
558
666
 
@@ -567,5 +675,89 @@ export function main(): void {
567
675
  process.exit(1);
568
676
  }
569
677
 
570
- startLogServer(result.options);
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
+ }
571
763
  }