monomind 2.0.3 → 2.1.3

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 (113) hide show
  1. package/README.md +74 -92
  2. package/package.json +11 -7
  3. package/packages/@monomind/cli/.claude/helpers/control-start.cjs +13 -20
  4. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
  5. package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
  6. package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
  7. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +99 -28
  8. package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
  9. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +18 -9
  10. package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
  11. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +33 -4
  12. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +75 -45
  13. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
  14. package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
  15. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +75 -4
  16. package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
  17. package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
  18. package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
  19. package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
  20. package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
  21. package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
  22. package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
  23. package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +73 -2
  24. package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
  25. package/packages/@monomind/cli/README.md +74 -92
  26. package/packages/@monomind/cli/bin/cli.js +10 -1
  27. package/packages/@monomind/cli/dist/src/browser/workflow/store.d.ts +2 -2
  28. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
  29. package/packages/@monomind/cli/dist/src/commands/browse.d.ts +1 -1
  30. package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
  31. package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
  32. package/packages/@monomind/cli/dist/src/commands/doctor-env-checks.js +5 -0
  33. package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
  34. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
  35. package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
  36. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
  37. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
  38. package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
  39. package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
  40. package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
  41. package/packages/@monomind/cli/dist/src/commands/org.js +321 -127
  42. package/packages/@monomind/cli/dist/src/commands/platforms.d.ts +1 -1
  43. package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
  44. package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
  45. package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
  46. package/packages/@monomind/cli/dist/src/index.js +64 -28
  47. package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
  48. package/packages/@monomind/cli/dist/src/init/executor.js +93 -55
  49. package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
  50. package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
  51. package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
  52. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
  53. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
  54. package/packages/@monomind/cli/dist/src/mcp-tools/auto-install.d.ts +8 -8
  55. package/packages/@monomind/cli/dist/src/mcp-tools/coherence/types.d.ts +31 -31
  56. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
  57. package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
  58. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +56 -35
  59. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
  60. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
  61. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +88 -41
  62. package/packages/@monomind/cli/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +36 -36
  63. package/packages/@monomind/cli/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
  64. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
  65. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
  66. package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
  67. package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
  68. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.d.ts +12 -0
  69. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
  70. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
  71. package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
  72. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
  73. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
  74. package/packages/@monomind/cli/dist/src/memory/sona-optimizer.d.ts +12 -0
  75. package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +26 -0
  76. package/packages/@monomind/cli/dist/src/orgrt/broker.js +73 -0
  77. package/packages/@monomind/cli/dist/src/orgrt/bus.d.ts +5 -1
  78. package/packages/@monomind/cli/dist/src/orgrt/bus.js +5 -1
  79. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +46 -0
  80. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +201 -18
  81. package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
  82. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +138 -8
  83. package/packages/@monomind/cli/dist/src/orgrt/inbox.d.ts +11 -0
  84. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +56 -0
  85. package/packages/@monomind/cli/dist/src/orgrt/policy.d.ts +5 -1
  86. package/packages/@monomind/cli/dist/src/orgrt/policy.js +58 -5
  87. package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
  88. package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
  89. package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
  90. package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +9 -0
  91. package/packages/@monomind/cli/dist/src/orgrt/server.js +39 -0
  92. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +10 -1
  93. package/packages/@monomind/cli/dist/src/orgrt/session.js +24 -1
  94. package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
  95. package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
  96. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +52 -52
  97. package/packages/@monomind/cli/dist/src/output.d.ts +29 -29
  98. package/packages/@monomind/cli/dist/src/output.js +22 -7
  99. package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
  100. package/packages/@monomind/cli/dist/src/parser.js +130 -5
  101. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
  102. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
  103. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
  104. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
  105. package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
  106. package/packages/@monomind/cli/dist/src/types.d.ts +2 -2
  107. package/packages/@monomind/cli/dist/src/ui/dashboard.html +243 -49
  108. package/packages/@monomind/cli/dist/src/ui/orgs-files.js +192 -0
  109. package/packages/@monomind/cli/dist/src/ui/orgs.html +79 -11
  110. package/packages/@monomind/cli/dist/src/ui/server.mjs +346 -139
  111. package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
  112. package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
  113. package/packages/@monomind/cli/package.json +15 -15
@@ -2,12 +2,16 @@ import http from 'http';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
+ import crypto from 'crypto';
5
6
  import { fileURLToPath } from 'url';
6
7
  import { createRequire } from 'module';
7
8
  import { collectAll, getWatchPaths, collectProject, collectSessions, collectSwarm, collectSwarmHistory, appendSwarmHistory, collectSwarmEvents, getSwarmDataSize, cleanSwarmData, collectAgents, collectTokens, collectHooks, collectKnowledge, collectMetrics, collectMemory, collectMemoryFiles, collectSystem } from './collector.mjs';
8
9
  import { addSseClient, removeSseClient, broadcast, getSseClientCount, closeSseClients, addMmClient, removeMmClient, broadcastMm, getMmClientCount } from './sse-manager.mjs';
9
10
 
10
11
  const JSONL_SIZE_CAP = 10 * 1024 * 1024; // 10 MB — skip files larger than this in /api/graph
12
+ // Session id format for data/sessions/<id>.jsonl persistence — no path traversal (".."), starts
13
+ // with a word char, rest is word chars/dot/dash. Shared by every session-id-accepting endpoint.
14
+ const SESSION_ID_RE = /^(?!.*\.\.)[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/;
11
15
  const buildDocsState = new Map();
12
16
 
13
17
  // Pricing per token (mirrors token-tracker.cjs FALLBACK_PRICING)
@@ -242,12 +246,77 @@ async function _initRunDb(monoHome) {
242
246
  }
243
247
  }
244
248
 
249
+ // Single-writer guard for the run-events.db snapshot persist: bindServer() can start a
250
+ // SECOND dashboard instance on port+1 when 4242 is already taken (two projects' dashboards
251
+ // running simultaneously, or a stale-but-still-bound old instance). Each instance holds an
252
+ // independent in-memory sql.js copy; without a lock, each instance's periodic full-snapshot
253
+ // overwrite silently clobbers the other's accumulated events (last writer wins, no error).
254
+ // Claiming this lock before every snapshot write ensures only ONE live instance actually
255
+ // persists at a time — the other still runs, just skips its write until it can claim the
256
+ // lock (e.g. after the current holder exits and its lock goes stale). This doesn't merge
257
+ // both instances' events into one file, but it stops them from destructively overwriting
258
+ // each other. Pattern mirrors .claude/helpers/utils/fs-helpers.cjs's claimLock/releaseLock
259
+ // (wx-create, rename-to-reclaim-stale) — replicated inline here since this file ships
260
+ // standalone in the published package and can't depend on repo-local dev tooling.
261
+ function _runDbLockPath() {
262
+ return _runDbPath ? `${_runDbPath}.lock` : null;
263
+ }
264
+
265
+ function _claimRunDbLock(staleMs) {
266
+ const lockPath = _runDbLockPath();
267
+ if (!lockPath) return false;
268
+ staleMs = staleMs || 5000;
269
+ const tryCreate = () => {
270
+ try {
271
+ fs.writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
272
+ return true;
273
+ } catch (_) {
274
+ return false;
275
+ }
276
+ };
277
+ if (tryCreate()) return true;
278
+ try {
279
+ const stat = fs.statSync(lockPath);
280
+ if (Date.now() - stat.mtimeMs < staleMs) return false; // held by a live/fresh owner
281
+ // Stale lock — reclaim via atomic rename so only one racing process wins it.
282
+ const claimed = `${lockPath}.${process.pid}.${Date.now()}.stale`;
283
+ try {
284
+ fs.renameSync(lockPath, claimed);
285
+ } catch (_) {
286
+ return false; // another process already reclaimed it
287
+ }
288
+ try { fs.unlinkSync(claimed); } catch (_) {}
289
+ return tryCreate();
290
+ } catch (_) {
291
+ // Lock vanished between our failed create and this stat — retry once.
292
+ return tryCreate();
293
+ }
294
+ }
295
+
296
+ function _releaseRunDbLock() {
297
+ const lockPath = _runDbLockPath();
298
+ if (!lockPath) return;
299
+ try {
300
+ if (Number(fs.readFileSync(lockPath, 'utf8')) === process.pid) fs.unlinkSync(lockPath);
301
+ } catch (_) {}
302
+ }
303
+
304
+ function _writeRunDbSnapshot() {
305
+ if (!_runDb || !_runDbPath) return;
306
+ if (!_claimRunDbLock()) return; // another live instance owns the write right now
307
+ try {
308
+ fs.writeFileSync(_runDbPath, Buffer.from(_runDb.export()));
309
+ } catch (_) {
310
+ // best-effort — matches prior swallow-on-error behavior
311
+ } finally {
312
+ _releaseRunDbLock();
313
+ }
314
+ }
315
+
245
316
  function _persistRunDb() {
246
317
  if (!_runDb || !_runDbPath) return;
247
318
  clearTimeout(_runDbPersistTimer);
248
- _runDbPersistTimer = setTimeout(() => {
249
- try { fs.writeFileSync(_runDbPath, Buffer.from(_runDb.export())); } catch (_) {}
250
- }, 1000);
319
+ _runDbPersistTimer = setTimeout(_writeRunDbSnapshot, 1000);
251
320
  }
252
321
 
