@runtimescope/collector 0.10.8 → 0.10.10

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.
@@ -0,0 +1,38 @@
1
+ // src/log.ts
2
+ function formatArg(a) {
3
+ if (typeof a === "string") return a;
4
+ if (a instanceof Error) return a.stack ?? a.message;
5
+ if (a === null || a === void 0) return String(a);
6
+ if (typeof a === "object") {
7
+ try {
8
+ return JSON.stringify(a);
9
+ } catch {
10
+ return String(a);
11
+ }
12
+ }
13
+ return String(a);
14
+ }
15
+ function write(stream, args) {
16
+ try {
17
+ if (!stream.writable) {
18
+ process.exit(1);
19
+ }
20
+ const formatted = args.map(formatArg).join(" ");
21
+ stream.write(formatted + "\n");
22
+ } catch {
23
+ process.exit(1);
24
+ }
25
+ }
26
+ var safeLog = {
27
+ error(...args) {
28
+ write(process.stderr, args);
29
+ },
30
+ warn(...args) {
31
+ write(process.stderr, args);
32
+ }
33
+ };
34
+
35
+ export {
36
+ safeLog
37
+ };
38
+ //# sourceMappingURL=chunk-CYDXIW4L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/log.ts"],"sourcesContent":["// ============================================================\n// safeLog — EPIPE-resilient stderr writes\n//\n// Why this exists:\n// v0.10.8 fixed a class of bug where the npx-spawned MCP server got\n// reparented to init when Claude Code exited, then entered a tight\n// uncaughtException → console.error → uncaughtException loop against\n// a closed stderr pipe. The fix wrapped the two handler entry points,\n// but the codebase has 100+ raw `console.error` / `process.stderr.write`\n// sites in ordinary code paths (PM discovery, otel exporter, WAL\n// recovery, etc.). Any one of those can re-trigger the same loop if\n// it fires while stderr is broken — the v0.10.8 fix is necessary but\n// not sufficient.\n//\n// See audit: docs/audits/0001-collector-process-lifetime.md F1\n// See ADR: docs/decisions/0001-audit-then-rust.md\n//\n// Contract:\n// - Synchronous writes to stderr, formatted similarly to console.error.\n// - If stderr is unwritable (parent died, pipe closed), the call exits\n// the process with code 1 rather than throwing. We cannot meaningfully\n// surface anything to a dead pipe; the only behavior that doesn't\n// cascade into a CPU-pegged loop is to bail.\n// - Drop-in replacement for console.error / console.warn — supports\n// multi-arg format (`safeLog.error('foo:', err.message)`).\n// - Never throws; never re-enters. Safe to call from inside an\n// uncaughtException handler.\n// ============================================================\n\nfunction formatArg(a: unknown): string {\n if (typeof a === 'string') return a;\n if (a instanceof Error) return a.stack ?? a.message;\n if (a === null || a === undefined) return String(a);\n if (typeof a === 'object') {\n try {\n return JSON.stringify(a);\n } catch {\n // Circular refs or non-serializable values — fall back to toString.\n return String(a);\n }\n }\n return String(a);\n}\n\nfunction write(stream: NodeJS.WriteStream, args: unknown[]): void {\n try {\n if (!stream.writable) {\n // Pipe is gone. Don't try to log anything else, just exit.\n // We use code 1 to distinguish \"I exited because my stderr broke\"\n // from \"I exited because my stdin closed\" (which uses code 0).\n process.exit(1);\n }\n const formatted = args.map(formatArg).join(' ');\n stream.write(formatted + '\\n');\n } catch {\n // The write itself threw (EPIPE landed between the writable check and\n // the write). Same conclusion: exit, do not loop.\n process.exit(1);\n }\n}\n\nexport const safeLog = {\n error(...args: unknown[]): void {\n write(process.stderr, args);\n },\n warn(...args: unknown[]): void {\n write(process.stderr, args);\n },\n};\n"],"mappings":";AA6BA,SAAS,UAAU,GAAoB;AACrC,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,MAAI,aAAa,MAAO,QAAO,EAAE,SAAS,EAAE;AAC5C,MAAI,MAAM,QAAQ,MAAM,OAAW,QAAO,OAAO,CAAC;AAClD,MAAI,OAAO,MAAM,UAAU;AACzB,QAAI;AACF,aAAO,KAAK,UAAU,CAAC;AAAA,IACzB,QAAQ;AAEN,aAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AACA,SAAO,OAAO,CAAC;AACjB;AAEA,SAAS,MAAM,QAA4B,MAAuB;AAChE,MAAI;AACF,QAAI,CAAC,OAAO,UAAU;AAIpB,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM,YAAY,KAAK,IAAI,SAAS,EAAE,KAAK,GAAG;AAC9C,WAAO,MAAM,YAAY,IAAI;AAAA,EAC/B,QAAQ;AAGN,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEO,IAAM,UAAU;AAAA,EACrB,SAAS,MAAuB;AAC9B,UAAM,QAAQ,QAAQ,IAAI;AAAA,EAC5B;AAAA,EACA,QAAQ,MAAuB;AAC7B,UAAM,QAAQ,QAAQ,IAAI;AAAA,EAC5B;AACF;","names":[]}
@@ -1,3 +1,6 @@
1
+ import {
2
+ safeLog
3
+ } from "./chunk-CYDXIW4L.js";
1
4
  import {
2
5
  __require
3
6
  } from "./chunk-UP2VWCW5.js";
