amalgm 0.1.36 → 0.1.37

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.
@@ -46,12 +46,34 @@ const pty = loadPty();
46
46
  const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
47
47
  const STATE_FILE = path.join(AMALGM_DIR, 'runtime-state.json');
48
48
  const ARTIFACTS_FILE = path.join(AMALGM_DIR, 'artifacts.json');
49
+ const LOG_DIR = path.join(AMALGM_DIR, 'logs');
49
50
  const BIND_HOST = process.env.AMALGM_BIND_HOST || '127.0.0.1';
50
51
  const OWNER = process.env.AMALGM_RUNTIME_SOURCE || 'local';
51
52
  const VERSION = process.env.npm_package_version || process.env.AMALGM_RUNTIME_VERSION || '';
52
53
  const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
53
54
  const PORT = Number.parseInt(process.env.AMALGM_GATEWAY_PORT || '28781', 10);
54
55
  const RUNTIME_TOKEN_HEADER = 'x-amalgm-runtime-token';
56
+ const DEFAULT_DIAGNOSTIC_LOG_TAIL_BYTES = 256 * 1024;
57
+ const MAX_DIAGNOSTIC_LOG_TAIL_BYTES = 2 * 1024 * 1024;
58
+
59
+ const DIAGNOSTIC_LOG_FILES = Object.freeze({
60
+ daemon: 'daemon.log',
61
+ 'chat-server': 'chat-server.log',
62
+ 'local-gateway': 'local-gateway.log',
63
+ 'amalgm-mcp': 'amalgm-mcp.log',
64
+ 'fs-watcher': 'fs-watcher.log',
65
+ 'port-monitor': 'port-monitor.log',
66
+ });
67
+
68
+ const DIAGNOSTIC_LOG_ALIASES = Object.freeze({
69
+ chat: 'chat-server',
70
+ mcp: 'amalgm-mcp',
71
+ gateway: 'local-gateway',
72
+ events: 'daemon',
73
+ 'event-tunnel': 'daemon',
74
+ 'chat-tunnel': 'daemon',
75
+ tunnel: 'daemon',
76
+ });
55
77
 