253
322
  function _insertRunEvent(ev, source) {
@@ -266,6 +335,50 @@ function _insertRunEvent(ev, source) {
266
335
  }
267
336
  // ─────────────────────────────────────────────────────────────────────────────
268
337
 
338
+ // ── Bounded file reads for session JSONL (P3-6b) ────────────────────────────
339
+ // /api/session already caps memory correctly by tailing; /api/session-journal and
340
+ // /api/session-errors used to fs.readFileSync() whole session files regardless of
341
+ // size, so one request against a multi-hundred-MB session log (a real scenario for
342
+ // long-running org sessions) buffers the entire file. These helpers cap the read to
343
+ // a fixed byte window via openSync/readSync (mirrors the byte-offset tail-read the
344
+ // /api/loops handler already uses for ScheduleWakeup lookups) instead of loading the
345
+ // whole file.
346
+ function _readTailLines(filePath, maxBytes) {
347
+ maxBytes = maxBytes || 4 * 1024 * 1024; // 4MB — generous for the most recent activity
348
+ const stat = fs.statSync(filePath);
349
+ const start = Math.max(0, stat.size - maxBytes);
350
+ const len = stat.size - start;
351
+ const buf = Buffer.alloc(len);
352
+ const fd = fs.openSync(filePath, 'r');
353
+ try {
354
+ fs.readSync(fd, buf, 0, len, start);
355
+ } finally {
356
+ fs.closeSync(fd);
357
+ }
358
+ let text = buf.toString('utf8');
359
+ if (start > 0) {
360
+ // We started mid-file — the first line is very likely a partial line, drop it.
361
+ const nl = text.indexOf('\n');
362
+ text = nl === -1 ? '' : text.slice(nl + 1);
363
+ }
364
+ return text.split('\n').filter(Boolean);
365
+ }
366
+
367
+ function _readHeadText(filePath, maxBytes) {
368
+ maxBytes = maxBytes || 8192; // enough for the JSONL header line(s) we need to inspect
369
+ const stat = fs.statSync(filePath);
370
+ const len = Math.min(maxBytes, stat.size);
371
+ const buf = Buffer.alloc(len);
372
+ const fd = fs.openSync(filePath, 'r');
373
+ try {
374
+ fs.readSync(fd, buf, 0, len, 0);
375
+ } finally {
376
+ fs.closeSync(fd);
377
+ }
378
+ return buf.toString('utf8');
379
+ }
380
+ // ─────────────────────────────────────────────────────────────────────────────
381
+
269
382
  function appendToFile(filePath, line) {
270
383
  const prev = _writeQueue.get(filePath) || Promise.resolve();
271
384
  const next = prev.then(() => {
@@ -466,7 +579,7 @@ function bindServer(server, port) {
466
579
  let attempt = 0;
467
580
 
468
581
  function tryPort(p) {
469
- server.listen(p, () => resolve(p));
582
+ server.listen(p, '127.0.0.1', () => resolve(p));
470
583
  server.once('error', (err) => {
471
584
  if (err.code === 'EADDRINUSE' && attempt < maxTries) {
472
585
  attempt += 1;
@@ -498,10 +611,26 @@ function bindServer(server, port) {
498
611
  * uses a greedy BFS over the real filesystem to find the correct path.
499
612
  * Falls back to cwd in session files, then to direct slug replacement.
500
613
  */
614
+ const _slugPathCache = new Map();
615
+
501
616
  function resolveSlugToPath(slug, projDir) {
502
- // 1. Try filesystem BFS (most reliable)
617
+ if (_slugPathCache.has(slug)) return _slugPathCache.get(slug);
618
+ const resolved = _resolveSlugToPathUncached(slug, projDir);
619
+ _slugPathCache.set(slug, resolved);
620
+ return resolved;
621
+ }
622
+
623
+ function _resolveSlugToPathUncached(slug, projDir) {
624
+ // 1. Try filesystem BFS (most reliable). Branching is O(2^hyphens) in the
625
+ // worst case (a slug with N hyphens can require exploring both "new path
626
+ // segment" and "hyphen is part of this segment's name" at each of the N
627
+ // positions) — bound total exploration so a slug with many hyphens (e.g.
628
+ // a UUID-embedded temp/scratchpad dir) can't hang the event loop.
503
629
  const parts = slug.replace(/^-/, '').split('-');
630
+ const MAX_CALLS = 20000;
631
+ let calls = 0;
504
632
  function tryPaths(idx, current) {
633
+ if (++calls > MAX_CALLS) return null;
505
634
  if (idx === parts.length) return fs.existsSync(current) ? current : null;
506
635
  // Option A: next part is a new path component
507
636
  const asDir = path.join(current, parts[idx]);
@@ -534,6 +663,18 @@ function resolveSlugToPath(slug, projDir) {
534
663
  }
535
664
 
536
665
  export async function startServer({ port = 4242, projectDir, openBrowser = true } = {}) {
666
+ // ── Security: per-process auth credential for mutating (non-GET) requests ─
667
+ // Generated once per server start and written to a well-known location so
668
+ // trusted local callers (CLI, control-start.cjs) can read it and pass it
669
+ // back via an auth header or query param on non-GET requests.
670
+ const authBytes = crypto.randomBytes(24);
671
+ const dashboardAuthValue = authBytes.toString('hex');
672
+ try {
673
+ const authFileDir = path.join(projectDir || process.cwd(), '.monomind');
674
+ fs.mkdirSync(authFileDir, { recursive: true });
675
+ fs.writeFileSync(path.join(authFileDir, 'dashboard-token'), dashboardAuthValue, { mode: 0o600 });
676
+ } catch (_) {}
677
+
537
678
  // Parse a .claude/agents/*.md definition into { name, description, capability{}, document }.
538
679
  // Tolerant line-based parse of the YAML frontmatter (expertise / task_types as lists).
539
680
  function parseAgentDef(raw) {
@@ -568,7 +709,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
568
709
  // Extracted from the request dispatcher to reduce cyclomatic complexity.
569
710
  // Handles POST /api/mastermind/event: parses body, enriches with runId/session,
570
711
  // persists to JSONL files, broadcasts to SSE clients, returns {ok:true}.
571
- async function handleMastermindEvent(req, res) {
712
+ async function handleMastermindEvent(req, res, corsOrigin) {
572
713
  let body = '';
573
714
  for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
574
715
  let event = {};
@@ -765,7 +906,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
765
906
  // Format: data/sessions/<sessionId>.jsonl + data/sessions/_index.json
766
907
  try {
767
908
  const _sid = String(event.session || '').trim();
768
- if (_sid.length > 0 && _sid.length <= 128 && /^(?!.*\.\.)[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(_sid)) {
909
+ if (_sid.length > 0 && _sid.length <= 128 && SESSION_ID_RE.test(_sid)) {
769
910
  const sessDir = path.join(dataDir, 'sessions');
770
911
  fs.mkdirSync(sessDir, { recursive: true });
771
912
  // Append event to per-session JSONL (O(1), no read)
@@ -845,13 +986,46 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
845
986
  }
846
987
  }
847
988
  }
848
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
989
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
849
990
  res.end('{"ok":true}');
850
991
  }
851
992
 
852
993
  const server = http.createServer(async (req, res) => {
853
994
  const url = req.url.split('?')[0];
854
995
 
996
+ // ── Security: strict CORS allow-list ────────────────────────────────────
997
+ // Only reflect Origin when it is this dashboard's own loopback origin.
998
+ // Otherwise the header is omitted entirely, so cross-origin reads fail
999
+ // closed rather than defaulting to '*'.
1000
+ const _reqOrigin = req.headers.origin || '';
1001
+ const _boundPortForCors = currentPort || boundPort || port;
1002
+ const _allowedOrigins = new Set([
1003
+ `http://localhost:${_boundPortForCors}`,
1004
+ `http://127.0.0.1:${_boundPortForCors}`,
1005
+ ]);
1006
+ const corsOrigin = _allowedOrigins.has(_reqOrigin) ? _reqOrigin : undefined;
1007
+
1008
+ // ── Security: require a matching auth credential on all non-GET routes ──
1009
+ // GET requests remain open (read-only loopback dashboard viewing).
1010
+ if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') {
1011
+ let _suppliedAuth = req.headers['x-monomind-token'] || '';
1012
+ if (!_suppliedAuth) {
1013
+ try { _suppliedAuth = new URL(req.url, 'http://localhost').searchParams.get('token') || ''; } catch (_) {}
1014
+ }
1015
+ const _suppliedAuthBuf = Buffer.from(String(_suppliedAuth));
1016
+ const _expectedAuthBuf = Buffer.from(dashboardAuthValue);
1017
+ const _authMatches = _suppliedAuthBuf.length === _expectedAuthBuf.length
1018
+ && crypto.timingSafeEqual(_suppliedAuthBuf, _expectedAuthBuf);
1019
+ if (!_suppliedAuth || !_authMatches) {
1020
+ res.writeHead(401, {
1021
+ 'Content-Type': 'application/json',
1022
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1023
+ });
1024
+ res.end(JSON.stringify({ error: 'Unauthorized: missing or invalid auth token' }));
1025
+ return;
1026
+ }
1027
+ }
1028
+
855
1029
  // ------------------------------------------------------------------ GET /
856
1030
  if (req.method === 'GET' && url === '/') {
857
1031
  const htmlPath = path.join(__dirname, 'dashboard.html');
@@ -905,11 +1079,11 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
905
1079
  }
906
1080
  let branch = '';
907
1081
  try { branch = gitExec('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim(); } catch {}
908
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1082
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
909
1083
  res.end(JSON.stringify({ name, email, cwd, remoteUrl, branch }));
910
1084
  } catch (_) {
911
1085
  const cwd2 = projectDir || process.cwd();
912
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1086
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
913
1087
  res.end(JSON.stringify({ name: '', email: '', cwd: cwd2, remoteUrl: '', branch: '' }));
914
1088
  }
915
1089
  return;
@@ -923,7 +1097,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
923
1097
  const snapshot = await collectAll(dir);
924
1098
  res.writeHead(200, {
925
1099
  'Content-Type': 'application/json',
926
- 'Access-Control-Allow-Origin': '*',
1100
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
927
1101
  'Cache-Control': 'no-cache',
928
1102
  });
929
1103
  res.end(JSON.stringify(snapshot));
@@ -950,7 +1124,11 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
950
1124
  // system files to any process that can reach localhost:4242.
951
1125
  const _resolvedFile = path.resolve(file);
952
1126
  const _homeDir = os.homedir();
953
- if (!_resolvedFile.startsWith(_homeDir + path.sep) && !_resolvedFile.startsWith(_homeDir)) {
1127
+ // Bare-prefix check (no path.sep) is bypassable: with home "/Users/bob", a path
1128
+ // like "/Users/bobsecrets/x.jsonl" starts with the literal string "/Users/bob"
1129
+ // even though it's a sibling directory, not a subdirectory of home. Require the
1130
+ // separator (or exact equality to the home dir itself) so only true descendants pass.
1131
+ if (_resolvedFile !== _homeDir && !_resolvedFile.startsWith(_homeDir + path.sep)) {
954
1132
  res.writeHead(403, { 'Content-Type': 'application/json' });
955
1133
  res.end(JSON.stringify({ error: 'Access denied: file must be within the home directory' }));
956
1134
  return;
@@ -967,7 +1145,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
967
1145
  const events = parseSessionLines(lines);
968
1146
  res.writeHead(200, {
969
1147
  'Content-Type': 'application/json',
970
- 'Access-Control-Allow-Origin': '*',
1148
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
971
1149
  'Cache-Control': 'no-cache',
972
1150
  });
973
1151
  res.end(JSON.stringify({ events, total: allLines.length, shown: lines.length }));
@@ -1005,7 +1183,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1005
1183
  const modelBreakdown = {};
1006
1184
  const filesTouchedSet = new Set();
1007
1185
  try {
1008
- const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean);
1186
+ const lines = _readTailLines(fp);
1009
1187
  let pendingCompact = false;
1010
1188
  for (const line of lines) {
1011
1189
  let e; try { e = JSON.parse(line); } catch { continue; }
@@ -1062,7 +1240,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1062
1240
  const summary = summaries.length ? summaries[summaries.length - 1].text : null;
1063
1241
  sessions.push({ id, mtime, firstTs, lastTs, lastPrompt, summaries, summary, compactCount, errorCount, totalDurationMs, totalMessages, totalCost, toolCalls, userMessages, cacheReadTokens, totalInputTokens, modelBreakdown, filesTouched, file: fp });
1064
1242
  }
1065
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1243
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1066
1244
  res.end(JSON.stringify({ sessions }));
1067
1245
  } catch (err) {
1068
1246
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1077,7 +1255,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1077
1255
  const dir = qs.get('dir') || '';
1078
1256
  const q = (qs.get('q') || '').toLowerCase().trim();
1079
1257
  if (!q || q.length < 2) {
1080
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1258
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1081
1259
  res.end(JSON.stringify({ results: [] }));
1082
1260
  return;
1083
1261
  }
@@ -1118,7 +1296,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1118
1296
  } catch {}
1119
1297
  if (matches.length) results.push({ id, file: fp, lastPrompt, mtime, matches });
1120
1298
  }
1121
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1299
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1122
1300
  res.end(JSON.stringify({ results, q }));
1123
1301
  } catch (err) {
1124
1302
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1181,7 +1359,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1181
1359
  return tb - ta;
1182
1360
  });
1183
1361
 
1184
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1362
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1185
1363
  res.end(JSON.stringify({ events: events.slice(0, limit) }));
1186
1364
  } catch (err) {
1187
1365
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1233,7 +1411,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1233
1411
  const errors = Object.entries(errorCounts)
1234
1412
  .map(([tool, count]) => ({ tool, count, total: totalCounts[tool] || count }))
1235
1413
  .sort((a,b) => b.count - a.count);
1236
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1414
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1237
1415
  res.end(JSON.stringify({ errors }));
1238
1416
  } catch (err) {
1239
1417
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1284,7 +1462,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1284
1462
  const tools = Object.entries(toolCounts)
1285
1463
  .map(([tool, count]) => ({ tool, count, errors: errorCounts[tool] || 0 }))
1286
1464
  .sort((a,b) => b.count - a.count);
1287
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1465
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1288
1466
  res.end(JSON.stringify({ tools }));
1289
1467
  } catch (err) {
1290
1468
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1319,7 +1497,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1319
1497
  if (totalCost > 0) projectCosts.push({ path: projPath, cost: totalCost, sessions: sessionFiles.length });
1320
1498
  }
1321
1499
  projectCosts.sort((a, b) => b.cost - a.cost);
1322
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1500
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1323
1501
  res.end(JSON.stringify({ projects: projectCosts }));
1324
1502
  } catch (err) {
1325
1503
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1352,7 +1530,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1352
1530
  } catch {}
1353
1531
  return { slug, path: projPath, name, sessionCount, memoryCount, lastActivity: lastActivity || null };
1354
1532
  }).filter(p => p.sessionCount > 0 || fs.existsSync(path.join(p.path, '.monomind'))).sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0));
1355
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1533
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1356
1534
  res.end(JSON.stringify({ projects }));
1357
1535
  } catch (err) {
1358
1536
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1381,7 +1559,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1381
1559
  let kg = [];
1382
1560
  try { const raw = fs.readFileSync(path.join(palaceDir, 'kg.json'), 'utf8'); kg = JSON.parse(raw); } catch {}
1383
1561
 
1384
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1562
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1385
1563
  res.end(JSON.stringify({ drawers, identity, kg }));
1386
1564
  } catch (err) {
1387
1565
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1432,7 +1610,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1432
1610
  }
1433
1611
  }
1434
1612
 
1435
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1613
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1436
1614
  res.end(JSON.stringify({ adrs }));
1437
1615
  } catch (err) {
1438
1616
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1501,7 +1679,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1501
1679
  }
1502
1680
  } catch {}
1503
1681
 
1504
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1682
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1505
1683
  res.end(JSON.stringify({ memories: memories.concat(backend), memDir }));
1506
1684
  } catch (err) {
1507
1685
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1534,7 +1712,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1534
1712
  }
1535
1713
  fs.mkdirSync(memDir, { recursive: true });
1536
1714
  fs.writeFileSync(fp, content || '', 'utf8');
1537
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1715
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1538
1716
  res.end(JSON.stringify({ ok: true }));
1539
1717
  } catch (err) {
1540
1718
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1567,7 +1745,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1567
1745
  return;
1568
1746
  }
1569
1747
  fs.unlinkSync(fp);
1570
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1748
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1571
1749
  res.end(JSON.stringify({ ok: true }));
1572
1750
  } catch (err) {
1573
1751
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1588,7 +1766,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1588
1766
  const raw = fs.readFileSync(feedbackPath, 'utf-8');
1589
1767
  rows = raw.split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
1590
1768
  }
1591
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1769
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1592
1770
  res.end(JSON.stringify(rows));
1593
1771
  } catch (err) {
1594
1772
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1663,7 +1841,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1663
1841
  lastWrite,
1664
1842
  memDir,
1665
1843
  };
1666
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1844
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1667
1845
  res.end(JSON.stringify({ stats }));
1668
1846
  } catch (err) {
1669
1847
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1792,7 +1970,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1792
1970
  if (hasRepeatLoops) loops = loops.filter(l => l.source !== 'scheduled_tasks_lock' && l.source !== 'schedule_wakeup_hook');
1793
1971
 
1794
1972
  loops.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
1795
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
1973
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
1796
1974
  res.end(JSON.stringify({ loops }));
1797
1975
  } catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message })); }