@@ -483,6 +486,7 @@ var SqliteStore = class _SqliteStore {
483
486
  this.db = this.openDatabase(options);
484
487
  const flushInterval = options.flushIntervalMs ?? 100;
485
488
  this.flushTimer = setInterval(() => this.flush(), flushInterval);
489
+ this.flushTimer.unref?.();
486
490
  }
487
491
  openDatabase(options) {
488
492
  const Db = getDatabase();
@@ -500,14 +504,14 @@ var SqliteStore = class _SqliteStore {
500
504
  this.prepareStatements(db);
501
505
  return db;
502
506
  } catch (err) {
503
- console.error(
507
+ safeLog.error(
504
508
  `[RuntimeScope] SQLite database corrupt or unreadable (${err.message}), recreating...`
505
509
  );
506
510
  try {
507
511
  if (existsSync(options.dbPath)) {
508
512
  const backupPath = `${options.dbPath}.corrupt.${Date.now()}`;
509
513
  renameSync(options.dbPath, backupPath);
510
- console.error(`[RuntimeScope] Renamed corrupt DB to ${backupPath}`);
514
+ safeLog.error(`[RuntimeScope] Renamed corrupt DB to ${backupPath}`);
511
515
  }
512
516
  for (const suffix of ["-wal", "-shm"]) {
513
517
  const p = options.dbPath + suffix;
@@ -626,7 +630,7 @@ var SqliteStore = class _SqliteStore {
626
630
  try {
627
631
  insertMany();
628
632
  } catch (err) {
629
- console.error("[RuntimeScope] SQLite flush error:", err.message);
633
+ safeLog.error("[RuntimeScope] SQLite flush error:", err.message);
630
634
  }
631
635
  }
632
636
  saveSession(info) {
@@ -890,7 +894,7 @@ function isSqliteAvailable() {
890
894
  _available = true;
891
895
  } catch {
892
896
  _available = false;
893
- console.error(
897
+ safeLog.error(
894
898
  "[RuntimeScope] better-sqlite3 is not available \u2014 running in memory-only mode.\n[RuntimeScope] Historical data persistence is disabled. To fix this:\n[RuntimeScope] macOS: xcode-select --install\n[RuntimeScope] Ubuntu: sudo apt-get install build-essential python3\n[RuntimeScope] Windows: npm install --global windows-build-tools\n[RuntimeScope] Then run: npm rebuild better-sqlite3"
895
899
  );
896
900
  }
@@ -1503,7 +1507,7 @@ var OtelExporter = class {
1503
1507
  const now = Date.now();
1504
1508
  if (now - this.lastFailureLog < 6e4) return;
1505
1509
  this.lastFailureLog = now;
1506
- console.error(`[RuntimeScope] ${msg}`);
1510
+ safeLog.error(`[RuntimeScope] ${msg}`);
1507
1511
  }
1508
1512
  };
1509
1513
  function traceIdFromSession(sessionId) {
@@ -1644,7 +1648,7 @@ var SessionRateLimiter = class {
1644
1648
  maybeWarn(sessionId, w, now) {
1645
1649
  if (now - w.lastWarning >= 6e4) {
1646
1650
  w.lastWarning = now;
1647
- console.error(
1651
+ safeLog.error(
1648
1652
  `[RuntimeScope] Rate limiting session ${sessionId.slice(0, 8)}... (dropped ${this._droppedTotal} total)`
1649
1653
  );
1650
1654
  }
@@ -1694,6 +1698,15 @@ var CollectorServer = class {
1694
1698
  // RSS. ensureSqliteStore() updates this on every access.
1695
1699
  sqliteStoreLastAccess = /* @__PURE__ */ new Map();
1696
1700
  wals = /* @__PURE__ */ new Map();
1701
+ // Per-project last-access timestamps for WAL handles, mirroring
1702
+ // sqliteStoreLastAccess. The audit (docs/audits/0001 §F3) found that
1703
+ // WAL handles followed the same "open on first use, only closed on
1704
+ // stop()" lifetime as the SQLite stores did pre-v0.10.8 — fewer bytes
1705
+ // per handle than SQLite, but each holds an open FD, so on machines
1706
+ // with many projects the leak is real (ulimit risk + small RSS drift).
1707
+ // ensureWal() updates this on every access; the sqliteEvictTimer sweep
1708
+ // evicts idle handles in the same pass.
1709
+ walsLastAccess = /* @__PURE__ */ new Map();
1697
1710
  ready = false;
1698
1711
  metrics = new MetricsRegistry();
1699
1712
  startedAt = Date.now();
@@ -1717,6 +1730,7 @@ var CollectorServer = class {
1717
1730
  }
1718
1731
  if (this.rateLimiter.isEnabled()) {
1719
1732
  this.pruneTimer = setInterval(() => this.rateLimiter.prune(), 6e4);
1733
+ this.pruneTimer.unref?.();
1720
1734
  }
1721
1735
  this.counters = {
1722
1736
  eventsTotal: this.metrics.counter(
@@ -1774,7 +1788,7 @@ var CollectorServer = class {
1774
1788
  this.store.onEvent((event) => {
1775
1789
  this.otelExporter?.ingest(event);
1776
1790
  });
1777
- console.error(
1791
+ safeLog.error(
1778
1792
  `[RuntimeScope] OpenTelemetry export enabled \u2192 ${otelOptions.endpoint}`
1779
1793
  );
1780
1794
  }
@@ -1802,6 +1816,24 @@ var CollectorServer = class {
1802
1816
  getSqliteStores() {
1803
1817
  return this.sqliteStores;
1804
1818
  }
1819
+ /**
1820
+ * Diagnostic counters for currently-open per-project resource handles
1821
+ * and pending bidirectional commands. Exposed for tests + future
1822
+ * tray-app health surfaces.
1823
+ *
1824
+ * - `sqliteStores` / `wals`: open handle counts. The eviction sweep
1825
+ * (5-minute idle in production, env-overridable for tests) drives
1826
+ * these down to zero for idle projects and back up on reconnect.
1827
+ * - `pendingCommands`: in-flight WS commands (server → SDK). Settles
1828
+ * on response or via the per-command timeout — see audit F5.
1829
+ */
1830
+ getOpenHandleCounts() {
1831
+ return {
1832
+ sqliteStores: this.sqliteStores.size,
1833
+ wals: this.wals.size,
1834
+ pendingCommands: this.pendingCommands.size
1835
+ };
1836
+ }
1805
1837
  getRateLimiter() {
1806
1838
  return this.rateLimiter;
1807
1839
  }
@@ -1848,7 +1880,7 @@ var CollectorServer = class {
1848
1880
  sqliteBytes = sqliteStore.snapshotTo(sqlitePath);
1849
1881
  eventCount = sqliteStore.getEventCount({ project: projectName });
1850
1882
  } catch (err) {
1851
- console.error(
1883
+ safeLog.error(
1852
1884
  `[RuntimeScope] Snapshot of "${projectName}" SQLite failed:`,
1853
1885
  err.message
1854
1886
  );
@@ -1860,7 +1892,7 @@ var CollectorServer = class {
1860
1892
  try {
1861
1893
  walBytes = wal.snapshotTo(join2(projectDir, "wal"));
1862
1894
  } catch (err) {
1863
- console.error(
1895
+ safeLog.error(
1864
1896
  `[RuntimeScope] Snapshot of "${projectName}" WAL failed:`,
1865
1897
  err.message
1866
1898
  );
@@ -1889,7 +1921,7 @@ var CollectorServer = class {
1889
1921
  try {
1890
1922
  this.runStartupRecovery();
1891
1923
  } catch (err) {
1892
- console.error("[RuntimeScope] Startup recovery failed (non-fatal):", err.message);
1924
+ safeLog.error("[RuntimeScope] Startup recovery failed (non-fatal):", err.message);
1893
1925
  }
1894
1926
  }
1895
1927
  this.ready = true;
@@ -1929,7 +1961,7 @@ var CollectorServer = class {
1929
1961
  try {
1930
1962
  store.close();
1931
1963
  } catch (e) {
1932
- console.error(
1964
+ safeLog.error(
1933
1965
  `[RuntimeScope] Failed to close idle SQLite store "${projectName}":`,
1934
1966
  e.message
1935
1967
  );
@@ -1939,6 +1971,22 @@ var CollectorServer = class {
1939
1971
  this.sqliteStoreLastAccess.delete(projectName);
1940
1972
  this.store.clearSqliteStoreIfMatches(projectName);
1941
1973
  }
1974
+ for (const [projectName, wal] of this.wals) {
1975
+ if (liveProjects.has(projectName)) continue;
1976
+ const lastAccess = this.walsLastAccess.get(projectName) ?? 0;
1977
+ if (now - lastAccess < IDLE_TIMEOUT_MS) continue;
1978
+ try {
1979
+ wal.close();
1980
+ } catch (e) {
1981
+ safeLog.error(
1982
+ `[RuntimeScope] Failed to close idle WAL "${projectName}":`,
1983
+ e.message
1984
+ );
1985
+ continue;
1986
+ }
1987
+ this.wals.delete(projectName);
1988
+ this.walsLastAccess.delete(projectName);
1989
+ }
1942
1990
  }, SWEEP_INTERVAL_MS);
1943
1991
  this.sqliteEvictTimer.unref?.();
1944
1992
  }
@@ -1990,7 +2038,7 @@ var CollectorServer = class {
1990
2038
  }
1991
2039
  }
1992
2040
  if (walReplayed > 0 || warmed > 0) {
1993
- console.error(
2041
+ safeLog.error(
1994
2042
  `[RuntimeScope] Recovery: ${walReplayed} WAL events replayed, ${warmed} events warmed into ring buffer.`
1995
2043
  );
1996
2044
  }
@@ -2006,7 +2054,7 @@ var CollectorServer = class {
2006
2054
  this.setupConnectionHandler(wss);
2007
2055
  this.setupPersistentErrorHandler(wss);
2008
2056
  this.startHeartbeat(wss);
2009
- console.error(`[RuntimeScope] Collector listening on wss://${host}:${port}`);
2057
+ safeLog.error(`[RuntimeScope] Collector listening on wss://${host}:${port}`);
2010
2058
  resolve2();
2011
2059
  });
2012
2060
  httpsServer.on("error", (err) => {
@@ -2021,7 +2069,7 @@ var CollectorServer = class {
2021
2069
  this.setupConnectionHandler(wss);
2022
2070
  this.setupPersistentErrorHandler(wss);
2023
2071
  this.startHeartbeat(wss);
2024
- console.error(`[RuntimeScope] Collector listening on ws://${host}:${port}`);
2072
+ safeLog.error(`[RuntimeScope] Collector listening on ws://${host}:${port}`);
2025
2073
  resolve2();
2026
2074
  });
2027
2075
  wss.on("error", (err) => {
@@ -2034,12 +2082,12 @@ var CollectorServer = class {
2034
2082
  handleStartError(err, port, host, retriesLeft, retryDelayMs, tls, resolve2, reject) {
2035
2083
  if (err.code === "EADDRINUSE" && retriesLeft > 0) {
2036
2084
  const nextPort = port + 1;
2037
- console.error(
2085
+ safeLog.error(
2038
2086
  `[RuntimeScope] Port ${port} in use, trying ${nextPort}...`
2039
2087
  );
2040
2088
  this.tryStart(nextPort, host, retriesLeft - 1, retryDelayMs, tls).then(resolve2).catch(reject);
2041
2089
  } else {
2042
- console.error("[RuntimeScope] WebSocket server error:", err.message);
2090
+ safeLog.error("[RuntimeScope] WebSocket server error:", err.message);
2043
2091
  reject(err);
2044
2092
  }
2045
2093
  }
@@ -2055,9 +2103,9 @@ var CollectorServer = class {
2055
2103
  sqliteStore = new SqliteStore({ dbPath });
2056
2104
  this.sqliteStores.set(projectName, sqliteStore);
2057
2105
  this.store.setSqliteStore(sqliteStore, projectName);
2058
- console.error(`[RuntimeScope] SQLite store opened for project "${projectName}"`);
2106
+ safeLog.error(`[RuntimeScope] SQLite store opened for project "${projectName}"`);
2059
2107
  } catch (err) {
2060
- console.error(
2108
+ safeLog.error(
2061
2109
  `[RuntimeScope] Failed to open SQLite for "${projectName}":`,
2062
2110
  err.message
2063
2111
  );
@@ -2084,6 +2132,7 @@ var CollectorServer = class {
2084
2132
  */
2085
2133
  ensureWal(projectName) {
2086
2134
  if (!this.projectManager) return null;
2135
+ this.walsLastAccess.set(projectName, Date.now());
2087
2136
  let wal = this.wals.get(projectName);
2088
2137
  if (wal) return wal;
2089
2138
  const dir = this.walDirFor(projectName);
@@ -2094,7 +2143,7 @@ var CollectorServer = class {
2094
2143
  this.wals.set(projectName, wal);
2095
2144
  return wal;
2096
2145
  } catch (err) {
2097
- console.error(
2146
+ safeLog.error(
2098
2147
  `[RuntimeScope] Failed to open WAL for "${projectName}":`,
2099
2148
  err.message
2100
2149
  );
@@ -2131,7 +2180,7 @@ var CollectorServer = class {
2131
2180
  if (sqliteStore && replayed > 0) {
2132
2181
  sqliteStore.flush();
2133
2182
  for (const file of files) Wal.deleteSealed(file);
2134
- console.error(
2183
+ safeLog.error(
2135
2184
  `[RuntimeScope] WAL recovery: replayed ${replayed} events for "${projectName}"`
2136
2185
  );
2137
2186
  }
@@ -2151,7 +2200,7 @@ var CollectorServer = class {
2151
2200
  /** Catch runtime errors on the WSS so an unhandled error doesn't crash the process */
2152
2201
  setupPersistentErrorHandler(wss) {
2153
2202
  wss.on("error", (err) => {
2154
- console.error("[RuntimeScope] WebSocket server runtime error:", err.message);
2203
+ safeLog.error("[RuntimeScope] WebSocket server runtime error:", err.message);
2155
2204
  });
2156
2205
  }
2157
2206
  /** Ping all connected clients every 15s — terminate those that don't respond */
@@ -2167,6 +2216,7 @@ var CollectorServer = class {
2167
2216
  ws.ping();
2168
2217
  }
2169
2218
  }, 15e3);
2219
+ this.heartbeatTimer.unref?.();
2170
2220
  }
2171
2221
  setupConnectionHandler(wss) {
2172
2222
  wss.on("connection", (ws) => {
@@ -2201,7 +2251,7 @@ var CollectorServer = class {
2201
2251
  const msg = JSON.parse(data.toString());
2202
2252
  this.handleMessage(ws, msg);
2203
2253
  } catch {
2204
- console.error("[RuntimeScope] Malformed WebSocket message, ignoring");
2254
+ safeLog.error("[RuntimeScope] Malformed WebSocket message, ignoring");
2205
2255
  }
2206
2256
  });
2207
2257
  ws.on("close", (code) => {
@@ -2214,7 +2264,7 @@ var CollectorServer = class {
2214
2264
  if (sqliteStore) {
2215
2265
  sqliteStore.updateSessionDisconnected(clientInfo.sessionId, Date.now());
2216
2266
  }
2217
- console.error(`[RuntimeScope] Session ${clientInfo.sessionId} disconnected`);
2267
+ safeLog.error(`[RuntimeScope] Session ${clientInfo.sessionId} disconnected`);
2218
2268
  for (const cb of this.disconnectCallbacks) {
2219
2269
  try {
2220
2270
  cb(clientInfo.sessionId, clientInfo.projectName, clientInfo.projectId);
@@ -2225,7 +2275,7 @@ var CollectorServer = class {
2225
2275
  this.clients.delete(ws);
2226
2276
  });
2227
2277
  ws.on("error", (err) => {
2228
- console.error("[RuntimeScope] WebSocket client error:", err.message);
2278
+ safeLog.error("[RuntimeScope] WebSocket client error:", err.message);
2229
2279
  });
2230
2280
  });
2231
2281
  }
@@ -2299,7 +2349,7 @@ var CollectorServer = class {
2299
2349
  connectedAt: msg.timestamp,
2300
2350
  sdkVersion: payload.sdkVersion
2301
2351
  });
2302
- console.error(
2352
+ safeLog.error(
2303
2353
  `[RuntimeScope] Session ${payload.sessionId} connected (${payload.appName} v${payload.sdkVersion})`
2304
2354
  );
2305
2355
  for (const cb of this.connectCallbacks) {
@@ -2334,7 +2384,7 @@ var CollectorServer = class {
2334
2384
  wal.append(accepted);
2335
2385
  wal.commit();
2336
2386
  } catch (err) {
2337
- console.error("[RuntimeScope] WAL append/commit failed:", err.message);
2387
+ safeLog.error("[RuntimeScope] WAL append/commit failed:", err.message);
2338
2388
  this.counters.eventsDropped.inc(accepted.length, { reason: "wal_backpressure" });
2339
2389
  }
2340
2390
  }
@@ -2345,7 +2395,7 @@ var CollectorServer = class {
2345
2395
  try {
2346
2396
  this.checkpointWal(clientInfo.projectName, wal);
2347
2397
  } catch (err) {
2348
- console.error("[RuntimeScope] WAL checkpoint failed:", err.message);
2398
+ safeLog.error("[RuntimeScope] WAL checkpoint failed:", err.message);
2349
2399
  }
2350
2400
  }
2351
2401
  break;
@@ -2457,14 +2507,14 @@ var CollectorServer = class {
2457
2507
  try {
2458
2508
  wal.close();
2459
2509
  } catch {
2460
- console.error(`[RuntimeScope] WAL close error for "${name}" (non-fatal)`);
2510
+ safeLog.error(`[RuntimeScope] WAL close error for "${name}" (non-fatal)`);
2461
2511
  }
2462
2512
  }
2463
2513
  this.wals.clear();
2464
2514
  for (const [name, sqliteStore] of this.sqliteStores) {
2465
2515
  try {
2466
2516
  sqliteStore.close();
2467
- console.error(`[RuntimeScope] SQLite store closed for "${name}"`);
2517
+ safeLog.error(`[RuntimeScope] SQLite store closed for "${name}"`);
2468
2518
  } catch {
2469
2519
  }
2470
2520
  }
@@ -2472,7 +2522,7 @@ var CollectorServer = class {
2472
2522
  if (this.wss) {
2473
2523
  this.wss.close();
2474
2524
  this.wss = null;
2475
- console.error("[RuntimeScope] Collector stopped");
2525
+ safeLog.error("[RuntimeScope] Collector stopped");
2476
2526
  }
2477
2527
  this.ready = false;
2478
2528
  }
@@ -4992,6 +5042,30 @@ var HttpServer = class {
4992
5042
  }
4993
5043
  return null;
4994
5044
  }
5045
+ /**
5046
+ * Resolve the directory containing the built dashboard SPA. The tsup
5047
+ * onSuccess hook copies packages/dashboard/dist/ into the collector's
5048
+ * dist/dashboard-assets/ so the dashboard ships inside the @runtimescope/
5049
+ * collector npm package. Cached after first resolution.
5050
+ */
5051
+ dashboardAssetsRoot = null;
5052
+ resolveDashboardAssets() {
5053
+ if (this.dashboardAssetsRoot) return this.dashboardAssetsRoot;
5054
+ const __dir = dirname2(fileURLToPath(import.meta.url));
5055
+ const candidates = [
5056
+ resolve(__dir, "dashboard-assets"),
5057
+ // npm published: dist/dashboard-assets/
5058
+ resolve(__dir, "../../dashboard/dist")
5059
+ // monorepo dev: sibling package
5060
+ ];
5061
+ for (const p of candidates) {
5062
+ if (existsSync6(resolve(p, "index.html"))) {
5063
+ this.dashboardAssetsRoot = p;
5064
+ return p;
5065
+ }
5066
+ }
5067
+ return null;
5068
+ }
4995
5069
  getPort() {
4996
5070
  return this.activePort;
4997
5071
  }
@@ -5047,7 +5121,7 @@ var HttpServer = class {
5047
5121
  } catch (err) {
5048
5122
  const isAddrInUse = err.code === "EADDRINUSE";
5049
5123
  if (isAddrInUse && attempt < maxRetries) {
5050
- console.error(`[RuntimeScope] HTTP port ${port} in use, trying ${port + 1}...`);
5124
+ safeLog.error(`[RuntimeScope] HTTP port ${port} in use, trying ${port + 1}...`);
5051
5125
  continue;
5052
5126
  }
5053
5127
  throw err;
@@ -5081,7 +5155,7 @@ var HttpServer = class {
5081
5155
  this.activePort = boundPort;
5082
5156
  this.startedAt = Date.now();
5083
5157
  const proto = tls ? "https" : "http";
5084
- console.error(`[RuntimeScope] HTTP API listening on ${proto}://${host}:${boundPort}`);
5158
+ safeLog.error(`[RuntimeScope] HTTP API listening on ${proto}://${host}:${boundPort}`);
5085
5159
  resolve2();
5086
5160
  });
5087
5161
  server.on("error", (err) => {
@@ -5113,7 +5187,7 @@ var HttpServer = class {
5113
5187
  return new Promise((resolve2) => {
5114
5188
  this.server.close(() => {
5115
5189
  this.server = null;
5116
- console.error("[RuntimeScope] HTTP API stopped");
5190
+ safeLog.error("[RuntimeScope] HTTP API stopped");
5117
5191
  resolve2();
5118
5192
  });
5119
5193
  });
@@ -5166,7 +5240,7 @@ var HttpServer = class {
5166
5240
  res.end();
5167
5241
  return;
5168
5242
  }
5169
- const isPublic = url.pathname === "/api/health" || url.pathname === "/readyz" || url.pathname === "/metrics" || url.pathname === "/runtimescope.js" || url.pathname === "/snippet";
5243
+ const isPublic = url.pathname === "/api/health" || url.pathname === "/readyz" || url.pathname === "/metrics" || url.pathname === "/runtimescope.js" || url.pathname === "/snippet" || url.pathname === "/dashboard" || url.pathname.startsWith("/dashboard/") || url.pathname.startsWith("/assets/");
5170
5244
  const workspaceKeysExist = !!this.pmStore?.hasActiveApiKeys?.();
5171
5245
  const authActive = !!this.authManager?.isEnabled() || workspaceKeysExist;
5172
5246
  const caller = {
@@ -5200,6 +5274,31 @@ var HttpServer = class {
5200
5274
  }
5201
5275
  return;
5202
5276
  }
5277
+ const isDashboardRoute = url.pathname === "/dashboard" || url.pathname.startsWith("/dashboard/");
5278
+ const isAssetRoute = url.pathname.startsWith("/assets/");
5279
+ if (req.method === "GET" && (isDashboardRoute || isAssetRoute)) {
5280
+ const assetsRoot = this.resolveDashboardAssets();
5281
+ if (!assetsRoot) {
5282
+ res.writeHead(404, { "Content-Type": "text/plain" });
5283
+ res.end("Dashboard assets not found. Run: npm run build -w packages/dashboard");
5284
+ return;
5285
+ }
5286
+ const relativePath = isAssetRoute ? url.pathname.slice(1) : url.pathname === "/dashboard" || url.pathname === "/dashboard/" ? "index.html" : url.pathname.slice("/dashboard/".length);
5287
+ const filePath = resolve(assetsRoot, relativePath);
5288
+ const hasExtension = /\.[a-zA-Z0-9]+$/.test(relativePath);
5289
+ const targetPath = existsSync6(filePath) ? filePath : hasExtension || isAssetRoute ? null : resolve(assetsRoot, "index.html");
5290
+ if (!targetPath || !existsSync6(targetPath)) {
5291
+ res.writeHead(404, { "Content-Type": "text/plain" });
5292
+ res.end("Not found");
5293
+ return;
5294
+ }
5295
+ const ext = targetPath.slice(targetPath.lastIndexOf(".")).toLowerCase();
5296
+ const contentType = ext === ".html" ? "text/html; charset=utf-8" : ext === ".js" ? "application/javascript; charset=utf-8" : ext === ".mjs" ? "application/javascript; charset=utf-8" : ext === ".css" ? "text/css; charset=utf-8" : ext === ".json" ? "application/json; charset=utf-8" : ext === ".svg" ? "image/svg+xml" : ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".ico" ? "image/x-icon" : ext === ".woff" ? "font/woff" : ext === ".woff2" ? "font/woff2" : "application/octet-stream";
5297
+ const cacheControl = targetPath.endsWith("index.html") || targetPath.endsWith("/index.html") ? "no-cache" : "public, max-age=31536000, immutable";
5298
+ res.writeHead(200, { "Content-Type": contentType, "Cache-Control": cacheControl });
5299
+ res.end(readFileSync5(targetPath));
5300
+ return;
5301
+ }
5203
5302
  if (req.method === "GET" && url.pathname === "/snippet") {
5204
5303
  const appName = (url.searchParams.get("app") || "my-app").replace(/[^a-zA-Z0-9_-]/g, "");
5205
5304
  const projectId = url.searchParams.get("project_id") || "proj_xxx";
@@ -6996,7 +7095,7 @@ var ProjectDiscovery = class {
6996
7095
  return mergeResults(claudeResult, runtimeResult);
6997
7096
  } catch (err) {
6998
7097
  const msg = err instanceof Error ? err.message : String(err);
6999
- console.error(`${LOG_PREFIX} Fatal discovery error: ${msg}`);
7098
+ safeLog.error(`${LOG_PREFIX} Fatal discovery error: ${msg}`);
7000
7099
  result.errors.push(`Fatal discovery error: ${msg}`);
7001
7100
  return result;
7002
7101
  }
@@ -7024,7 +7123,7 @@ var ProjectDiscovery = class {
7024
7123
  entries = dirEntries.filter((d) => d.isDirectory()).map((d) => d.name);
7025
7124
  } catch (err) {
7026
7125
  const msg = err instanceof Error ? err.message : String(err);
7027
- console.error(`${LOG_PREFIX} Failed to read Claude projects dir: ${msg}`);
7126
+ safeLog.error(`${LOG_PREFIX} Failed to read Claude projects dir: ${msg}`);
7028
7127
  result.errors.push(`Failed to read Claude projects dir: ${msg}`);
7029
7128
  return result;
7030
7129
  }
@@ -7033,7 +7132,7 @@ var ProjectDiscovery = class {
7033
7132
  await this.processClaudeProject(key, result);
7034
7133
  } catch (err) {
7035
7134
  const msg = err instanceof Error ? err.message : String(err);
7036
- console.error(`${LOG_PREFIX} Error processing Claude project ${key}: ${msg}`);
7135
+ safeLog.error(`${LOG_PREFIX} Error processing Claude project ${key}: ${msg}`);
7037
7136
  result.errors.push(`Claude project ${key}: ${msg}`);
7038
7137
  }
7039
7138
  }
@@ -7100,13 +7199,13 @@ var ProjectDiscovery = class {
7100
7199
  }
7101
7200
  } catch (err) {
7102
7201
  const msg = err instanceof Error ? err.message : String(err);
7103
- console.error(`${LOG_PREFIX} Error processing RuntimeScope project ${projectName}: ${msg}`);
7202
+ safeLog.error(`${LOG_PREFIX} Error processing RuntimeScope project ${projectName}: ${msg}`);
7104
7203
  result.errors.push(`RuntimeScope project ${projectName}: ${msg}`);
7105
7204
  }
7106
7205
  }
7107
7206
  } catch (err) {
7108
7207
  const msg = err instanceof Error ? err.message : String(err);
7109
- console.error(`${LOG_PREFIX} Failed to list RuntimeScope projects: ${msg}`);
7208
+ safeLog.error(`${LOG_PREFIX} Failed to list RuntimeScope projects: ${msg}`);
7110
7209
  result.errors.push(`Failed to list RuntimeScope projects: ${msg}`);
7111
7210
  }
7112
7211
  return result;
@@ -7119,7 +7218,7 @@ var ProjectDiscovery = class {
7119
7218
  const existingProjects = await this.pmStore.listProjects();
7120
7219
  const project = existingProjects.find((p) => p.id === projectId);
7121
7220
  if (!project) {
7122
- console.error(`${LOG_PREFIX} Project not found: ${projectId}`);
7221
+ safeLog.error(`${LOG_PREFIX} Project not found: ${projectId}`);
7123
7222
  return 0;
7124
7223
  }
7125
7224
  if (!project.claudeProjectKey) {
@@ -7160,12 +7259,12 @@ var ProjectDiscovery = class {
7160
7259
  sessionsIndexed++;
7161
7260
  } catch (err) {
7162
7261
  const msg = err instanceof Error ? err.message : String(err);
7163
- console.error(`${LOG_PREFIX} Error indexing session ${jsonlFile}: ${msg}`);
7262
+ safeLog.error(`${LOG_PREFIX} Error indexing session ${jsonlFile}: ${msg}`);
7164
7263
  }
7165
7264
  }
7166
7265
  } catch (err) {
7167
7266
  const msg = err instanceof Error ? err.message : String(err);
7168
- console.error(`${LOG_PREFIX} Error indexing sessions for project ${projectId}: ${msg}`);
7267
+ safeLog.error(`${LOG_PREFIX} Error indexing sessions for project ${projectId}: ${msg}`);
7169
7268
  }
7170
7269
  return sessionsIndexed;
7171
7270
  }
@@ -7283,12 +7382,12 @@ var ProjectDiscovery = class {
7283
7382
  }
7284
7383
  } catch (err) {
7285
7384
  const msg = err instanceof Error ? err.message : String(err);
7286
- console.error(`${LOG_PREFIX} Error indexing session ${jsonlFile} in ${claudeKey}: ${msg}`);
7385
+ safeLog.error(`${LOG_PREFIX} Error indexing session ${jsonlFile} in ${claudeKey}: ${msg}`);
7287
7386
  }
7288
7387
  }
7289
7388
  } catch (err) {
7290
7389
  const msg = err instanceof Error ? err.message : String(err);
7291
- console.error(`${LOG_PREFIX} Error scanning sessions for ${claudeKey}: ${msg}`);
7390
+ safeLog.error(`${LOG_PREFIX} Error scanning sessions for ${claudeKey}: ${msg}`);
7292
7391
  }
7293
7392
  return counts;
7294
7393
  }
@@ -7382,7 +7481,7 @@ var ProjectDiscovery = class {
7382
7481
  };
7383
7482
  } catch (err) {
7384
7483
  const msg = err instanceof Error ? err.message : String(err);
7385
- console.error(`${LOG_PREFIX} Failed to parse session ${sessionId}: ${msg}`);
7484
+ safeLog.error(`${LOG_PREFIX} Failed to parse session ${sessionId}: ${msg}`);
7386
7485
  return {
7387
7486
  id: sessionId,
7388
7487
  projectId,
@@ -7432,7 +7531,7 @@ var ProjectDiscovery = class {
7432
7531
  await this.pmStore.upsertCapexEntry(entry);
7433
7532
  } catch (err) {
7434
7533
  const msg = err instanceof Error ? err.message : String(err);
7435
- console.error(`${LOG_PREFIX} Failed to create CapEx stub for session ${session.id}: ${msg}`);
7534
+ safeLog.error(`${LOG_PREFIX} Failed to create CapEx stub for session ${session.id}: ${msg}`);
7436
7535
  }
7437
7536
  }
7438
7537
  };
@@ -7482,4 +7581,4 @@ export {
7482
7581
  parseSessionJsonl,
7483
7582
  ProjectDiscovery
7484
7583
  };
7485
- //# sourceMappingURL=chunk-TT3VVKUE.js.map
7584
+ //# sourceMappingURL=chunk-VNRQCY6B.js.map