56
78
  const SERVICE_PORTS = {
57
79
  gateway: PORT,
@@ -314,6 +336,137 @@ function sendJson(res, statusCode, payload) {
314
336
  res.end(JSON.stringify(payload));
315
337
  }
316
338
 
339
+ function diagnosticLogServices() {
340
+ return Object.keys(DIAGNOSTIC_LOG_FILES);
341
+ }
342
+
343
+ function normalizeDiagnosticLogService(value) {
344
+ let service = String(value || 'daemon').trim().toLowerCase();
345
+ if (service.endsWith('.log')) service = service.slice(0, -4);
346
+ service = service.replace(/[^a-z0-9_-]/g, '');
347
+ return DIAGNOSTIC_LOG_ALIASES[service] || service;
348
+ }
349
+
350
+ function parseDiagnosticTailBytes(value) {
351
+ const parsed = Number.parseInt(String(value || ''), 10);
352
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_DIAGNOSTIC_LOG_TAIL_BYTES;
353
+ return Math.min(parsed, MAX_DIAGNOSTIC_LOG_TAIL_BYTES);
354
+ }
355
+
356
+ function parseDiagnosticLineLimit(value) {
357
+ const parsed = Number.parseInt(String(value || ''), 10);
358
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
359
+ return Math.min(parsed, 5000);
360
+ }
361
+
362
+ function redactDiagnosticLogContent(content) {
363
+ return String(content)
364
+ .replace(/(authorization\s*[:=]\s*)(?:Bearer\s+)?[A-Za-z0-9._~+/=-]{8,}/gi, '$1[REDACTED]')
365
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/gi, 'Bearer [REDACTED]')
366
+ .replace(/\b(?:sk|pk|rk|ghp|github_pat|glpat|xox[baprs])[-_][A-Za-z0-9_./+=-]{16,}\b/g, '[REDACTED_TOKEN]')
367
+ .replace(
368
+ /((?:api[_-]?key|apikey|token|secret|password|cookie|set-cookie)[^:=\n]{0,32}[:=]\s*["']?)[^\s"',;}]+/gi,
369
+ '$1[REDACTED]',
370
+ );
371
+ }
372
+
373
+ function applyDiagnosticLineLimit(content, lineLimit) {
374
+ if (!lineLimit) return { content, lineTruncated: false };
375
+ const lines = String(content).split(/\r?\n/);
376
+ if (lines.length <= lineLimit) return { content, lineTruncated: false };
377
+ return {
378
+ content: lines.slice(-lineLimit).join('\n'),
379
+ lineTruncated: true,
380
+ };
381
+ }
382
+
383
+ async function readDiagnosticLogTail(logPath, maxBytes) {
384
+ const stats = await fs.promises.stat(logPath);
385
+ if (!stats.isFile()) {
386
+ const error = new Error('Diagnostic log path is not a file');
387
+ error.statusCode = 400;
388
+ throw error;
389
+ }
390
+
391
+ const bytes = Math.min(stats.size, maxBytes);
392
+ if (bytes === 0) {
393
+ return {
394
+ stats,
395
+ bytes,
396
+ content: '',
397
+ byteTruncated: false,
398
+ };
399
+ }
400
+
401
+ const handle = await fs.promises.open(logPath, 'r');
402
+ try {
403
+ const buffer = Buffer.alloc(bytes);
404
+ await handle.read(buffer, 0, bytes, stats.size - bytes);
405
+ return {
406
+ stats,
407
+ bytes,
408
+ content: buffer.toString('utf8'),
409
+ byteTruncated: stats.size > bytes,
410
+ };
411
+ } finally {
412
+ await handle.close();
413
+ }
414
+ }
415
+
416
+ async function handleDiagnosticLogs(req, res, url) {
417
+ if (req.method !== 'GET') {
418
+ res.writeHead(405, { Allow: 'GET' });
419
+ res.end();
420
+ return;
421
+ }
422
+
423
+ const service = normalizeDiagnosticLogService(
424
+ url.searchParams.get('service') || url.searchParams.get('name'),
425
+ );
426
+ const filename = DIAGNOSTIC_LOG_FILES[service];
427
+ if (!filename) {
428
+ sendJson(res, 400, {
429
+ error: 'Unknown diagnostic log service',
430
+ availableServices: diagnosticLogServices(),
431
+ });
432
+ return;
433
+ }
434
+
435
+ const tailBytes = parseDiagnosticTailBytes(url.searchParams.get('tailBytes'));
436
+ const lineLimit = parseDiagnosticLineLimit(url.searchParams.get('lines'));
437
+ const logPath = path.join(LOG_DIR, filename);
438
+
439
+ try {
440
+ const tail = await readDiagnosticLogTail(logPath, tailBytes);
441
+ const lineLimited = applyDiagnosticLineLimit(tail.content, lineLimit);
442
+ sendJson(res, 200, {
443
+ service,
444
+ path: logPath,
445
+ exists: true,
446
+ size: tail.stats.size,
447
+ mtime: tail.stats.mtime.toISOString(),
448
+ tailBytes: tail.bytes,
449
+ truncated: tail.byteTruncated || lineLimited.lineTruncated,
450
+ content: redactDiagnosticLogContent(lineLimited.content),
451
+ });
452
+ } catch (error) {
453
+ if (error && error.code === 'ENOENT') {
454
+ sendJson(res, 404, {
455
+ service,
456
+ path: logPath,
457
+ exists: false,
458
+ error: 'Diagnostic log file not found',
459
+ });
460
+ return;
461
+ }
462
+ const status = typeof error?.statusCode === 'number' ? error.statusCode : 500;
463
+ sendJson(res, status, {
464
+ service,
465
+ error: error instanceof Error ? error.message : 'Failed to read diagnostic log',
466
+ });
467
+ }
468
+ }
469
+
317
470
  async function readBody(req) {
318
471
  const chunks = [];
319
472
  for await (const chunk of req) chunks.push(Buffer.from(chunk));
@@ -760,6 +913,11 @@ const server = http.createServer(async (req, res) => {
760
913
  return;
761
914
  }
762
915
 
916
+ if (url.pathname === '/diagnostics/logs') {
917
+ await handleDiagnosticLogs(req, res, url);
918
+ return;
919
+ }
920
+
763
921
  if (url.pathname === '/pty/session' || url.pathname === '/pty/resize') {
764
922
  await handlePtyHttp(req, res, url.pathname);
765
923
  return;