1798
1976
  return;
@@ -1811,7 +1989,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1811
1989
  const loopsDir = path.join(_stopDir, '.monomind', 'loops');
1812
1990
  fs.mkdirSync(loopsDir, { recursive: true });
1813
1991
  fs.writeFileSync(path.join(loopsDir, `${id}.stop`), `stop-requested-${Date.now()}`);
1814
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1992
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1815
1993
  res.end(JSON.stringify({ ok: true }));
1816
1994
  } catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message })); }
1817
1995
  });
@@ -1843,7 +2021,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1843
2021
  const nowMs = Date.now();
1844
2022
  const loop = { id, type: 'repeat', name: name || prompt.slice(0, 40), prompt, interval: interval || '1h', maxReps, status: 'active', currentRep: 0, startedAt: nowMs, lastRunAt: null };
1845
2023
  fs.writeFileSync(path.join(loopsDir, `${id}.json`), JSON.stringify(loop, null, 2));
1846
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2024
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1847
2025
  res.end(JSON.stringify({ ok: true, id }));
1848
2026
  } catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message })); }
1849
2027
  });
@@ -1868,18 +2046,20 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1868
2046
  if (f.includes(sessionId) || sessionId === f.replace('.jsonl', '')) { fp = path.join(projectClaudeDir, f); break; }
