@dollhousemcp/mcp-server 2.0.0-rc.6 → 2.0.1

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 (229) hide show
  1. package/CHANGELOG.md +52 -61
  2. package/README.github.md +2 -2
  3. package/README.md +284 -224
  4. package/README.md.backup +284 -224
  5. package/README.npm.md +284 -224
  6. package/dist/cache/LRUCache.d.ts +3 -0
  7. package/dist/cache/LRUCache.d.ts.map +1 -1
  8. package/dist/cache/LRUCache.js +36 -26
  9. package/dist/config/env.d.ts +14 -4
  10. package/dist/config/env.d.ts.map +1 -1
  11. package/dist/config/env.js +20 -6
  12. package/dist/di/Container.d.ts +21 -0
  13. package/dist/di/Container.d.ts.map +1 -1
  14. package/dist/di/Container.js +250 -53
  15. package/dist/elements/BaseElement.d.ts.map +1 -1
  16. package/dist/elements/BaseElement.js +5 -10
  17. package/dist/elements/base/BaseElementManager.d.ts +22 -0
  18. package/dist/elements/base/BaseElementManager.d.ts.map +1 -1
  19. package/dist/elements/base/BaseElementManager.js +47 -7
  20. package/dist/elements/memories/Memory.d.ts +1 -0
  21. package/dist/elements/memories/Memory.d.ts.map +1 -1
  22. package/dist/elements/memories/Memory.js +12 -8
  23. package/dist/elements/memories/MemoryManager.d.ts.map +1 -1
  24. package/dist/elements/memories/MemoryManager.js +23 -42
  25. package/dist/elements/memories/MemorySearchIndex.js +2 -2
  26. package/dist/generated/version.d.ts +2 -2
  27. package/dist/generated/version.d.ts.map +1 -1
  28. package/dist/generated/version.js +3 -3
  29. package/dist/handlers/EnhancedIndexHandler.js +6 -6
  30. package/dist/handlers/element-crud/listElements.d.ts +2 -0
  31. package/dist/handlers/element-crud/listElements.d.ts.map +1 -1
  32. package/dist/handlers/element-crud/listElements.js +3 -1
  33. package/dist/handlers/mcp-aql/Gatekeeper.d.ts.map +1 -1
  34. package/dist/handlers/mcp-aql/Gatekeeper.js +23 -17
  35. package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts +14 -0
  36. package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
  37. package/dist/handlers/mcp-aql/MCPAQLHandler.js +110 -14
  38. package/dist/handlers/mcp-aql/OperationRouter.d.ts.map +1 -1
  39. package/dist/handlers/mcp-aql/OperationRouter.js +13 -1
  40. package/dist/handlers/mcp-aql/OperationSchema.d.ts +7 -0
  41. package/dist/handlers/mcp-aql/OperationSchema.d.ts.map +1 -1
  42. package/dist/handlers/mcp-aql/OperationSchema.js +52 -1
  43. package/dist/handlers/mcp-aql/evaluatePermission.d.ts +53 -0
  44. package/dist/handlers/mcp-aql/evaluatePermission.d.ts.map +1 -0
  45. package/dist/handlers/mcp-aql/evaluatePermission.js +132 -0
  46. package/dist/handlers/mcp-aql/policies/ToolClassification.d.ts.map +1 -1
  47. package/dist/handlers/mcp-aql/policies/ToolClassification.js +2 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -3
  50. package/dist/logging/LogHooks.js +11 -11
  51. package/dist/logging/LogManager.d.ts +0 -2
  52. package/dist/logging/LogManager.d.ts.map +1 -1
  53. package/dist/logging/LogManager.js +1 -3
  54. package/dist/logging/sinks/MemoryLogSink.d.ts +2 -0
  55. package/dist/logging/sinks/MemoryLogSink.d.ts.map +1 -1
  56. package/dist/logging/sinks/MemoryLogSink.js +12 -3
  57. package/dist/logging/types.d.ts +0 -2
  58. package/dist/logging/types.d.ts.map +1 -1
  59. package/dist/logging/types.js +1 -1
  60. package/dist/metrics/GatekeeperMetricsTracker.d.ts +32 -0
  61. package/dist/metrics/GatekeeperMetricsTracker.d.ts.map +1 -0
  62. package/dist/metrics/GatekeeperMetricsTracker.js +42 -0
  63. package/dist/metrics/MetricsManager.d.ts +47 -0
  64. package/dist/metrics/MetricsManager.d.ts.map +1 -0
  65. package/dist/metrics/MetricsManager.js +232 -0
  66. package/dist/metrics/OperationMetricsTracker.d.ts +32 -0
  67. package/dist/metrics/OperationMetricsTracker.d.ts.map +1 -0
  68. package/dist/metrics/OperationMetricsTracker.js +53 -0
  69. package/dist/metrics/collectors/DefaultElementProviderCollector.d.ts +27 -0
  70. package/dist/metrics/collectors/DefaultElementProviderCollector.d.ts.map +1 -0
  71. package/dist/metrics/collectors/DefaultElementProviderCollector.js +69 -0
  72. package/dist/metrics/collectors/FileLockManagerCollector.d.ts +16 -0
  73. package/dist/metrics/collectors/FileLockManagerCollector.d.ts.map +1 -0
  74. package/dist/metrics/collectors/FileLockManagerCollector.js +51 -0
  75. package/dist/metrics/collectors/GatekeeperMetricsCollector.d.ts +16 -0
  76. package/dist/metrics/collectors/GatekeeperMetricsCollector.d.ts.map +1 -0
  77. package/dist/metrics/collectors/GatekeeperMetricsCollector.js +76 -0
  78. package/dist/metrics/collectors/LRUCacheCollector.d.ts +18 -0
  79. package/dist/metrics/collectors/LRUCacheCollector.d.ts.map +1 -0
  80. package/dist/metrics/collectors/LRUCacheCollector.js +95 -0
  81. package/dist/metrics/collectors/OperationMetricsCollector.d.ts +16 -0
  82. package/dist/metrics/collectors/OperationMetricsCollector.d.ts.map +1 -0
  83. package/dist/metrics/collectors/OperationMetricsCollector.js +80 -0
  84. package/dist/metrics/collectors/OperationalTelemetryCollector.d.ts +17 -0
  85. package/dist/metrics/collectors/OperationalTelemetryCollector.d.ts.map +1 -0
  86. package/dist/metrics/collectors/OperationalTelemetryCollector.js +26 -0
  87. package/dist/metrics/collectors/PerformanceMonitorCollector.d.ts +14 -0
  88. package/dist/metrics/collectors/PerformanceMonitorCollector.d.ts.map +1 -0
  89. package/dist/metrics/collectors/PerformanceMonitorCollector.js +141 -0
  90. package/dist/metrics/collectors/SecurityMonitorCollector.d.ts +21 -0
  91. package/dist/metrics/collectors/SecurityMonitorCollector.d.ts.map +1 -0
  92. package/dist/metrics/collectors/SecurityMonitorCollector.js +56 -0
  93. package/dist/metrics/collectors/SecurityTelemetryCollector.d.ts +15 -0
  94. package/dist/metrics/collectors/SecurityTelemetryCollector.d.ts.map +1 -0
  95. package/dist/metrics/collectors/SecurityTelemetryCollector.js +112 -0
  96. package/dist/metrics/collectors/TriggerMetricsTrackerCollector.d.ts +16 -0
  97. package/dist/metrics/collectors/TriggerMetricsTrackerCollector.d.ts.map +1 -0
  98. package/dist/metrics/collectors/TriggerMetricsTrackerCollector.js +26 -0
  99. package/dist/metrics/collectors/index.d.ts +11 -0
  100. package/dist/metrics/collectors/index.d.ts.map +1 -0
  101. package/dist/metrics/collectors/index.js +11 -0
  102. package/dist/metrics/sinks/MemoryMetricsSink.d.ts +22 -0
  103. package/dist/metrics/sinks/MemoryMetricsSink.d.ts.map +1 -0
  104. package/dist/metrics/sinks/MemoryMetricsSink.js +121 -0
  105. package/dist/metrics/types.d.ts +98 -0
  106. package/dist/metrics/types.d.ts.map +1 -0
  107. package/dist/metrics/types.js +24 -0
  108. package/dist/portfolio/DefaultElementProvider.d.ts.map +1 -1
  109. package/dist/portfolio/DefaultElementProvider.js +1 -7
  110. package/dist/portfolio/EnhancedIndexManager.d.ts.map +1 -1
  111. package/dist/portfolio/EnhancedIndexManager.js +18 -18
  112. package/dist/portfolio/NLPScoringManager.d.ts.map +1 -1
  113. package/dist/portfolio/NLPScoringManager.js +5 -9
  114. package/dist/portfolio/PortfolioIndexManager.js +2 -2
  115. package/dist/portfolio/RelationshipManager.js +2 -2
  116. package/dist/portfolio/VerbTriggerManager.d.ts.map +1 -1
  117. package/dist/portfolio/VerbTriggerManager.js +5 -19
  118. package/dist/portfolio/config/IndexConfig.d.ts.map +1 -1
  119. package/dist/portfolio/config/IndexConfig.js +1 -12
  120. package/dist/portfolio/enhanced-index/ElementDefinitionBuilder.d.ts.map +1 -1
  121. package/dist/portfolio/enhanced-index/ElementDefinitionBuilder.js +3 -15
  122. package/dist/portfolio/enhanced-index/SemanticRelationshipService.d.ts.map +1 -1
  123. package/dist/portfolio/enhanced-index/SemanticRelationshipService.js +2 -16
  124. package/dist/portfolio/types/RelationshipTypes.d.ts.map +1 -1
  125. package/dist/portfolio/types/RelationshipTypes.js +3 -17
  126. package/dist/security/audit/config/suppressions.d.ts.map +1 -1
  127. package/dist/security/audit/config/suppressions.js +36 -8
  128. package/dist/security/constants.d.ts.map +1 -1
  129. package/dist/security/constants.js +10 -6
  130. package/dist/security/fileLockManager.d.ts.map +1 -1
  131. package/dist/security/fileLockManager.js +8 -6
  132. package/dist/security/secureYamlParser.d.ts.map +1 -1
  133. package/dist/security/secureYamlParser.js +1 -13
  134. package/dist/security/securityMonitor.d.ts +2 -1
  135. package/dist/security/securityMonitor.d.ts.map +1 -1
  136. package/dist/security/securityMonitor.js +14 -3
  137. package/dist/security/telemetry/SecurityTelemetry.d.ts +16 -0
  138. package/dist/security/telemetry/SecurityTelemetry.d.ts.map +1 -1
  139. package/dist/security/telemetry/SecurityTelemetry.js +30 -2
  140. package/dist/security/tokenManager.d.ts +3 -0
  141. package/dist/security/tokenManager.d.ts.map +1 -1
  142. package/dist/security/tokenManager.js +13 -5
  143. package/dist/security/validation/BackgroundValidator.d.ts.map +1 -1
  144. package/dist/security/validation/BackgroundValidator.js +7 -7
  145. package/dist/server/startup.d.ts.map +1 -1
  146. package/dist/server/startup.js +8 -24
  147. package/dist/server/tools/MCPAQLTools.js +4 -1
  148. package/dist/services/ActivationStore.d.ts.map +1 -1
  149. package/dist/services/ActivationStore.js +9 -3
  150. package/dist/services/FileWatchService.d.ts +1 -0
  151. package/dist/services/FileWatchService.d.ts.map +1 -1
  152. package/dist/services/FileWatchService.js +83 -48
  153. package/dist/services/MetadataService.d.ts.map +1 -1
  154. package/dist/services/MetadataService.js +7 -2
  155. package/dist/services/query/ElementQueryService.d.ts.map +1 -1
  156. package/dist/services/query/ElementQueryService.js +1 -41
  157. package/dist/services/query/PaginationService.d.ts.map +1 -1
  158. package/dist/services/query/PaginationService.js +1 -14
  159. package/dist/services/query/SortService.d.ts.map +1 -1
  160. package/dist/services/query/SortService.js +1 -6
  161. package/dist/services/validation/ValidationService.d.ts.map +1 -1
  162. package/dist/services/validation/ValidationService.js +3 -8
  163. package/dist/storage/ElementStorageLayer.d.ts.map +1 -1
  164. package/dist/storage/ElementStorageLayer.js +5 -2
  165. package/dist/storage/MemoryStorageLayer.d.ts.map +1 -1
  166. package/dist/storage/MemoryStorageLayer.js +5 -2
  167. package/dist/telemetry/OperationalTelemetry.js +2 -2
  168. package/dist/utils/EventDeduplicator.d.ts +44 -0
  169. package/dist/utils/EventDeduplicator.d.ts.map +1 -0
  170. package/dist/utils/EventDeduplicator.js +93 -0
  171. package/dist/utils/FileLock.d.ts.map +1 -1
  172. package/dist/utils/FileLock.js +1 -9
  173. package/dist/utils/PerformanceMonitor.d.ts.map +1 -1
  174. package/dist/utils/PerformanceMonitor.js +5 -5
  175. package/dist/utils/SlidingWindowRateLimiter.d.ts +13 -0
  176. package/dist/utils/SlidingWindowRateLimiter.d.ts.map +1 -0
  177. package/dist/utils/SlidingWindowRateLimiter.js +23 -0
  178. package/dist/web/console/IngestRoutes.d.ts +84 -0
  179. package/dist/web/console/IngestRoutes.d.ts.map +1 -0
  180. package/dist/web/console/IngestRoutes.js +252 -0
  181. package/dist/web/console/LeaderElection.d.ts +89 -0
  182. package/dist/web/console/LeaderElection.d.ts.map +1 -0
  183. package/dist/web/console/LeaderElection.js +205 -0
  184. package/dist/web/console/LeaderForwardingSink.d.ts +61 -0
  185. package/dist/web/console/LeaderForwardingSink.d.ts.map +1 -0
  186. package/dist/web/console/LeaderForwardingSink.js +197 -0
  187. package/dist/web/console/SessionNames.d.ts +46 -0
  188. package/dist/web/console/SessionNames.d.ts.map +1 -0
  189. package/dist/web/console/SessionNames.js +257 -0
  190. package/dist/web/console/UnifiedConsole.d.ts +64 -0
  191. package/dist/web/console/UnifiedConsole.d.ts.map +1 -0
  192. package/dist/web/console/UnifiedConsole.js +119 -0
  193. package/dist/web/contentPipeline.d.ts +58 -0
  194. package/dist/web/contentPipeline.d.ts.map +1 -0
  195. package/dist/web/contentPipeline.js +112 -0
  196. package/dist/web/portDiscovery.d.ts +58 -0
  197. package/dist/web/portDiscovery.d.ts.map +1 -0
  198. package/dist/web/portDiscovery.js +143 -0
  199. package/dist/web/public/app.js +148 -60
  200. package/dist/web/public/logs.js +638 -0
  201. package/dist/web/public/metrics.js +682 -0
  202. package/dist/web/public/permissions.js +394 -0
  203. package/dist/web/public/sessions.js +369 -0
  204. package/dist/web/routes/healthRoutes.d.ts +16 -0
  205. package/dist/web/routes/healthRoutes.d.ts.map +1 -0
  206. package/dist/web/routes/healthRoutes.js +29 -0
  207. package/dist/web/routes/logRoutes.d.ts +18 -0
  208. package/dist/web/routes/logRoutes.d.ts.map +1 -0
  209. package/dist/web/routes/logRoutes.js +126 -0
  210. package/dist/web/routes/metricsRoutes.d.ts +17 -0
  211. package/dist/web/routes/metricsRoutes.d.ts.map +1 -0
  212. package/dist/web/routes/metricsRoutes.js +90 -0
  213. package/dist/web/routes/permissionRoutes.d.ts +16 -0
  214. package/dist/web/routes/permissionRoutes.d.ts.map +1 -0
  215. package/dist/web/routes/permissionRoutes.js +133 -0
  216. package/dist/web/routes.d.ts.map +1 -1
  217. package/dist/web/routes.js +309 -339
  218. package/dist/web/server.d.ts +21 -1
  219. package/dist/web/server.d.ts.map +1 -1
  220. package/dist/web/server.js +42 -4
  221. package/dist/web/sinks/WebSSELogSink.d.ts +15 -0
  222. package/dist/web/sinks/WebSSELogSink.d.ts.map +1 -0
  223. package/dist/web/sinks/WebSSELogSink.js +22 -0
  224. package/dist/web/sinks/WebSSEMetricsSink.d.ts +16 -0
  225. package/dist/web/sinks/WebSSEMetricsSink.d.ts.map +1 -0
  226. package/dist/web/sinks/WebSSEMetricsSink.js +23 -0
  227. package/package.json +2 -2
  228. package/server.json +2 -2
  229. package/dist/web/public/public/app.js +0 -1878
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Unified web console orchestrator.
3
+ *
4
+ * Ties together leader election, console startup, follower wiring,
5
+ * and session lifecycle management. This is the main entry point
6
+ * called by the DI container during deferred setup.
7
+ *
8
+ * Flow:
9
+ * 1. Run leader election (read lock file, claim or follow)
10
+ * 2. If leader: start web server on fixed port, mount ingest routes, start heartbeat
11
+ * 3. If follower: register forwarding sinks with LogManager, start session heartbeat
12
+ *
13
+ * @since v2.1.0 — Issue #1700
14
+ */
15
+ import type { UnifiedLogEntry } from '../../logging/types.js';
16
+ import type { MetricSnapshot } from '../../metrics/types.js';
17
+ import type { MemoryLogSink } from '../../logging/sinks/MemoryLogSink.js';
18
+ import type { MemoryMetricsSink } from '../../metrics/sinks/MemoryMetricsSink.js';
19
+ import { type ElectionResult } from './LeaderElection.js';
20
+ /**
21
+ * Options for starting the unified console.
22
+ */
23
+ export interface UnifiedConsoleOptions {
24
+ /** This process's unique session ID */
25
+ sessionId: string;
26
+ /** Portfolio base directory (for startWebServer) */
27
+ portfolioDir: string;
28
+ /** Log memory sink (for console history) */
29
+ memorySink: MemoryLogSink;
30
+ /** Metrics memory sink */
31
+ metricsSink?: MemoryMetricsSink;
32
+ /** MCP-AQL handler for permission routes (typed as any to avoid circular imports) */
33
+ mcpAqlHandler?: any;
34
+ /** Callback to register a log sink with the LogManager */
35
+ registerLogSink: (sink: {
36
+ write(entry: UnifiedLogEntry): void;
37
+ flush(): Promise<void>;
38
+ close(): Promise<void>;
39
+ }) => void;
40
+ /** Callback to wire SSE broadcasts after web server starts */
41
+ wireSSEBroadcasts: (webResult: {
42
+ logBroadcast?: (entry: UnifiedLogEntry) => void;
43
+ metricsOnSnapshot?: (snapshot: MetricSnapshot) => void;
44
+ }, metricsSink?: MemoryMetricsSink) => void;
45
+ }
46
+ /**
47
+ * Result of starting the unified console.
48
+ */
49
+ export interface UnifiedConsoleResult {
50
+ role: 'leader' | 'follower';
51
+ election: ElectionResult;
52
+ /** Port the console is running on (leader only) */
53
+ port?: number;
54
+ /** Cleanup function to call on shutdown */
55
+ cleanup: () => Promise<void>;
56
+ }
57
+ /**
58
+ * Start the unified web console.
59
+ *
60
+ * Runs leader election, then either starts the full console (leader)
61
+ * or sets up event forwarding (follower).
62
+ */
63
+ export declare function startUnifiedConsole(options: UnifiedConsoleOptions): Promise<UnifiedConsoleResult>;
64
+ //# sourceMappingURL=UnifiedConsole.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnifiedConsole.d.ts","sourceRoot":"","sources":["../../../src/web/console/UnifiedConsole.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAC;AAElF,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAU7B;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,UAAU,EAAE,aAAa,CAAC;IAC1B,0BAA0B;IAC1B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,qFAAqF;IACrF,aAAa,CAAC,EAAE,GAAG,CAAC;IACpB,0DAA0D;IAC1D,eAAe,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;QAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IACzH,8DAA8D;IAC9D,iBAAiB,EAAE,CAAC,SAAS,EAAE;QAAE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;QAAC,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAA;KAAE,EAAE,WAAW,CAAC,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACtL;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC5B,QAAQ,EAAE,cAAc,CAAC;IACzB,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAQvG"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Unified web console orchestrator.
3
+ *
4
+ * Ties together leader election, console startup, follower wiring,
5
+ * and session lifecycle management. This is the main entry point
6
+ * called by the DI container during deferred setup.
7
+ *
8
+ * Flow:
9
+ * 1. Run leader election (read lock file, claim or follow)
10
+ * 2. If leader: start web server on fixed port, mount ingest routes, start heartbeat
11
+ * 3. If follower: register forwarding sinks with LogManager, start session heartbeat
12
+ *
13
+ * @since v2.1.0 — Issue #1700
14
+ */
15
+ import { logger } from '../../utils/logger.js';
16
+ import { electLeader, startHeartbeat, registerLeaderCleanup, } from './LeaderElection.js';
17
+ import { createIngestRoutes } from './IngestRoutes.js';
18
+ import { LeaderForwardingLogSink, SessionHeartbeat, } from './LeaderForwardingSink.js';
19
+ /** Fixed port for the unified console leader */
20
+ const CONSOLE_PORT = 3939;
21
+ /**
22
+ * Start the unified web console.
23
+ *
24
+ * Runs leader election, then either starts the full console (leader)
25
+ * or sets up event forwarding (follower).
26
+ */
27
+ export async function startUnifiedConsole(options) {
28
+ const election = await electLeader(options.sessionId, CONSOLE_PORT);
29
+ if (election.role === 'leader') {
30
+ return startAsLeader(options, election);
31
+ }
32
+ else {
33
+ return startAsFollower(options, election);
34
+ }
35
+ }
36
+ /**
37
+ * Start as the console leader.
38
+ * Binds port 3939, mounts all routes including ingestion, starts heartbeat.
39
+ */
40
+ async function startAsLeader(options, election) {
41
+ const { startWebServer } = await import('../server.js');
42
+ // Pre-create a placeholder broadcast that we'll wire up after the server starts
43
+ let liveBroadcast;
44
+ let liveMetricsOnSnapshot;
45
+ // Create ingestion routes with a deferred broadcast (wired after server starts)
46
+ const ingestResult = createIngestRoutes({
47
+ logBroadcast: (entry) => liveBroadcast?.(entry),
48
+ metricsOnSnapshot: (snapshot) => liveMetricsOnSnapshot?.(snapshot),
49
+ });
50
+ // Register the leader as a session
51
+ ingestResult.registerLeaderSession(options.sessionId, process.pid);
52
+ // Start the web server with ingest routes mounted before the SPA fallback
53
+ const webResult = await startWebServer({
54
+ portfolioDir: options.portfolioDir,
55
+ memorySink: options.memorySink,
56
+ metricsSink: options.metricsSink,
57
+ port: CONSOLE_PORT,
58
+ additionalRouters: [ingestResult.router],
59
+ ...(options.mcpAqlHandler ? { mcpAqlHandler: options.mcpAqlHandler } : {}),
60
+ });
61
+ // Wire SSE broadcasts for this leader's own events
62
+ options.wireSSEBroadcasts(webResult, options.metricsSink);
63
+ // Now wire the live broadcast functions into the ingest routes
64
+ if (webResult.logBroadcast) {
65
+ const originalBroadcast = webResult.logBroadcast;
66
+ // Stamp leader's own entries with session ID
67
+ liveBroadcast = (entry) => {
68
+ const stamped = {
69
+ ...entry,
70
+ data: { ...entry.data, _sessionId: options.sessionId },
71
+ };
72
+ originalBroadcast(stamped);
73
+ };
74
+ }
75
+ liveMetricsOnSnapshot = webResult.metricsOnSnapshot;
76
+ logger.info('[UnifiedConsole] Ingestion routes mounted');
77
+ // Start heartbeat and register cleanup
78
+ const stopHeartbeat = startHeartbeat(election.leaderInfo);
79
+ registerLeaderCleanup();
80
+ logger.info('[UnifiedConsole] Leader started', {
81
+ sessionId: options.sessionId, port: CONSOLE_PORT, pid: process.pid,
82
+ role: 'leader', ingestRoutes: ['/api/ingest/logs', '/api/ingest/metrics', '/api/ingest/session', '/api/sessions'],
83
+ });
84
+ return {
85
+ role: 'leader',
86
+ election,
87
+ port: CONSOLE_PORT,
88
+ cleanup: async () => {
89
+ stopHeartbeat();
90
+ },
91
+ };
92
+ }
93
+ /**
94
+ * Start as a follower.
95
+ * Registers forwarding sinks with the LogManager, starts session heartbeat.
96
+ */
97
+ async function startAsFollower(options, election) {
98
+ const leaderUrl = `http://127.0.0.1:${election.leaderInfo.port}`;
99
+ // Register a forwarding log sink
100
+ const forwardingSink = new LeaderForwardingLogSink(leaderUrl, options.sessionId);
101
+ options.registerLogSink(forwardingSink);
102
+ // Start session heartbeat to the leader
103
+ const sessionHeartbeat = new SessionHeartbeat(leaderUrl, options.sessionId, process.pid);
104
+ await sessionHeartbeat.start();
105
+ logger.info('[UnifiedConsole] Follower started', {
106
+ sessionId: options.sessionId, pid: process.pid, role: 'follower',
107
+ leaderSession: election.leaderInfo.sessionId, leaderPid: election.leaderInfo.pid,
108
+ leaderPort: election.leaderInfo.port, leaderUrl,
109
+ });
110
+ return {
111
+ role: 'follower',
112
+ election,
113
+ cleanup: async () => {
114
+ await sessionHeartbeat.stop();
115
+ await forwardingSink.close();
116
+ },
117
+ };
118
+ }
119
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiVW5pZmllZENvbnNvbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvd2ViL2NvbnNvbGUvVW5pZmllZENvbnNvbGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7R0FhRztBQU1ILE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUMvQyxPQUFPLEVBQ0wsV0FBVyxFQUNYLGNBQWMsRUFDZCxxQkFBcUIsR0FFdEIsTUFBTSxxQkFBcUIsQ0FBQztBQUM3QixPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUN2RCxPQUFPLEVBQ0wsdUJBQXVCLEVBQ3ZCLGdCQUFnQixHQUNqQixNQUFNLDJCQUEyQixDQUFDO0FBRW5DLGdEQUFnRDtBQUNoRCxNQUFNLFlBQVksR0FBRyxJQUFJLENBQUM7QUFrQzFCOzs7OztHQUtHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxtQkFBbUIsQ0FBQyxPQUE4QjtJQUN0RSxNQUFNLFFBQVEsR0FBRyxNQUFNLFdBQVcsQ0FBQyxPQUFPLENBQUMsU0FBUyxFQUFFLFlBQVksQ0FBQyxDQUFDO0lBRXBFLElBQUksUUFBUSxDQUFDLElBQUksS0FBSyxRQUFRLEVBQUUsQ0FBQztRQUMvQixPQUFPLGFBQWEsQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDMUMsQ0FBQztTQUFNLENBQUM7UUFDTixPQUFPLGVBQWUsQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDNUMsQ0FBQztBQUNILENBQUM7QUFFRDs7O0dBR0c7QUFDSCxLQUFLLFVBQVUsYUFBYSxDQUMxQixPQUE4QixFQUM5QixRQUF3QjtJQUV4QixNQUFNLEVBQUUsY0FBYyxFQUFFLEdBQUcsTUFBTSxNQUFNLENBQUMsY0FBYyxDQUFDLENBQUM7SUFFeEQsZ0ZBQWdGO0lBQ2hGLElBQUksYUFBNkQsQ0FBQztJQUNsRSxJQUFJLHFCQUF1RSxDQUFDO0lB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LHdDQUF3QztJQUN4QyxNQUFNLGdCQUFnQixHQUFHLElBQUksZ0JBQWdCLENBQUMsU0FBUyxFQUFFLE9BQU8sQ0FBQyxTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ3pGLE1BQU0sZ0JBQWdCLENBQUMsS0FBSyxFQUFFLENBQUM7SUFFL0IsTUFBTSxDQUFDLElBQUksQ0FBQyxtQ0FBbUMsRUFBRTtRQUMvQyxTQUFTLEVBQUUsT0FBTyxDQUFDLFNBQVMsRUFBRSxHQUFHLEVBQUUsT0FBTyxDQUFDLEdBQUcsRUFBRSxJQUFJLEVBQUUsVUFBVTtRQUNoRSxhQUFhLEVBQUUsUUFBUSxDQUFDLFVBQVUsQ0FBQyxTQUFTLEVBQUUsU0FBUyxFQUFFLFFBQVEsQ0FBQyxVQUFVLENBQUMsR0FBRztRQUNoRixVQUFVLEVBQUUsUUFBUSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsU0FBUztLQUNoRCxDQUFDLENBQUM7SUFFSCxPQUFPO1FBQ0wsSUFBSSxFQUFFLFVBQVU7UUFDaEIsUUFBUTtRQUNSLE9BQU8sRUFBRSxLQUFLLElBQUksRUFBRTtZQUNsQixNQUFNLGdCQUFnQixDQUFDLElBQUksRUFBRSxDQUFDO1lBQzlCLE1BQU0sY0FBYyxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQy9CLENBQUM7S0FDRixDQUFDO0FBQ0osQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogVW5pZmllZCB3ZWIgY29uc29sZSBvcmNoZXN0cmF0b3IuXG4gKlxuICogVGllcyB0b2dldGhlciBsZWFkZXIgZWxlY3Rpb24sIGNvbnNvbGUgc3RhcnR1cCwgZm9sbG93ZXIgd2lyaW5nLFxuICogYW5kIHNlc3Npb24gbGlmZWN5Y2xlIG1hbmFnZW1lbnQuIFRoaXMgaXMgdGhlIG1haW4gZW50cnkgcG9pbnRcbiAqIGNhbGxlZCBieSB0aGUgREkgY29udGFpbmVyIGR1cmluZyBkZWZlcnJlZCBzZXR1cC5cbiAqXG4gKiBGbG93OlxuICogMS4gUnVuIGxlYWRlciBlbGVjdGlvbiAocmVhZCBsb2NrIGZpbGUsIGNsYWltIG9yIGZvbGxvdylcbiAqIDIuIElmIGxlYWRlcjogc3RhcnQgd2ViIHNlcnZlciBvbiBmaXhlZCBwb3J0LCBtb3VudCBpbmdlc3Qgcm91dGVzLCBzdGFydCBoZWFydGJlYXRcbiAqIDMuIElmIGZvbGxvd2VyOiByZWdpc3RlciBmb3J3YXJkaW5nIHNpbmtzIHdpdGggTG9nTWFuYWdlciwgc3RhcnQgc2Vzc2lvbiBoZWFydGJlYXRcbiAqXG4gKiBAc2luY2UgdjIuMS4wIOKAlCBJc3N1ZSAjMTcwMFxuICovXG5cbmltcG9ydCB0eXBlIHsgVW5pZmllZExvZ0VudHJ5IH0gZnJvbSAnLi4vLi4vbG9nZ2luZy90eXBlcy5qcyc7XG5pbXBvcnQgdHlwZSB7IE1ldHJpY1NuYXBzaG90IH0gZnJvbSAnLi4vLi4vbWV0cmljcy90eXBlcy5qcyc7XG5pbXBvcnQgdHlwZSB7IE1lbW9yeUxvZ1NpbmsgfSBmcm9tICcuLi8uLi9sb2dnaW5nL3NpbmtzL01lbW9yeUxvZ1NpbmsuanMnO1xuaW1wb3J0IHR5cGUgeyBNZW1vcnlNZXRyaWNzU2luayB9IGZyb20gJy4uLy4uL21ldHJpY3Mvc2lua3MvTWVtb3J5TWV0cmljc1NpbmsuanMnO1xuaW1wb3J0IHsgbG9nZ2VyIH0gZnJvbSAnLi4vLi4vdXRpbHMvbG9nZ2VyLmpzJztcbmltcG9ydCB7XG4gIGVsZWN0TGVhZGVyLFxuICBzdGFydEhlYXJ0YmVhdCxcbiAgcmVnaXN0ZXJMZWFkZXJDbGVhbnVwLFxuICB0eXBlIEVsZWN0aW9uUmVzdWx0LFxufSBmcm9tICcuL0xlYWRlckVsZWN0aW9uLmpzJztcbmltcG9ydCB7IGNyZWF0ZUluZ2VzdFJvdXRlcyB9IGZyb20gJy4vSW5nZXN0Um91dGVzLmpzJztcbmltcG9ydCB7XG4gIExlYWRlckZvcndhcmRpbmdMb2dTaW5rLFxuICBTZXNzaW9uSGVhcnRiZWF0LFxufSBmcm9tICcuL0xlYWRlckZvcndhcmRpbmdTaW5rLmpzJztcblxuLyoqIEZpeGVkIHBvcnQgZm9yIHRoZSB1bmlmaWVkIGNvbnNvbGUgbGVhZGVyICovXG5jb25zdCBDT05TT0xFX1BPUlQgPSAzOTM5O1xuXG4vKipcbiAqIE9wdGlvbnMgZm9yIHN0YXJ0aW5nIHRoZSB1bmlmaWVkIGNvbnNvbGUuXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgVW5pZmllZENvbnNvbGVPcHRpb25zIHtcbiAgLyoqIFRoaXMgcHJvY2VzcydzIHVuaXF1ZSBzZXNzaW9uIElEICovXG4gIHNlc3Npb25JZDogc3RyaW5nO1xuICAvKiogUG9ydGZvbGlvIGJhc2UgZGlyZWN0b3J5IChmb3Igc3RhcnRXZWJTZXJ2ZXIpICovXG4gIHBvcnRmb2xpb0Rpcjogc3RyaW5nO1xuICAvKiogTG9nIG1lbW9yeSBzaW5rIChmb3IgY29uc29sZSBoaXN0b3J5KSAqL1xuICBtZW1vcnlTaW5rOiBNZW1vcnlMb2dTaW5rO1xuICAvKiogTWV0cmljcyBtZW1vcnkgc2luayAqL1xuICBtZXRyaWNzU2luaz86IE1lbW9yeU1ldHJpY3NTaW5rO1xuICAvKiogTUNQLUFRTCBoYW5kbGVyIGZvciBwZXJtaXNzaW9uIHJvdXRlcyAodHlwZWQgYXMgYW55IHRvIGF2b2lkIGNpcmN1bGFyIGltcG9ydHMpICovXG4gIG1jcEFxbEhhbmRsZXI/OiBhbnk7XG4gIC8qKiBDYWxsYmFjayB0byByZWdpc3RlciBhIGxvZyBzaW5rIHdpdGggdGhlIExvZ01hbmFnZXIgKi9cbiAgcmVnaXN0ZXJMb2dTaW5rOiAoc2luazogeyB3cml0ZShlbnRyeTogVW5pZmllZExvZ0VudHJ5KTogdm9pZDsgZmx1c2goKTogUHJvbWlzZTx2b2lkPjsgY2xvc2UoKTogUHJvbWlzZTx2b2lkPiB9KSA9PiB2b2lkO1xuICAvKiogQ2FsbGJhY2sgdG8gd2lyZSBTU0UgYnJvYWRjYXN0cyBhZnRlciB3ZWIgc2VydmVyIHN0YXJ0cyAqL1xuICB3aXJlU1NFQnJvYWRjYXN0czogKHdlYlJlc3VsdDogeyBsb2dCcm9hZGNhc3Q/OiAoZW50cnk6IFVuaWZpZWRMb2dFbnRyeSkgPT4gdm9pZDsgbWV0cmljc09uU25hcHNob3Q/OiAoc25hcHNob3Q6IE1ldHJpY1NuYXBzaG90KSA9PiB2b2lkIH0sIG1ldHJpY3NTaW5rPzogTWVtb3J5TWV0cmljc1NpbmspID0+IHZvaWQ7XG59XG5cbi8qKlxuICogUmVzdWx0IG9mIHN0YXJ0aW5nIHRoZSB1bmlmaWVkIGNvbnNvbGUuXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgVW5pZmllZENvbnNvbGVSZXN1bHQge1xuICByb2xlOiAnbGVhZGVyJyB8ICdmb2xsb3dlcic7XG4gIGVsZWN0aW9uOiBFbGVjdGlvblJlc3VsdDtcbiAgLyoqIFBvcnQgdGhlIGNvbnNvbGUgaXMgcnVubmluZyBvbiAobGVhZGVyIG9ubHkpICovXG4gIHBvcnQ/OiBudW1iZXI7XG4gIC8qKiBDbGVhbnVwIGZ1bmN0aW9uIHRvIGNhbGwgb24gc2h1dGRvd24gKi9cbiAgY2xlYW51cDogKCkgPT4gUHJvbWlzZTx2b2lkPjtcbn1cblxuLyoqXG4gKiBTdGFydCB0aGUgdW5pZmllZCB3ZWIgY29uc29sZS5cbiAqXG4gKiBSdW5zIGxlYWRlciBlbGVjdGlvbiwgdGhlbiBlaXRoZXIgc3RhcnRzIHRoZSBmdWxsIGNvbnNvbGUgKGxlYWRlcilcbiAqIG9yIHNldHMgdXAgZXZlbnQgZm9yd2FyZGluZyAoZm9sbG93ZXIpLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc3RhcnRVbmlmaWVkQ29uc29sZShvcHRpb25zOiBVbmlmaWVkQ29uc29sZU9wdGlvbnMpOiBQcm9taXNlPFVuaWZpZWRDb25zb2xlUmVzdWx0PiB7XG4gIGNvbnN0IGVsZWN0aW9uID0gYXdhaXQgZWxlY3RMZWFkZXIob3B0aW9ucy5zZXNzaW9uSWQsIENPTlNPTEVfUE9SVCk7XG5cbiAgaWYgKGVsZWN0aW9uLnJvbGUgPT09ICdsZWFkZXInKSB7XG4gICAgcmV0dXJuIHN0YXJ0QXNMZWFkZXIob3B0aW9ucywgZWxlY3Rpb24pO1xuICB9IGVsc2Uge1xuICAgIHJldHVybiBzdGFydEFzRm9sbG93ZXIob3B0aW9ucywgZWxlY3Rpb24pO1xuICB9XG59XG5cbi8qKlxuICogU3RhcnQgYXMgdGhlIGNvbnNvbGUgbGVhZGVyLlxuICogQmluZHMgcG9ydCAzOTM5LCBtb3VudHMgYWxsIHJvdXRlcyBpbmNsdWRpbmcgaW5nZXN0aW9uLCBzdGFydHMgaGVhcnRiZWF0LlxuICovXG5hc3luYyBmdW5jdGlvbiBzdGFydEFzTGVhZGVyKFxuICBvcHRpb25zOiBVbmlmaWVkQ29uc29sZU9wdGlvbnMsXG4gIGVsZWN0aW9uOiBFbGVjdGlvblJlc3VsdCxcbik6IFByb21pc2U8VW5pZmllZENvbnNvbGVSZXN1bHQ+IHtcbiAgY29uc3QgeyBzdGFydFdlYlNlcnZlciB9ID0gYXdhaXQgaW1wb3J0KCcuLi9zZXJ2ZXIuanMnKTtcblxuICAvLyBQcmUtY3JlYXRlIGEgcGxhY2Vob2xkZXIgYnJvYWRjYXN0IHRoYXQgd2UnbGwgd2lyZSB1cCBhZnRlciB0aGUgc2VydmVyIHN0YXJ0c1xuICBsZXQgbGl2ZUJyb2FkY2FzdDogKChlbnRyeTogVW5pZmllZExvZ0VudHJ5KSA9PiB2b2lkKSB8IHVuZGVmaW5lZDtcbiAgbGV0IGxpdmVNZXRyaWNzT25TbmFwc2hvdDogKChzbmFwc2hvdDogTWV0cmljU25hcHNob3QpID0+IHZvaWQpIHwgdW5kZWZpbmVkO1xuXG4gIC8vIENyZWF0ZSBpbmdlc3Rpb24gcm91dGVzIHdpdGggYSBkZWZlcnJlZCBicm9hZGNhc3QgKHdpcmVkIGFmdGVyIHNlcnZlciBzdGFydHMpXG4gIGNvbnN0IGluZ2VzdFJlc3VsdCA9IGNyZWF0ZUluZ2VzdFJvdXRlcyh7XG4gICAgbG9nQnJvYWRjYXN0OiAoZW50cnkpID0+IGxpdmVCcm9hZGNhc3Q/LihlbnRyeSksXG4gICAgbWV0cmljc09uU25hcHNob3Q6IChzbmFwc2hvdCkgPT4gbGl2ZU1ldHJpY3NPblNuYXBzaG90Py4oc25hcHNob3QpLFxuICB9KTtcblxuICAvLyBSZWdpc3RlciB0aGUgbGVhZGVyIGFzIGEgc2Vzc2lvblxuICBpbmdlc3RSZXN1bHQucmVnaXN0ZXJMZWFkZXJTZXNzaW9uKG9wdGlvbnMuc2Vzc2lvbklkLCBwcm9jZXNzLnBpZCk7XG5cbiAgLy8gU3RhcnQgdGhlIHdlYiBzZXJ2ZXIgd2l0aCBpbmdlc3Qgcm91dGVzIG1vdW50ZWQgYmVmb3JlIHRoZSBTUEEgZmFsbGJhY2tcbiAgY29uc3Qgd2ViUmVzdWx0ID0gYXdhaXQgc3RhcnRXZWJTZXJ2ZXIoe1xuICAgIHBvcnRmb2xpb0Rpcjogb3B0aW9ucy5wb3J0Zm9saW9EaXIsXG4gICAgbWVtb3J5U2luazogb3B0aW9ucy5tZW1vcnlTaW5rLFxuICAgIG1ldHJpY3NTaW5rOiBvcHRpb25zLm1ldHJpY3NTaW5rLFxuICAgIHBvcnQ6IENPTlNPTEVfUE9SVCxcbiAgICBhZGRpdGlvbmFsUm91dGVyczogW2luZ2VzdFJlc3VsdC5yb3V0ZXJdLFxuICAgIC4uLihvcHRpb25zLm1jcEFxbEhhbmRsZXIgPyB7IG1jcEFxbEhhbmRsZXI6IG9wdGlvbnMubWNwQXFsSGFuZGxlciB9IDoge30pLFxuICB9KTtcblxuICAvLyBXaXJlIFNTRSBicm9hZGNhc3RzIGZvciB0aGlzIGxlYWRlcidzIG93biBldmVudHNcbiAgb3B0aW9ucy53aXJlU1NFQnJvYWRjYXN0cyh3ZWJSZXN1bHQsIG9wdGlvbnMubWV0cmljc1NpbmspO1xuXG4gIC8vIE5vdyB3aXJlIHRoZSBsaXZlIGJyb2FkY2FzdCBmdW5jdGlvbnMgaW50byB0aGUgaW5nZXN0IHJvdXRlc1xuICBpZiAod2ViUmVzdWx0LmxvZ0Jyb2FkY2FzdCkge1xuICAgIGNvbnN0IG9yaWdpbmFsQnJvYWRjYXN0ID0gd2ViUmVzdWx0LmxvZ0Jyb2FkY2FzdDtcbiAgICAvLyBTdGFtcCBsZWFkZXIncyBvd24gZW50cmllcyB3aXRoIHNlc3Npb24gSURcbiAgICBsaXZlQnJvYWRjYXN0ID0gKGVudHJ5OiBVbmlmaWVkTG9nRW50cnkpID0+IHtcbiAgICAgIGNvbnN0IHN0YW1wZWQ6IFVuaWZpZWRMb2dFbnRyeSA9IHtcbiAgICAgICAgLi4uZW50cnksXG4gICAgICAgIGRhdGE6IHsgLi4uZW50cnkuZGF0YSwgX3Nlc3Npb25JZDogb3B0aW9ucy5zZXNzaW9uSWQgfSxcbiAgICAgIH07XG4gICAgICBvcmlnaW5hbEJyb2FkY2FzdChzdGFtcGVkKTtcbiAgICB9O1xuICB9XG4gIGxpdmVNZXRyaWNzT25TbmFwc2hvdCA9IHdlYlJlc3VsdC5tZXRyaWNzT25TbmFwc2hvdDtcblxuICBsb2dnZXIuaW5mbygnW1VuaWZpZWRDb25zb2xlXSBJbmdlc3Rpb24gcm91dGVzIG1vdW50ZWQnKTtcblxuICAvLyBTdGFydCBoZWFydGJlYXQgYW5kIHJlZ2lzdGVyIGNsZWFudXBcbiAgY29uc3Qgc3RvcEhlYXJ0YmVhdCA9IHN0YXJ0SGVhcnRiZWF0KGVsZWN0aW9uLmxlYWRlckluZm8pO1xuICByZWdpc3RlckxlYWRlckNsZWFudXAoKTtcblxuICBsb2dnZXIuaW5mbygnW1VuaWZpZWRDb25zb2xlXSBMZWFkZXIgc3RhcnRlZCcsIHtcbiAgICBzZXNzaW9uSWQ6IG9wdGlvbnMuc2Vzc2lvbklkLCBwb3J0OiBDT05TT0xFX1BPUlQsIHBpZDogcHJvY2Vzcy5waWQsXG4gICAgcm9sZTogJ2xlYWRlcicsIGluZ2VzdFJvdXRlczogWycvYXBpL2luZ2VzdC9sb2dzJywgJy9hcGkvaW5nZXN0L21ldHJpY3MnLCAnL2FwaS9pbmdlc3Qvc2Vzc2lvbicsICcvYXBpL3Nlc3Npb25zJ10sXG4gIH0pO1xuXG4gIHJldHVybiB7XG4gICAgcm9sZTogJ2xlYWRlcicsXG4gICAgZWxlY3Rpb24sXG4gICAgcG9ydDogQ09OU09MRV9QT1JULFxuICAgIGNsZWFudXA6IGFzeW5jICgpID0+IHtcbiAgICAgIHN0b3BIZWFydGJlYXQoKTtcbiAgICB9LFxuICB9O1xufVxuXG4vKipcbiAqIFN0YXJ0IGFzIGEgZm9sbG93ZXIuXG4gKiBSZWdpc3RlcnMgZm9yd2FyZGluZyBzaW5rcyB3aXRoIHRoZSBMb2dNYW5hZ2VyLCBzdGFydHMgc2Vzc2lvbiBoZWFydGJlYXQuXG4gKi9cbmFzeW5jIGZ1bmN0aW9uIHN0YXJ0QXNGb2xsb3dlcihcbiAgb3B0aW9uczogVW5pZmllZENvbnNvbGVPcHRpb25zLFxuICBlbGVjdGlvbjogRWxlY3Rpb25SZXN1bHQsXG4pOiBQcm9taXNlPFVuaWZpZWRDb25zb2xlUmVzdWx0PiB7XG4gIGNvbnN0IGxlYWRlclVybCA9IGBodHRwOi8vMTI3LjAuMC4xOiR7ZWxlY3Rpb24ubGVhZGVySW5mby5wb3J0fWA7XG5cbiAgLy8gUmVnaXN0ZXIgYSBmb3J3YXJkaW5nIGxvZyBzaW5rXG4gIGNvbnN0IGZvcndhcmRpbmdTaW5rID0gbmV3IExlYWRlckZvcndhcmRpbmdMb2dTaW5rKGxlYWRlclVybCwgb3B0aW9ucy5zZXNzaW9uSWQpO1xuICBvcHRpb25zLnJlZ2lzdGVyTG9nU2luayhmb3J3YXJkaW5nU2luayk7XG5cbiAgLy8gU3RhcnQgc2Vzc2lvbiBoZWFydGJlYXQgdG8gdGhlIGxlYWRlclxuICBjb25zdCBzZXNzaW9uSGVhcnRiZWF0ID0gbmV3IFNlc3Npb25IZWFydGJlYXQobGVhZGVyVXJsLCBvcHRpb25zLnNlc3Npb25JZCwgcHJvY2Vzcy5waWQpO1xuICBhd2FpdCBzZXNzaW9uSGVhcnRiZWF0LnN0YXJ0KCk7XG5cbiAgbG9nZ2VyLmluZm8oJ1tVbmlmaWVkQ29uc29sZV0gRm9sbG93ZXIgc3RhcnRlZCcsIHtcbiAgICBzZXNzaW9uSWQ6IG9wdGlvbnMuc2Vzc2lvbklkLCBwaWQ6IHByb2Nlc3MucGlkLCByb2xlOiAnZm9sbG93ZXInLFxuICAgIGxlYWRlclNlc3Npb246IGVsZWN0aW9uLmxlYWRlckluZm8uc2Vzc2lvbklkLCBsZWFkZXJQaWQ6IGVsZWN0aW9uLmxlYWRlckluZm8ucGlkLFxuICAgIGxlYWRlclBvcnQ6IGVsZWN0aW9uLmxlYWRlckluZm8ucG9ydCwgbGVhZGVyVXJsLFxuICB9KTtcblxuICByZXR1cm4ge1xuICAgIHJvbGU6ICdmb2xsb3dlcicsXG4gICAgZWxlY3Rpb24sXG4gICAgY2xlYW51cDogYXN5bmMgKCkgPT4ge1xuICAgICAgYXdhaXQgc2Vzc2lvbkhlYXJ0YmVhdC5zdG9wKCk7XG4gICAgICBhd2FpdCBmb3J3YXJkaW5nU2luay5jbG9zZSgpO1xuICAgIH0sXG4gIH07XG59XG4iXX0=
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Content Validation Pipeline for Web UI
3
+ *
4
+ * Composes the standalone security validators into a single pipeline
5
+ * for validating portfolio element content before serving to the browser.
6
+ *
7
+ * Uses the same validators as the MCPAQLHandler but without coupling
8
+ * to the handler's operation dispatch. This is the extraction point
9
+ * for the upcoming MCPAQLHandler decomposition.
10
+ *
11
+ * auto-dollhouse#5 / upstream #1679
12
+ * DMCP-SEC-004 compliant: uses UnicodeValidator.normalize() on all inputs
13
+ */
14
+ /** Known metadata fields extracted from YAML frontmatter for web display */
15
+ export interface ElementDisplayMetadata {
16
+ name?: string;
17
+ description?: string;
18
+ version?: string;
19
+ author?: string;
20
+ category?: string;
21
+ created?: string;
22
+ created_date?: string;
23
+ modified?: string;
24
+ tags?: string[];
25
+ license?: string;
26
+ age_rating?: string;
27
+ triggers?: string[];
28
+ instructions?: string;
29
+ coordination_strategy?: string;
30
+ use_cases?: string[];
31
+ proficiency_levels?: Record<string, string>;
32
+ gatekeeper?: Record<string, unknown>;
33
+ goal?: Record<string, unknown>;
34
+ autonomy?: Record<string, unknown>;
35
+ memoryType?: string;
36
+ [key: string]: unknown;
37
+ }
38
+ export interface PipelineResult {
39
+ valid: boolean;
40
+ content: string;
41
+ metadata: ElementDisplayMetadata;
42
+ body: string;
43
+ rejection?: {
44
+ reason: string;
45
+ severity?: string;
46
+ patterns?: string[];
47
+ };
48
+ }
49
+ /**
50
+ * Validate element content through the security pipeline.
51
+ *
52
+ * @param filename - The element filename (e.g., "alex-sterling.md")
53
+ * @param rawContent - The raw file content string
54
+ * @param elementType - The plural element type directory (e.g., "personas")
55
+ * @returns Validated content with parsed metadata, or rejection with reason
56
+ */
57
+ export declare function validateElementContent(filename: string, rawContent: string, elementType: string): PipelineResult;
58
+ //# sourceMappingURL=contentPipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contentPipeline.d.ts","sourceRoot":"","sources":["../../src/web/contentPipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAiBH,4EAA4E;AAC5E,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,sBAAsB,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE;QACV,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,cAAc,CAgFhB"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Content Validation Pipeline for Web UI
3
+ *
4
+ * Composes the standalone security validators into a single pipeline
5
+ * for validating portfolio element content before serving to the browser.
6
+ *
7
+ * Uses the same validators as the MCPAQLHandler but without coupling
8
+ * to the handler's operation dispatch. This is the extraction point
9
+ * for the upcoming MCPAQLHandler decomposition.
10
+ *
11
+ * auto-dollhouse#5 / upstream #1679
12
+ * DMCP-SEC-004 compliant: uses UnicodeValidator.normalize() on all inputs
13
+ */
14
+ import { extname } from 'node:path';
15
+ import { SecureYamlParser } from '../security/secureYamlParser.js';
16
+ import { ContentValidator } from '../security/contentValidator.js';
17
+ import { UnicodeValidator } from '../security/validators/unicodeValidator.js';
18
+ import { logger } from '../utils/logger.js';
19
+ /** Element types that map to content validation contexts */
20
+ const TYPE_TO_CONTEXT = {
21
+ personas: 'persona',
22
+ skills: 'skill',
23
+ templates: 'template',
24
+ agents: 'agent',
25
+ memories: 'memory',
26
+ };
27
+ /**
28
+ * Validate element content through the security pipeline.
29
+ *
30
+ * @param filename - The element filename (e.g., "alex-sterling.md")
31
+ * @param rawContent - The raw file content string
32
+ * @param elementType - The plural element type directory (e.g., "personas")
33
+ * @returns Validated content with parsed metadata, or rejection with reason
34
+ */
35
+ export function validateElementContent(filename, rawContent, elementType) {
36
+ // DMCP-SEC-004: Normalize all inputs via UnicodeValidator to prevent
37
+ // homograph attacks, direction override bypasses, and suspicious patterns.
38
+ const filenameValidation = UnicodeValidator.normalize(filename);
39
+ const contentValidation = UnicodeValidator.normalize(rawContent);
40
+ const normalizedFilename = filenameValidation.normalizedContent;
41
+ const normalizedContent = contentValidation.normalizedContent;
42
+ const normalizedType = elementType.normalize('NFC');
43
+ const ext = extname(normalizedFilename);
44
+ const isYaml = ext === '.yaml' || ext === '.yml';
45
+ const contentContext = TYPE_TO_CONTEXT[normalizedType];
46
+ // Step 1: Parse and validate structure (YAML bomb detection, circular refs)
47
+ let metadata = {};
48
+ let body = '';
49
+ try {
50
+ if (isYaml) {
51
+ // Pure YAML file (memories) — use parseRawYaml for structural validation
52
+ // (bomb detection, circular refs, size limits).
53
+ // Skip ContentValidator.validateYamlContent() — it produces false positives
54
+ // on memory content that legitimately contains code patterns, security
55
+ // keywords, and technical documentation. Memories are locally-generated
56
+ // trusted content, not untrusted external submissions.
57
+ const parsed = SecureYamlParser.parseRawYaml(normalizedContent);
58
+ metadata = (typeof parsed === 'object' && parsed !== null) ? parsed : {};
59
+ }
60
+ else {
61
+ // Markdown with YAML frontmatter
62
+ const parsed = SecureYamlParser.parse(normalizedContent, { contentContext });
63
+ metadata = parsed.data;
64
+ body = parsed.content;
65
+ }
66
+ }
67
+ catch (err) {
68
+ return {
69
+ valid: false,
70
+ content: normalizedContent,
71
+ metadata: {},
72
+ body: '',
73
+ rejection: { reason: `Parse validation failed: ${err.message}`, severity: 'high' },
74
+ };
75
+ }
76
+ // Step 2: Content injection pattern detection (markdown elements only)
77
+ // YAML memories skip this — they contain legitimate code patterns and
78
+ // technical content that triggers false positives. The structural YAML
79
+ // parsing above (bomb/circular ref detection) is sufficient for local content.
80
+ if (!isYaml) {
81
+ const validation = ContentValidator.validateAndSanitize(normalizedContent, {
82
+ contentContext,
83
+ });
84
+ if (!validation.isValid) {
85
+ logger.warn('[ContentPipeline] Content rejected', {
86
+ filename,
87
+ elementType,
88
+ patterns: validation.detectedPatterns,
89
+ severity: validation.severity,
90
+ });
91
+ return {
92
+ valid: false,
93
+ content: normalizedContent,
94
+ metadata,
95
+ body,
96
+ rejection: {
97
+ reason: 'Content failed security validation',
98
+ severity: validation.severity,
99
+ patterns: validation.detectedPatterns,
100
+ },
101
+ };
102
+ }
103
+ }
104
+ // Step 3: Return validated content
105
+ return {
106
+ valid: true,
107
+ content: rawContent,
108
+ metadata,
109
+ body,
110
+ };
111
+ }
112
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGVudFBpcGVsaW5lLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3dlYi9jb250ZW50UGlwZWxpbmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7OztHQVlHO0FBRUgsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUNwQyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNuRSxPQUFPLEVBQUUsZ0JBQWdCLEVBQWdDLE1BQU0saUNBQWlDLENBQUM7QUFDakcsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sNENBQTRDLENBQUM7QUFDOUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRTVDLDREQUE0RDtBQUM1RCxNQUFNLGVBQWUsR0FBMEU7SUFDN0YsUUFBUSxFQUFFLFNBQVM7SUFDbkIsTUFBTSxFQUFFLE9BQU87SUFDZixTQUFTLEVBQUUsVUFBVTtJQUNyQixNQUFNLEVBQUUsT0FBTztJQUNmLFFBQVEsRUFBRSxRQUFRO0NBQ25CLENBQUM7QUF1Q0Y7Ozs7Ozs7R0FPRztBQUNILE1BQU0sVUFBVSxzQkFBc0IsQ0FDcEMsUUFBZ0IsRUFDaEIsVUFBa0IsRUFDbEIsV0FBbUI7SUFFbkIscUVBQXFFO0lBQ3JFLDJFQUEyRTtJQUMzRSxNQUFNLGtCQUFrQixHQUFHLGdCQUFnQixDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUNoRSxNQUFNLGlCQUFpQixHQUFHLGdCQUFnQixDQUFDLFNBQVMsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUNqRSxNQUFNLGtCQUFrQixHQUFHLGtCQUFrQixDQUFDLGlCQUFpQixDQUFDO0lBQ2hFLE1BQU0saUJBQWlCLEdBQUcsaUJBQWlCLENBQUMsaUJBQWlCLENBQUM7SUFDOUQsTUFBTSxjQUFjLEdBQUcsV0FBVyxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUVwRCxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsa0JBQWtCLENBQUMsQ0FBQztJQUN4QyxNQUFNLE1BQU0sR0FBRyxHQUFHLEtBQUssT0FBTyxJQUFJLEdBQUcsS0FBSyxNQUFNLENBQUM7SUFDakQsTUFBTSxjQUFjLEdBQUcsZUFBZSxDQUFDLGNBQWMsQ0FBQyxDQUFDO0lBRXZELDRFQUE0RTtJQUM1RSxJQUFJLFFBQVEsR0FBMkIsRUFBRSxDQUFDO0lBQzFDLElBQUksSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUVkLElBQUksQ0FBQztRQUNILElBQUksTUFBTSxFQUFFLENBQUM7WUFDWCx5RUFBeUU7WUFDekUsZ0RBQWdEO1lBQ2hELDRFQUE0RTtZQUM1RSx1RUFBdUU7WUFDdkUsd0VBQXdFO1lBQ3hFLHVEQUF1RDtZQUN2RCxNQUFNLE1BQU0sR0FBRyxnQkFBZ0IsQ0FBQyxZQUFZLENBQUMsaUJBQWlCLENBQUMsQ0FBQztZQUNoRSxRQUFRLEdBQUcsQ0FBQyxPQUFPLE1BQU0sS0FBSyxRQUFRLElBQUksTUFBTSxLQUFLLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUMzRSxDQUFDO2FBQU0sQ0FBQztZQUNOLGlDQUFpQztZQUNqQyxNQUFNLE1BQU0sR0FBRyxnQkFBZ0IsQ0FBQyxLQUFLLENBQUMsaUJBQWlCLEVBQUUsRUFBRSxjQUFjLEVBQUUsQ0FBQyxDQUFDO1lBQzdFLFFBQVEsR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDO1lBQ3ZCLElBQUksR0FBRyxNQUFNLENBQUMsT0FBTyxDQUFDO1FBQ3hCLENBQUM7SUFDSCxDQUFDO0lBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztRQUNiLE9BQU87WUFDTCxLQUFLLEVBQUUsS0FBSztZQUNaLE9BQU8sRUFBRSxpQkFBaUI7WUFDMUIsUUFBUSxFQUFFLEVBQUU7WUFDWixJQUFJLEVBQUUsRUFBRTtZQUNSLFNBQVMsRUFBRSxFQUFFLE1BQU0sRUFBRSw0QkFBNkIsR0FBYSxDQUFDLE9BQU8sRUFBRSxFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUU7U0FDOUYsQ0FBQztJQUNKLENBQUM7SUFFRCx1RUFBdUU7SUFDdkUsc0VBQXNFO0lBQ3RFLHVFQUF1RTtJQUN2RSwrRUFBK0U7SUFDL0UsSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ1osTUFBTSxVQUFVLEdBQTRCLGdCQUFnQixDQUFDLG1CQUFtQixDQUFDLGlCQUFpQixFQUFFO1lBQ2xHLGNBQWM7U0FDZixDQUFDLENBQUM7UUFFSCxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ3hCLE1BQU0sQ0FBQyxJQUFJLENBQUMsb0NBQW9DLEVBQUU7Z0JBQ2hELFFBQVE7Z0JBQ1IsV0FBVztnQkFDWCxRQUFRLEVBQUUsVUFBVSxDQUFDLGdCQUFnQjtnQkFDckMsUUFBUSxFQUFFLFVBQVUsQ0FBQyxRQUFRO2FBQzlCLENBQUMsQ0FBQztZQUNILE9BQU87Z0JBQ0wsS0FBSyxFQUFFLEtBQUs7Z0JBQ1osT0FBTyxFQUFFLGlCQUFpQjtnQkFDMUIsUUFBUTtnQkFDUixJQUFJO2dCQUNKLFNBQVMsRUFBRTtvQkFDVCxNQUFNLEVBQUUsb0NBQW9DO29CQUM1QyxRQUFRLEVBQUUsVUFBVSxDQUFDLFFBQVE7b0JBQzdCLFFBQVEsRUFBRSxVQUFVLENBQUMsZ0JBQWdCO2lCQUN0QzthQUNGLENBQUM7UUFDSixDQUFDO0lBQ0gsQ0FBQztJQUVELG1DQUFtQztJQUNuQyxPQUFPO1FBQ0wsS0FBSyxFQUFFLElBQUk7UUFDWCxPQUFPLEVBQUUsVUFBVTtRQUNuQixRQUFRO1FBQ1IsSUFBSTtLQUNMLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDb250ZW50IFZhbGlkYXRpb24gUGlwZWxpbmUgZm9yIFdlYiBVSVxuICpcbiAqIENvbXBvc2VzIHRoZSBzdGFuZGFsb25lIHNlY3VyaXR5IHZhbGlkYXRvcnMgaW50byBhIHNpbmdsZSBwaXBlbGluZVxuICogZm9yIHZhbGlkYXRpbmcgcG9ydGZvbGlvIGVsZW1lbnQgY29udGVudCBiZWZvcmUgc2VydmluZyB0byB0aGUgYnJvd3Nlci5cbiAqXG4gKiBVc2VzIHRoZSBzYW1lIHZhbGlkYXRvcnMgYXMgdGhlIE1DUEFRTEhhbmRsZXIgYnV0IHdpdGhvdXQgY291cGxpbmdcbiAqIHRvIHRoZSBoYW5kbGVyJ3Mgb3BlcmF0aW9uIGRpc3BhdGNoLiBUaGlzIGlzIHRoZSBleHRyYWN0aW9uIHBvaW50XG4gKiBmb3IgdGhlIHVwY29taW5nIE1DUEFRTEhhbmRsZXIgZGVjb21wb3NpdGlvbi5cbiAqXG4gKiBhdXRvLWRvbGxob3VzZSM1IC8gdXBzdHJlYW0gIzE2NzlcbiAqIERNQ1AtU0VDLTAwNCBjb21wbGlhbnQ6IHVzZXMgVW5pY29kZVZhbGlkYXRvci5ub3JtYWxpemUoKSBvbiBhbGwgaW5wdXRzXG4gKi9cblxuaW1wb3J0IHsgZXh0bmFtZSB9IGZyb20gJ25vZGU6cGF0aCc7XG5pbXBvcnQgeyBTZWN1cmVZYW1sUGFyc2VyIH0gZnJvbSAnLi4vc2VjdXJpdHkvc2VjdXJlWWFtbFBhcnNlci5qcyc7XG5pbXBvcnQgeyBDb250ZW50VmFsaWRhdG9yLCB0eXBlIENvbnRlbnRWYWxpZGF0aW9uUmVzdWx0IH0gZnJvbSAnLi4vc2VjdXJpdHkvY29udGVudFZhbGlkYXRvci5qcyc7XG5pbXBvcnQgeyBVbmljb2RlVmFsaWRhdG9yIH0gZnJvbSAnLi4vc2VjdXJpdHkvdmFsaWRhdG9ycy91bmljb2RlVmFsaWRhdG9yLmpzJztcbmltcG9ydCB7IGxvZ2dlciB9IGZyb20gJy4uL3V0aWxzL2xvZ2dlci5qcyc7XG5cbi8qKiBFbGVtZW50IHR5cGVzIHRoYXQgbWFwIHRvIGNvbnRlbnQgdmFsaWRhdGlvbiBjb250ZXh0cyAqL1xuY29uc3QgVFlQRV9UT19DT05URVhUOiBSZWNvcmQ8c3RyaW5nLCAncGVyc29uYScgfCAnc2tpbGwnIHwgJ3RlbXBsYXRlJyB8ICdhZ2VudCcgfCAnbWVtb3J5Jz4gPSB7XG4gIHBlcnNvbmFzOiAncGVyc29uYScsXG4gIHNraWxsczogJ3NraWxsJyxcbiAgdGVtcGxhdGVzOiAndGVtcGxhdGUnLFxuICBhZ2VudHM6ICdhZ2VudCcsXG4gIG1lbW9yaWVzOiAnbWVtb3J5Jyxcbn07XG5cbi8qKiBLbm93biBtZXRhZGF0YSBmaWVsZHMgZXh0cmFjdGVkIGZyb20gWUFNTCBmcm9udG1hdHRlciBmb3Igd2ViIGRpc3BsYXkgKi9cbmV4cG9ydCBpbnRlcmZhY2UgRWxlbWVudERpc3BsYXlNZXRhZGF0YSB7XG4gIG5hbWU/OiBzdHJpbmc7XG4gIGRlc2NyaXB0aW9uPzogc3RyaW5nO1xuICB2ZXJzaW9uPzogc3RyaW5nO1xuICBhdXRob3I/OiBzdHJpbmc7XG4gIGNhdGVnb3J5Pzogc3RyaW5nO1xuICBjcmVhdGVkPzogc3RyaW5nO1xuICBjcmVhdGVkX2RhdGU/OiBzdHJpbmc7XG4gIG1vZGlmaWVkPzogc3RyaW5nO1xuICB0YWdzPzogc3RyaW5nW107XG4gIGxpY2Vuc2U/OiBzdHJpbmc7XG4gIGFnZV9yYXRpbmc/OiBzdHJpbmc7XG4gIHRyaWdnZXJzPzogc3RyaW5nW107XG4gIGluc3RydWN0aW9ucz86IHN0cmluZztcbiAgY29vcmRpbmF0aW9uX3N0cmF0ZWd5Pzogc3RyaW5nO1xuICB1c2VfY2FzZXM/OiBzdHJpbmdbXTtcbiAgcHJvZmljaWVuY3lfbGV2ZWxzPzogUmVjb3JkPHN0cmluZywgc3RyaW5nPjtcbiAgZ2F0ZWtlZXBlcj86IFJlY29yZDxzdHJpbmcsIHVua25vd24+O1xuICBnb2FsPzogUmVjb3JkPHN0cmluZywgdW5rbm93bj47XG4gIGF1dG9ub215PzogUmVjb3JkPHN0cmluZywgdW5rbm93bj47XG4gIG1lbW9yeVR5cGU/OiBzdHJpbmc7XG4gIFtrZXk6IHN0cmluZ106IHVua25vd247XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUGlwZWxpbmVSZXN1bHQge1xuICB2YWxpZDogYm9vbGVhbjtcbiAgY29udGVudDogc3RyaW5nO1xuICBtZXRhZGF0YTogRWxlbWVudERpc3BsYXlNZXRhZGF0YTtcbiAgYm9keTogc3RyaW5nO1xuICByZWplY3Rpb24/OiB7XG4gICAgcmVhc29uOiBzdHJpbmc7XG4gICAgc2V2ZXJpdHk/OiBzdHJpbmc7XG4gICAgcGF0dGVybnM/OiBzdHJpbmdbXTtcbiAgfTtcbn1cblxuLyoqXG4gKiBWYWxpZGF0ZSBlbGVtZW50IGNvbnRlbnQgdGhyb3VnaCB0aGUgc2VjdXJpdHkgcGlwZWxpbmUuXG4gKlxuICogQHBhcmFtIGZpbGVuYW1lIC0gVGhlIGVsZW1lbnQgZmlsZW5hbWUgKGUuZy4sIFwiYWxleC1zdGVybGluZy5tZFwiKVxuICogQHBhcmFtIHJhd0NvbnRlbnQgLSBUaGUgcmF3IGZpbGUgY29udGVudCBzdHJpbmdcbiAqIEBwYXJhbSBlbGVtZW50VHlwZSAtIFRoZSBwbHVyYWwgZWxlbWVudCB0eXBlIGRpcmVjdG9yeSAoZS5nLiwgXCJwZXJzb25hc1wiKVxuICogQHJldHVybnMgVmFsaWRhdGVkIGNvbnRlbnQgd2l0aCBwYXJzZWQgbWV0YWRhdGEsIG9yIHJlamVjdGlvbiB3aXRoIHJlYXNvblxuICovXG5leHBvcnQgZnVuY3Rpb24gdmFsaWRhdGVFbGVtZW50Q29udGVudChcbiAgZmlsZW5hbWU6IHN0cmluZyxcbiAgcmF3Q29udGVudDogc3RyaW5nLFxuICBlbGVtZW50VHlwZTogc3RyaW5nLFxuKTogUGlwZWxpbmVSZXN1bHQge1xuICAvLyBETUNQLVNFQy0wMDQ6IE5vcm1hbGl6ZSBhbGwgaW5wdXRzIHZpYSBVbmljb2RlVmFsaWRhdG9yIHRvIHByZXZlbnRcbiAgLy8gaG9tb2dyYXBoIGF0dGFja3MsIGRpcmVjdGlvbiBvdmVycmlkZSBieXBhc3NlcywgYW5kIHN1c3BpY2lvdXMgcGF0dGVybnMuXG4gIGNvbnN0IGZpbGVuYW1lVmFsaWRhdGlvbiA9IFVuaWNvZGVWYWxpZGF0b3Iubm9ybWFsaXplKGZpbGVuYW1lKTtcbiAgY29uc3QgY29udGVudFZhbGlkYXRpb24gPSBVbmljb2RlVmFsaWRhdG9yLm5vcm1hbGl6ZShyYXdDb250ZW50KTtcbiAgY29uc3Qgbm9ybWFsaXplZEZpbGVuYW1lID0gZmlsZW5hbWVWYWxpZGF0aW9uLm5vcm1hbGl6ZWRDb250ZW50O1xuICBjb25zdCBub3JtYWxpemVkQ29udGVudCA9IGNvbnRlbnRWYWxpZGF0aW9uLm5vcm1hbGl6ZWRDb250ZW50O1xuICBjb25zdCBub3JtYWxpemVkVHlwZSA9IGVsZW1lbnRUeXBlLm5vcm1hbGl6ZSgnTkZDJyk7XG5cbiAgY29uc3QgZXh0ID0gZXh0bmFtZShub3JtYWxpemVkRmlsZW5hbWUpO1xuICBjb25zdCBpc1lhbWwgPSBleHQgPT09ICcueWFtbCcgfHwgZXh0ID09PSAnLnltbCc7XG4gIGNvbnN0IGNvbnRlbnRDb250ZXh0ID0gVFlQRV9UT19DT05URVhUW25vcm1hbGl6ZWRUeXBlXTtcblxuICAvLyBTdGVwIDE6IFBhcnNlIGFuZCB2YWxpZGF0ZSBzdHJ1Y3R1cmUgKFlBTUwgYm9tYiBkZXRlY3Rpb24sIGNpcmN1bGFyIHJlZnMpXG4gIGxldCBtZXRhZGF0YTogRWxlbWVudERpc3BsYXlNZXRhZGF0YSA9IHt9O1xuICBsZXQgYm9keSA9ICcnO1xuXG4gIHRyeSB7XG4gICAgaWYgKGlzWWFtbCkge1xuICAgICAgLy8gUHVyZSBZQU1MIGZpbGUgKG1lbW9yaWVzKSDigJQgdXNlIHBhcnNlUmF3WWFtbCBmb3Igc3RydWN0dXJhbCB2YWxpZGF0aW9uXG4gICAgICAvLyAoYm9tYiBkZXRlY3Rpb24sIGNpcmN1bGFyIHJlZnMsIHNpemUgbGltaXRzKS5cbiAgICAgIC8vIFNraXAgQ29udGVudFZhbGlkYXRvci52YWxpZGF0ZVlhbWxDb250ZW50KCkg4oCUIGl0IHByb2R1Y2VzIGZhbHNlIHBvc2l0aXZlc1xuICAgICAgLy8gb24gbWVtb3J5IGNvbnRlbnQgdGhhdCBsZWdpdGltYXRlbHkgY29udGFpbnMgY29kZSBwYXR0ZXJucywgc2VjdXJpdHlcbiAgICAgIC8vIGtleXdvcmRzLCBhbmQgdGVjaG5pY2FsIGRvY3VtZW50YXRpb24uIE1lbW9yaWVzIGFyZSBsb2NhbGx5LWdlbmVyYXRlZFxuICAgICAgLy8gdHJ1c3RlZCBjb250ZW50LCBub3QgdW50cnVzdGVkIGV4dGVybmFsIHN1Ym1pc3Npb25zLlxuICAgICAgY29uc3QgcGFyc2VkID0gU2VjdXJlWWFtbFBhcnNlci5wYXJzZVJhd1lhbWwobm9ybWFsaXplZENvbnRlbnQpO1xuICAgICAgbWV0YWRhdGEgPSAodHlwZW9mIHBhcnNlZCA9PT0gJ29iamVjdCcgJiYgcGFyc2VkICE9PSBudWxsKSA/IHBhcnNlZCA6IHt9O1xuICAgIH0gZWxzZSB7XG4gICAgICAvLyBNYXJrZG93biB3aXRoIFlBTUwgZnJvbnRtYXR0ZXJcbiAgICAgIGNvbnN0IHBhcnNlZCA9IFNlY3VyZVlhbWxQYXJzZXIucGFyc2Uobm9ybWFsaXplZENvbnRlbnQsIHsgY29udGVudENvbnRleHQgfSk7XG4gICAgICBtZXRhZGF0YSA9IHBhcnNlZC5kYXRhO1xuICAgICAgYm9keSA9IHBhcnNlZC5jb250ZW50O1xuICAgIH1cbiAgfSBjYXRjaCAoZXJyKSB7XG4gICAgcmV0dXJuIHtcbiAgICAgIHZhbGlkOiBmYWxzZSxcbiAgICAgIGNvbnRlbnQ6IG5vcm1hbGl6ZWRDb250ZW50LFxuICAgICAgbWV0YWRhdGE6IHt9LFxuICAgICAgYm9keTogJycsXG4gICAgICByZWplY3Rpb246IHsgcmVhc29uOiBgUGFyc2UgdmFsaWRhdGlvbiBmYWlsZWQ6ICR7KGVyciBhcyBFcnJvcikubWVzc2FnZX1gLCBzZXZlcml0eTogJ2hpZ2gnIH0sXG4gICAgfTtcbiAgfVxuXG4gIC8vIFN0ZXAgMjogQ29udGVudCBpbmplY3Rpb24gcGF0dGVybiBkZXRlY3Rpb24gKG1hcmtkb3duIGVsZW1lbnRzIG9ubHkpXG4gIC8vIFlBTUwgbWVtb3JpZXMgc2tpcCB0aGlzIOKAlCB0aGV5IGNvbnRhaW4gbGVnaXRpbWF0ZSBjb2RlIHBhdHRlcm5zIGFuZFxuICAvLyB0ZWNobmljYWwgY29udGVudCB0aGF0IHRyaWdnZXJzIGZhbHNlIHBvc2l0aXZlcy4gVGhlIHN0cnVjdHVyYWwgWUFNTFxuICAvLyBwYXJzaW5nIGFib3ZlIChib21iL2NpcmN1bGFyIHJlZiBkZXRlY3Rpb24pIGlzIHN1ZmZpY2llbnQgZm9yIGxvY2FsIGNvbnRlbnQuXG4gIGlmICghaXNZYW1sKSB7XG4gICAgY29uc3QgdmFsaWRhdGlvbjogQ29udGVudFZhbGlkYXRpb25SZXN1bHQgPSBDb250ZW50VmFsaWRhdG9yLnZhbGlkYXRlQW5kU2FuaXRpemUobm9ybWFsaXplZENvbnRlbnQsIHtcbiAgICAgIGNvbnRlbnRDb250ZXh0LFxuICAgIH0pO1xuXG4gICAgaWYgKCF2YWxpZGF0aW9uLmlzVmFsaWQpIHtcbiAgICAgIGxvZ2dlci53YXJuKCdbQ29udGVudFBpcGVsaW5lXSBDb250ZW50IHJlamVjdGVkJywge1xuICAgICAgICBmaWxlbmFtZSxcbiAgICAgICAgZWxlbWVudFR5cGUsXG4gICAgICAgIHBhdHRlcm5zOiB2YWxpZGF0aW9uLmRldGVjdGVkUGF0dGVybnMsXG4gICAgICAgIHNldmVyaXR5OiB2YWxpZGF0aW9uLnNldmVyaXR5LFxuICAgICAgfSk7XG4gICAgICByZXR1cm4ge1xuICAgICAgICB2YWxpZDogZmFsc2UsXG4gICAgICAgIGNvbnRlbnQ6IG5vcm1hbGl6ZWRDb250ZW50LFxuICAgICAgICBtZXRhZGF0YSxcbiAgICAgICAgYm9keSxcbiAgICAgICAgcmVqZWN0aW9uOiB7XG4gICAgICAgICAgcmVhc29uOiAnQ29udGVudCBmYWlsZWQgc2VjdXJpdHkgdmFsaWRhdGlvbicsXG4gICAgICAgICAgc2V2ZXJpdHk6IHZhbGlkYXRpb24uc2V2ZXJpdHksXG4gICAgICAgICAgcGF0dGVybnM6IHZhbGlkYXRpb24uZGV0ZWN0ZWRQYXR0ZXJucyxcbiAgICAgICAgfSxcbiAgICAgIH07XG4gICAgfVxuICB9XG5cbiAgLy8gU3RlcCAzOiBSZXR1cm4gdmFsaWRhdGVkIGNvbnRlbnRcbiAgcmV0dXJuIHtcbiAgICB2YWxpZDogdHJ1ZSxcbiAgICBjb250ZW50OiByYXdDb250ZW50LFxuICAgIG1ldGFkYXRhLFxuICAgIGJvZHksXG4gIH07XG59XG4iXX0=
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Dynamic port allocation and port file discovery for the web console.
3
+ *
4
+ * When multiple DollhouseMCP sessions run simultaneously (e.g., multiple
5
+ * Claude Code windows, IDE instances, or agent sessions), each needs its
6
+ * own web console port. This module handles:
7
+ *
8
+ * 1. Finding an available port starting from the default (3939)
9
+ * 2. Writing port discovery files so external tools know which port to use
10
+ * 3. Cleaning up port files on process exit
11
+ *
12
+ * Port files are written to ~/.dollhouse/run/:
13
+ * - permission-server-{pid}.port — per-process file (cleaned on exit)
14
+ * - permission-server.port — latest port (convenience for scripts)
15
+ */
16
+ /**
17
+ * Find an available port starting from the given port.
18
+ *
19
+ * Tries sequential ports, logging each attempt. Returns both the port number
20
+ * and a held server to prevent TOCTOU race conditions. The caller should
21
+ * close the held server after binding their own Express app to the port.
22
+ *
23
+ * @param startPort - Port to try first (default: 3939)
24
+ * @param maxAttempts - Maximum ports to try (default: 10, configurable via DOLLHOUSE_MAX_PORT_ATTEMPTS)
25
+ * @returns Object with port number and held server, or throws if all attempts fail
26
+ */
27
+ export declare function findAvailablePort(startPort: number, maxAttempts?: number): Promise<number>;
28
+ /**
29
+ * Write the active server port to a discoverable file.
30
+ *
31
+ * Creates two files:
32
+ * - PID-keyed file for per-process cleanup
33
+ * - Latest file for convenience (scripts can read this without knowing the PID)
34
+ *
35
+ * @param port - The port the web server is listening on
36
+ * @returns Path to the PID-keyed port file
37
+ */
38
+ export declare function writePortFile(port: number): Promise<string>;
39
+ /**
40
+ * Clean up port file on shutdown.
41
+ */
42
+ export declare function cleanupPortFile(): Promise<void>;
43
+ /**
44
+ * Register process exit handlers to clean up port files.
45
+ * Should be called once after the web server successfully starts.
46
+ */
47
+ export declare function registerPortCleanup(): void;
48
+ /**
49
+ * Discover an available port, write discovery files, and register cleanup.
50
+ *
51
+ * This is the recommended entry point for Container startup. Combines
52
+ * port discovery, file writing, and cleanup registration in one call.
53
+ *
54
+ * @param defaultPort - Port to try first (default: 3939)
55
+ * @returns The port the server should bind to, or undefined if discovery failed
56
+ */
57
+ export declare function discoverAndBindPort(defaultPort?: number): Promise<number | undefined>;
58
+ //# sourceMappingURL=portDiscovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portDiscovery.d.ts","sourceRoot":"","sources":["../../src/web/portDiscovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAwCH;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EACjB,WAAW,GAAE,MAAqF,GACjG,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASjE;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAIrD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,WAAW,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAYjG"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Dynamic port allocation and port file discovery for the web console.
3
+ *
4
+ * When multiple DollhouseMCP sessions run simultaneously (e.g., multiple
5
+ * Claude Code windows, IDE instances, or agent sessions), each needs its
6
+ * own web console port. This module handles:
7
+ *
8
+ * 1. Finding an available port starting from the default (3939)
9
+ * 2. Writing port discovery files so external tools know which port to use
10
+ * 3. Cleaning up port files on process exit
11
+ *
12
+ * Port files are written to ~/.dollhouse/run/:
13
+ * - permission-server-{pid}.port — per-process file (cleaned on exit)
14
+ * - permission-server.port — latest port (convenience for scripts)
15
+ */
16
+ import { createServer } from 'node:net';
17
+ import { homedir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { mkdir, writeFile, unlink } from 'node:fs/promises';
20
+ import { logger } from '../utils/logger.js';
21
+ /** Default maximum port attempts before giving up */
22
+ const DEFAULT_MAX_PORT_ATTEMPTS = 10;
23
+ /** Directory for runtime state files (port discovery, PID files) */
24
+ const RUN_DIR = join(homedir(), '.dollhouse', 'run');
25
+ /** Track port file path for cleanup */
26
+ let portFilePath = null;
27
+ /**
28
+ * Attempt to bind to a single port. Returns the port if successful, null if in use.
29
+ * Keeps the server listening to prevent TOCTOU race conditions — caller must
30
+ * close the returned server after their own server binds.
31
+ */
32
+ function tryBindPort(port) {
33
+ return new Promise((resolve) => {
34
+ const server = createServer();
35
+ server.once('error', (err) => {
36
+ if (err.code === 'EADDRINUSE') {
37
+ resolve(null);
38
+ }
39
+ else {
40
+ resolve(null);
41
+ logger.debug(`[PortDiscovery] Unexpected error on port ${port}: ${err.code}`);
42
+ }
43
+ });
44
+ server.once('listening', () => {
45
+ resolve({ port, server });
46
+ });
47
+ server.listen(port, '127.0.0.1');
48
+ });
49
+ }
50
+ /**
51
+ * Find an available port starting from the given port.
52
+ *
53
+ * Tries sequential ports, logging each attempt. Returns both the port number
54
+ * and a held server to prevent TOCTOU race conditions. The caller should
55
+ * close the held server after binding their own Express app to the port.
56
+ *
57
+ * @param startPort - Port to try first (default: 3939)
58
+ * @param maxAttempts - Maximum ports to try (default: 10, configurable via DOLLHOUSE_MAX_PORT_ATTEMPTS)
59
+ * @returns Object with port number and held server, or throws if all attempts fail
60
+ */
61
+ export async function findAvailablePort(startPort, maxAttempts = Number(process.env.DOLLHOUSE_MAX_PORT_ATTEMPTS) || DEFAULT_MAX_PORT_ATTEMPTS) {
62
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
63
+ const port = startPort + attempt;
64
+ logger.debug(`[PortDiscovery] Trying port ${port} (attempt ${attempt + 1}/${maxAttempts})`);
65
+ const result = await tryBindPort(port);
66
+ if (result) {
67
+ // Close the probe server — there's a brief TOCTOU window here between
68
+ // closing and the caller binding, but it's negligible on localhost.
69
+ // A future improvement could return the server for the caller to adopt.
70
+ result.server.close();
71
+ if (attempt > 0) {
72
+ logger.info(`[PortDiscovery] Port ${startPort} in use, using ${port} (after ${attempt} skip${attempt > 1 ? 's' : ''})`);
73
+ }
74
+ return port;
75
+ }
76
+ logger.debug(`[PortDiscovery] Port ${port} in use, trying next`);
77
+ }
78
+ throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1} ` +
79
+ `after ${maxAttempts} attempts. Set DOLLHOUSE_MAX_PORT_ATTEMPTS to increase the range.`);
80
+ }
81
+ /**
82
+ * Write the active server port to a discoverable file.
83
+ *
84
+ * Creates two files:
85
+ * - PID-keyed file for per-process cleanup
86
+ * - Latest file for convenience (scripts can read this without knowing the PID)
87
+ *
88
+ * @param port - The port the web server is listening on
89
+ * @returns Path to the PID-keyed port file
90
+ */
91
+ export async function writePortFile(port) {
92
+ await mkdir(RUN_DIR, { recursive: true });
93
+ const pidFile = join(RUN_DIR, `permission-server-${process.pid}.port`);
94
+ const latestFile = join(RUN_DIR, 'permission-server.port');
95
+ await writeFile(pidFile, String(port), 'utf-8');
96
+ await writeFile(latestFile, String(port), 'utf-8');
97
+ portFilePath = pidFile;
98
+ logger.debug(`[PortDiscovery] Port file written: ${pidFile}`);
99
+ return pidFile;
100
+ }
101
+ /**
102
+ * Clean up port file on shutdown.
103
+ */
104
+ export async function cleanupPortFile() {
105
+ if (portFilePath) {
106
+ try {
107
+ await unlink(portFilePath);
108
+ }
109
+ catch { /* already gone */ }
110
+ }
111
+ }
112
+ /**
113
+ * Register process exit handlers to clean up port files.
114
+ * Should be called once after the web server successfully starts.
115
+ */
116
+ export function registerPortCleanup() {
117
+ const exitCleanup = () => { cleanupPortFile().catch(() => { }); };
118
+ process.once('exit', exitCleanup);
119
+ process.once('SIGTERM', exitCleanup);
120
+ process.once('SIGINT', exitCleanup);
121
+ }
122
+ /**
123
+ * Discover an available port, write discovery files, and register cleanup.
124
+ *
125
+ * This is the recommended entry point for Container startup. Combines
126
+ * port discovery, file writing, and cleanup registration in one call.
127
+ *
128
+ * @param defaultPort - Port to try first (default: 3939)
129
+ * @returns The port the server should bind to, or undefined if discovery failed
130
+ */
131
+ export async function discoverAndBindPort(defaultPort = 3939) {
132
+ try {
133
+ const port = await findAvailablePort(defaultPort);
134
+ await writePortFile(port);
135
+ registerPortCleanup();
136
+ return port;
137
+ }
138
+ catch (err) {
139
+ logger.warn('[PortDiscovery] Port discovery failed:', err);
140
+ return undefined;
141
+ }
142
+ }
143
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicG9ydERpc2NvdmVyeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy93ZWIvcG9ydERpc2NvdmVyeS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7R0FjRztBQUVILE9BQU8sRUFBRSxZQUFZLEVBQWUsTUFBTSxVQUFVLENBQUM7QUFDckQsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNsQyxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ2pDLE9BQU8sRUFBRSxLQUFLLEVBQUUsU0FBUyxFQUFFLE1BQU0sRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzVELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUU1QyxxREFBcUQ7QUFDckQsTUFBTSx5QkFBeUIsR0FBRyxFQUFFLENBQUM7QUFFckMsb0VBQW9FO0FBQ3BFLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxPQUFPLEVBQUUsRUFBRSxZQUFZLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFFckQsdUNBQXVDO0FBQ3ZDLElBQUksWUFBWSxHQUFrQixJQUFJLENBQUM7QUFFdkM7Ozs7R0FJRztBQUNILFNBQVMsV0FBVyxDQUFDLElBQVk7SUFDL0IsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQzdCLE1BQU0sTUFBTSxHQUFHLFlBQVksRUFBRSxDQUFDO1FBQzlCLE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUMsR0FBMEIsRUFBRSxFQUFFO1lBQ2xELElBQUksR0FBRyxDQUFDLElBQUksS0FBSyxZQUFZLEVBQUUsQ0FBQztnQkFDOUIsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ2hCLENBQUM7aUJBQU0sQ0FBQztnQkFDTixPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQ2QsTUFBTSxDQUFDLEtBQUssQ0FBQyw0Q0FBNEMsSUFBSSxLQUFL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a0JBQWtCLENBQUMsQ0FBQztJQUNsRSxDQUFDO0FBQ0gsQ0FBQztBQUVEOzs7R0FHRztBQUNILE1BQU0sVUFBVSxtQkFBbUI7SUFDakMsTUFBTSxXQUFXLEdBQUcsR0FBRyxFQUFFLEdBQUcsZUFBZSxFQUFFLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxHQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ2pFLE9BQU8sQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQ2xDLE9BQU8sQ0FBQyxJQUFJLENBQUMsU0FBUyxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQ3JDLE9BQU8sQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLFdBQVcsQ0FBQyxDQUFDO0FBQ3RDLENBQUM7QUFFRDs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsbUJBQW1CLENBQUMsY0FBc0IsSUFBSTtJQUNsRSxJQUFJLENBQUM7UUFDSCxNQUFNLElBQUksR0FBRyxNQUFNLGlCQUFpQixDQUFDLFdBQVcsQ0FBQyxDQUFDO1FBRWxELE1BQU0sYUFBYSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQzFCLG1CQUFtQixFQUFFLENBQUM7UUFFdEIsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztRQUNiLE1BQU0sQ0FBQyxJQUFJLENBQUMsd0NBQXdDLEVBQUUsR0FBRyxDQUFDLENBQUM7UUFDM0QsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztBQUNILENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIER5bmFtaWMgcG9ydCBhbGxvY2F0aW9uIGFuZCBwb3J0IGZpbGUgZGlzY292ZXJ5IGZvciB0aGUgd2ViIGNvbnNvbGUuXG4gKlxuICogV2hlbiBtdWx0aXBsZSBEb2xsaG91c2VNQ1Agc2Vzc2lvbnMgcnVuIHNpbXVsdGFuZW91c2x5IChlLmcuLCBtdWx0aXBsZVxuICogQ2xhdWRlIENvZGUgd2luZG93cywgSURFIGluc3RhbmNlcywgb3IgYWdlbnQgc2Vzc2lvbnMpLCBlYWNoIG5lZWRzIGl0c1xuICogb3duIHdlYiBjb25zb2xlIHBvcnQuIFRoaXMgbW9kdWxlIGhhbmRsZXM6XG4gKlxuICogMS4gRmluZGluZyBhbiBhdmFpbGFibGUgcG9ydCBzdGFydGluZyBmcm9tIHRoZSBkZWZhdWx0ICgzOTM5KVxuICogMi4gV3JpdGluZyBwb3J0IGRpc2NvdmVyeSBmaWxlcyBzbyBleHRlcm5hbCB0b29scyBrbm93IHdoaWNoIHBvcnQgdG8gdXNlXG4gKiAzLiBDbGVhbmluZyB1cCBwb3J0IGZpbGVzIG9uIHByb2Nlc3MgZXhpdFxuICpcbiAqIFBvcnQgZmlsZXMgYXJlIHdyaXR0ZW4gdG8gfi8uZG9sbGhvdXNlL3J1bi86XG4gKiAtIHBlcm1pc3Npb24tc2VydmVyLXtwaWR9LnBvcnQg4oCUIHBlci1wcm9jZXNzIGZpbGUgKGNsZWFuZWQgb24gZXhpdClcbiAqIC0gcGVybWlzc2lvbi1zZXJ2ZXIucG9ydCDigJQgbGF0ZXN0IHBvcnQgKGNvbnZlbmllbmNlIGZvciBzY3JpcHRzKVxuICovXG5cbmltcG9ydCB7IGNyZWF0ZVNlcnZlciwgdHlwZSBTZXJ2ZXIgfSBmcm9tICdub2RlOm5ldCc7XG5pbXBvcnQgeyBob21lZGlyIH0gZnJvbSAnbm9kZTpvcyc7XG5pbXBvcnQgeyBqb2luIH0gZnJvbSAnbm9kZTpwYXRoJztcbmltcG9ydCB7IG1rZGlyLCB3cml0ZUZpbGUsIHVubGluayB9IGZyb20gJ25vZGU6ZnMvcHJvbWlzZXMnO1xuaW1wb3J0IHsgbG9nZ2VyIH0gZnJvbSAnLi4vdXRpbHMvbG9nZ2VyLmpzJztcblxuLyoqIERlZmF1bHQgbWF4aW11bSBwb3J0IGF0dGVtcHRzIGJlZm9yZSBnaXZpbmcgdXAgKi9cbmNvbnN0IERFRkFVTFRfTUFYX1BPUlRfQVRURU1QVFMgPSAxMDtcblxuLyoqIERpcmVjdG9yeSBmb3IgcnVudGltZSBzdGF0ZSBmaWxlcyAocG9ydCBkaXNjb3ZlcnksIFBJRCBmaWxlcykgKi9cbmNvbnN0IFJVTl9ESVIgPSBqb2luKGhvbWVkaXIoKSwgJy5kb2xsaG91c2UnLCAncnVuJyk7XG5cbi8qKiBUcmFjayBwb3J0IGZpbGUgcGF0aCBmb3IgY2xlYW51cCAqL1xubGV0IHBvcnRGaWxlUGF0aDogc3RyaW5nIHwgbnVsbCA9IG51bGw7XG5cbi8qKlxuICogQXR0ZW1wdCB0byBiaW5kIHRvIGEgc2luZ2xlIHBvcnQuIFJldHVybnMgdGhlIHBvcnQgaWYgc3VjY2Vzc2Z1bCwgbnVsbCBpZiBpbiB1c2UuXG4gKiBLZWVwcyB0aGUgc2VydmVyIGxpc3RlbmluZyB0byBwcmV2ZW50IFRPQ1RPVSByYWNlIGNvbmRpdGlvbnMg4oCUIGNhbGxlciBtdXN0XG4gKiBjbG9zZSB0aGUgcmV0dXJuZWQgc2VydmVyIGFmdGVyIHRoZWlyIG93biBzZXJ2ZXIgYmluZHMuXG4gKi9cbmZ1bmN0aW9uIHRyeUJpbmRQb3J0KHBvcnQ6IG51bWJlcik6IFByb21pc2U8eyBwb3J0OiBudW1iZXI7IHNlcnZlcjogU2VydmVyIH0gfCBudWxsPiB7XG4gIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4ge1xuICAgIGNvbnN0IHNlcnZlciA9IGNyZWF0ZVNlcnZlcigpO1xuICAgIHNlcnZlci5vbmNlKCdlcnJvcicsIChlcnI6IE5vZGVKUy5FcnJub0V4Y2VwdGlvbikgPT4ge1xuICAgICAgaWYgKGVyci5jb2RlID09PSAnRUFERFJJTlVTRScpIHtcbiAgICAgICAgcmVzb2x2ZShudWxsKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJlc29sdmUobnVsbCk7XG4gICAgICAgIGxvZ2dlci5kZWJ1ZyhgW1BvcnREaXNjb3ZlcnldIFVuZXhwZWN0ZWQgZXJyb3Igb24gcG9ydCAke3BvcnR9OiAke2Vyci5jb2RlfWApO1xuICAgICAgfVxuICAgIH0pO1xuICAgIHNlcnZlci5vbmNlKCdsaXN0ZW5pbmcnLCAoKSA9PiB7XG4gICAgICByZXNvbHZlKHsgcG9ydCwgc2VydmVyIH0pO1xuICAgIH0pO1xuICAgIHNlcnZlci5saXN0ZW4ocG9ydCwgJzEyNy4wLjAuMScpO1xuICB9KTtcbn1cblxuLyoqXG4gKiBGaW5kIGFuIGF2YWlsYWJsZSBwb3J0IHN0YXJ0aW5nIGZyb20gdGhlIGdpdmVuIHBvcnQuXG4gKlxuICogVHJpZXMgc2VxdWVudGlhbCBwb3J0cywgbG9nZ2luZyBlYWNoIGF0dGVtcHQuIFJldHVybnMgYm90aCB0aGUgcG9ydCBudW1iZXJcbiAqIGFuZCBhIGhlbGQgc2VydmVyIHRvIHByZXZlbnQgVE9DVE9VIHJhY2UgY29uZGl0aW9ucy4gVGhlIGNhbGxlciBzaG91bGRcbiAqIGNsb3NlIHRoZSBoZWxkIHNlcnZlciBhZnRlciBiaW5kaW5nIHRoZWlyIG93biBFeHByZXNzIGFwcCB0byB0aGUgcG9ydC5cbiAqXG4gKiBAcGFyYW0gc3RhcnRQb3J0IC0gUG9ydCB0byB0cnkgZmlyc3QgKGRlZmF1bHQ6IDM5MzkpXG4gKiBAcGFyYW0gbWF4QXR0ZW1wdHMgLSBNYXhpbXVtIHBvcnRzIHRvIHRyeSAoZGVmYXVsdDogMTAsIGNvbmZpZ3VyYWJsZSB2aWEgRE9MTEhPVVNFX01BWF9QT1JUX0FUVEVNUFRTKVxuICogQHJldHVybnMgT2JqZWN0IHdpdGggcG9ydCBudW1iZXIgYW5kIGhlbGQgc2VydmVyLCBvciB0aHJvd3MgaWYgYWxsIGF0dGVtcHRzIGZhaWxcbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGZpbmRBdmFpbGFibGVQb3J0KFxuICBzdGFydFBvcnQ6IG51bWJlcixcbiAgbWF4QXR0ZW1wdHM6IG51bWJlciA9IE51bWJlcihwcm9jZXNzLmVudi5ET0xMSE9VU0VfTUFYX1BPUlRfQVRURU1QVFMpIHx8IERFRkFVTFRfTUFYX1BPUlRfQVRURU1QVFMsXG4pOiBQcm9taXNlPG51bWJlcj4ge1xuICBmb3IgKGxldCBhdHRlbXB0ID0gMDsgYXR0ZW1wdCA8IG1heEF0dGVtcHRzOyBhdHRlbXB0KyspIHtcbiAgICBjb25zdCBwb3J0ID0gc3RhcnRQb3J0ICsgYXR0ZW1wdDtcbiAgICBsb2dnZXIuZGVidWcoYFtQb3J0RGlzY292ZXJ5XSBUcnlpbmcgcG9ydCAke3BvcnR9IChhdHRlbXB0ICR7YXR0ZW1wdCArIDF9LyR7bWF4QXR0ZW1wdHN9KWApO1xuXG4gICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgdHJ5QmluZFBvcnQocG9ydCk7XG4gICAgaWYgKHJlc3VsdCkge1xuICAgICAgLy8gQ2xvc2UgdGhlIHByb2JlIHNlcnZlciDigJQgdGhlcmUncyBhIGJyaWVmIFRPQ1RPVSB3aW5kb3cgaGVyZSBiZXR3ZWVuXG4gICAgICAvLyBjbG9zaW5nIGFuZCB0aGUgY2FsbGVyIGJpbmRpbmcsIGJ1dCBpdCdzIG5lZ2xpZ2libGUgb24gbG9jYWxob3N0LlxuICAgICAgLy8gQSBmdXR1cmUgaW1wcm92ZW1lbnQgY291bGQgcmV0dXJuIHRoZSBzZXJ2ZXIgZm9yIHRoZSBjYWxsZXIgdG8gYWRvcHQuXG4gICAgICByZXN1bHQuc2VydmVyLmNsb3NlKCk7XG4gICAgICBpZiAoYXR0ZW1wdCA+IDApIHtcbiAgICAgICAgbG9nZ2VyLmluZm8oYFtQb3J0RGlzY292ZXJ5XSBQb3J0ICR7c3RhcnRQb3J0fSBpbiB1c2UsIHVzaW5nICR7cG9ydH0gKGFmdGVyICR7YXR0ZW1wdH0gc2tpcCR7YXR0ZW1wdCA+IDEgPyAncycgOiAnJ30pYCk7XG4gICAgICB9XG4gICAgICByZXR1cm4gcG9ydDtcbiAgICB9XG4gICAgbG9nZ2VyLmRlYnVnKGBbUG9ydERpc2NvdmVyeV0gUG9ydCAke3BvcnR9IGluIHVzZSwgdHJ5aW5nIG5leHRgKTtcbiAgfVxuXG4gIHRocm93IG5ldyBFcnJvcihcbiAgICBgTm8gYXZhaWxhYmxlIHBvcnQgZm91bmQgaW4gcmFuZ2UgJHtzdGFydFBvcnR9LSR7c3RhcnRQb3J0ICsgbWF4QXR0ZW1wdHMgLSAxfSBgICtcbiAgICBgYWZ0ZXIgJHttYXhBdHRlbXB0c30gYXR0ZW1wdHMuIFNldCBET0xMSE9VU0VfTUFYX1BPUlRfQVRURU1QVFMgdG8gaW5jcmVhc2UgdGhlIHJhbmdlLmBcbiAgKTtcbn1cblxuLyoqXG4gKiBXcml0ZSB0aGUgYWN0aXZlIHNlcnZlciBwb3J0IHRvIGEgZGlzY292ZXJhYmxlIGZpbGUuXG4gKlxuICogQ3JlYXRlcyB0d28gZmlsZXM6XG4gKiAtIFBJRC1rZXllZCBmaWxlIGZvciBwZXItcHJvY2VzcyBjbGVhbnVwXG4gKiAtIExhdGVzdCBmaWxlIGZvciBjb252ZW5pZW5jZSAoc2NyaXB0cyBjYW4gcmVhZCB0aGlzIHdpdGhvdXQga25vd2luZyB0aGUgUElEKVxuICpcbiAqIEBwYXJhbSBwb3J0IC0gVGhlIHBvcnQgdGhlIHdlYiBzZXJ2ZXIgaXMgbGlzdGVuaW5nIG9uXG4gKiBAcmV0dXJucyBQYXRoIHRvIHRoZSBQSUQta2V5ZWQgcG9ydCBmaWxlXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiB3cml0ZVBvcnRGaWxlKHBvcnQ6IG51bWJlcik6IFByb21pc2U8c3RyaW5nPiB7XG4gIGF3YWl0IG1rZGlyKFJVTl9ESVIsIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xuICBjb25zdCBwaWRGaWxlID0gam9pbihSVU5fRElSLCBgcGVybWlzc2lvbi1zZXJ2ZXItJHtwcm9jZXNzLnBpZH0ucG9ydGApO1xuICBjb25zdCBsYXRlc3RGaWxlID0gam9pbihSVU5fRElSLCAncGVybWlzc2lvbi1zZXJ2ZXIucG9ydCcpO1xuICBhd2FpdCB3cml0ZUZpbGUocGlkRmlsZSwgU3RyaW5nKHBvcnQpLCAndXRmLTgnKTtcbiAgYXdhaXQgd3JpdGVGaWxlKGxhdGVzdEZpbGUsIFN0cmluZyhwb3J0KSwgJ3V0Zi04Jyk7XG4gIHBvcnRGaWxlUGF0aCA9IHBpZEZpbGU7XG4gIGxvZ2dlci5kZWJ1ZyhgW1BvcnREaXNjb3ZlcnldIFBvcnQgZmlsZSB3cml0dGVuOiAke3BpZEZpbGV9YCk7XG4gIHJldHVybiBwaWRGaWxlO1xufVxuXG4vKipcbiAqIENsZWFuIHVwIHBvcnQgZmlsZSBvbiBzaHV0ZG93bi5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNsZWFudXBQb3J0RmlsZSgpOiBQcm9taXNlPHZvaWQ+IHtcbiAgaWYgKHBvcnRGaWxlUGF0aCkge1xuICAgIHRyeSB7IGF3YWl0IHVubGluayhwb3J0RmlsZVBhdGgpOyB9IGNhdGNoIHsgLyogYWxyZWFkeSBnb25lICovIH1cbiAgfVxufVxuXG4vKipcbiAqIFJlZ2lzdGVyIHByb2Nlc3MgZXhpdCBoYW5kbGVycyB0byBjbGVhbiB1cCBwb3J0IGZpbGVzLlxuICogU2hvdWxkIGJlIGNhbGxlZCBvbmNlIGFmdGVyIHRoZSB3ZWIgc2VydmVyIHN1Y2Nlc3NmdWxseSBzdGFydHMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZWdpc3RlclBvcnRDbGVhbnVwKCk6IHZvaWQge1xuICBjb25zdCBleGl0Q2xlYW51cCA9ICgpID0+IHsgY2xlYW51cFBvcnRGaWxlKCkuY2F0Y2goKCkgPT4ge30pOyB9O1xuICBwcm9jZXNzLm9uY2UoJ2V4aXQnLCBleGl0Q2xlYW51cCk7XG4gIHByb2Nlc3Mub25jZSgnU0lHVEVSTScsIGV4aXRDbGVhbnVwKTtcbiAgcHJvY2Vzcy5vbmNlKCdTSUdJTlQnLCBleGl0Q2xlYW51cCk7XG59XG5cbi8qKlxuICogRGlzY292ZXIgYW4gYXZhaWxhYmxlIHBvcnQsIHdyaXRlIGRpc2NvdmVyeSBmaWxlcywgYW5kIHJlZ2lzdGVyIGNsZWFudXAuXG4gKlxuICogVGhpcyBpcyB0aGUgcmVjb21tZW5kZWQgZW50cnkgcG9pbnQgZm9yIENvbnRhaW5lciBzdGFydHVwLiBDb21iaW5lc1xuICogcG9ydCBkaXNjb3ZlcnksIGZpbGUgd3JpdGluZywgYW5kIGNsZWFudXAgcmVnaXN0cmF0aW9uIGluIG9uZSBjYWxsLlxuICpcbiAqIEBwYXJhbSBkZWZhdWx0UG9ydCAtIFBvcnQgdG8gdHJ5IGZpcnN0IChkZWZhdWx0OiAzOTM5KVxuICogQHJldHVybnMgVGhlIHBvcnQgdGhlIHNlcnZlciBzaG91bGQgYmluZCB0bywgb3IgdW5kZWZpbmVkIGlmIGRpc2NvdmVyeSBmYWlsZWRcbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGRpc2NvdmVyQW5kQmluZFBvcnQoZGVmYXVsdFBvcnQ6IG51bWJlciA9IDM5MzkpOiBQcm9taXNlPG51bWJlciB8IHVuZGVmaW5lZD4ge1xuICB0cnkge1xuICAgIGNvbnN0IHBvcnQgPSBhd2FpdCBmaW5kQXZhaWxhYmxlUG9ydChkZWZhdWx0UG9ydCk7XG5cbiAgICBhd2FpdCB3cml0ZVBvcnRGaWxlKHBvcnQpO1xuICAgIHJlZ2lzdGVyUG9ydENsZWFudXAoKTtcblxuICAgIHJldHVybiBwb3J0O1xuICB9IGNhdGNoIChlcnIpIHtcbiAgICBsb2dnZXIud2FybignW1BvcnREaXNjb3ZlcnldIFBvcnQgZGlzY292ZXJ5IGZhaWxlZDonLCBlcnIpO1xuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbn1cbiJdfQ==