@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.
- package/README.md +944 -28
- package/bin/remote-log-server.js +3 -0
- package/dist/client/RemoteLogClient.d.ts +116 -0
- package/dist/client/RemoteLogClient.d.ts.map +1 -0
- package/dist/client/RemoteLogClient.js +269 -0
- package/dist/client/RemoteLogClient.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +6 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +60 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +6 -0
- package/dist/client/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +32 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +270 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +14 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +14 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/logs-clear.d.ts +76 -0
- package/dist/mcp/tools/logs-clear.d.ts.map +1 -0
- package/dist/mcp/tools/logs-clear.js +58 -0
- package/dist/mcp/tools/logs-clear.js.map +1 -0
- package/dist/mcp/tools/logs-get-all.d.ts +60 -0
- package/dist/mcp/tools/logs-get-all.d.ts.map +1 -0
- package/dist/mcp/tools/logs-get-all.js +50 -0
- package/dist/mcp/tools/logs-get-all.js.map +1 -0
- package/dist/mcp/tools/logs-get-errors.d.ts +65 -0
- package/dist/mcp/tools/logs-get-errors.d.ts.map +1 -0
- package/dist/mcp/tools/logs-get-errors.js +46 -0
- package/dist/mcp/tools/logs-get-errors.js.map +1 -0
- package/dist/mcp/tools/logs-get-file-path.d.ts +75 -0
- package/dist/mcp/tools/logs-get-file-path.d.ts.map +1 -0
- package/dist/mcp/tools/logs-get-file-path.js +90 -0
- package/dist/mcp/tools/logs-get-file-path.js.map +1 -0
- package/dist/mcp/tools/logs-get-recent.d.ts +89 -0
- package/dist/mcp/tools/logs-get-recent.d.ts.map +1 -0
- package/dist/mcp/tools/logs-get-recent.js +74 -0
- package/dist/mcp/tools/logs-get-recent.js.map +1 -0
- package/dist/mcp/tools/logs-list-sessions.d.ts +64 -0
- package/dist/mcp/tools/logs-list-sessions.d.ts.map +1 -0
- package/dist/mcp/tools/logs-list-sessions.js +48 -0
- package/dist/mcp/tools/logs-list-sessions.js.map +1 -0
- package/dist/mcp/tools/logs-receive.d.ts +150 -0
- package/dist/mcp/tools/logs-receive.d.ts.map +1 -0
- package/dist/mcp/tools/logs-receive.js +68 -0
- package/dist/mcp/tools/logs-receive.js.map +1 -0
- package/dist/mcp/tools/logs-search.d.ts +91 -0
- package/dist/mcp/tools/logs-search.d.ts.map +1 -0
- package/dist/mcp/tools/logs-search.js +68 -0
- package/dist/mcp/tools/logs-search.js.map +1 -0
- package/dist/mcp/tools/logs-status.d.ts +45 -0
- package/dist/mcp/tools/logs-status.d.ts.map +1 -0
- package/dist/mcp/tools/logs-status.js +45 -0
- package/dist/mcp/tools/logs-status.js.map +1 -0
- package/dist/server/dual-server.d.ts +76 -0
- package/dist/server/dual-server.d.ts.map +1 -0
- package/dist/server/dual-server.js +214 -0
- package/dist/server/dual-server.js.map +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +12 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/jsonl-writer.d.ts +93 -0
- package/dist/server/jsonl-writer.d.ts.map +1 -0
- package/dist/server/jsonl-writer.js +205 -0
- package/dist/server/jsonl-writer.js.map +1 -0
- package/dist/server/log-server.d.ts +126 -0
- package/dist/server/log-server.d.ts.map +1 -0
- package/dist/server/log-server.js +589 -0
- package/dist/server/log-server.js.map +1 -0
- package/dist/server/log-storage.d.ts +301 -0
- package/dist/server/log-storage.d.ts.map +1 -0
- package/dist/server/log-storage.js +408 -0
- package/dist/server/log-storage.js.map +1 -0
- package/dist/server/marker-utils.d.ts +69 -0
- package/dist/server/marker-utils.d.ts.map +1 -0
- package/dist/server/marker-utils.js +118 -0
- package/dist/server/marker-utils.js.map +1 -0
- package/dist/server/self-signed-cert.d.ts +30 -0
- package/dist/server/self-signed-cert.d.ts.map +1 -0
- package/dist/server/self-signed-cert.js +83 -0
- package/dist/server/self-signed-cert.js.map +1 -0
- package/dist/ui/ConsoleCaptureUI.d.ts +118 -0
- package/dist/ui/ConsoleCaptureUI.d.ts.map +1 -0
- package/dist/ui/ConsoleCaptureUI.js +571 -0
- package/dist/ui/ConsoleCaptureUI.js.map +1 -0
- package/dist/ui/index.d.ts +15 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/vite/index.d.ts +8 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +8 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vite/plugin.d.ts +42 -0
- package/dist/vite/plugin.d.ts.map +1 -0
- package/dist/vite/plugin.js +46 -0
- package/dist/vite/plugin.js.map +1 -0
- package/package.json +90 -7
- package/src/client/RemoteLogClient.ts +328 -0
- package/src/client/index.ts +7 -0
- package/src/client/types.ts +62 -0
- package/src/index.ts +28 -0
- package/src/mcp/index.ts +25 -0
- package/src/mcp/mcp-server.ts +364 -0
- package/src/mcp/tools/index.ts +69 -0
- package/src/mcp/tools/logs-clear.ts +86 -0
- package/src/mcp/tools/logs-get-all.ts +78 -0
- package/src/mcp/tools/logs-get-errors.ts +71 -0
- package/src/mcp/tools/logs-get-file-path.ts +121 -0
- package/src/mcp/tools/logs-get-recent.ts +104 -0
- package/src/mcp/tools/logs-list-sessions.ts +71 -0
- package/src/mcp/tools/logs-receive.ts +96 -0
- package/src/mcp/tools/logs-search.ts +95 -0
- package/src/mcp/tools/logs-status.ts +69 -0
- package/src/server/dual-server.ts +308 -0
- package/src/server/index.ts +54 -0
- package/src/server/jsonl-writer.ts +277 -0
- package/src/server/log-server.ts +763 -0
- package/src/server/log-storage.ts +651 -0
- package/src/server/marker-utils.ts +144 -0
- package/src/server/self-signed-cert.ts +93 -0
- package/src/ui/ConsoleCaptureUI.ts +649 -0
- package/src/ui/index.ts +15 -0
- package/src/vite/index.ts +8 -0
- 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
|
+
}
|