1869
2047
  }
1870
2048
  if (!fp) {
1871
- // fallback: find by scanning
2049
+ // fallback: find by scanning — the sessionId lives in the first JSONL line, so a
2050
+ // small head-read is enough; no need to load the whole (potentially huge) file.
1872
2051
  for (const f of files) {
1873
- const raw = fs.readFileSync(path.join(projectClaudeDir, f), 'utf8');
1874
- const lines = raw.trim().split('\n').filter(Boolean);
1875
- if (lines.length > 0) {
1876
- try { const first = JSON.parse(lines[0]); if (first.sessionId === sessionId) { fp = path.join(projectClaudeDir, f); break; } } catch {}
2052
+ const head = _readHeadText(path.join(projectClaudeDir, f));
2053
+ const firstLine = head.split('\n', 1)[0];
2054
+ if (firstLine) {
2055
+ try { const first = JSON.parse(firstLine); if (first.sessionId === sessionId) { fp = path.join(projectClaudeDir, f); break; } } catch {}
1877
2056
  }
1878
2057
  }
1879
2058
  }
1880
2059
  if (!fp) { res.writeHead(404); res.end(JSON.stringify({ errors: [] })); return; }
1881
- const raw = fs.readFileSync(fp, 'utf8');
1882
- const lines = raw.trim().split('\n').filter(Boolean);
2060
+ // Only the most recent portion of the file matters for a dashboard error feed —
2061
+ // tail-cap the read so a multi-hundred-MB session log doesn't get buffered whole.
2062
+ const lines = _readTailLines(fp);
1883
2063
  const errors = [];
1884
2064
  for (const line of lines) {
1885
2065
  try {
@@ -1894,7 +2074,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1894
2074
  }
1895
2075
  } catch {}
1896
2076
  }
1897
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2077
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1898
2078
  res.end(JSON.stringify({ errors: errors.slice(0, 50) }));
1899
2079
  } catch (err) { res.writeHead(500); res.end(JSON.stringify({ errors: [], error: err.message })); }
1900
2080
  return;
@@ -1910,7 +2090,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1910
2090
  'Content-Type': 'text/event-stream',
1911
2091
  'Cache-Control': 'no-cache',
1912
2092
  'Connection': 'keep-alive',
1913
- 'Access-Control-Allow-Origin': '*',
2093
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1914
2094
  });
1915
2095
  const send = (ev, data) => { try { res.write(`event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`); } catch {} };
1916
2096
  send('connected', { ts: Date.now() });
@@ -1954,7 +2134,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1954
2134
  return;
1955
2135
  }
1956
2136
  fs.writeFileSync(chunksFile, filtered.map(e => JSON.stringify(e)).join('\n') + (filtered.length ? '\n' : ''), 'utf8');
1957
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2137
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1958
2138
  res.end(JSON.stringify({ ok: true, removed: before - filtered.length }));
1959
2139
  } catch (err) {
1960
2140
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1993,7 +2173,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1993
2173
  }
1994
2174
  entries[idx] = { ...entries[idx], text };
1995
2175
  fs.writeFileSync(chunksFile, entries.map(e => JSON.stringify(e)).join('\n') + '\n', 'utf8');
1996
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2176
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
1997
2177
  res.end(JSON.stringify({ ok: true }));
1998
2178
  } catch (err) {
1999
2179
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -2037,9 +2217,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
2037
2217
  } finally { closeDb(db); }
