network-ai 4.15.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/INTEGRATION_GUIDE.md +11 -4
  2. package/QUICKSTART.md +31 -4
  3. package/README.md +37 -15
  4. package/bin/dashboard.ts +146 -0
  5. package/bin/mcp-server.ts +3 -2
  6. package/dist/adapters/adapter-registry.d.ts +33 -1
  7. package/dist/adapters/adapter-registry.d.ts.map +1 -1
  8. package/dist/adapters/adapter-registry.js +49 -0
  9. package/dist/adapters/adapter-registry.js.map +1 -1
  10. package/dist/adapters/anthropic-computer-use-adapter.d.ts +132 -0
  11. package/dist/adapters/anthropic-computer-use-adapter.d.ts.map +1 -0
  12. package/dist/adapters/anthropic-computer-use-adapter.js +180 -0
  13. package/dist/adapters/anthropic-computer-use-adapter.js.map +1 -0
  14. package/dist/adapters/browser-agent-adapter.d.ts +121 -0
  15. package/dist/adapters/browser-agent-adapter.d.ts.map +1 -0
  16. package/dist/adapters/browser-agent-adapter.js +219 -0
  17. package/dist/adapters/browser-agent-adapter.js.map +1 -0
  18. package/dist/adapters/copilot-adapter.d.ts +59 -0
  19. package/dist/adapters/copilot-adapter.d.ts.map +1 -0
  20. package/dist/adapters/copilot-adapter.js +132 -0
  21. package/dist/adapters/copilot-adapter.js.map +1 -0
  22. package/dist/adapters/index.d.ts +15 -1
  23. package/dist/adapters/index.d.ts.map +1 -1
  24. package/dist/adapters/index.js +22 -1
  25. package/dist/adapters/index.js.map +1 -1
  26. package/dist/adapters/langgraph-adapter.d.ts +70 -0
  27. package/dist/adapters/langgraph-adapter.d.ts.map +1 -0
  28. package/dist/adapters/langgraph-adapter.js +119 -0
  29. package/dist/adapters/langgraph-adapter.js.map +1 -0
  30. package/dist/adapters/openai-agents-adapter.d.ts +100 -0
  31. package/dist/adapters/openai-agents-adapter.d.ts.map +1 -0
  32. package/dist/adapters/openai-agents-adapter.js +118 -0
  33. package/dist/adapters/openai-agents-adapter.js.map +1 -0
  34. package/dist/adapters/pydantic-ai-adapter.d.ts +104 -0
  35. package/dist/adapters/pydantic-ai-adapter.d.ts.map +1 -0
  36. package/dist/adapters/pydantic-ai-adapter.js +163 -0
  37. package/dist/adapters/pydantic-ai-adapter.js.map +1 -0
  38. package/dist/adapters/vertex-ai-adapter.d.ts +122 -0
  39. package/dist/adapters/vertex-ai-adapter.d.ts.map +1 -0
  40. package/dist/adapters/vertex-ai-adapter.js +166 -0
  41. package/dist/adapters/vertex-ai-adapter.js.map +1 -0
  42. package/dist/bin/dashboard.d.ts +11 -0
  43. package/dist/bin/dashboard.d.ts.map +1 -0
  44. package/dist/bin/dashboard.js +135 -0
  45. package/dist/bin/dashboard.js.map +1 -0
  46. package/dist/bin/mcp-server.js +3 -2
  47. package/dist/bin/mcp-server.js.map +1 -1
  48. package/dist/index.d.ts +103 -559
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +295 -1074
  51. package/dist/index.js.map +1 -1
  52. package/dist/lib/adapter-test-harness.d.ts +88 -0
  53. package/dist/lib/adapter-test-harness.d.ts.map +1 -0
  54. package/dist/lib/adapter-test-harness.js +118 -0
  55. package/dist/lib/adapter-test-harness.js.map +1 -0
  56. package/dist/lib/agent-conversation.d.ts +115 -0
  57. package/dist/lib/agent-conversation.d.ts.map +1 -0
  58. package/dist/lib/agent-conversation.js +155 -0
  59. package/dist/lib/agent-conversation.js.map +1 -0
  60. package/dist/lib/agent-debate.d.ts +115 -0
  61. package/dist/lib/agent-debate.d.ts.map +1 -0
  62. package/dist/lib/agent-debate.js +146 -0
  63. package/dist/lib/agent-debate.js.map +1 -0
  64. package/dist/lib/agent-memory.d.ts +157 -0
  65. package/dist/lib/agent-memory.d.ts.map +1 -0
  66. package/dist/lib/agent-memory.js +336 -0
  67. package/dist/lib/agent-memory.js.map +1 -0
  68. package/dist/lib/agent-vcr.d.ts +133 -0
  69. package/dist/lib/agent-vcr.d.ts.map +1 -0
  70. package/dist/lib/agent-vcr.js +218 -0
  71. package/dist/lib/agent-vcr.js.map +1 -0
  72. package/dist/lib/anomaly-detector.d.ts +112 -0
  73. package/dist/lib/anomaly-detector.d.ts.map +1 -0
  74. package/dist/lib/anomaly-detector.js +178 -0
  75. package/dist/lib/anomaly-detector.js.map +1 -0
  76. package/dist/lib/approval-inbox.d.ts +147 -0
  77. package/dist/lib/approval-inbox.d.ts.map +1 -0
  78. package/dist/lib/approval-inbox.js +385 -0
  79. package/dist/lib/approval-inbox.js.map +1 -0
  80. package/dist/lib/auth-guardian.d.ts +170 -0
  81. package/dist/lib/auth-guardian.d.ts.map +1 -0
  82. package/dist/lib/auth-guardian.js +604 -0
  83. package/dist/lib/auth-guardian.js.map +1 -0
  84. package/dist/lib/auth-validator.d.ts +70 -0
  85. package/dist/lib/auth-validator.d.ts.map +1 -0
  86. package/dist/lib/auth-validator.js +32 -0
  87. package/dist/lib/auth-validator.js.map +1 -0
  88. package/dist/lib/blackboard-validator.d.ts +56 -0
  89. package/dist/lib/blackboard-validator.d.ts.map +1 -1
  90. package/dist/lib/blackboard-validator.js +181 -4
  91. package/dist/lib/blackboard-validator.js.map +1 -1
  92. package/dist/lib/comparison-runner.d.ts +99 -0
  93. package/dist/lib/comparison-runner.d.ts.map +1 -0
  94. package/dist/lib/comparison-runner.js +138 -0
  95. package/dist/lib/comparison-runner.js.map +1 -0
  96. package/dist/lib/config-watcher.d.ts +109 -0
  97. package/dist/lib/config-watcher.d.ts.map +1 -0
  98. package/dist/lib/config-watcher.js +215 -0
  99. package/dist/lib/config-watcher.js.map +1 -0
  100. package/dist/lib/cost-governor.d.ts +105 -0
  101. package/dist/lib/cost-governor.d.ts.map +1 -0
  102. package/dist/lib/cost-governor.js +128 -0
  103. package/dist/lib/cost-governor.js.map +1 -0
  104. package/dist/lib/cost-heatmap.d.ts +104 -0
  105. package/dist/lib/cost-heatmap.d.ts.map +1 -0
  106. package/dist/lib/cost-heatmap.js +161 -0
  107. package/dist/lib/cost-heatmap.js.map +1 -0
  108. package/dist/lib/coverage-reporter.d.ts +92 -0
  109. package/dist/lib/coverage-reporter.d.ts.map +1 -0
  110. package/dist/lib/coverage-reporter.js +177 -0
  111. package/dist/lib/coverage-reporter.js.map +1 -0
  112. package/dist/lib/dashboard-server.d.ts +71 -0
  113. package/dist/lib/dashboard-server.d.ts.map +1 -0
  114. package/dist/lib/dashboard-server.js +403 -0
  115. package/dist/lib/dashboard-server.js.map +1 -0
  116. package/dist/lib/dry-run.d.ts +73 -0
  117. package/dist/lib/dry-run.d.ts.map +1 -0
  118. package/dist/lib/dry-run.js +130 -0
  119. package/dist/lib/dry-run.js.map +1 -0
  120. package/dist/lib/errors.d.ts +15 -0
  121. package/dist/lib/errors.d.ts.map +1 -1
  122. package/dist/lib/errors.js +38 -0
  123. package/dist/lib/errors.js.map +1 -1
  124. package/dist/lib/event-bus.d.ts +167 -0
  125. package/dist/lib/event-bus.d.ts.map +1 -0
  126. package/dist/lib/event-bus.js +229 -0
  127. package/dist/lib/event-bus.js.map +1 -0
  128. package/dist/lib/explainability.d.ts +85 -0
  129. package/dist/lib/explainability.d.ts.map +1 -0
  130. package/dist/lib/explainability.js +102 -0
  131. package/dist/lib/explainability.js.map +1 -0
  132. package/dist/lib/goal-dsl.d.ts +157 -0
  133. package/dist/lib/goal-dsl.d.ts.map +1 -0
  134. package/dist/lib/goal-dsl.js +392 -0
  135. package/dist/lib/goal-dsl.js.map +1 -0
  136. package/dist/lib/job-queue.d.ts +183 -0
  137. package/dist/lib/job-queue.d.ts.map +1 -0
  138. package/dist/lib/job-queue.js +310 -0
  139. package/dist/lib/job-queue.js.map +1 -0
  140. package/dist/lib/learning-loop.d.ts +113 -0
  141. package/dist/lib/learning-loop.d.ts.map +1 -0
  142. package/dist/lib/learning-loop.js +181 -0
  143. package/dist/lib/learning-loop.js.map +1 -0
  144. package/dist/lib/lifecycle-hooks.d.ts +116 -0
  145. package/dist/lib/lifecycle-hooks.d.ts.map +1 -0
  146. package/dist/lib/lifecycle-hooks.js +148 -0
  147. package/dist/lib/lifecycle-hooks.js.map +1 -0
  148. package/dist/lib/locked-blackboard.d.ts.map +1 -1
  149. package/dist/lib/locked-blackboard.js +9 -5
  150. package/dist/lib/locked-blackboard.js.map +1 -1
  151. package/dist/lib/mcp-tool-consumer.d.ts +153 -0
  152. package/dist/lib/mcp-tool-consumer.d.ts.map +1 -0
  153. package/dist/lib/mcp-tool-consumer.js +320 -0
  154. package/dist/lib/mcp-tool-consumer.js.map +1 -0
  155. package/dist/lib/metrics.d.ts +119 -0
  156. package/dist/lib/metrics.d.ts.map +1 -0
  157. package/dist/lib/metrics.js +284 -0
  158. package/dist/lib/metrics.js.map +1 -0
  159. package/dist/lib/orchestrator-types.d.ts +309 -0
  160. package/dist/lib/orchestrator-types.d.ts.map +1 -0
  161. package/dist/lib/orchestrator-types.js +61 -0
  162. package/dist/lib/orchestrator-types.js.map +1 -0
  163. package/dist/lib/otel-bridge.d.ts +74 -0
  164. package/dist/lib/otel-bridge.d.ts.map +1 -0
  165. package/dist/lib/otel-bridge.js +167 -0
  166. package/dist/lib/otel-bridge.js.map +1 -0
  167. package/dist/lib/playground.d.ts +76 -0
  168. package/dist/lib/playground.d.ts.map +1 -0
  169. package/dist/lib/playground.js +224 -0
  170. package/dist/lib/playground.js.map +1 -0
  171. package/dist/lib/quadtree.d.ts +114 -0
  172. package/dist/lib/quadtree.d.ts.map +1 -0
  173. package/dist/lib/quadtree.js +259 -0
  174. package/dist/lib/quadtree.js.map +1 -0
  175. package/dist/lib/shared-blackboard.d.ts +101 -0
  176. package/dist/lib/shared-blackboard.d.ts.map +1 -0
  177. package/dist/lib/shared-blackboard.js +249 -0
  178. package/dist/lib/shared-blackboard.js.map +1 -0
  179. package/dist/lib/speculative-executor.d.ts +89 -0
  180. package/dist/lib/speculative-executor.d.ts.map +1 -0
  181. package/dist/lib/speculative-executor.js +107 -0
  182. package/dist/lib/speculative-executor.js.map +1 -0
  183. package/dist/lib/swarm-transport.d.ts +150 -0
  184. package/dist/lib/swarm-transport.d.ts.map +1 -0
  185. package/dist/lib/swarm-transport.js +307 -0
  186. package/dist/lib/swarm-transport.js.map +1 -0
  187. package/dist/lib/task-decomposer.d.ts +41 -0
  188. package/dist/lib/task-decomposer.d.ts.map +1 -0
  189. package/dist/lib/task-decomposer.js +272 -0
  190. package/dist/lib/task-decomposer.js.map +1 -0
  191. package/dist/lib/timeline-scrubber.d.ts +84 -0
  192. package/dist/lib/timeline-scrubber.d.ts.map +1 -0
  193. package/dist/lib/timeline-scrubber.js +173 -0
  194. package/dist/lib/timeline-scrubber.js.map +1 -0
  195. package/dist/lib/topology.d.ts +361 -0
  196. package/dist/lib/topology.d.ts.map +1 -0
  197. package/dist/lib/topology.js +591 -0
  198. package/dist/lib/topology.js.map +1 -0
  199. package/dist/security.d.ts +95 -0
  200. package/dist/security.d.ts.map +1 -1
  201. package/dist/security.js +267 -5
  202. package/dist/security.js.map +1 -1
  203. package/package.json +7 -5
  204. package/types/agent-adapter.d.ts +5 -0
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ /**
3
+ * CoverageReporter — Lightweight code coverage reporting without external deps
4
+ *
5
+ * Integrates with Node.js built-in V8 coverage (`NODE_V8_COVERAGE`).
6
+ * Parses V8 coverage JSON output into a structured report with file-level
7
+ * line and function coverage metrics. Supports thresholds and reporters.
8
+ *
9
+ * Usage:
10
+ * // Run tests with V8 coverage:
11
+ * // NODE_V8_COVERAGE=./coverage npx ts-node test.ts
12
+ *
13
+ * const reporter = new CoverageReporter({ coverageDir: './coverage' });
14
+ * const report = await reporter.collect();
15
+ * reporter.printSummary(report);
16
+ * reporter.enforce(report, { linePct: 80, branchPct: 70 });
17
+ *
18
+ * @module CoverageReporter
19
+ * @version 1.0.0
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.CoverageReporter = void 0;
23
+ const promises_1 = require("fs/promises");
24
+ const path_1 = require("path");
25
+ // ============================================================================
26
+ // REPORTER
27
+ // ============================================================================
28
+ /**
29
+ * CoverageReporter — Collects and reports V8 code coverage data.
30
+ */
31
+ class CoverageReporter {
32
+ config;
33
+ constructor(config) {
34
+ this.config = {
35
+ coverageDir: (0, path_1.resolve)(config.coverageDir),
36
+ projectRoot: config.projectRoot ?? process.cwd(),
37
+ include: config.include ?? ['.ts', '.js'],
38
+ exclude: config.exclude ?? ['node_modules', 'test-', 'dist/'],
39
+ };
40
+ }
41
+ /**
42
+ * Collect coverage data from V8 coverage directory.
43
+ */
44
+ async collect() {
45
+ const files = [];
46
+ const coverageFiles = await this.findCoverageFiles();
47
+ for (const cf of coverageFiles) {
48
+ const raw = await (0, promises_1.readFile)(cf, 'utf-8');
49
+ const data = JSON.parse(raw);
50
+ for (const script of data.result) {
51
+ if (!this.shouldInclude(script.url))
52
+ continue;
53
+ const relPath = this.toRelativePath(script.url);
54
+ const existing = files.find((f) => f.file === relPath);
55
+ if (existing)
56
+ continue; // Deduplicate
57
+ const totalFunctions = script.functions.length;
58
+ const coveredFunctions = script.functions.filter((fn) => fn.ranges.some((r) => r.count > 0)).length;
59
+ let totalRanges = 0;
60
+ let coveredRanges = 0;
61
+ for (const fn of script.functions) {
62
+ for (const range of fn.ranges) {
63
+ totalRanges++;
64
+ if (range.count > 0)
65
+ coveredRanges++;
66
+ }
67
+ }
68
+ files.push({
69
+ file: relPath,
70
+ totalFunctions,
71
+ coveredFunctions,
72
+ functionPct: totalFunctions > 0 ? round((coveredFunctions / totalFunctions) * 100) : 100,
73
+ totalRanges,
74
+ coveredRanges,
75
+ rangePct: totalRanges > 0 ? round((coveredRanges / totalRanges) * 100) : 100,
76
+ });
77
+ }
78
+ }
79
+ files.sort((a, b) => a.file.localeCompare(b.file));
80
+ const totalFns = files.reduce((s, f) => s + f.totalFunctions, 0);
81
+ const coveredFns = files.reduce((s, f) => s + f.coveredFunctions, 0);
82
+ const totalRng = files.reduce((s, f) => s + f.totalRanges, 0);
83
+ const coveredRng = files.reduce((s, f) => s + f.coveredRanges, 0);
84
+ return {
85
+ files,
86
+ totalFunctionPct: totalFns > 0 ? round((coveredFns / totalFns) * 100) : 100,
87
+ totalRangePct: totalRng > 0 ? round((coveredRng / totalRng) * 100) : 100,
88
+ fileCount: files.length,
89
+ generatedAt: Date.now(),
90
+ };
91
+ }
92
+ /**
93
+ * Print a summary table to console.
94
+ */
95
+ printSummary(report) {
96
+ const header = 'File'.padEnd(50) + 'Functions'.padEnd(15) + 'Ranges';
97
+ console.log('─'.repeat(75));
98
+ console.log(header);
99
+ console.log('─'.repeat(75));
100
+ for (const f of report.files) {
101
+ const name = f.file.length > 48 ? '…' + f.file.slice(-47) : f.file;
102
+ const fnCol = `${f.coveredFunctions}/${f.totalFunctions} (${f.functionPct}%)`.padEnd(15);
103
+ const rgCol = `${f.coveredRanges}/${f.totalRanges} (${f.rangePct}%)`;
104
+ console.log(name.padEnd(50) + fnCol + rgCol);
105
+ }
106
+ console.log('─'.repeat(75));
107
+ console.log(`Total: ${report.fileCount} files | ` +
108
+ `Functions: ${report.totalFunctionPct}% | ` +
109
+ `Ranges: ${report.totalRangePct}%`);
110
+ console.log('─'.repeat(75));
111
+ }
112
+ /**
113
+ * Enforce coverage thresholds. Throws if below threshold.
114
+ */
115
+ enforce(report, thresholds) {
116
+ const failures = [];
117
+ if (thresholds.functionPct !== undefined && report.totalFunctionPct < thresholds.functionPct) {
118
+ failures.push(`Function coverage ${report.totalFunctionPct}% below threshold ${thresholds.functionPct}%`);
119
+ }
120
+ if (thresholds.rangePct !== undefined && report.totalRangePct < thresholds.rangePct) {
121
+ failures.push(`Range coverage ${report.totalRangePct}% below threshold ${thresholds.rangePct}%`);
122
+ }
123
+ if (thresholds.perFileFunctionPct !== undefined) {
124
+ for (const f of report.files) {
125
+ if (f.functionPct < thresholds.perFileFunctionPct) {
126
+ failures.push(`${f.file}: function coverage ${f.functionPct}% below per-file threshold ${thresholds.perFileFunctionPct}%`);
127
+ }
128
+ }
129
+ }
130
+ if (failures.length > 0) {
131
+ throw new Error(`Coverage thresholds not met:\n${failures.join('\n')}`);
132
+ }
133
+ }
134
+ // --------------------------------------------------------------------------
135
+ // Internal
136
+ // --------------------------------------------------------------------------
137
+ async findCoverageFiles() {
138
+ const results = [];
139
+ try {
140
+ const entries = await (0, promises_1.readdir)(this.config.coverageDir);
141
+ for (const entry of entries) {
142
+ if (entry.endsWith('.json')) {
143
+ results.push((0, path_1.join)(this.config.coverageDir, entry));
144
+ }
145
+ }
146
+ }
147
+ catch {
148
+ // Directory doesn't exist or not readable
149
+ }
150
+ return results;
151
+ }
152
+ shouldInclude(url) {
153
+ if (!url || url.startsWith('node:'))
154
+ return false;
155
+ const hasInclude = this.config.include.some((p) => url.includes(p));
156
+ const hasExclude = this.config.exclude.some((p) => url.includes(p));
157
+ return hasInclude && !hasExclude;
158
+ }
159
+ toRelativePath(url) {
160
+ let path = url;
161
+ // Strip file:// prefix
162
+ if (path.startsWith('file://')) {
163
+ path = path.slice(7);
164
+ }
165
+ // On Windows, strip leading slash before drive letter
166
+ if (/^\/[A-Za-z]:/.test(path)) {
167
+ path = path.slice(1);
168
+ }
169
+ return (0, path_1.relative)(this.config.projectRoot, path).replace(/\\/g, '/');
170
+ }
171
+ }
172
+ exports.CoverageReporter = CoverageReporter;
173
+ /** Round to 1 decimal */
174
+ function round(n) {
175
+ return Math.round(n * 10) / 10;
176
+ }
177
+ //# sourceMappingURL=coverage-reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage-reporter.js","sourceRoot":"","sources":["../../lib/coverage-reporter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Dashboard Server — Serves the live agent topology dashboard
3
+ *
4
+ * A zero-dependency HTTP + WebSocket server that streams topology events
5
+ * to a browser-based visualization. Uses only Node.js built-in modules.
6
+ *
7
+ * @module DashboardServer
8
+ * @version 1.0.0
9
+ */
10
+ import { EventEmitter } from 'events';
11
+ import type { TopologyTracker } from './topology';
12
+ /** Options for creating a DashboardServer */
13
+ export interface DashboardServerOptions {
14
+ /** TCP port to listen on (default: 4820) */
15
+ port?: number;
16
+ /** Hostname to bind to (default: '127.0.0.1') */
17
+ host?: string;
18
+ /** Whether to open the browser automatically (default: true) */
19
+ open?: boolean;
20
+ }
21
+ /**
22
+ * HTTP + WebSocket server for the live agent topology dashboard.
23
+ *
24
+ * Usage:
25
+ * ```typescript
26
+ * const topo = new TopologyTracker();
27
+ * const dashboard = new DashboardServer(topo, { port: 4820 });
28
+ * await dashboard.start();
29
+ * // Dashboard available at http://127.0.0.1:4820
30
+ * ```
31
+ */
32
+ export declare class DashboardServer extends EventEmitter {
33
+ private readonly tracker;
34
+ private readonly port;
35
+ private readonly host;
36
+ private server;
37
+ private clients;
38
+ private pingInterval;
39
+ private eventHandler;
40
+ private clientCounter;
41
+ private deltaPending;
42
+ constructor(tracker: TopologyTracker, options?: DashboardServerOptions);
43
+ /**
44
+ * Start the dashboard server.
45
+ */
46
+ start(): Promise<void>;
47
+ /**
48
+ * Stop the dashboard server.
49
+ */
50
+ stop(): Promise<void>;
51
+ /**
52
+ * Number of connected WebSocket clients.
53
+ */
54
+ clientCount(): number;
55
+ /**
56
+ * The URL the dashboard is serving on.
57
+ */
58
+ get url(): string;
59
+ private handleHTTP;
60
+ private handleUpgrade;
61
+ private handleClientMessage;
62
+ private broadcast;
63
+ /**
64
+ * Broadcast a delta patch to all connected clients.
65
+ * Each client tracks its own lastSeq so we can compute per-client deltas.
66
+ * For simplicity, we use a single shared delta and reset after broadcast.
67
+ */
68
+ private broadcastDelta;
69
+ private pingClients;
70
+ }
71
+ //# sourceMappingURL=dashboard-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../../lib/dashboard-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,KAAK,EAAE,eAAe,EAAkD,MAAM,YAAY,CAAC;AAMlG,6CAA6C;AAC7C,MAAM,WAAW,sBAAsB;IACrC,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA+FD;;;;;;;;;;GAUG;AACH,qBAAa,eAAgB,SAAQ,YAAY;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAgD;IAC9D,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,YAAY,CAAiD;IACrE,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,YAAY,CAAS;gBAEjB,OAAO,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAOtE;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B3B;;OAEG;IACH,WAAW,IAAI,MAAM;IAIrB;;OAEG;IACH,IAAI,GAAG,IAAI,MAAM,CAEhB;IAMD,OAAO,CAAC,UAAU;IA8ClB,OAAO,CAAC,aAAa;IAoGrB,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,SAAS;IAMjB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,WAAW;CAqBpB"}
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ /**
3
+ * Dashboard Server — Serves the live agent topology dashboard
4
+ *
5
+ * A zero-dependency HTTP + WebSocket server that streams topology events
6
+ * to a browser-based visualization. Uses only Node.js built-in modules.
7
+ *
8
+ * @module DashboardServer
9
+ * @version 1.0.0
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.DashboardServer = void 0;
13
+ const http_1 = require("http");
14
+ const crypto_1 = require("crypto");
15
+ const events_1 = require("events");
16
+ const fs_1 = require("fs");
17
+ const path_1 = require("path");
18
+ // ============================================================================
19
+ // WEBSOCKET HELPERS (RFC 6455, minimal implementation)
20
+ // ============================================================================
21
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
22
+ function computeAcceptKey(key) {
23
+ return (0, crypto_1.createHash)('sha1').update(key + WS_GUID).digest('base64');
24
+ }
25
+ function encodeWSFrame(data) {
26
+ const payload = Buffer.from(data, 'utf8');
27
+ const len = payload.length;
28
+ let header;
29
+ if (len < 126) {
30
+ header = Buffer.alloc(2);
31
+ header[0] = 0x81; // FIN + text opcode
32
+ header[1] = len;
33
+ }
34
+ else if (len < 65536) {
35
+ header = Buffer.alloc(4);
36
+ header[0] = 0x81;
37
+ header[1] = 126;
38
+ header.writeUInt16BE(len, 2);
39
+ }
40
+ else {
41
+ header = Buffer.alloc(10);
42
+ header[0] = 0x81;
43
+ header[1] = 127;
44
+ // Write as two 32-bit values (safe for strings < 4GB)
45
+ header.writeUInt32BE(0, 2);
46
+ header.writeUInt32BE(len, 6);
47
+ }
48
+ return Buffer.concat([header, payload]);
49
+ }
50
+ function decodeWSFrame(buf) {
51
+ if (buf.length < 2)
52
+ return null;
53
+ const opcode = buf[0] & 0x0f;
54
+ const masked = (buf[1] & 0x80) !== 0;
55
+ let payloadLen = buf[1] & 0x7f;
56
+ let offset = 2;
57
+ if (payloadLen === 126) {
58
+ if (buf.length < 4)
59
+ return null;
60
+ payloadLen = buf.readUInt16BE(2);
61
+ offset = 4;
62
+ }
63
+ else if (payloadLen === 127) {
64
+ if (buf.length < 10)
65
+ return null;
66
+ payloadLen = buf.readUInt32BE(6); // ignore high 32 bits
67
+ offset = 10;
68
+ }
69
+ if (masked) {
70
+ if (buf.length < offset + 4 + payloadLen)
71
+ return null;
72
+ const mask = buf.subarray(offset, offset + 4);
73
+ offset += 4;
74
+ const payload = Buffer.alloc(payloadLen);
75
+ for (let i = 0; i < payloadLen; i++) {
76
+ payload[i] = buf[offset + i] ^ mask[i % 4];
77
+ }
78
+ return { opcode, payload };
79
+ }
80
+ if (buf.length < offset + payloadLen)
81
+ return null;
82
+ return { opcode, payload: buf.subarray(offset, offset + payloadLen) };
83
+ }
84
+ // ============================================================================
85
+ // DASHBOARD HTML (inline single-page app)
86
+ // ============================================================================
87
+ function getDashboardHTML(wsPort) {
88
+ // HTML loaded from the separate dashboard.html asset (Phase 1.2 extraction)
89
+ const htmlPath = (0, path_1.join)(__dirname, 'dashboard.html');
90
+ const html = (0, fs_1.readFileSync)(htmlPath, 'utf-8');
91
+ return html.replace('__WS_PORT__', String(wsPort));
92
+ }
93
+ // ============================================================================
94
+ // DASHBOARD SERVER
95
+ // ============================================================================
96
+ /**
97
+ * HTTP + WebSocket server for the live agent topology dashboard.
98
+ *
99
+ * Usage:
100
+ * ```typescript
101
+ * const topo = new TopologyTracker();
102
+ * const dashboard = new DashboardServer(topo, { port: 4820 });
103
+ * await dashboard.start();
104
+ * // Dashboard available at http://127.0.0.1:4820
105
+ * ```
106
+ */
107
+ class DashboardServer extends events_1.EventEmitter {
108
+ tracker;
109
+ port;
110
+ host;
111
+ server = null;
112
+ clients = new Map();
113
+ pingInterval = null;
114
+ eventHandler = null;
115
+ clientCounter = 0;
116
+ deltaPending = false;
117
+ constructor(tracker, options) {
118
+ super();
119
+ this.tracker = tracker;
120
+ this.port = options?.port ?? 4820;
121
+ this.host = options?.host ?? '127.0.0.1';
122
+ }
123
+ /**
124
+ * Start the dashboard server.
125
+ */
126
+ async start() {
127
+ return new Promise((resolve, reject) => {
128
+ const server = (0, http_1.createServer)((req, res) => this.handleHTTP(req, res));
129
+ server.on('upgrade', (req, socket) => {
130
+ this.handleUpgrade(req, socket);
131
+ });
132
+ server.on('error', (err) => {
133
+ this.emit('error', err);
134
+ reject(err);
135
+ });
136
+ server.listen(this.port, this.host, () => {
137
+ this.server = server;
138
+ // Subscribe to topology events and broadcast deltas
139
+ this.eventHandler = (_event) => {
140
+ this.broadcastDelta();
141
+ };
142
+ this.tracker.on('event', this.eventHandler);
143
+ // Ping clients every 30s
144
+ this.pingInterval = setInterval(() => this.pingClients(), 30000);
145
+ this.emit('listening', { port: this.port, host: this.host });
146
+ resolve();
147
+ });
148
+ });
149
+ }
150
+ /**
151
+ * Stop the dashboard server.
152
+ */
153
+ async stop() {
154
+ if (this.pingInterval) {
155
+ clearInterval(this.pingInterval);
156
+ this.pingInterval = null;
157
+ }
158
+ if (this.eventHandler) {
159
+ this.tracker.off('event', this.eventHandler);
160
+ this.eventHandler = null;
161
+ }
162
+ // Close all WebSocket clients
163
+ for (const client of this.clients.values()) {
164
+ client.close();
165
+ }
166
+ this.clients.clear();
167
+ return new Promise((resolve) => {
168
+ if (!this.server) {
169
+ resolve();
170
+ return;
171
+ }
172
+ this.server.close(() => {
173
+ this.server = null;
174
+ resolve();
175
+ });
176
+ });
177
+ }
178
+ /**
179
+ * Number of connected WebSocket clients.
180
+ */
181
+ clientCount() {
182
+ return this.clients.size;
183
+ }
184
+ /**
185
+ * The URL the dashboard is serving on.
186
+ */
187
+ get url() {
188
+ return `http://${this.host}:${this.port}`;
189
+ }
190
+ // --------------------------------------------------------------------------
191
+ // HTTP HANDLER
192
+ // --------------------------------------------------------------------------
193
+ handleHTTP(req, res) {
194
+ const url = req.url ?? '/';
195
+ if (url === '/' || url === '/index.html') {
196
+ res.writeHead(200, {
197
+ 'Content-Type': 'text/html; charset=utf-8',
198
+ 'Cache-Control': 'no-cache',
199
+ 'X-Content-Type-Options': 'nosniff',
200
+ });
201
+ res.end(getDashboardHTML(this.port));
202
+ return;
203
+ }
204
+ if (url === '/api/snapshot') {
205
+ const snapshot = this.tracker.snapshot();
206
+ res.writeHead(200, {
207
+ 'Content-Type': 'application/json',
208
+ 'Cache-Control': 'no-cache',
209
+ 'X-Content-Type-Options': 'nosniff',
210
+ });
211
+ res.end(JSON.stringify(snapshot));
212
+ return;
213
+ }
214
+ if (url === '/api/health') {
215
+ res.writeHead(200, {
216
+ 'Content-Type': 'application/json',
217
+ 'X-Content-Type-Options': 'nosniff',
218
+ });
219
+ res.end(JSON.stringify({
220
+ status: 'ok',
221
+ clients: this.clients.size,
222
+ nodes: this.tracker.nodeCount(),
223
+ edges: this.tracker.edgeCount(),
224
+ }));
225
+ return;
226
+ }
227
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
228
+ res.end('Not Found');
229
+ }
230
+ // --------------------------------------------------------------------------
231
+ // WEBSOCKET UPGRADE
232
+ // --------------------------------------------------------------------------
233
+ handleUpgrade(req, socket) {
234
+ const key = req.headers['sec-websocket-key'];
235
+ if (!key) {
236
+ socket.destroy();
237
+ return;
238
+ }
239
+ const acceptKey = computeAcceptKey(key);
240
+ const headers = [
241
+ 'HTTP/1.1 101 Switching Protocols',
242
+ 'Upgrade: websocket',
243
+ 'Connection: Upgrade',
244
+ `Sec-WebSocket-Accept: ${acceptKey}`,
245
+ '',
246
+ '',
247
+ ].join('\r\n');
248
+ socket.write(headers);
249
+ const clientId = `ws-${++this.clientCounter}`;
250
+ const client = {
251
+ id: clientId,
252
+ send: (data) => {
253
+ try {
254
+ socket.write(encodeWSFrame(data));
255
+ }
256
+ catch {
257
+ // Client disconnected
258
+ }
259
+ },
260
+ close: () => {
261
+ try {
262
+ // Send close frame
263
+ const closeFrame = Buffer.alloc(2);
264
+ closeFrame[0] = 0x88; // FIN + close
265
+ closeFrame[1] = 0x00;
266
+ socket.write(closeFrame);
267
+ socket.end();
268
+ }
269
+ catch {
270
+ // Already closed
271
+ }
272
+ },
273
+ alive: true,
274
+ lastSeq: 0,
275
+ };
276
+ this.clients.set(clientId, client);
277
+ this.emit('client:connected', clientId);
278
+ // Send initial snapshot
279
+ const snapshot = this.tracker.snapshotQuiet();
280
+ client.send(JSON.stringify({ type: 'snapshot', data: snapshot }));
281
+ client.lastSeq = this.tracker.currentSeq();
282
+ // Handle incoming messages
283
+ let buffer = Buffer.alloc(0);
284
+ socket.on('data', (chunk) => {
285
+ buffer = Buffer.concat([buffer, chunk]);
286
+ const frame = decodeWSFrame(buffer);
287
+ if (!frame)
288
+ return;
289
+ buffer = Buffer.alloc(0);
290
+ if (frame.opcode === 0x08) {
291
+ // Close frame
292
+ this.clients.delete(clientId);
293
+ socket.end();
294
+ this.emit('client:disconnected', clientId);
295
+ return;
296
+ }
297
+ if (frame.opcode === 0x0a) {
298
+ // Pong
299
+ client.alive = true;
300
+ return;
301
+ }
302
+ if (frame.opcode === 0x01) {
303
+ // Text message — handle commands
304
+ try {
305
+ const msg = JSON.parse(frame.payload.toString('utf8'));
306
+ this.handleClientMessage(client, msg);
307
+ }
308
+ catch {
309
+ // Ignore malformed messages
310
+ }
311
+ }
312
+ });
313
+ socket.on('close', () => {
314
+ this.clients.delete(clientId);
315
+ this.emit('client:disconnected', clientId);
316
+ });
317
+ socket.on('error', () => {
318
+ this.clients.delete(clientId);
319
+ });
320
+ }
321
+ // --------------------------------------------------------------------------
322
+ // WEBSOCKET MESSAGE HANDLING
323
+ // --------------------------------------------------------------------------
324
+ handleClientMessage(client, msg) {
325
+ if (msg.action === 'snapshot') {
326
+ const snapshot = this.tracker.snapshotQuiet();
327
+ snapshot.clusters = this.tracker.computeClusters();
328
+ client.send(JSON.stringify({ type: 'snapshot', data: snapshot }));
329
+ client.lastSeq = this.tracker.currentSeq();
330
+ }
331
+ }
332
+ broadcast(data) {
333
+ for (const client of this.clients.values()) {
334
+ client.send(data);
335
+ }
336
+ }
337
+ /**
338
+ * Broadcast a delta patch to all connected clients.
339
+ * Each client tracks its own lastSeq so we can compute per-client deltas.
340
+ * For simplicity, we use a single shared delta and reset after broadcast.
341
+ */
342
+ broadcastDelta() {
343
+ if (this.clients.size === 0)
344
+ return;
345
+ // Debounce: accumulate rapid events into one delta per tick
346
+ if (this.deltaPending)
347
+ return;
348
+ this.deltaPending = true;
349
+ // Use queueMicrotask so multiple sync events within one tick batch together
350
+ queueMicrotask(() => {
351
+ this.deltaPending = false;
352
+ if (this.clients.size === 0)
353
+ return;
354
+ // If there are many nodes (>200), send delta; otherwise send full snapshot
355
+ // for simplicity on small topologies
356
+ const nodeCount = this.tracker.nodeCount();
357
+ if (nodeCount <= 200) {
358
+ const snapshot = this.tracker.snapshotQuiet();
359
+ const data = JSON.stringify({ type: 'snapshot', data: snapshot });
360
+ for (const client of this.clients.values()) {
361
+ client.send(data);
362
+ client.lastSeq = this.tracker.currentSeq();
363
+ }
364
+ }
365
+ else {
366
+ // Delta protocol for large topologies
367
+ const minSeq = Math.min(...Array.from(this.clients.values()).map(c => c.lastSeq));
368
+ const delta = this.tracker.delta(minSeq);
369
+ delta.clusters = this.tracker.computeClusters();
370
+ const data = JSON.stringify({ type: 'delta', data: delta });
371
+ for (const client of this.clients.values()) {
372
+ client.send(data);
373
+ client.lastSeq = this.tracker.currentSeq();
374
+ }
375
+ }
376
+ this.tracker.resetDelta();
377
+ });
378
+ }
379
+ pingClients() {
380
+ for (const [id, client] of this.clients.entries()) {
381
+ if (!client.alive) {
382
+ this.clients.delete(id);
383
+ client.close();
384
+ this.emit('client:disconnected', id);
385
+ continue;
386
+ }
387
+ client.alive = false;
388
+ // Send ping frame
389
+ try {
390
+ const ping = Buffer.alloc(2);
391
+ ping[0] = 0x89; // FIN + ping
392
+ ping[1] = 0x00;
393
+ // We can't write directly to socket from here, so mark alive = false
394
+ // and wait for next data activity
395
+ }
396
+ catch {
397
+ // ignore
398
+ }
399
+ }
400
+ }
401
+ }
402
+ exports.DashboardServer = DashboardServer;
403
+ //# sourceMappingURL=dashboard-server.js.map