2038
2218
  } catch {
2039
2219
  // Fallback: sqlite3 CLI + inline Sigma.js graph
2040
- const { execSync } = await import('child_process');
2220
+ const { execFileSync } = await import('child_process');
2041
2221
  const runSql = (sql) => {
2042
- try { return JSON.parse(execSync(`sqlite3 -json "${dbPath}"`, { encoding: 'utf-8', timeout: 15000, maxBuffer: 50*1024*1024, input: sql + ';' }) || '[]'); } catch { return []; }
2222
+ try { return JSON.parse(execFileSync('sqlite3', ['-json', dbPath], { encoding: 'utf-8', timeout: 15000, maxBuffer: 50*1024*1024, input: sql + ';' }) || '[]'); } catch { return []; }
2043
2223
  };
2044
2224
  const rawNodes = runSql('SELECT id, name, label, file_path, community_id FROM nodes LIMIT 2000');
2045
2225
  const rawEdges = runSql('SELECT source_id, target_id, relation FROM edges');
@@ -2070,7 +2250,7 @@ ${edgesJson}.forEach(e=>{try{g.addEdge(e.source,e.target,{size:0.5,color:'#333'}
2070
2250
  new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{color:'#ccc'},labelSize:10});
2071
2251
  <\/script></body></html>`;
2072
2252
  }
2073
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
2253
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
2074
2254
  res.end(html);
2075
2255
  return;
2076
2256
  }
@@ -2078,7 +2258,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2078
2258
  // Fallback: try legacy graph.html on disk
2079
2259
  const htmlPath = path.join(d, '.monomind', 'graph', 'graph.html');
2080
2260
  const html = fs.readFileSync(htmlPath, 'utf-8');
2081
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
2261
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
2082
2262
  res.end(html);
2083
2263
  } catch (err) {
2084
2264
  res.writeHead(404, { 'Content-Type': 'text/html' });
@@ -2122,7 +2302,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2122
2302
  ].join('\n');
2123
2303
  } finally { closeDb(db); }
2124
2304
  }
2125
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
2305
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
2126
2306
  res.end(JSON.stringify({ exists, report, stats }));
2127
2307
  } catch (err) {
2128
2308
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -2140,10 +2320,10 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2140
2320
  const dbPath = path.join(d, '.monomind', 'monograph.db');
2141
2321
  let nodes = [], edges = [];
2142
2322
  if (fs.existsSync(dbPath)) {
2143
- const { execSync } = await import('child_process');
2323
+ const { execFileSync } = await import('child_process');
2144
2324
  const runSql = (sql, timeout = 10000) => {
2145
2325
  try {
2146
- return JSON.parse(execSync(`sqlite3 -json "${dbPath}"`,
2326
+ return JSON.parse(execFileSync('sqlite3', ['-json', dbPath],
2147
2327
  { encoding: 'utf-8', timeout, maxBuffer: 50 * 1024 * 1024, input: sql + ';' }) || '[]');
2148
2328
  } catch { return []; }
2149
2329
  };
@@ -2166,7 +2346,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2166
2346
  nodes = topNodes.map(n => ({ id: n.id, label: n.name || n.id, type: n.label || 'unknown', degree: degree.get(n.id) || 0 }));
2167
2347
  edges = rawEdges.filter(e => topIds.has(e.source_id) && topIds.has(e.target_id)).slice(0, 2000).map(e => ({ source: e.source_id, target: e.target_id, relation: e.relation || 'REF' }));
2168
2348
  }
2169
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
2349
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
2170
2350
  res.end(JSON.stringify({ nodes, edges }));
2171
2351
  } catch (err) {
2172
2352
  res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
@@ -2195,7 +2375,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2195
2375
  const importScript = path.join(process.cwd(), 'scripts', 'ua-import.mjs');
2196
2376
  const enrichScript = path.join(process.cwd(), 'scripts', 'ua-enrich.mjs');
2197
2377
 
2198
- res.writeHead(202, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2378
+ res.writeHead(202, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2199
2379
 
2200
2380
  if (uaGraph && fs.existsSync(importScript)) {
2201
2381
  res.end(JSON.stringify({ status: 'importing', source: uaGraph }));
@@ -2224,7 +2404,19 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2224
2404
  const dir = qs.get('dir') || projectDir || process.cwd();
2225
2405
  const d = path.resolve(dir || process.cwd());
2226
2406
 
2227
- res.writeHead(202, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2407
+ // Security: this route spawns `node --eval` with `cwd: d`, which lets
2408
+ // Node resolve '@monoes/monograph' against d's node_modules. Only ever
2409
+ // allow this for the server's own project root — never an
2410
+ // attacker-controlled `?dir=` — since a planted node_modules there
2411
+ // would achieve RCE. (See P0-6.)
2412
+ const _serverRoot = path.resolve(projectDir || process.cwd());
2413
+ if (d !== _serverRoot) {
2414
+ res.writeHead(400, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2415
+ res.end(JSON.stringify({ error: 'monograph-build only supports the server project root' }));
2416
+ return;
2417
+ }
2418
+
2419
+ res.writeHead(202, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2228
2420
  res.end(JSON.stringify({ status: 'building', dir: d }));
2229
2421
 
2230
2422
  // Build via monograph in background
@@ -2245,7 +2437,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2245
2437
  const qs2 = new URL(req.url, 'http://localhost').searchParams;
2246
2438
  const d2 = path.resolve(qs2.get('dir') || projectDir || process.cwd());
2247
2439
  const state = buildDocsState.get(d2) || { status: 'idle' };
2248
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2440
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2249
2441
  res.end(JSON.stringify(state));
2250
2442
  return;
2251
2443
  }
@@ -2258,7 +2450,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2258
2450
  const d = path.resolve(dir || process.cwd());
2259
2451
  const dbPath = path.join(d, '.monomind', 'monograph.db');
2260
2452
  if (!fs.existsSync(dbPath)) {
2261
- res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2453
+ res.writeHead(400, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2262
2454
  res.end(JSON.stringify({ error: 'monograph.db not found — run BUILD GRAPH first' }));
2263
2455
  return;
2264
2456
  }
@@ -2266,14 +2458,14 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2266
2458
  // Reject if already running
2267
2459
  const existing = buildDocsState.get(d);
2268
2460
  if (existing && existing.status === 'pending') {
2269
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2461
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2270
2462
  res.end(JSON.stringify({ status: 'pending', message: 'Build already in progress' }));
2271
2463
  return;
2272
2464
  }
2273
2465
 
2274
2466
  const startedAt = Date.now();
2275
2467
  buildDocsState.set(d, { status: 'pending', sections: 0, files: 0, error: null, startedAt });
2276
- res.writeHead(202, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2468
+ res.writeHead(202, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2277
2469
  res.end(JSON.stringify({ status: 'pending', dir: d }));
2278
2470
 
2279
2471
  // Run doc parsing in background
@@ -2472,7 +2664,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2472
2664
  } catch {}
2473
2665
  }
2474
2666
  } finally { closeDb(db); }
2475
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2667
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2476
2668
  res.end(JSON.stringify({ content, filePath, startLine, endLine, language, name, type }));
2477
2669
  } catch (err) {
2478
2670
  res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
@@ -2492,7 +2684,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2492
2684
  const d = path.resolve(dir || process.cwd());
2493
2685
  const dbPath = path.join(d, '.monomind', 'monograph.db');
2494
2686
  if (!q) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?q=' })); return; }
2495
- if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ nodes: [] })); return; }
2687
+ if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ nodes: [] })); return; }
2496
2688
  const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
2497
2689
  const { ftsSearch } = await import(new URL('../../../../monograph/dist/src/storage/fts-store.js', import.meta.url).href);
2498
2690
  const db = openDb(dbPath);
@@ -2505,7 +2697,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2505
2697
  return { id: h.id, label: h.name, type: h.label, degree: 0, filePath: h.filePath || h.file_path, startLine: h.startLine || h.start_line, snippet };
2506
2698
  });
2507
2699
  } finally { closeDb(db); }
2508
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2700
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2509
2701
  res.end(JSON.stringify({ nodes }));
2510
2702
  } catch (err) {
2511
2703
  res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
@@ -2524,7 +2716,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2524
2716
  const maxDepth = Math.min(4, parseInt(qs.get('depth') || '3', 10));
2525
2717
  const d = path.resolve(dir || process.cwd());
2526
2718
  const dbPath = path.join(d, '.monomind', 'monograph.db');
2527
- if (!id || !fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ related: [] })); return; }
2719
+ if (!id || !fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ related: [] })); return; }
2528
2720
  const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
2529
2721
  const db = openDb(dbPath);
2530
2722
  const related = [];
@@ -2548,7 +2740,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2548
2740
  frontier = next;
2549
2741
  }
2550
2742
  } finally { closeDb(db); }
2551
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2743
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2552
2744
  res.end(JSON.stringify({ related }));
2553
2745
  } catch (err) {
2554
2746
  res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
@@ -2608,7 +2800,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2608
2800
  }
2609
2801
  result.markdown = lines2.join('\n');
2610
2802
  } finally { closeDb(db); }
2611
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2803
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2612
2804
  res.end(JSON.stringify(result));
2613
2805
  } catch (err) {
2614
2806
  res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
@@ -2625,7 +2817,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2625
2817
  const d = path.resolve(dir || process.cwd());
2626
2818
  const dbPath = path.join(d, '.monomind', 'monograph.db');
2627
2819
  if (!q) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?q= parameter' })); return; }
2628
- if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, result: 'Graph not built yet. Run: monomind monograph build' })); return; }
2820
+ if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ success: false, result: 'Graph not built yet. Run: monomind monograph build' })); return; }
2629
2821
  const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
2630
2822
  const { ftsSearch } = await import(new URL('../../../../monograph/dist/src/storage/fts-store.js', import.meta.url).href);
2631
2823
  const db = openDb(dbPath);
@@ -2644,7 +2836,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2644
2836
  }
2645
2837
  }
2646
2838
  } finally { closeDb(db); }
2647
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2839
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2648
2840
  res.end(JSON.stringify({ success: true, query: q, result }));
2649
2841
  } catch (err) {
2650
2842
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -2662,7 +2854,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2662
2854
  const d = path.resolve(dir || process.cwd());
2663
2855
  const dbPath = path.join(d, '.monomind', 'monograph.db');
2664
2856
  if (!nodeQ) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?node= parameter' })); return; }
2665
- if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, explanation: 'Graph not built yet. Run: monomind monograph build' })); return; }
2857
+ if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ success: false, explanation: 'Graph not built yet. Run: monomind monograph build' })); return; }
2666
2858
  const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
2667
2859
  const { ftsSearch } = await import(new URL('../../../../monograph/dist/src/storage/fts-store.js', import.meta.url).href);
2668
2860
  const db = openDb(dbPath);
@@ -2686,7 +2878,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2686
2878
  ].filter(Boolean).join('\n');
2687
2879
  }
2688
2880
  } finally { closeDb(db); }
2689
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2881
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2690
2882
  res.end(JSON.stringify({ success: true, node: nodeQ, explanation }));
2691
2883
  } catch (err) {
2692
2884
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -2705,7 +2897,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2705
2897
  const d = path.resolve(dir || process.cwd());
2706
2898
  const dbPath = path.join(d, '.monomind', 'monograph.db');
2707
2899
  if (!from || !to) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?from= and ?to= parameters' })); return; }
2708
- if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, path: 'Graph not built yet.' })); return; }
2900
+ if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ success: false, path: 'Graph not built yet.' })); return; }
2709
2901
  // Import only graphology-free storage modules to avoid broken graphology dep
2710
2902
  const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
2711
2903
  const { ftsSearch } = await import(new URL('../../../../monograph/dist/src/storage/fts-store.js', import.meta.url).href);
@@ -2752,7 +2944,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2752
2944
  pathResult = names.join(' → ') + ` (${p.length - 1} hop${p.length !== 2 ? 's' : ''})`;
2753
2945
  }
2754
2946
  } finally { closeDb(db); }
2755
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2947
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2756
2948
  res.end(JSON.stringify({ success: true, from, to, path: pathResult }));
2757
2949
  } catch (err) {
2758
2950
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -2774,7 +2966,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2774
2966
  process.kill(pid, 0);
2775
2967
  running = true;
2776
2968
  } catch {}
2777
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2969
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2778
2970
  res.end(JSON.stringify({ running, pid }));
2779
2971
  } catch (err) {
2780
2972
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -2800,7 +2992,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2800
2992
  } catch {}
2801
2993
 
2802
2994
  if (wasRunning) {
2803
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2995
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2804
2996
  res.end(JSON.stringify({ running: false, action: 'stopped' }));
2805
2997
  } else {
2806
2998
  const { spawn: sp } = await import('child_process');
@@ -2808,7 +3000,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2808
3000
  child.unref();
2809
3001
  try { fs.mkdirSync(path.join(d, '.monomind'), { recursive: true }); } catch {}
2810
3002
  try { fs.writeFileSync(pidPath, String(child.pid)); } catch {}
2811
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3003
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
2812
3004
  res.end(JSON.stringify({ running: true, pid: child.pid, action: 'started' }));
2813
3005
  }
2814
3006
  } catch (err) {
@@ -2823,7 +3015,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
2823
3015
  let body = '';
2824
3016
  req.on('data', c => { body += c; if (body.length > 2097152) { req.destroy(); return; } });
2825
3017
  req.on('end', async () => {
2826
- const json = res => { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); };
3018
+ const json = res => { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); };
2827
3019
  const ok = (data) => { json(res); res.end(JSON.stringify({ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] })); };
2828
3020
  const err = (msg) => { json(res); res.end(JSON.stringify({ error: msg })); };
2829
3021
  try {
@@ -3272,14 +3464,14 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3272
3464
  const gp = fs.existsSync(graphPath) ? graphPath : (fs.existsSync(legacyPath) ? legacyPath : null);
3273
3465
 
3274
3466
  if (!gp) {
3275
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3467
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3276
3468
  res.end(JSON.stringify({ available: false }));
3277
3469
  return;
3278
3470
  }
3279
3471
 
3280
3472
  const { execSync: ex } = await import('child_process');
3281
3473
  const out = ex(`graphify benchmark ${gp}`, { encoding: 'utf8', cwd: d, timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] });
3282
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3474
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3283
3475
  res.end(JSON.stringify({ available: true, result: out }));
3284
3476
  } catch (err) {
3285
3477
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -3417,7 +3609,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3417
3609
  }
3418
3610
  }
3419
3611
 
3420
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
3612
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
3421
3613
  res.end(JSON.stringify({ nodes, edges }));
3422
3614
  } catch (err) {
3423
3615
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -3434,7 +3626,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3434
3626
  const entries = collectSwarmHistory(path.resolve(dir));
3435
3627
  res.writeHead(200, {
3436
3628
  'Content-Type': 'application/json',
3437
- 'Access-Control-Allow-Origin': '*',
3629
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3438
3630
  'Cache-Control': 'no-cache',
3439
3631
  });
3440
3632
  res.end(JSON.stringify({ entries }));
@@ -3459,7 +3651,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3459
3651
  const agentId = typeof _rawAgentId === 'string' ? _rawAgentId.slice(0, 256) : undefined;
3460
3652
  const last = qs.get('last') ? parseInt(qs.get('last')) : undefined;
3461
3653
  const events = collectSwarmEvents(path.resolve(dir), { swarmId, agentId, last });
3462
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
3654
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
3463
3655
  res.end(JSON.stringify({ events, count: events.length }));
3464
3656
  } catch (err) {
3465
3657
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -3473,7 +3665,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3473
3665
  try {
3474
3666
  const dir = new URL(req.url, 'http://localhost').searchParams.get('dir') || projectDir || process.cwd();
3475
3667
  const size = getSwarmDataSize(path.resolve(dir));
3476
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3668
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3477
3669
  res.end(JSON.stringify(size));
3478
3670
  } catch (err) {
3479
3671
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -3487,7 +3679,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3487
3679
  try {
3488
3680
  const dir = new URL(req.url, 'http://localhost').searchParams.get('dir') || projectDir || process.cwd();
3489
3681
  const result = cleanSwarmData(path.resolve(dir));
3490
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3682
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3491
3683
  res.end(JSON.stringify({ success: true, ...result }));
3492
3684
  } catch (err) {
3493
3685
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -3507,7 +3699,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3507
3699
  const trackerPath = path.join(dir, '.claude', 'helpers', 'token-tracker.cjs');
3508
3700
  const fallback = () => {
3509
3701
  const summary = (() => { try { return JSON.parse(fs.readFileSync(path.join(dir, '.monomind', 'metrics', 'token-summary.json'), 'utf8')); } catch { return {}; } })();
3510
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
3702
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
3511
3703
  const fbSum = { todayCost: summary.todayCost || 0, cost: summary.todayCost || 0, todayCalls: summary.todayCalls || 0, calls: summary.todayCalls || 0, totalTokens: 0, totalTokensIn: 0, totalTokensOut: 0, cacheTokens: 0, modelCount: 0 };
3512
3704
  res.end(JSON.stringify({ summary: fbSum, totalCost: summary.todayCost || 0, totalCalls: summary.todayCalls || 0, totalIn: 0, totalOut: 0, totalCR: 0, totalCW: 0, rows: [], models: [], categories: [], tools: [], mcpServers: [], projects: [], modelBreakdown: {}, categoryBreakdown: {}, toolBreakdown: {}, mcpBreakdown: {}, periodLabel: period }));
3513
3705
  };
@@ -3564,7 +3756,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3564
3756
  rows.sort((a, b) => b.cost - a.cost);
3565
3757
  // Summary object matching client expectations
3566
3758
  const summary = { todayCost: totalCost, cost: totalCost, todayCalls: totalCalls, calls: totalCalls, totalTokens: totalIn + totalOut, totalTokensIn: totalIn, totalTokensOut: totalOut, cacheTokens: totalCR, modelCount: models.length };
3567
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
3759
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
3568
3760
  res.end(JSON.stringify({ summary, totalCost, totalCalls, totalIn, totalOut, totalCR, totalCW, rows, models, categories, tools, mcpServers, projects: projectRows, modelBreakdown, categoryBreakdown, toolBreakdown, mcpBreakdown, periodLabel: period }));
3569
3761
  } catch (e) { fallback(); }
3570
3762
  } catch (err) {
@@ -3594,7 +3786,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3594
3786
  }
3595
3787
  res.writeHead(200, {
3596
3788
  'Content-Type': 'application/json',
3597
- 'Access-Control-Allow-Origin': '*',
3789
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3598
3790
  'Cache-Control': 'no-cache',
3599
3791
  });
3600
3792
  res.end(JSON.stringify(partial));
@@ -3611,7 +3803,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3611
3803
  'Content-Type': 'text/event-stream',
3612
3804
  'Cache-Control': 'no-cache',
3613
3805
  Connection: 'keep-alive',
3614
- 'Access-Control-Allow-Origin': '*',
3806
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
3615
3807
  'X-Accel-Buffering': 'no',
3616
3808
  });
3617
3809
 
@@ -3660,11 +3852,11 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3660
3852
  res.end(JSON.stringify({ exists: false }));
3661
3853
  return;
3662
3854
  }
3663
- const { execSync } = await import('child_process');
3855
+ const { execFileSync } = await import('child_process');
3664
3856
  // Pipe SQL via stdin to avoid shell quoting issues with single-quoted SQL strings.
3665
3857
  const runSql = (sql, timeout = 5000) => {
3666
3858
  try {
3667
- return execSync(`sqlite3 -json "${dbPath}"`,
3859
+ return execFileSync('sqlite3', ['-json', dbPath],
3668
3860
  { encoding: 'utf-8', timeout: timeout, input: sql + ';' });
3669
3861
  } catch (e) { return '[]'; }
3670
3862
  };
@@ -3750,7 +3942,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3750
3942
  } catch(_) {}
3751
3943
  }
3752
3944
  }
3753
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3945
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3754
3946
  res.end(JSON.stringify(orgs));
3755
3947
  } catch(_) { res.writeHead(500); res.end('[]'); }
3756
3948
  return;
@@ -3772,7 +3964,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3772
3964
  fs.mkdirSync(orgsDir, { recursive: true });
3773
3965
  const destFile = path.join(orgsDir, `${orgName}.json`);
3774
3966
  fs.writeFileSync(destFile, JSON.stringify({ ...cfg, name: orgName }, null, 2), 'utf8');
3775
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3967
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3776
3968
  res.end(JSON.stringify({ ok: true, name: orgName, file: destFile }));
3777
3969
  } catch (e) {
3778
3970
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -3798,7 +3990,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3798
3990
  const destFile = path.join(orgsDir, `${name}.json`);
3799
3991
  const cleanCfg = Object.fromEntries(Object.entries({ ...cfg, name }).filter(([k]) => !k.startsWith('_')));
3800
3992
  fs.writeFileSync(destFile, JSON.stringify(cleanCfg, null, 2), 'utf8');
3801
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
3993
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3802
3994
  res.end(JSON.stringify({ ok: true, name, file: destFile }));
3803
3995
  } catch (e) {
3804
3996
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -3818,7 +4010,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3818
4010
  const _orgsOneProjDir = _resolveOrgProjectDir(orgName, _orgsOneRoot) || _orgsOneRoot;
3819
4011
  const f = path.join(_orgsOneProjDir, '.monomind', 'orgs', `${orgName}.json`);
3820
4012
  if (!fs.existsSync(f)) { res.writeHead(404); res.end('{"error":"not found"}'); return; }
3821
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4013
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3822
4014
  res.end(fs.readFileSync(f, 'utf8'));
3823
4015
  } catch(_) { res.writeHead(500); res.end('{}'); }
3824
4016
  return;
@@ -3904,7 +4096,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3904
4096
  lastEventAt: _runstateData?.lastEventAt || null,
3905
4097
  agentStates: _runstateData?.agentStates || {} };
3906
4098
 
3907
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4099
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3908
4100
  res.end(JSON.stringify(result));
3909
4101
  } catch(_) { res.writeHead(500); res.end('{}'); }
3910
4102
  return;
@@ -3955,7 +4147,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3955
4147
  }
3956
4148
 
3957
4149
  const out = events.filter(e => e && e.ts).sort((a, b) => b.ts - a.ts).slice(0, 100);
3958
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4150
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3959
4151
  res.end(JSON.stringify(out));
3960
4152
  } catch(_) { res.writeHead(500); res.end('[]'); }
3961
4153
  return;
@@ -3970,9 +4162,9 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3970
4162
  const _projsQs = new URL(req.url, 'http://localhost').searchParams;
3971
4163
  const d = path.resolve(_projsQs.get('dir') || projectDir || process.cwd());
3972
4164
  const projFile = path.join(d, '.monomind', 'orgs', `${orgName}-projects.json`);
3973
- if (!fs.existsSync(projFile)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end('[]'); return; }
4165
+ if (!fs.existsSync(projFile)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end('[]'); return; }
3974
4166
  const data = JSON.parse(fs.readFileSync(projFile, 'utf8'));
3975
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4167
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3976
4168
  res.end(JSON.stringify(data.projects || []));
3977
4169
  } catch(_) { res.writeHead(500); res.end('[]'); }
3978
4170
  return;
@@ -3988,12 +4180,12 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
3988
4180
  const d = path.resolve(_membersQs.get('dir') || projectDir || process.cwd());
3989
4181
  const membersFile = path.join(d, '.monomind', 'orgs', `${orgName}-members.json`);
3990
4182
  if (!fs.existsSync(membersFile)) {
3991
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4183
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3992
4184
  res.end('{"members":[],"join_requests":[]}');
3993
4185
  return;
3994
4186
  }
3995
4187
  const data = JSON.parse(fs.readFileSync(membersFile, 'utf8'));
3996
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4188
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
3997
4189
  res.end(JSON.stringify(data));
3998
4190
  } catch(_) { res.writeHead(500); res.end('{}'); }
3999
4191
  return;
@@ -4014,7 +4206,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4014
4206
  const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
4015
4207
  let defaultAdapter = 'claude-sonnet-4-6';
4016
4208
  try { defaultAdapter = JSON.parse(fs.readFileSync(orgFile, 'utf8'))?.run_config?.ceo_adapter || defaultAdapter; } catch(_) {}
4017
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4209
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4018
4210
  res.end(JSON.stringify({ default_adapter: defaultAdapter, adapters: [
4019
4211
  { type: 'claude-local', label: 'Claude (local CLI)', source: 'built-in', disabled: false, modelsCount: 3 },
4020
4212
  { type: 'gemini-local', label: 'Gemini (local)', source: 'built-in', disabled: false, modelsCount: 1 },
@@ -4023,7 +4215,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4023
4215
  return;
4024
4216
  }
4025
4217
  const data = JSON.parse(fs.readFileSync(adaptersFile, 'utf8'));
4026
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4218
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4027
4219
  res.end(JSON.stringify(data));
4028
4220
  } catch(_) { res.writeHead(500); res.end('{}'); }
4029
4221
  return;
@@ -4078,7 +4270,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4078
4270
  } catch(_) {}
4079
4271
  }
4080
4272
 
4081
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4273
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4082
4274
  res.end(JSON.stringify({ skills, role_skill_map: roleSkillMap }));
4083
4275
  } catch(_) { res.writeHead(500); res.end('{}'); }
4084
4276
  return;
@@ -4134,7 +4326,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4134
4326
  }
4135
4327
  }
4136
4328
 
4137
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
4329
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
4138
4330
  res.end(JSON.stringify({ role, definition }));
4139
4331
  } catch (_) { res.writeHead(500); res.end('{}'); }
4140
4332
  return;
@@ -4147,7 +4339,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4147
4339
  const orgName = decodeURIComponent(urlObj.pathname.split('/')[3]);
4148
4340
  if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
4149
4341
  const q = (urlObj.searchParams.get('q') || '').toLowerCase().trim();
4150
- if (!q || q.length < 2) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end('{"hits":[]}'); return; }
4342
+ if (!q || q.length < 2) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end('{"hits":[]}'); return; }
4151
4343
 
4152
4344
  const d = path.resolve(urlObj.searchParams.get('dir') || projectDir || process.cwd());
4153
4345
  const orgsDir = path.join(d, '.monomind', 'orgs');
@@ -4219,7 +4411,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4219
4411
  }
4220
4412
  }
4221
4413
 
4222
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4414
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4223
4415
  res.end(JSON.stringify({ q, hits: hits.slice(0, 50) }));
4224
4416
  } catch(_) { res.writeHead(500); res.end('{}'); }
4225
4417
  return;
@@ -4505,7 +4697,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4505
4697
  skills: r.skills || [],
4506
4698
  };
4507
4699
  });
4508
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4700
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4509
4701
  res.end(JSON.stringify({ agents }));
4510
4702
  } catch(_) { res.writeHead(500); res.end('{"agents":[]}'); }
4511
4703
  return;
@@ -4542,7 +4734,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4542
4734
  ts: a.ts || null,
4543
4735
  }));
4544
4736
  const pending = approvals.filter(a => a.status === 'pending' || a.status === 'revision_requested').length;
4545
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4737
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4546
4738
  res.end(JSON.stringify({ approvals, pending }));
4547
4739
  } catch(_) { res.writeHead(500); res.end('{"approvals":[],"pending":0}'); }
4548
4740
  return;
@@ -4585,7 +4777,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4585
4777
  const event = { type: 'org:approval:resolved', org: orgName, approval_id: approvalId, status, ts: Date.now() };
4586
4778
  appendToFile(path.join(path.resolve(_postApprovalsQs.get('dir') || projectDir || process.cwd()), 'data', 'mastermind-events.jsonl'), JSON.stringify(event) + '\n').catch(() => {});
4587
4779
  broadcastMm(event);
4588
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4780
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4589
4781
  res.end(JSON.stringify({ ok: true, status }));
4590
4782
  } catch(_) { res.writeHead(500); res.end('{}'); }
4591
4783
  return;
@@ -4613,7 +4805,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4613
4805
  lastUsedAt: s.lastUsedAt || null,
4614
4806
  usageCount: s.usageCount || 0,
4615
4807
  }));
4616
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4808
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4617
4809
  res.end(JSON.stringify({ secrets }));
4618
4810
  } catch(_) { res.writeHead(500); res.end('{"secrets":[]}'); }
4619
4811
  return;
@@ -4770,7 +4962,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4770
4962
  const goalsFile = path.join(_goalsProjDir, '.monomind', 'orgs', `${orgName}-goals.json`);
4771
4963
  let data = { goals: [] };
4772
4964
  try { data = JSON.parse(fs.readFileSync(goalsFile, 'utf8')); } catch(_) {}
4773
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
4965
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4774
4966
  res.end(JSON.stringify({ goals: data.goals || [] }));
4775
4967
  } catch(_) { res.writeHead(500); res.end('{"goals":[]}'); }
4776
4968
  return;
@@ -4819,7 +5011,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4819
5011
  }
4820
5012
  } catch(_) {}
4821
5013
  }
4822
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5014
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4823
5015
  res.end(JSON.stringify({ routines: data.routines || [] }));
4824
5016
  } catch(_) { res.writeHead(500); res.end('{"routines":[]}'); }
4825
5017
  return;
@@ -4841,7 +5033,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4841
5033
  const payload = { org: orgName, updated_at: new Date().toISOString(), goals: parsed.goals };
4842
5034
  fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
4843
5035
  fs.renameSync(tmp, goalsFile);
4844
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5036
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4845
5037
  res.end(JSON.stringify({ ok: true, count: parsed.goals.length }));
4846
5038
  } catch(_) { res.writeHead(500); res.end('{"error":"' + String(_).replace(/"/g, '\\"') + '"}'); }
4847
5039
  return;
@@ -4863,7 +5055,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4863
5055
  const payload = { org: orgName, updated_at: new Date().toISOString(), routines: parsed.routines };
4864
5056
  fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
4865
5057
  fs.renameSync(tmp, routinesFile);
4866
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5058
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4867
5059
  res.end(JSON.stringify({ ok: true, count: parsed.routines.length }));
4868
5060
  } catch(_) { res.writeHead(500); res.end('{"error":"' + String(_).replace(/"/g, '\\"') + '"}'); }
4869
5061
  return;
@@ -4917,7 +5109,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4917
5109
  if (fs.existsSync(agentsDir)) walkAgents(agentsDir);
4918
5110
  }
4919
5111
  files.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
4920
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
5112
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
4921
5113
  res.end(JSON.stringify(files));
4922
5114
  } catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); }
4923
5115
  return;
@@ -4941,7 +5133,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4941
5133
  if (!stat.isFile()) { res.writeHead(400); res.end('Not a file'); return; }
4942
5134
  if (stat.size > 524288) { res.writeHead(413); res.end('File too large'); return; }
4943
5135
  const content = fs.readFileSync(resolved, 'utf8');
4944
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
5136
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4945
5137
  res.end(content);
4946
5138
  } catch(_) { res.writeHead(500); res.end('Internal error'); }
4947
5139
  return;
@@ -4984,7 +5176,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
4984
5176
  const deleteEvent = { type: 'org:delete', org: orgName, ts: Date.now() };
4985
5177
  appendToFile(path.join(projectDir || process.cwd(), 'data', 'mastermind-events.jsonl'), JSON.stringify(deleteEvent) + '\n').catch(() => {});
4986
5178
  broadcastMm(deleteEvent);
4987
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5179
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
4988
5180
  res.end('{"ok":true}');
4989
5181
  } catch(_) { res.writeHead(500); res.end('{}'); }
4990
5182
  return;
@@ -5008,7 +5200,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5008
5200
  fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
5009
5201
  } catch(_) {}
5010
5202
  broadcastMm(stopEvent);
5011
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5203
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5012
5204
  res.end('{"ok":true}');
5013
5205
  } catch(_) { res.writeHead(500); res.end('{}'); }
5014
5206
  return;
@@ -5034,7 +5226,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5034
5226
  try { fs.mkdirSync(destOrgsDir, { recursive: true }); } catch(_) {}
5035
5227
  const destFile = path.join(destOrgsDir, `${orgName}.json`);
5036
5228
  fs.copyFileSync(srcFile, destFile);
5037
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5229
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5038
5230
  res.end(JSON.stringify({ ok: true, destFile }));
5039
5231
  } catch(e) { res.writeHead(500); res.end(JSON.stringify({ error: String(e.message || e) })); }
5040
5232
  return;
@@ -5121,7 +5313,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5121
5313
  break;
5122
5314
  }
5123
5315
  }
5124
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
5316
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
5125
5317
  res.end(JSON.stringify(runs));
5126
5318
  } catch (_) { res.writeHead(500); res.end('[]'); }
5127
5319
  return;
@@ -5147,7 +5339,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5147
5339
  if (!fs.existsSync(_tf)) continue;
5148
5340
  const _tLines = fs.readFileSync(_tf, 'utf8').split('\n').filter(Boolean);
5149
5341
  const _tEvs = _tLines.map(l => { try { const e = JSON.parse(l); return { type: 'org:comms', from: e.role || e.from || 'agent', to: e.to || 'all', msg: e.message || e.msg || '', ts: e.ts ? (typeof e.ts === 'number' ? e.ts : new Date(e.ts).getTime()) : Date.now(), org: _rvOrgName, runId: _rvRunId }; } catch { return null; } }).filter(Boolean);
5150
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5342
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5151
5343
  res.end(JSON.stringify(_tEvs));
5152
5344
  return;
5153
5345
  }
@@ -5187,7 +5379,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5187
5379
  // For in-progress runs (no .warm.jsonl), org:comms also go to .convs.jsonl (stripped form).
5188
5380
  // They're already in .jsonl as full events, so .convs.jsonl would duplicate — skip it.
5189
5381
  events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
5190
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
5382
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
5191
5383
  res.end(JSON.stringify(events));
5192
5384
  } catch (_) { res.writeHead(500); res.end('[]'); }
5193
5385
  return;
@@ -5210,12 +5402,12 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5210
5402
  // Reject files >2MB to avoid blocking the event loop
5211
5403
  if (_size > 2 * 1024 * 1024) { res.writeHead(413); res.end(JSON.stringify({ error: 'file too large', size: _size })); return; }
5212
5404
  if (!_mime.startsWith('text/') && _mime !== 'application/json') {
5213
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5405
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5214
5406
  res.end(JSON.stringify({ binary: true, mimeType: _mime, size: _size }));
5215
5407
  return;
5216
5408
  }
5217
5409
  const _content = fs.readFileSync(_filePath, 'utf8');
5218
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5410
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5219
5411
  res.end(JSON.stringify({ content: _content, mimeType: _mime, size: _size }));
5220
5412
  } catch (_e) { res.writeHead(500); res.end(JSON.stringify({ error: 'read failed' })); }
5221
5413
  return;
@@ -5224,7 +5416,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5224
5416
  // ------------------------------------------------- Mastermind event system
5225
5417
  // POST /api/mastermind/event — ingest event from mastermind skill
5226
5418
  if (req.method === 'POST' && url === '/api/mastermind/event') {
5227
- return handleMastermindEvent(req, res);
5419
+ return handleMastermindEvent(req, res, corsOrigin);
5228
5420
  }
5229
5421
 
5230
5422
  // GET /api/mastermind-stream — SSE for real-time events
@@ -5233,7 +5425,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5233
5425
  'Content-Type': 'text/event-stream',
5234
5426
  'Cache-Control': 'no-cache',
5235
5427
  'Connection': 'keep-alive',
5236
- 'Access-Control-Allow-Origin': '*',
5428
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
5237
5429
  });
5238
5430
  res.write(': connected\n\n');
5239
5431
  addMmClient(res);
@@ -5276,7 +5468,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5276
5468
  const top = idx.slice(0, limitParam);
5277
5469
  for (const entry of top) {
5278
5470
  const _sid = String(entry.id || '').trim();
5279
- if (!_sid || !/^(?!.*\.\.)[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(_sid)) continue;
5471
+ if (!_sid || !SESSION_ID_RE.test(_sid)) continue;
5280
5472
  let events = [];
5281
5473
  try {
5282
5474
  const jl = fs.readFileSync(path.join(sessDir, `${_sid}.jsonl`), 'utf8');
@@ -5300,7 +5492,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5300
5492
  }
5301
5493
  }
5302
5494
  allSessions.sort((a,b) => (b.ts||b.startedAt||0)-(a.ts||a.startedAt||0));
5303
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5495
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5304
5496
  res.end(JSON.stringify(allSessions.slice(0, limitParam)));
5305
5497
  } catch (_) { res.writeHead(200); res.end('[]'); }
5306
5498
  return;
@@ -5332,7 +5524,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5332
5524
  else if (ev.type === 'session:complete') lines.push(`\`${t}\` **SESSION COMPLETE** — status: ${ev.status}, domains: ${(ev.domains || []).join(', ')}`);
5333
5525
  else lines.push(`\`${t}\` ${ev.type} ${JSON.stringify(ev)}`);
5334
5526
  }
5335
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
5527
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5336
5528
  res.end(lines.join('\n'));
5337
5529
  } catch (_) { res.writeHead(500); res.end('Error'); }
5338
5530
  return;
@@ -5345,14 +5537,14 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5345
5537
  // Check individual session file first
5346
5538
  const sessFile = path.join(projectDir || process.cwd(), 'data', 'sessions', `${sid}.json`);
5347
5539
  if (fs.existsSync(sessFile)) {
5348
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5540
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5349
5541
  res.end(fs.readFileSync(sessFile, 'utf8'));
5350
5542
  return;
5351
5543
  }
5352
5544
  const f = path.join(projectDir || process.cwd(), 'data', 'mastermind-sessions.json');
5353
5545
  const sessions = JSON.parse(fs.readFileSync(f, 'utf8'));
5354
5546
  const s = sessions.find(x => x.id === sid);
5355
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5547
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5356
5548
  res.end(JSON.stringify(s || null));
5357
5549
  } catch (_) { res.writeHead(200); res.end('null'); }
5358
5550
  return;
@@ -5384,6 +5576,23 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5384
5576
  return;
5385
5577
  }
5386
5578
 
5579
+ // ------------------------------------------------ GET /orgs-files.js
5580
+ // Files-tab + diff-view script, split out of orgs.html (see orgs.html's
5581
+ // script-src comment). Not a generic static handler — this UI serves each
5582
+ // sibling asset via its own hardcoded route, same as GET /orgs above.
5583
+ if (req.method === 'GET' && url === '/orgs-files.js') {
5584
+ try {
5585
+ const jsPath = path.join(__dirname, 'orgs-files.js');
5586
+ const js = fs.readFileSync(jsPath, 'utf8');
5587
+ res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
5588
+ res.end(js);
5589
+ } catch (err) {
5590
+ res.writeHead(404);
5591
+ res.end(`orgs-files.js not found: ${err.message}`);
5592
+ }
5593
+ return;
5594
+ }
5595
+
5387
5596
  // GET /api/mastermind/loops — list all active loop state files
5388
5597
  if (req.method === 'GET' && url === '/api/mastermind/loops') {
5389
5598
  try {
@@ -5419,7 +5628,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5419
5628
  const lines = fs.readFileSync(evPath, 'utf8').split('\n').filter(l => l.trim()).slice(-10);
5420
5629
  recentEvents = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
5421
5630
  } catch(_) {}
5422
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5631
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5423
5632
  res.end(JSON.stringify({
5424
5633
  ts: Date.now(),
5425
5634
  pid: process.pid,
@@ -5469,7 +5678,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5469
5678
  const detectedRunId = path.basename(runFile, '.jsonl');
5470
5679
  const lines = fs.readFileSync(runFile, 'utf8').split('\n').filter(l => l.trim()).slice(-100);
5471
5680
  const events = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
5472
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5681
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5473
5682
  res.end(JSON.stringify({ runId: detectedRunId, events, active: activeOrgRuns.has(orgName) }));
5474
5683
  } catch(err) {
5475
5684
  res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
@@ -5672,7 +5881,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5672
5881
  } catch (_) {}
5673
5882
  }
5674
5883
  }
5675
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
5884
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
5676
5885
  res.end(JSON.stringify(result));
5677
5886
  } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }
5678
5887
  return;
@@ -5746,12 +5955,12 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5746
5955
  const dir = path.resolve(qp.get('dir') || projectDir || process.cwd());
5747
5956
  const covPath = path.join(dir, '.monomind', 'audit', 'coverage.json');
5748
5957
  if (!fs.existsSync(covPath)) {
5749
- res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
5958
+ res.writeHead(404, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5750
5959
  res.end('{}');
5751
5960
  return;
5752
5961
  }
5753
5962
  const coverage = JSON.parse(fs.readFileSync(covPath, 'utf8'));
5754
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
5963
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
5755
5964
  res.end(JSON.stringify(coverage));
5756
5965
  } catch (e) {
5757
5966
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -5819,7 +6028,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5819
6028
  const _mcLine = `data: ${JSON.stringify(_mcEvent)}\n\n`;
5820
6029
  for (const _cl of _mcFwdClients) { try { _cl.write(_mcLine); } catch (_) { _mcFwdClients.delete(_cl); } }
5821
6030
  }
5822
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
6031
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5823
6032
  res.end(JSON.stringify({ ok: true, runId: _mcRunId }));
5824
6033
  } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }
5825
6034
  return;
@@ -5837,7 +6046,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5837
6046
  'Content-Type': 'text/event-stream',
5838
6047
  'Cache-Control': 'no-cache',
5839
6048
  'Connection': 'keep-alive',
5840
- 'Access-Control-Allow-Origin': '*',
6049
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
5841
6050
  'X-Accel-Buffering': 'no',
5842
6051
  });
5843
6052
  res.write(': connected\n\n');
@@ -5934,7 +6143,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5934
6143
  const _chLine = `data: ${JSON.stringify(_chEvent)}\n\n`;
5935
6144
  for (const _cl of _chFwdClients) { try { _cl.write(_chLine); } catch(_) { _chFwdClients.delete(_cl); } }
5936
6145
  }
5937
- res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
6146
+ res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5938
6147
  res.end(JSON.stringify({ ok: true }));
5939
6148
  } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); }
5940
6149
  return;
@@ -6020,7 +6229,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
6020
6229
  const _migIndex = [];
6021
6230
  for (const sess of (_migOld || [])) {
6022
6231
  const _msid = String(sess.id || '').trim();
6023
- if (!_msid || !/^(?!.*\.\.)[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(_msid)) continue;
6232
+ if (!_msid || !SESSION_ID_RE.test(_msid)) continue;
6024
6233
  // Write per-session JSONL
6025
6234
  const _mEvts = (sess.events || []);
6026
6235
  const _mLines = _mEvts.map(e => JSON.stringify(e)).join('\n');
@@ -6338,9 +6547,7 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
6338
6547
  clearInterval(_heartbeatTimer);
6339
6548
  // Flush SQLite run-event index to disk before exit (bypasses 1000ms debounce timer)
6340
6549
  clearTimeout(_runDbPersistTimer);
6341
- if (_runDb && _runDbPath) {
6342
- try { fs.writeFileSync(_runDbPath, Buffer.from(_runDb.export())); } catch (_) {}
6343
- }
6550
+ _writeRunDbSnapshot();
6344
6551
  for (const w of activeWatchers) {
6345
6552
  try {
6346
6553
  w.close();