@a-company/paradigm 3.12.0 → 3.13.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 (96) hide show
  1. package/dist/{accept-orchestration-YKMKMWGA.js → accept-orchestration-ORQRKKGR.js} +4 -4
  2. package/dist/{agents-suggest-35LIQKDH.js → agents-suggest-65SER5IS.js} +1 -1
  3. package/dist/{aggregate-V4KPR3RW.js → aggregate-M5WMUI6B.js} +1 -1
  4. package/dist/{auto-6MOGYQ4G.js → auto-B22FVSQI.js} +1 -1
  5. package/dist/{beacon-XRXL5KZB.js → beacon-XL2ALH5O.js} +1 -1
  6. package/dist/{check-UZY647TB.js → check-5RKOAN7S.js} +1 -1
  7. package/dist/{chunk-RDPXBMHK.js → chunk-3BAMPB6I.js} +6 -6
  8. package/dist/{chunk-5TUAVVIG.js → chunk-6GWRQWQB.js} +1 -1
  9. package/dist/{chunk-F6EJKLF4.js → chunk-O5ZO5LSW.js} +1 -1
  10. package/dist/{chunk-UVI3OH3G.js → chunk-R2SGQ22F.js} +11 -58
  11. package/dist/{chunk-4PEQHWD7.js → chunk-XKAFTZOZ.js} +1 -1
  12. package/dist/{chunk-5N5LR2KS.js → chunk-XNUWLW73.js} +6 -6
  13. package/dist/{chunk-JQYGPVLQ.js → chunk-YOFP72IB.js} +1 -1
  14. package/dist/{chunk-CQFNTBFJ.js → chunk-Z42FOOVT.js} +3 -3
  15. package/dist/{chunk-CYGHL7PQ.js → chunk-ZMN3RAIT.js} +5 -5
  16. package/dist/{chunk-4ZO3ZOPM.js → chunk-ZRPEI35Q.js} +14 -59
  17. package/dist/chunk-ZXMDA7VB.js +16 -0
  18. package/dist/{claude-SUYNN72C.js → claude-63ISJAZK.js} +1 -1
  19. package/dist/{claude-cli-OF43XAO3.js → claude-cli-ABML5RHX.js} +1 -1
  20. package/dist/{claude-code-PW6SKD2M.js → claude-code-JRLMRPTO.js} +1 -1
  21. package/dist/{claude-code-teams-JLZ5IXB6.js → claude-code-teams-CAJBEFIZ.js} +1 -1
  22. package/dist/commands-VTFOZPUA.js +5387 -0
  23. package/dist/{constellation-GNK5DIMH.js → constellation-NXU6Q2HM.js} +1 -1
  24. package/dist/{cost-AGO5N7DD.js → cost-CTGSLSOC.js} +1 -1
  25. package/dist/{cost-KYXIQ62X.js → cost-XEBADYFT.js} +1 -1
  26. package/dist/{cursor-cli-IHJMPRCW.js → cursor-cli-QUOOF2N4.js} +1 -1
  27. package/dist/{cursorrules-LQFA7M62.js → cursorrules-XBWFX66V.js} +1 -1
  28. package/dist/{delete-3YXAJ5AA.js → delete-OINCSDQH.js} +2 -2
  29. package/dist/{diff-4FV7T35U.js → diff-4XJZN4OB.js} +4 -4
  30. package/dist/{dist-ZEMSQV74.js → dist-3RVKEJRT.js} +1 -1
  31. package/dist/{dist-Q6SAZI7X.js → dist-7U64HDSC.js} +1 -1
  32. package/dist/{dist-AG5JNIZU-XSEZ2LLK.js → dist-AG5JNIZU-HW2FWNTZ.js} +1 -1
  33. package/dist/dist-KY5HGDDL.js +1304 -0
  34. package/dist/{dist-JOHRYQUA.js → dist-PSF5CP4I.js} +1 -1
  35. package/dist/{dist-6SX5ZKKF.js → dist-RMAIFRTW.js} +3 -3
  36. package/dist/{dist-YB7T54QE.js → dist-YHDSIZQD.js} +1 -1
  37. package/dist/{doctor-2KM5HOK6.js → doctor-FINKMI66.js} +2 -2
  38. package/dist/{drift-FH2UY64B.js → drift-YGT4LJ7Q.js} +1 -1
  39. package/dist/{echo-VYZW3OTT.js → echo-A6HD5UP7.js} +1 -1
  40. package/dist/{edit-EOMPXOG5.js → edit-7FSQNAPE.js} +2 -2
  41. package/dist/{export-R4FJ5NOH.js → export-T7CMMJIB.js} +1 -1
  42. package/dist/{flow-MCKPJGRJ.js → flow-UFMPVOEM.js} +1 -1
  43. package/dist/{global-AXILUM5X.js → global-HHUJSBG5.js} +1 -1
  44. package/dist/{habits-NC2TRMRV.js → habits-KD4RLIN2.js} +3 -3
  45. package/dist/{history-EVO3L6SC.js → history-CETCSUCP.js} +1 -1
  46. package/dist/{hooks-JXYHVGIN.js → hooks-TCUHQMPF.js} +1 -1
  47. package/dist/index.js +138 -137
  48. package/dist/{lint-N4LMMEXH.js → lint-53GPXKKI.js} +1 -1
  49. package/dist/{list-JKBJ7ESH.js → list-Q4R7L7WJ.js} +2 -2
  50. package/dist/{lore-server-RQH5REZV.js → lore-server-GKZ6ESNJ.js} +1 -1
  51. package/dist/{manual-Y3QOXWYA.js → manual-AFJ2J2V3.js} +1 -1
  52. package/dist/mcp.js +4 -4
  53. package/dist/{orchestrate-IV54FMHD.js → orchestrate-6XGEA655.js} +4 -4
  54. package/dist/{portal-check-2HI4FFD6.js → portal-check-FF5EKZE5.js} +1 -1
  55. package/dist/{portal-compliance-KQCTAQTJ.js → portal-compliance-VU4NIFEN.js} +1 -1
  56. package/dist/{probe-X3J2JX62.js → probe-T77FFIAG.js} +1 -1
  57. package/dist/{promote-HZH5E5CO.js → promote-XO63XMAN.js} +2 -2
  58. package/dist/{providers-IONB4YRJ.js → providers-VIBWDN5D.js} +2 -2
  59. package/dist/{record-EECZ3E4I.js → record-YJ3D3462.js} +2 -2
  60. package/dist/{reindex-ZM6J53UP.js → reindex-4OOME3TT.js} +1 -1
  61. package/dist/{remember-3KJZGDUG.js → remember-IEBQHXHZ.js} +1 -1
  62. package/dist/{review-BF26ILZB.js → review-3OW3KVW7.js} +2 -2
  63. package/dist/{ripple-JIUAMBLA.js → ripple-DFMXLFWI.js} +1 -1
  64. package/dist/{sentinel-BGCISNIK.js → sentinel-RERNMWSE.js} +2 -2
  65. package/dist/sentinel-mcp.js +4181 -0
  66. package/dist/sentinel.js +35 -0
  67. package/dist/{serve-H7ZBMODT.js → serve-XLKEMQEH.js} +2 -2
  68. package/dist/server-CAXNYVV7.js +1616 -0
  69. package/dist/{server-E2CNZC4K.js → server-V3ANAXDP.js} +1 -1
  70. package/dist/{setup-UKJ3VGHI.js → setup-HOI52TN3.js} +2 -2
  71. package/dist/{setup-363IB6MO.js → setup-YNZJQLW7.js} +1 -1
  72. package/dist/{shift-G2ZCIR5Q.js → shift-SW3GSODO.js} +7 -7
  73. package/dist/{show-SAMTXEHG.js → show-CJGHREFS.js} +2 -2
  74. package/dist/{snapshot-KCMONZAO.js → snapshot-XHINQBZS.js} +1 -1
  75. package/dist/{spawn-7SDONTJN.js → spawn-JSV2HST3.js} +3 -3
  76. package/dist/{summary-F46FRO3Y.js → summary-NV7SBV5O.js} +1 -1
  77. package/dist/{switch-CC2KACXO.js → switch-WYUMVNA5.js} +1 -1
  78. package/dist/{sync-4CNRHUWX.js → sync-ZM4Q3R4U.js} +1 -1
  79. package/dist/{sync-llms-MCWB37HN.js → sync-llms-JIPP3XX4.js} +1 -1
  80. package/dist/{team-XUZBPIFZ.js → team-YIYA4ZLX.js} +5 -5
  81. package/dist/{test-DK2RWLTK.js → test-WTR5Q33E.js} +1 -1
  82. package/dist/{thread-RNSLADXN.js → thread-3WM7KKID.js} +1 -1
  83. package/dist/{timeline-TJDVVVA3.js → timeline-ELO5JTQO.js} +2 -2
  84. package/dist/{triage-MKKIWBSW.js → triage-GJ6GK647.js} +3 -3
  85. package/dist/{tutorial-L5Q3ZDHK.js → tutorial-GC6QL4US.js} +1 -1
  86. package/dist/{university-65YJZ2LW.js → university-KVYNACJZ.js} +2 -2
  87. package/dist/{upgrade-HGF4MBGV.js → upgrade-65QOQXRC.js} +1 -1
  88. package/dist/{validate-F3YHBCRZ.js → validate-ITC5D6QG.js} +1 -1
  89. package/dist/{validate-2LTHHORX.js → validate-TKKRGJKC.js} +1 -1
  90. package/dist/{watch-CL2PPS2K.js → watch-ERBEJUJW.js} +1 -1
  91. package/dist/{watch-NBPOMOEX.js → watch-X64UK7K4.js} +2 -2
  92. package/dist/{wisdom-LRM4FFCH.js → wisdom-L2WC7J62.js} +1 -1
  93. package/dist/{workspace-7CWY4IWV.js → workspace-S5Q5LVA6.js} +1 -1
  94. package/package.json +7 -2
  95. package/dist/chunk-MO4EEYFW.js +0 -38
  96. package/dist/server-3K3TTJH3.js +0 -10539
@@ -0,0 +1,1616 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ SentinelStorage,
4
+ loadServerConfig
5
+ } from "./chunk-R2SGQ22F.js";
6
+ import "./chunk-ZXMDA7VB.js";
7
+
8
+ // ../sentinel/dist/server/index.js
9
+ import express from "express";
10
+ import * as http from "http";
11
+ import * as path3 from "path";
12
+ import * as fs2 from "fs";
13
+ import { fileURLToPath } from "url";
14
+ import chalk3 from "chalk";
15
+ import { WebSocketServer, WebSocket } from "ws";
16
+ import { Router } from "express";
17
+ import chalk2 from "chalk";
18
+ import * as fs from "fs";
19
+ import * as path from "path";
20
+ import chalk from "chalk";
21
+ import { Router as Router2 } from "express";
22
+ import { Router as Router3 } from "express";
23
+ import simpleGit from "simple-git";
24
+ import * as path2 from "path";
25
+ import { Router as Router4 } from "express";
26
+ import { Router as Router5 } from "express";
27
+ import { Router as Router6 } from "express";
28
+ import { v4 as uuidv4 } from "uuid";
29
+ import { Router as Router7 } from "express";
30
+ import { Router as Router8 } from "express";
31
+ import { Router as Router9 } from "express";
32
+ import { Router as Router10 } from "express";
33
+ import { Router as Router11 } from "express";
34
+ var LOG_LEVEL = process.env.SENTINEL_LOG_LEVEL || process.env.LOG_LEVEL || "info";
35
+ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
36
+ function shouldLog(level) {
37
+ return LOG_LEVELS[level] >= LOG_LEVELS[LOG_LEVEL];
38
+ }
39
+ function formatData(data) {
40
+ if (!data) return "";
41
+ const entries = Object.entries(data).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
42
+ return chalk.gray(` ${entries}`);
43
+ }
44
+ var log = {
45
+ component(name) {
46
+ const symbol = chalk.magenta(`#${name}`);
47
+ return {
48
+ debug: (msg, data) => {
49
+ if (shouldLog("debug")) console.log(`${chalk.gray("\u25CB")} ${symbol} ${msg}${formatData(data)}`);
50
+ },
51
+ info: (msg, data) => {
52
+ if (shouldLog("info")) console.log(`${chalk.blue("\u2139")} ${symbol} ${msg}${formatData(data)}`);
53
+ },
54
+ warn: (msg, data) => {
55
+ if (shouldLog("warn")) console.log(`${chalk.yellow("\u26A0")} ${symbol} ${msg}${formatData(data)}`);
56
+ },
57
+ error: (msg, data) => {
58
+ if (shouldLog("error")) console.error(`${chalk.red("\u2716")} ${symbol} ${msg}${formatData(data)}`);
59
+ }
60
+ };
61
+ },
62
+ flow(name) {
63
+ const symbol = chalk.yellow(`$${name}`);
64
+ return {
65
+ debug: (msg, data) => {
66
+ if (shouldLog("debug")) console.log(`${chalk.gray("\u25CB")} ${symbol} ${msg}${formatData(data)}`);
67
+ },
68
+ info: (msg, data) => {
69
+ if (shouldLog("info")) console.log(`${chalk.blue("\u2139")} ${symbol} ${msg}${formatData(data)}`);
70
+ },
71
+ warn: (msg, data) => {
72
+ if (shouldLog("warn")) console.log(`${chalk.yellow("\u26A0")} ${symbol} ${msg}${formatData(data)}`);
73
+ },
74
+ error: (msg, data) => {
75
+ if (shouldLog("error")) console.error(`${chalk.red("\u2716")} ${symbol} ${msg}${formatData(data)}`);
76
+ }
77
+ };
78
+ }
79
+ };
80
+ var SYMBOL_BLOCKLIST = /* @__PURE__ */ new Set([
81
+ "$lib",
82
+ "$env",
83
+ "$app",
84
+ "$service-worker",
85
+ "$virtual",
86
+ "$schema",
87
+ "$ref",
88
+ "$id",
89
+ "$type"
90
+ ]);
91
+ async function loadParadigmConfig(projectDir) {
92
+ const configPath = path.join(projectDir, ".paradigm", "config.yaml");
93
+ if (!fs.existsSync(configPath)) {
94
+ const packagePath = path.join(projectDir, "package.json");
95
+ if (fs.existsSync(packagePath)) {
96
+ try {
97
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
98
+ return { name: pkg.name };
99
+ } catch {
100
+ }
101
+ }
102
+ return {};
103
+ }
104
+ try {
105
+ const content = fs.readFileSync(configPath, "utf-8");
106
+ const config = {};
107
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
108
+ if (nameMatch) config.name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
109
+ const disciplineMatch = content.match(/^discipline:\s*(.+)$/m);
110
+ if (disciplineMatch) config.discipline = disciplineMatch[1].trim();
111
+ const versionMatch = content.match(/^version:\s*(.+)$/m);
112
+ if (versionMatch) config.version = versionMatch[1].trim();
113
+ return config;
114
+ } catch (error) {
115
+ log.component("config-loader").error("Failed to load Paradigm config", { error: String(error) });
116
+ return {};
117
+ }
118
+ }
119
+ async function loadWithPremiseCore(projectDir) {
120
+ try {
121
+ const { aggregateFromDirectory } = await import("./dist-AG5JNIZU-HW2FWNTZ.js");
122
+ log.flow("load-symbols").info("Using premise-core aggregator", { path: projectDir });
123
+ const result = await aggregateFromDirectory(projectDir);
124
+ const counts = {};
125
+ for (const sym of result.symbols) {
126
+ counts[sym.type] = (counts[sym.type] || 0) + 1;
127
+ }
128
+ log.flow("load-symbols").info("Aggregation complete", {
129
+ total: result.symbols.length,
130
+ ...counts,
131
+ purposeFiles: result.purposeFiles.length,
132
+ portalFiles: result.portalFiles.length
133
+ });
134
+ if (result.errors.length > 0) {
135
+ for (const err of result.errors) {
136
+ log.component("aggregator").warn("Aggregation error", {
137
+ source: err.source,
138
+ file: err.filePath,
139
+ message: err.message
140
+ });
141
+ }
142
+ }
143
+ for (const file of result.purposeFiles) {
144
+ log.component("purpose-loader").info("Loaded .purpose file", { file: path.relative(projectDir, file) });
145
+ }
146
+ for (const file of result.portalFiles) {
147
+ log.component("gate-loader").info("Loaded portal.yaml", { file: path.relative(projectDir, file) });
148
+ }
149
+ return result.symbols;
150
+ } catch (error) {
151
+ log.component("premise-core").warn("premise-core not available, using fallback scanner", {
152
+ error: error instanceof Error ? error.message : String(error)
153
+ });
154
+ return null;
155
+ }
156
+ }
157
+ async function loadSymbolIndex(projectDir) {
158
+ log.flow("load-symbols").info("Loading symbols", { projectDir });
159
+ const indexPath = path.join(projectDir, ".paradigm", "index.json");
160
+ if (fs.existsSync(indexPath)) {
161
+ try {
162
+ log.component("index-loader").info("Found cached index", { path: indexPath });
163
+ const content = fs.readFileSync(indexPath, "utf-8");
164
+ const index = JSON.parse(content);
165
+ const entries = Array.isArray(index.entries) ? index.entries : Array.isArray(index) ? index : null;
166
+ if (entries) {
167
+ log.flow("load-symbols").info("Loaded from cached index", { count: entries.length });
168
+ return entries;
169
+ }
170
+ } catch (error) {
171
+ log.component("index-loader").error("Failed to load cached index", { error: String(error) });
172
+ }
173
+ }
174
+ const premiseResult = await loadWithPremiseCore(projectDir);
175
+ if (premiseResult) {
176
+ return premiseResult;
177
+ }
178
+ log.flow("load-symbols").info("Using fallback scanner");
179
+ return scanPurposeFiles(projectDir);
180
+ }
181
+ async function scanPurposeFiles(projectDir) {
182
+ const symbols = [];
183
+ const seenIds = /* @__PURE__ */ new Set();
184
+ const scanDirs = ["src", "lib", "packages", "apps", "."];
185
+ for (const dir of scanDirs) {
186
+ const fullPath = path.join(projectDir, dir);
187
+ if (fs.existsSync(fullPath)) {
188
+ await scanDirectory(fullPath, symbols, seenIds, projectDir);
189
+ }
190
+ }
191
+ const portalPath = path.join(projectDir, "portal.yaml");
192
+ if (fs.existsSync(portalPath)) {
193
+ log.component("gate-loader").debug("Found portal.yaml", { path: "portal.yaml" });
194
+ try {
195
+ const content = fs.readFileSync(portalPath, "utf-8");
196
+ const gatesSection = content.match(/^gates:\s*\n((?: .+\n)*)/m);
197
+ if (gatesSection) {
198
+ const gateMatches = gatesSection[1].matchAll(/^ ([a-z][a-z0-9-]*):/gm);
199
+ for (const match of gateMatches) {
200
+ const gateName = match[1];
201
+ const id = `gate-${gateName}`;
202
+ if (!seenIds.has(id)) {
203
+ seenIds.add(id);
204
+ symbols.push({
205
+ id,
206
+ symbol: `^${gateName}`,
207
+ type: "gate",
208
+ source: "portal",
209
+ filePath: "portal.yaml",
210
+ data: {},
211
+ references: [],
212
+ referencedBy: []
213
+ });
214
+ log.component("gate-loader").debug("Extracted gate", { symbol: `^${gateName}` });
215
+ }
216
+ }
217
+ }
218
+ } catch (error) {
219
+ log.component("gate-loader").error("Failed to parse portal.yaml", { error: String(error) });
220
+ }
221
+ }
222
+ log.flow("load-symbols").info("Fallback scan complete", { count: symbols.length });
223
+ return symbols;
224
+ }
225
+ async function scanDirectory(dir, symbols, seenIds, projectDir) {
226
+ const skipDirs = ["node_modules", ".git", "dist", "build", ".paradigm", "coverage", ".next", ".svelte-kit"];
227
+ let entries;
228
+ try {
229
+ entries = fs.readdirSync(dir, { withFileTypes: true });
230
+ } catch {
231
+ return;
232
+ }
233
+ for (const entry of entries) {
234
+ const fullPath = path.join(dir, entry.name);
235
+ if (entry.isDirectory()) {
236
+ if (!skipDirs.includes(entry.name)) {
237
+ await scanDirectory(fullPath, symbols, seenIds, projectDir);
238
+ }
239
+ } else if (entry.name === ".purpose") {
240
+ const relativePath = path.relative(projectDir, fullPath);
241
+ log.component("purpose-loader").debug("Scanning .purpose file", { path: relativePath });
242
+ try {
243
+ const content = fs.readFileSync(fullPath, "utf-8");
244
+ const parsed = parsePurposeFile(content, fullPath, projectDir);
245
+ for (const symbol of parsed) {
246
+ if (!seenIds.has(symbol.id)) {
247
+ seenIds.add(symbol.id);
248
+ symbols.push(symbol);
249
+ log.component("purpose-loader").debug("Extracted symbol", {
250
+ symbol: symbol.symbol,
251
+ type: symbol.type,
252
+ file: relativePath
253
+ });
254
+ }
255
+ }
256
+ } catch (error) {
257
+ log.component("purpose-loader").error("Failed to parse .purpose file", {
258
+ path: relativePath,
259
+ error: String(error)
260
+ });
261
+ }
262
+ }
263
+ }
264
+ }
265
+ function parsePurposeFile(content, filePath, projectDir) {
266
+ const symbols = [];
267
+ const relativePath = path.relative(projectDir, filePath);
268
+ const componentMatches = content.matchAll(/(?:^|\s)#([a-z][a-z0-9-]*)/gm);
269
+ for (const match of componentMatches) {
270
+ const name = match[1];
271
+ symbols.push({
272
+ id: `component-${name}`,
273
+ symbol: `#${name}`,
274
+ type: "component",
275
+ source: "purpose",
276
+ filePath: relativePath,
277
+ data: {},
278
+ description: extractDescription(content, `#${name}`),
279
+ references: extractReferences(content),
280
+ referencedBy: [],
281
+ tags: extractTags(content)
282
+ });
283
+ }
284
+ const flowMatches = content.matchAll(/\$([a-z][a-z0-9-]*)/gm);
285
+ for (const match of flowMatches) {
286
+ const name = match[1];
287
+ const symbol = `$${name}`;
288
+ if (SYMBOL_BLOCKLIST.has(symbol)) {
289
+ log.component("purpose-loader").debug("Skipping blocklisted symbol", { symbol });
290
+ continue;
291
+ }
292
+ if (!symbols.find((s) => s.symbol === symbol)) {
293
+ symbols.push({
294
+ id: `flow-${name}`,
295
+ symbol,
296
+ type: "flow",
297
+ source: "purpose",
298
+ filePath: relativePath,
299
+ data: {},
300
+ references: [],
301
+ referencedBy: []
302
+ });
303
+ }
304
+ }
305
+ const signalMatches = content.matchAll(/!([a-z][a-z0-9-]*)/gm);
306
+ for (const match of signalMatches) {
307
+ const name = match[1];
308
+ if (!symbols.find((s) => s.symbol === `!${name}`)) {
309
+ symbols.push({
310
+ id: `signal-${name}`,
311
+ symbol: `!${name}`,
312
+ type: "signal",
313
+ source: "purpose",
314
+ filePath: relativePath,
315
+ data: {},
316
+ references: [],
317
+ referencedBy: []
318
+ });
319
+ }
320
+ }
321
+ const gateMatches = content.matchAll(/\^([a-z][a-z0-9-]*)/gm);
322
+ for (const match of gateMatches) {
323
+ const name = match[1];
324
+ if (!symbols.find((s) => s.symbol === `^${name}`)) {
325
+ symbols.push({
326
+ id: `gate-${name}`,
327
+ symbol: `^${name}`,
328
+ type: "gate",
329
+ source: "purpose",
330
+ filePath: relativePath,
331
+ data: {},
332
+ references: [],
333
+ referencedBy: []
334
+ });
335
+ }
336
+ }
337
+ const aspectMatches = content.matchAll(/~([a-z][a-z0-9-]*)/gm);
338
+ for (const match of aspectMatches) {
339
+ const name = match[1];
340
+ if (!symbols.find((s) => s.symbol === `~${name}`)) {
341
+ symbols.push({
342
+ id: `aspect-${name}`,
343
+ symbol: `~${name}`,
344
+ type: "aspect",
345
+ source: "purpose",
346
+ filePath: relativePath,
347
+ data: {},
348
+ references: [],
349
+ referencedBy: []
350
+ });
351
+ }
352
+ }
353
+ return symbols;
354
+ }
355
+ function extractDescription(content, symbol) {
356
+ const regex = new RegExp(`${symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[-:]?\\s*(.+)`, "m");
357
+ const match = content.match(regex);
358
+ if (match && match[1]) {
359
+ return match[1].trim();
360
+ }
361
+ return void 0;
362
+ }
363
+ function extractReferences(content) {
364
+ const refs = /* @__PURE__ */ new Set();
365
+ const refMatches = content.matchAll(/[@#$!^~]([a-z][a-z0-9-]*)/g);
366
+ for (const match of refMatches) {
367
+ const symbol = match[0];
368
+ if (!SYMBOL_BLOCKLIST.has(symbol)) {
369
+ refs.add(symbol);
370
+ }
371
+ }
372
+ return Array.from(refs);
373
+ }
374
+ function extractTags(content) {
375
+ const tagMatch = content.match(/tags:\s*\[([^\]]+)\]/);
376
+ if (tagMatch) {
377
+ return tagMatch[1].split(",").map((t) => t.trim().replace(/^["']|["']$/g, ""));
378
+ }
379
+ return [];
380
+ }
381
+ async function getSymbolCount(projectDir) {
382
+ const symbols = await loadSymbolIndex(projectDir);
383
+ return symbols.length;
384
+ }
385
+ async function updateSymbol(projectDir, symbolId, updates) {
386
+ const symbols = await loadSymbolIndex(projectDir);
387
+ const symbol = symbols.find((s) => s.id === symbolId);
388
+ if (!symbol) {
389
+ return { success: false, error: "Symbol not found" };
390
+ }
391
+ const filePath = path.join(projectDir, symbol.filePath);
392
+ if (!fs.existsSync(filePath)) {
393
+ return { success: false, error: "Source file not found" };
394
+ }
395
+ try {
396
+ let content = fs.readFileSync(filePath, "utf-8");
397
+ let modified = false;
398
+ if (updates.description !== void 0) {
399
+ const symbolPattern = symbol.symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
400
+ const descRegex = new RegExp(`(${symbolPattern})\\s*[-:]?\\s*(.*)`, "m");
401
+ const match = content.match(descRegex);
402
+ if (match) {
403
+ const newLine = updates.description ? `${symbol.symbol}: ${updates.description}` : symbol.symbol;
404
+ content = content.replace(descRegex, newLine);
405
+ modified = true;
406
+ }
407
+ }
408
+ if (updates.tags !== void 0) {
409
+ const tagsStr = updates.tags.length > 0 ? `tags: [${updates.tags.map((t) => `"${t}"`).join(", ")}]` : "";
410
+ const tagsRegex = /^tags:\s*\[[^\]]*\]\s*$/m;
411
+ if (tagsRegex.test(content)) {
412
+ if (tagsStr) {
413
+ content = content.replace(tagsRegex, tagsStr);
414
+ } else {
415
+ content = content.replace(tagsRegex, "");
416
+ }
417
+ modified = true;
418
+ } else if (tagsStr) {
419
+ const symbolPattern = symbol.symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
420
+ const symbolLineRegex = new RegExp(`(${symbolPattern}[^\\n]*\\n)`, "m");
421
+ const symbolMatch = content.match(symbolLineRegex);
422
+ if (symbolMatch) {
423
+ content = content.replace(symbolLineRegex, `$1${tagsStr}
424
+ `);
425
+ modified = true;
426
+ }
427
+ }
428
+ }
429
+ if (modified) {
430
+ content = content.replace(/\n{3,}/g, "\n\n");
431
+ fs.writeFileSync(filePath, content, "utf-8");
432
+ log.component("symbol-updater").info("Updated symbol", { symbol: symbol.symbol, file: symbol.filePath });
433
+ const indexPath = path.join(projectDir, ".paradigm", "index.json");
434
+ if (fs.existsSync(indexPath)) {
435
+ try {
436
+ const indexContent = fs.readFileSync(indexPath, "utf-8");
437
+ const index = JSON.parse(indexContent);
438
+ const entries = Array.isArray(index.entries) ? index.entries : index;
439
+ const entryIndex = entries.findIndex((e) => e.id === symbolId);
440
+ if (entryIndex >= 0) {
441
+ if (updates.description !== void 0) {
442
+ entries[entryIndex].description = updates.description;
443
+ }
444
+ if (updates.tags !== void 0) {
445
+ entries[entryIndex].tags = updates.tags;
446
+ }
447
+ if (Array.isArray(index.entries)) {
448
+ index.entries = entries;
449
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), "utf-8");
450
+ } else {
451
+ fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), "utf-8");
452
+ }
453
+ }
454
+ } catch {
455
+ }
456
+ }
457
+ return { success: true };
458
+ }
459
+ return { success: true };
460
+ } catch (error) {
461
+ log.component("symbol-updater").error("Failed to update symbol", { error: String(error) });
462
+ return { success: false, error: "Failed to write file" };
463
+ }
464
+ }
465
+ var LOG_LEVEL2 = process.env.SENTINEL_LOG_LEVEL || process.env.LOG_LEVEL || "info";
466
+ var shouldLog2 = (level) => {
467
+ const levels = { debug: 0, info: 1, warn: 2, error: 3 };
468
+ return levels[level] >= levels[LOG_LEVEL2];
469
+ };
470
+ var log2 = {
471
+ gate(name) {
472
+ const symbol = chalk2.cyan(`^${name}`);
473
+ return {
474
+ info: (msg, data) => {
475
+ if (shouldLog2("info")) {
476
+ const dataStr = data ? chalk2.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
477
+ console.log(`${chalk2.blue("\u2139")} ${symbol} ${msg}${dataStr}`);
478
+ }
479
+ },
480
+ error: (msg, data) => {
481
+ if (shouldLog2("error")) {
482
+ const dataStr = data ? chalk2.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
483
+ console.error(`${chalk2.red("\u2716")} ${symbol} ${msg}${dataStr}`);
484
+ }
485
+ }
486
+ };
487
+ }
488
+ };
489
+ function createSymbolsRouter(projectDir) {
490
+ const router = Router();
491
+ router.get("/", async (_req, res) => {
492
+ try {
493
+ const symbols = await loadSymbolIndex(projectDir);
494
+ log2.gate("api-symbols").info("Symbols loaded", { count: symbols.length });
495
+ res.json({ symbols });
496
+ } catch (error) {
497
+ log2.gate("api-symbols").error("Failed to load symbols", { error: String(error) });
498
+ res.status(500).json({ error: "Failed to load symbols" });
499
+ }
500
+ });
501
+ router.put("/:id", async (req, res) => {
502
+ try {
503
+ const { id } = req.params;
504
+ const updates = req.body;
505
+ log2.gate("api-symbols").info("Update requested", { id, updates: JSON.stringify(updates) });
506
+ if (updates.tags && !Array.isArray(updates.tags)) {
507
+ res.status(400).json({ error: "Tags must be an array" });
508
+ return;
509
+ }
510
+ const result = await updateSymbol(projectDir, id, updates);
511
+ if (result.success) {
512
+ const symbols = await loadSymbolIndex(projectDir);
513
+ const updatedSymbol = symbols.find((s) => s.id === id);
514
+ log2.gate("api-symbols").info("Symbol updated", { id });
515
+ res.json({ success: true, symbol: updatedSymbol });
516
+ } else {
517
+ log2.gate("api-symbols").error("Update failed", { id, error: result.error });
518
+ res.status(400).json({ success: false, error: result.error });
519
+ }
520
+ } catch (error) {
521
+ log2.gate("api-symbols").error("Failed to update symbol", { error: String(error) });
522
+ res.status(500).json({ error: "Failed to update symbol" });
523
+ }
524
+ });
525
+ return router;
526
+ }
527
+ function createInfoRouter(projectDir) {
528
+ const router = Router2();
529
+ router.get("/", async (_req, res) => {
530
+ try {
531
+ const config = await loadParadigmConfig(projectDir);
532
+ const symbolCount = await getSymbolCount(projectDir);
533
+ res.json({
534
+ projectName: config.name || null,
535
+ discipline: config.discipline || null,
536
+ symbolCount,
537
+ projectDir
538
+ });
539
+ } catch (error) {
540
+ console.error("Failed to load project info:", error);
541
+ res.status(500).json({ error: "Failed to load project info" });
542
+ }
543
+ });
544
+ return router;
545
+ }
546
+ function extractSymbolsFromFiles(files) {
547
+ const symbols = /* @__PURE__ */ new Set();
548
+ for (const file of files) {
549
+ if (file.endsWith(".purpose")) {
550
+ const dir = path2.dirname(file);
551
+ const name = path2.basename(dir);
552
+ if (dir.includes("features/") || dir.includes("routes/") || dir.includes("api/")) {
553
+ symbols.add(`@${name}`);
554
+ } else if (dir.includes("components/") || dir.includes("lib/") || dir.includes("utils/")) {
555
+ symbols.add(`#${name}`);
556
+ } else if (dir.includes("middleware/") || dir.includes("auth/") || dir.includes("guards/")) {
557
+ symbols.add(`^${name}`);
558
+ } else if (dir.includes("flows/") || dir.includes("workflows/")) {
559
+ symbols.add(`$${name}`);
560
+ }
561
+ }
562
+ if (file.includes("portal.yaml")) {
563
+ symbols.add("^portal");
564
+ }
565
+ const featureMatch = file.match(/features\/([^/]+)/);
566
+ if (featureMatch) {
567
+ symbols.add(`@${featureMatch[1]}`);
568
+ }
569
+ const componentMatch = file.match(/components\/([^/]+)/);
570
+ if (componentMatch) {
571
+ symbols.add(`#${componentMatch[1]}`);
572
+ }
573
+ }
574
+ return Array.from(symbols);
575
+ }
576
+ async function loadGitHistory(projectDir, options = {}) {
577
+ const git = simpleGit(projectDir);
578
+ const isRepo = await git.checkIsRepo();
579
+ if (!isRepo) {
580
+ return [];
581
+ }
582
+ try {
583
+ const logOptions = {
584
+ maxCount: options.limit || 100
585
+ };
586
+ if (options.since) {
587
+ logOptions["--since"] = options.since;
588
+ }
589
+ const log4 = await git.log(logOptions);
590
+ const commits = [];
591
+ for (const commit of log4.all) {
592
+ let filesChanged = [];
593
+ let symbolsModified = [];
594
+ try {
595
+ const diff = await git.diffSummary([`${commit.hash}^`, commit.hash]);
596
+ filesChanged = diff.files.map((f) => f.file);
597
+ symbolsModified = extractSymbolsFromFiles(filesChanged);
598
+ } catch {
599
+ }
600
+ commits.push({
601
+ hash: commit.hash,
602
+ shortHash: commit.hash.slice(0, 7),
603
+ date: commit.date,
604
+ author: commit.author_name,
605
+ message: commit.message.split("\n")[0],
606
+ // First line only
607
+ symbolsModified,
608
+ filesChanged
609
+ });
610
+ }
611
+ return commits;
612
+ } catch (error) {
613
+ console.error("Failed to load git history:", error);
614
+ return [];
615
+ }
616
+ }
617
+ async function getSymbolsAtCommit(projectDir, commitHash) {
618
+ const git = simpleGit(projectDir);
619
+ try {
620
+ const result = await git.raw(["ls-tree", "-r", "--name-only", commitHash]);
621
+ const files = result.split("\n").filter(Boolean);
622
+ const purposeFiles = files.filter((f) => f.endsWith(".purpose"));
623
+ return extractSymbolsFromFiles(purposeFiles);
624
+ } catch (error) {
625
+ console.error("Failed to get symbols at commit:", error);
626
+ return [];
627
+ }
628
+ }
629
+ function createCommitsRouter(projectDir) {
630
+ const router = Router3();
631
+ router.get("/", async (req, res) => {
632
+ try {
633
+ const limit = parseInt(req.query.limit) || 100;
634
+ const since = req.query.since;
635
+ const commits = await loadGitHistory(projectDir, { limit, since });
636
+ res.json({ commits });
637
+ } catch (error) {
638
+ console.error("Failed to load commits:", error);
639
+ res.status(500).json({ error: "Failed to load commits" });
640
+ }
641
+ });
642
+ return router;
643
+ }
644
+ function createIncidentsRouter(_projectDir) {
645
+ const router = Router4();
646
+ const storage = new SentinelStorage();
647
+ router.get("/", async (req, res) => {
648
+ try {
649
+ const limit = parseInt(req.query.limit) || 50;
650
+ const status = req.query.status;
651
+ const environment = req.query.environment;
652
+ const symbol = req.query.symbol;
653
+ const options = { limit };
654
+ if (status && ["open", "investigating", "resolved", "wont-fix"].includes(status)) {
655
+ options.status = status;
656
+ }
657
+ if (environment) options.environment = environment;
658
+ if (symbol) options.symbol = symbol;
659
+ const incidents = storage.getRecentIncidents(options);
660
+ const summaries = incidents.map((incident) => ({
661
+ id: incident.id,
662
+ timestamp: incident.timestamp,
663
+ status: incident.status,
664
+ error: {
665
+ message: incident.error.message,
666
+ type: incident.error.type
667
+ },
668
+ symbols: incident.symbols,
669
+ environment: incident.environment,
670
+ patternMatches: []
671
+ // Would need PatternMatcher to populate
672
+ }));
673
+ res.json({ incidents: summaries });
674
+ } catch (error) {
675
+ console.error("Failed to load incidents:", error);
676
+ res.status(500).json({ error: "Failed to load incidents" });
677
+ }
678
+ });
679
+ router.get("/:id", async (req, res) => {
680
+ try {
681
+ const incident = storage.getIncident(req.params.id);
682
+ if (!incident) {
683
+ res.status(404).json({ error: "Incident not found" });
684
+ return;
685
+ }
686
+ res.json({ incident });
687
+ } catch (error) {
688
+ console.error("Failed to load incident:", error);
689
+ res.status(500).json({ error: "Failed to load incident" });
690
+ }
691
+ });
692
+ router.post("/:id/resolve", async (req, res) => {
693
+ try {
694
+ const incident = storage.getIncident(req.params.id);
695
+ if (!incident) {
696
+ res.status(404).json({ error: "Incident not found" });
697
+ return;
698
+ }
699
+ storage.resolveIncident(req.params.id, {
700
+ notes: req.body.notes,
701
+ patternId: req.body.patternId
702
+ });
703
+ res.json({ success: true });
704
+ } catch (error) {
705
+ console.error("Failed to resolve incident:", error);
706
+ res.status(500).json({ error: "Failed to resolve incident" });
707
+ }
708
+ });
709
+ return router;
710
+ }
711
+ function createPatternsRouter(_projectDir) {
712
+ const router = Router5();
713
+ const storage = new SentinelStorage();
714
+ router.get("/", async (req, res) => {
715
+ try {
716
+ const source = req.query.source;
717
+ const symbol = req.query.symbol;
718
+ const minConfidence = parseInt(req.query.minConfidence) || void 0;
719
+ const options = {};
720
+ if (source && ["manual", "suggested", "imported", "community"].includes(source)) {
721
+ options.source = source;
722
+ }
723
+ if (symbol) options.symbol = symbol;
724
+ if (minConfidence) options.minConfidence = minConfidence;
725
+ const patterns = storage.getAllPatterns(options);
726
+ const summaries = patterns.map((pattern) => ({
727
+ id: pattern.id,
728
+ name: pattern.name,
729
+ description: pattern.description,
730
+ confidence: {
731
+ score: pattern.confidence.score,
732
+ timesMatched: pattern.confidence.timesMatched,
733
+ timesResolved: pattern.confidence.timesResolved
734
+ },
735
+ tags: pattern.tags
736
+ }));
737
+ res.json({ patterns: summaries });
738
+ } catch (error) {
739
+ console.error("Failed to load patterns:", error);
740
+ res.status(500).json({ error: "Failed to load patterns" });
741
+ }
742
+ });
743
+ router.get("/:id", async (req, res) => {
744
+ try {
745
+ const pattern = storage.getPattern(req.params.id);
746
+ if (!pattern) {
747
+ res.status(404).json({ error: "Pattern not found" });
748
+ return;
749
+ }
750
+ res.json({ pattern });
751
+ } catch (error) {
752
+ console.error("Failed to load pattern:", error);
753
+ res.status(500).json({ error: "Failed to load pattern" });
754
+ }
755
+ });
756
+ return router;
757
+ }
758
+ function inferSymbolType(symbol) {
759
+ if (symbol.startsWith("#")) return "component";
760
+ if (symbol.startsWith("^")) return "gate";
761
+ if (symbol.startsWith("!")) return "signal";
762
+ if (symbol.startsWith("$")) return "flow";
763
+ if (symbol.startsWith("~")) return "aspect";
764
+ return "raw";
765
+ }
766
+ function validateSymbol(symbol, index) {
767
+ const entry = index.find((e) => e.symbol === symbol);
768
+ if (entry) return { known: true };
769
+ const symbolName = symbol.replace(/^[#^!$~]/, "");
770
+ let bestMatch;
771
+ let bestScore = 0;
772
+ for (const e of index) {
773
+ const eName = e.symbol.replace(/^[#^!$~]/, "");
774
+ let shared = 0;
775
+ for (let i = 0; i < Math.min(symbolName.length, eName.length); i++) {
776
+ if (symbolName[i] === eName[i]) shared++;
777
+ else break;
778
+ }
779
+ const score = shared / Math.max(symbolName.length, eName.length);
780
+ if (score > bestScore && score > 0.5) {
781
+ bestScore = score;
782
+ bestMatch = e.symbol;
783
+ }
784
+ }
785
+ return { known: false, suggestion: bestMatch };
786
+ }
787
+ function autoPromoteToIncident(entry, storage) {
788
+ try {
789
+ const symbolType = inferSymbolType(entry.symbol);
790
+ const symbols = {};
791
+ if (symbolType === "component") symbols.component = entry.symbol;
792
+ else if (symbolType === "gate") symbols.gate = entry.symbol;
793
+ else if (symbolType === "signal") symbols.signal = entry.symbol;
794
+ else if (symbolType === "flow") symbols.flow = entry.symbol;
795
+ else symbols.component = entry.symbol;
796
+ storage.recordIncident({
797
+ error: {
798
+ message: entry.message,
799
+ type: "LogError"
800
+ },
801
+ symbols,
802
+ environment: entry.environment || "unknown",
803
+ service: entry.service
804
+ });
805
+ } catch {
806
+ }
807
+ }
808
+ var insertsSincePrune = 0;
809
+ function createLogsRouter(options) {
810
+ const router = Router6();
811
+ const { storage, serverConfig, onLogReceived, symbolIndex } = options;
812
+ router.post("/", async (req, res) => {
813
+ try {
814
+ const body = req.body;
815
+ let entries;
816
+ if (Array.isArray(body.entries)) {
817
+ entries = body.entries;
818
+ } else if (body.level && body.symbol && body.message && body.service) {
819
+ entries = [body];
820
+ } else {
821
+ res.status(400).json({ error: "Expected {entries: [...]} or a single log entry with level, symbol, message, service" });
822
+ return;
823
+ }
824
+ if (entries.length > serverConfig.maxBatchSize) {
825
+ res.status(413).json({
826
+ error: `Batch too large: ${entries.length} entries, max ${serverConfig.maxBatchSize}`
827
+ });
828
+ return;
829
+ }
830
+ for (let i = 0; i < entries.length; i++) {
831
+ const e = entries[i];
832
+ if (!e.level || !e.symbol || !e.message || !e.service) {
833
+ res.status(400).json({
834
+ error: `Entry ${i}: missing required fields (level, symbol, message, service)`
835
+ });
836
+ return;
837
+ }
838
+ if (!["debug", "info", "warn", "error"].includes(e.level)) {
839
+ res.status(400).json({
840
+ error: `Entry ${i}: invalid level "${e.level}", must be debug|info|warn|error`
841
+ });
842
+ return;
843
+ }
844
+ }
845
+ const result = storage.insertLogBatch(entries);
846
+ for (const input of entries) {
847
+ const entry = {
848
+ id: input.id || uuidv4(),
849
+ timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
850
+ level: input.level,
851
+ symbol: input.symbol,
852
+ symbolType: input.symbolType || inferSymbolType(input.symbol),
853
+ message: input.message,
854
+ data: input.data,
855
+ service: input.service,
856
+ sessionId: input.sessionId,
857
+ correlationId: input.correlationId,
858
+ durationMs: input.durationMs,
859
+ environment: input.environment
860
+ };
861
+ let validation;
862
+ if (symbolIndex) {
863
+ validation = validateSymbol(entry.symbol, symbolIndex);
864
+ }
865
+ if (entry.level === "error") {
866
+ autoPromoteToIncident(entry, storage);
867
+ }
868
+ if (onLogReceived) {
869
+ onLogReceived(entry, validation);
870
+ }
871
+ }
872
+ insertsSincePrune += result.accepted;
873
+ if (serverConfig.maxLogs > 0 && insertsSincePrune >= serverConfig.pruneIntervalInserts) {
874
+ insertsSincePrune = 0;
875
+ storage.pruneLogs(serverConfig.maxLogs);
876
+ }
877
+ res.json({ accepted: result.accepted, errors: result.errors.length > 0 ? result.errors : void 0 });
878
+ } catch (error) {
879
+ res.status(500).json({ error: "Failed to insert logs" });
880
+ }
881
+ });
882
+ router.get("/", async (req, res) => {
883
+ try {
884
+ const options2 = {
885
+ level: req.query.level,
886
+ symbol: req.query.symbol,
887
+ service: req.query.service,
888
+ sessionId: req.query.sessionId,
889
+ correlationId: req.query.correlationId,
890
+ search: req.query.search,
891
+ since: req.query.since,
892
+ until: req.query.until,
893
+ limit: req.query.limit ? parseInt(req.query.limit) : 100,
894
+ offset: req.query.offset ? parseInt(req.query.offset) : 0
895
+ };
896
+ const logs = storage.queryLogs(options2);
897
+ const total = storage.getLogCount(options2);
898
+ res.json({ count: logs.length, total, logs });
899
+ } catch (error) {
900
+ res.status(500).json({ error: "Failed to query logs" });
901
+ }
902
+ });
903
+ return router;
904
+ }
905
+ function createServicesRouter(options) {
906
+ const router = Router7();
907
+ const { storage } = options;
908
+ router.post("/", async (req, res) => {
909
+ try {
910
+ const { name, version, pid, environment, metadata } = req.body;
911
+ if (!name) {
912
+ res.status(400).json({ error: "Missing required field: name" });
913
+ return;
914
+ }
915
+ storage.registerService({ name, version, pid, environment, metadata });
916
+ res.json({ success: true, service: name });
917
+ } catch (error) {
918
+ res.status(500).json({ error: "Failed to register service" });
919
+ }
920
+ });
921
+ router.get("/", async (_req, res) => {
922
+ try {
923
+ const services = storage.getServices();
924
+ res.json({ count: services.length, services });
925
+ } catch (error) {
926
+ res.status(500).json({ error: "Failed to list services" });
927
+ }
928
+ });
929
+ return router;
930
+ }
931
+ function createStateRouter(options) {
932
+ const router = Router7();
933
+ const { storage } = options;
934
+ router.post("/", async (req, res) => {
935
+ try {
936
+ const { service, sessionId, state, activeFlows, activeGates } = req.body;
937
+ if (!service || !sessionId || !state) {
938
+ res.status(400).json({ error: "Missing required fields: service, sessionId, state" });
939
+ return;
940
+ }
941
+ storage.upsertAppState({
942
+ service,
943
+ sessionId,
944
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
945
+ state,
946
+ activeFlows,
947
+ activeGates
948
+ });
949
+ storage.updateServiceLastSeen(service);
950
+ res.json({ success: true });
951
+ } catch (error) {
952
+ res.status(500).json({ error: "Failed to update state" });
953
+ }
954
+ });
955
+ router.get("/", async (_req, res) => {
956
+ try {
957
+ const states = storage.getAllAppStates();
958
+ res.json({ count: states.length, states });
959
+ } catch (error) {
960
+ res.status(500).json({ error: "Failed to get states" });
961
+ }
962
+ });
963
+ router.get("/:service", async (req, res) => {
964
+ try {
965
+ const states = storage.getAppState(req.params.service);
966
+ res.json({ count: states.length, states });
967
+ } catch (error) {
968
+ res.status(500).json({ error: "Failed to get service state" });
969
+ }
970
+ });
971
+ return router;
972
+ }
973
+ var VALID_METRIC_TYPES = ["counter", "gauge", "histogram"];
974
+ function createMetricsRouter(options) {
975
+ const router = Router8();
976
+ const { storage, serverConfig } = options;
977
+ router.post("/", (req, res) => {
978
+ try {
979
+ const body = req.body;
980
+ let entries;
981
+ if (Array.isArray(body.entries)) {
982
+ entries = body.entries;
983
+ } else if (body.name && body.type && body.value !== void 0 && body.service) {
984
+ entries = [body];
985
+ } else {
986
+ res.status(400).json({
987
+ error: "Expected {entries: [...]} or a single metric with name, type, value, service"
988
+ });
989
+ return;
990
+ }
991
+ if (entries.length > serverConfig.maxBatchSize) {
992
+ res.status(413).json({
993
+ error: `Batch too large: ${entries.length} entries, max ${serverConfig.maxBatchSize}`
994
+ });
995
+ return;
996
+ }
997
+ for (let i = 0; i < entries.length; i++) {
998
+ const e = entries[i];
999
+ if (!e.name || !e.type || e.value === void 0 || !e.service) {
1000
+ res.status(400).json({
1001
+ error: `Entry ${i}: missing required fields (name, type, value, service)`
1002
+ });
1003
+ return;
1004
+ }
1005
+ if (!VALID_METRIC_TYPES.includes(e.type)) {
1006
+ res.status(400).json({
1007
+ error: `Entry ${i}: invalid type "${e.type}", must be counter|gauge|histogram`
1008
+ });
1009
+ return;
1010
+ }
1011
+ }
1012
+ const result = storage.insertMetricBatch(entries);
1013
+ res.json({
1014
+ accepted: result.accepted,
1015
+ errors: result.errors.length > 0 ? result.errors : void 0
1016
+ });
1017
+ } catch {
1018
+ res.status(500).json({ error: "Failed to insert metrics" });
1019
+ }
1020
+ });
1021
+ router.get("/", (req, res) => {
1022
+ try {
1023
+ const options2 = {
1024
+ name: req.query.name,
1025
+ type: req.query.type,
1026
+ service: req.query.service,
1027
+ tag: req.query.tag,
1028
+ since: req.query.since,
1029
+ until: req.query.until,
1030
+ limit: req.query.limit ? parseInt(req.query.limit) : 100,
1031
+ offset: req.query.offset ? parseInt(req.query.offset) : 0
1032
+ };
1033
+ const metrics = storage.queryMetrics(options2);
1034
+ const total = storage.getMetricCount(options2);
1035
+ res.json({ count: metrics.length, total, metrics });
1036
+ } catch {
1037
+ res.status(500).json({ error: "Failed to query metrics" });
1038
+ }
1039
+ });
1040
+ router.get("/aggregate/:name", (req, res) => {
1041
+ try {
1042
+ const aggregation = storage.aggregateMetric(req.params.name, {
1043
+ service: req.query.service,
1044
+ since: req.query.since,
1045
+ until: req.query.until
1046
+ });
1047
+ res.json(aggregation);
1048
+ } catch {
1049
+ res.status(500).json({ error: "Failed to aggregate metric" });
1050
+ }
1051
+ });
1052
+ return router;
1053
+ }
1054
+ function createTracesRouter(options) {
1055
+ const router = Router9();
1056
+ const { storage } = options;
1057
+ router.post("/", (req, res) => {
1058
+ try {
1059
+ const body = req.body;
1060
+ if (!body.traceId || !body.service || !body.symbol || !body.operation) {
1061
+ res.status(400).json({
1062
+ error: "Missing required fields: traceId, service, symbol, operation"
1063
+ });
1064
+ return;
1065
+ }
1066
+ const spanId = storage.insertSpan(body);
1067
+ res.json({ spanId, traceId: body.traceId });
1068
+ } catch {
1069
+ res.status(500).json({ error: "Failed to insert trace span" });
1070
+ }
1071
+ });
1072
+ router.get("/", (req, res) => {
1073
+ try {
1074
+ const traces = storage.queryTraces({
1075
+ service: req.query.service,
1076
+ symbol: req.query.symbol,
1077
+ since: req.query.since,
1078
+ limit: req.query.limit ? parseInt(req.query.limit) : 20
1079
+ });
1080
+ res.json({ count: traces.length, traces });
1081
+ } catch {
1082
+ res.status(500).json({ error: "Failed to query traces" });
1083
+ }
1084
+ });
1085
+ router.get("/:traceId", (req, res) => {
1086
+ try {
1087
+ const trace = storage.getTrace(req.params.traceId);
1088
+ if (!trace) {
1089
+ res.status(404).json({ error: "Trace not found" });
1090
+ return;
1091
+ }
1092
+ res.json(trace);
1093
+ } catch {
1094
+ res.status(500).json({ error: "Failed to get trace" });
1095
+ }
1096
+ });
1097
+ return router;
1098
+ }
1099
+ function createSchemasRouter(options) {
1100
+ const router = Router10();
1101
+ const { storage } = options;
1102
+ router.post("/", (req, res) => {
1103
+ try {
1104
+ const body = req.body;
1105
+ if (!body.id || !body.version || !body.name || !body.scope || !body.eventTypes) {
1106
+ res.status(400).json({
1107
+ error: "Missing required fields: id, version, name, scope, eventTypes"
1108
+ });
1109
+ return;
1110
+ }
1111
+ if (!body.scope.field || !body.scope.type || !body.scope.label || !body.scope.ordering) {
1112
+ res.status(400).json({
1113
+ error: "Invalid scope: requires field, type, label, ordering"
1114
+ });
1115
+ return;
1116
+ }
1117
+ if (!Array.isArray(body.eventTypes) || body.eventTypes.length === 0) {
1118
+ res.status(400).json({
1119
+ error: "eventTypes must be a non-empty array"
1120
+ });
1121
+ return;
1122
+ }
1123
+ for (let i = 0; i < body.eventTypes.length; i++) {
1124
+ const et = body.eventTypes[i];
1125
+ if (!et.type || !et.category) {
1126
+ res.status(400).json({
1127
+ error: `eventTypes[${i}]: missing required fields (type, category)`
1128
+ });
1129
+ return;
1130
+ }
1131
+ }
1132
+ const schema = storage.registerSchema(body);
1133
+ res.status(201).json(schema);
1134
+ } catch (error) {
1135
+ res.status(500).json({ error: "Failed to register schema" });
1136
+ }
1137
+ });
1138
+ router.get("/", (_req, res) => {
1139
+ try {
1140
+ const schemas = storage.listSchemas();
1141
+ res.json({ count: schemas.length, schemas });
1142
+ } catch (error) {
1143
+ res.status(500).json({ error: "Failed to list schemas" });
1144
+ }
1145
+ });
1146
+ router.get("/:id", (req, res) => {
1147
+ try {
1148
+ const schema = storage.getSchema(req.params.id);
1149
+ if (!schema) {
1150
+ res.status(404).json({ error: "Schema not found" });
1151
+ return;
1152
+ }
1153
+ res.json(schema);
1154
+ } catch (error) {
1155
+ res.status(500).json({ error: "Failed to get schema" });
1156
+ }
1157
+ });
1158
+ return router;
1159
+ }
1160
+ function createEventsRouter(options) {
1161
+ const router = Router11();
1162
+ const { storage, serverConfig, onEventReceived } = options;
1163
+ let insertsSincePrune2 = 0;
1164
+ router.post("/", (req, res) => {
1165
+ try {
1166
+ const body = req.body;
1167
+ if (!body.schemaId || !body.service || !Array.isArray(body.events)) {
1168
+ res.status(400).json({
1169
+ error: "Expected { schemaId, service, events: [...] }"
1170
+ });
1171
+ return;
1172
+ }
1173
+ const { schemaId, service, events } = body;
1174
+ const schema = storage.getSchema(schemaId);
1175
+ if (!schema) {
1176
+ res.status(404).json({
1177
+ error: `Schema "${schemaId}" not found. Register it first via POST /api/schemas.`
1178
+ });
1179
+ return;
1180
+ }
1181
+ if (events.length > serverConfig.maxBatchSize) {
1182
+ res.status(413).json({
1183
+ error: `Batch too large: ${events.length} events, max ${serverConfig.maxBatchSize}`
1184
+ });
1185
+ return;
1186
+ }
1187
+ for (let i = 0; i < events.length; i++) {
1188
+ const e = events[i];
1189
+ if (!e.type) {
1190
+ res.status(400).json({
1191
+ error: `events[${i}]: missing required field "type"`
1192
+ });
1193
+ return;
1194
+ }
1195
+ }
1196
+ const result = storage.insertEventBatch(schemaId, service, events);
1197
+ if (onEventReceived) {
1198
+ const typeMap = /* @__PURE__ */ new Map();
1199
+ for (const et of schema.eventTypes) {
1200
+ typeMap.set(et.type, et.category);
1201
+ }
1202
+ for (const input of events) {
1203
+ const evt = {
1204
+ id: input.id || "",
1205
+ schemaId,
1206
+ eventType: input.type,
1207
+ category: typeMap.get(input.type) || "unknown",
1208
+ timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1209
+ scopeValue: input.scopeValue != null ? String(input.scopeValue) : void 0,
1210
+ sessionId: input.sessionId,
1211
+ service,
1212
+ data: input.data,
1213
+ severity: input.severity || "info",
1214
+ parentEventId: input.parentEventId,
1215
+ depth: input.depth
1216
+ };
1217
+ onEventReceived(evt);
1218
+ }
1219
+ }
1220
+ insertsSincePrune2 += result.accepted;
1221
+ if (serverConfig.maxLogs > 0 && insertsSincePrune2 >= serverConfig.pruneIntervalInserts) {
1222
+ insertsSincePrune2 = 0;
1223
+ storage.pruneEvents(serverConfig.maxLogs);
1224
+ }
1225
+ res.json({
1226
+ accepted: result.accepted,
1227
+ errors: result.errors.length > 0 ? result.errors : void 0
1228
+ });
1229
+ } catch (error) {
1230
+ res.status(500).json({ error: "Failed to ingest events" });
1231
+ }
1232
+ });
1233
+ router.get("/", (req, res) => {
1234
+ try {
1235
+ const query = {
1236
+ schemaId: req.query.schemaId,
1237
+ eventType: req.query.eventType,
1238
+ category: req.query.category,
1239
+ service: req.query.service,
1240
+ sessionId: req.query.sessionId,
1241
+ scopeValue: req.query.scopeValue,
1242
+ scopeFrom: req.query.scopeFrom,
1243
+ scopeTo: req.query.scopeTo,
1244
+ severity: req.query.severity,
1245
+ since: req.query.since,
1246
+ until: req.query.until,
1247
+ search: req.query.search,
1248
+ limit: req.query.limit ? parseInt(req.query.limit) : 100,
1249
+ offset: req.query.offset ? parseInt(req.query.offset) : 0
1250
+ };
1251
+ const events = storage.queryEvents(query);
1252
+ const total = storage.getEventCount(query);
1253
+ res.json({ count: events.length, total, events });
1254
+ } catch (error) {
1255
+ res.status(500).json({ error: "Failed to query events" });
1256
+ }
1257
+ });
1258
+ router.get("/scopes", (req, res) => {
1259
+ try {
1260
+ const schemaId = req.query.schemaId;
1261
+ if (!schemaId) {
1262
+ res.status(400).json({ error: "schemaId query parameter is required" });
1263
+ return;
1264
+ }
1265
+ const scopes = storage.getEventScopes(schemaId, {
1266
+ limit: req.query.limit ? parseInt(req.query.limit) : 100,
1267
+ offset: req.query.offset ? parseInt(req.query.offset) : 0,
1268
+ sessionId: req.query.sessionId
1269
+ });
1270
+ res.json({ count: scopes.length, scopes });
1271
+ } catch (error) {
1272
+ res.status(500).json({ error: "Failed to query scopes" });
1273
+ }
1274
+ });
1275
+ router.get("/scope/:value", (req, res) => {
1276
+ try {
1277
+ const schemaId = req.query.schemaId;
1278
+ if (!schemaId) {
1279
+ res.status(400).json({ error: "schemaId query parameter is required" });
1280
+ return;
1281
+ }
1282
+ const events = storage.queryEventsByScope(schemaId, req.params.value);
1283
+ res.json({ count: events.length, events });
1284
+ } catch (error) {
1285
+ res.status(500).json({ error: "Failed to query scope events" });
1286
+ }
1287
+ });
1288
+ return router;
1289
+ }
1290
+ function createAuthMiddleware(config) {
1291
+ return function authMiddleware(requiredPermission) {
1292
+ return (req, res, next) => {
1293
+ if (!config.enabled) {
1294
+ next();
1295
+ return;
1296
+ }
1297
+ const authHeader = req.headers.authorization;
1298
+ if (!authHeader) {
1299
+ res.status(401).json({ error: "Authentication required. Provide Authorization: Bearer <token>" });
1300
+ return;
1301
+ }
1302
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
1303
+ if (!match) {
1304
+ res.status(401).json({ error: "Invalid authorization format. Use: Bearer <token>" });
1305
+ return;
1306
+ }
1307
+ const tokenValue = match[1];
1308
+ const tokenEntry = config.tokens.find((t) => t.token === tokenValue);
1309
+ if (!tokenEntry) {
1310
+ res.status(401).json({ error: "Invalid token" });
1311
+ return;
1312
+ }
1313
+ if (tokenEntry.expiresAt && new Date(tokenEntry.expiresAt) < /* @__PURE__ */ new Date()) {
1314
+ res.status(401).json({ error: "Token expired" });
1315
+ return;
1316
+ }
1317
+ const permissionLevel = { read: 1, write: 2, admin: 3 };
1318
+ const hasPermission = tokenEntry.permissions.some(
1319
+ (p) => permissionLevel[p] >= permissionLevel[requiredPermission]
1320
+ );
1321
+ if (!hasPermission) {
1322
+ res.status(403).json({ error: `Insufficient permissions. Required: ${requiredPermission}` });
1323
+ return;
1324
+ }
1325
+ req.authToken = tokenEntry;
1326
+ next();
1327
+ };
1328
+ };
1329
+ }
1330
+ var serviceWindows = /* @__PURE__ */ new Map();
1331
+ var globalWindow = { count: 0, windowStart: Date.now() };
1332
+ var WINDOW_MS = 6e4;
1333
+ function getOrResetWindow(entry) {
1334
+ const now = Date.now();
1335
+ if (now - entry.windowStart > WINDOW_MS) {
1336
+ entry.count = 0;
1337
+ entry.windowStart = now;
1338
+ }
1339
+ return entry;
1340
+ }
1341
+ function createRateLimiter(config) {
1342
+ return (req, res, next) => {
1343
+ if (!config.enabled) {
1344
+ next();
1345
+ return;
1346
+ }
1347
+ const service = req.body?.service || req.body?.entries?.[0]?.service || req.query.service || "_unknown";
1348
+ const rule = config.perService[service] || config.global;
1349
+ if (rule.samplingRate < 1 && Math.random() > rule.samplingRate) {
1350
+ res.status(200).json({ accepted: 0, sampled: true, message: "Request dropped by sampling" });
1351
+ return;
1352
+ }
1353
+ const gw = getOrResetWindow(globalWindow);
1354
+ if (gw.count >= config.global.maxRequestsPerMinute) {
1355
+ res.status(429).json({
1356
+ error: "Global rate limit exceeded",
1357
+ retryAfterMs: WINDOW_MS - (Date.now() - gw.windowStart)
1358
+ });
1359
+ return;
1360
+ }
1361
+ if (!serviceWindows.has(service)) {
1362
+ serviceWindows.set(service, { count: 0, windowStart: Date.now() });
1363
+ }
1364
+ const sw = getOrResetWindow(serviceWindows.get(service));
1365
+ if (sw.count >= rule.maxRequestsPerMinute) {
1366
+ res.status(429).json({
1367
+ error: `Rate limit exceeded for service: ${service}`,
1368
+ retryAfterMs: WINDOW_MS - (Date.now() - sw.windowStart)
1369
+ });
1370
+ return;
1371
+ }
1372
+ const batchSize = req.body?.entries?.length || 1;
1373
+ if (batchSize > rule.maxEntriesPerBatch) {
1374
+ res.status(413).json({
1375
+ error: `Batch too large: ${batchSize} entries, max ${rule.maxEntriesPerBatch} for service ${service}`
1376
+ });
1377
+ return;
1378
+ }
1379
+ gw.count++;
1380
+ sw.count++;
1381
+ next();
1382
+ };
1383
+ }
1384
+ var __filename = fileURLToPath(import.meta.url);
1385
+ var __dirname = path3.dirname(__filename);
1386
+ var log3 = {
1387
+ component(name) {
1388
+ const symbol = chalk3.magenta(`#${name}`);
1389
+ return {
1390
+ info: (msg, data) => {
1391
+ const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
1392
+ console.log(`${chalk3.blue("\u2139")} ${symbol} ${msg}${dataStr}`);
1393
+ },
1394
+ success: (msg, data) => {
1395
+ const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
1396
+ console.log(`${chalk3.green("\u2714")} ${symbol} ${msg}${dataStr}`);
1397
+ },
1398
+ warn: (msg, data) => {
1399
+ const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
1400
+ console.log(`${chalk3.yellow("\u26A0")} ${symbol} ${msg}${dataStr}`);
1401
+ },
1402
+ error: (msg, data) => {
1403
+ const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
1404
+ console.error(`${chalk3.red("\u2716")} ${symbol} ${msg}${dataStr}`);
1405
+ }
1406
+ };
1407
+ }
1408
+ };
1409
+ function createApp(options) {
1410
+ const app = express();
1411
+ app.use(express.json({ limit: "5mb" }));
1412
+ app.use((_req, res, next) => {
1413
+ const corsOrigin = options.serverConfig?.cors?.origin;
1414
+ const origin = Array.isArray(corsOrigin) ? corsOrigin.join(", ") : corsOrigin ?? "*";
1415
+ res.header("Access-Control-Allow-Origin", origin);
1416
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
1417
+ res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
1418
+ if (options.serverConfig?.cors?.credentials) {
1419
+ res.header("Access-Control-Allow-Credentials", "true");
1420
+ }
1421
+ if (_req.method === "OPTIONS") {
1422
+ res.sendStatus(204);
1423
+ return;
1424
+ }
1425
+ next();
1426
+ });
1427
+ app.use("/api/symbols", createSymbolsRouter(options.projectDir));
1428
+ app.use("/api/info", createInfoRouter(options.projectDir));
1429
+ app.use("/api/commits", createCommitsRouter(options.projectDir));
1430
+ app.use("/api/incidents", createIncidentsRouter(options.projectDir));
1431
+ app.use("/api/patterns", createPatternsRouter(options.projectDir));
1432
+ if (options.storage && options.serverConfig) {
1433
+ const config = options.serverConfig;
1434
+ const auth = createAuthMiddleware(config.auth);
1435
+ const rateLimiter = createRateLimiter(config.rateLimit);
1436
+ app.use("/api/logs", rateLimiter, auth("write"), createLogsRouter({
1437
+ storage: options.storage,
1438
+ serverConfig: config,
1439
+ onLogReceived: options.onLogReceived,
1440
+ symbolIndex: options.symbolIndex
1441
+ }));
1442
+ app.use("/api/services", rateLimiter, auth("write"), createServicesRouter({ storage: options.storage }));
1443
+ app.use("/api/state", rateLimiter, auth("write"), createStateRouter({ storage: options.storage }));
1444
+ app.use("/api/metrics", rateLimiter, auth("write"), createMetricsRouter({
1445
+ storage: options.storage,
1446
+ serverConfig: config
1447
+ }));
1448
+ app.use("/api/traces", rateLimiter, auth("write"), createTracesRouter({
1449
+ storage: options.storage
1450
+ }));
1451
+ app.use("/api/schemas", rateLimiter, auth("write"), createSchemasRouter({
1452
+ storage: options.storage
1453
+ }));
1454
+ app.use("/api/events", rateLimiter, auth("write"), createEventsRouter({
1455
+ storage: options.storage,
1456
+ serverConfig: config,
1457
+ onEventReceived: options.onEventReceived
1458
+ }));
1459
+ }
1460
+ app.get("/api/health", (_req, res) => {
1461
+ res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1462
+ });
1463
+ const uiDistPath = path3.join(__dirname, "..", "..", "ui", "dist");
1464
+ if (fs2.existsSync(uiDistPath)) {
1465
+ app.use(express.static(uiDistPath));
1466
+ app.get("{*path}", (req, res) => {
1467
+ if (!req.path.startsWith("/api")) {
1468
+ res.sendFile(path3.join(uiDistPath, "index.html"));
1469
+ }
1470
+ });
1471
+ }
1472
+ return app;
1473
+ }
1474
+ async function startServer(options) {
1475
+ const serverConfig = loadServerConfig(options.projectDir);
1476
+ if (options.logPruneLimit !== void 0) {
1477
+ serverConfig.maxLogs = options.logPruneLimit;
1478
+ }
1479
+ const storage = new SentinelStorage(options.dbPath);
1480
+ await storage.ensureReady();
1481
+ let symbolIndex = [];
1482
+ try {
1483
+ symbolIndex = await loadSymbolIndex(options.projectDir);
1484
+ } catch {
1485
+ log3.component("sentinel-server").warn("Could not load symbol index for validation");
1486
+ }
1487
+ const wsClients = /* @__PURE__ */ new Set();
1488
+ function broadcast(message) {
1489
+ const data = JSON.stringify(message);
1490
+ for (const client of wsClients) {
1491
+ if (client.readyState === WebSocket.OPEN) {
1492
+ client.send(data);
1493
+ }
1494
+ }
1495
+ }
1496
+ function onLogReceived(entry, validation) {
1497
+ const message = { type: "log", entry };
1498
+ if (validation && !validation.known) {
1499
+ message.validation = validation;
1500
+ }
1501
+ broadcast(message);
1502
+ if (entry.symbolType === "signal" || entry.symbolType === "gate" || entry.symbolType === "flow") {
1503
+ broadcast({
1504
+ type: "flow_event",
1505
+ flowId: entry.symbolType === "flow" ? entry.symbol : void 0,
1506
+ nodeSymbol: entry.symbol,
1507
+ event: entry.symbolType,
1508
+ timestamp: entry.timestamp,
1509
+ service: entry.service
1510
+ });
1511
+ }
1512
+ }
1513
+ function onEventReceived(event) {
1514
+ broadcast({ type: "event", event });
1515
+ }
1516
+ const app = createApp({
1517
+ ...options,
1518
+ storage,
1519
+ serverConfig,
1520
+ symbolIndex,
1521
+ onLogReceived,
1522
+ onEventReceived
1523
+ });
1524
+ log3.component("sentinel-server").info("Starting server", { port: options.port });
1525
+ log3.component("sentinel-server").info("Project directory", { path: options.projectDir });
1526
+ return new Promise((resolve, reject) => {
1527
+ const httpServer = http.createServer(app);
1528
+ const wss = new WebSocketServer({ server: httpServer });
1529
+ wss.on("connection", (ws) => {
1530
+ if (wsClients.size >= serverConfig.wsMaxSubscribers) {
1531
+ ws.close(1013, "Max subscribers reached");
1532
+ return;
1533
+ }
1534
+ wsClients.add(ws);
1535
+ log3.component("sentinel-ws").info("Client connected", { total: wsClients.size });
1536
+ ws.on("message", (raw) => {
1537
+ try {
1538
+ const msg = JSON.parse(raw.toString());
1539
+ if (msg.method === "ping") {
1540
+ ws.send(JSON.stringify({
1541
+ jsonrpc: "2.0",
1542
+ result: { pong: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
1543
+ id: msg.id
1544
+ }));
1545
+ } else if (msg.method === "subscribe") {
1546
+ ws.send(JSON.stringify({
1547
+ jsonrpc: "2.0",
1548
+ result: { subscribed: true },
1549
+ id: msg.id
1550
+ }));
1551
+ } else if (msg.method === "query_logs") {
1552
+ const logs = storage.queryLogs(msg.params || {});
1553
+ ws.send(JSON.stringify({
1554
+ jsonrpc: "2.0",
1555
+ result: { logs },
1556
+ id: msg.id
1557
+ }));
1558
+ } else if (msg.method === "query_events") {
1559
+ const events = storage.queryEvents(msg.params || {});
1560
+ ws.send(JSON.stringify({
1561
+ jsonrpc: "2.0",
1562
+ result: { events },
1563
+ id: msg.id
1564
+ }));
1565
+ } else if (msg.method === "query_scopes") {
1566
+ const { schemaId, ...rest } = msg.params || {};
1567
+ const scopes = schemaId ? storage.getEventScopes(schemaId, rest) : [];
1568
+ ws.send(JSON.stringify({
1569
+ jsonrpc: "2.0",
1570
+ result: { scopes },
1571
+ id: msg.id
1572
+ }));
1573
+ }
1574
+ } catch {
1575
+ }
1576
+ });
1577
+ ws.on("close", () => {
1578
+ wsClients.delete(ws);
1579
+ log3.component("sentinel-ws").info("Client disconnected", { total: wsClients.size });
1580
+ });
1581
+ ws.on("error", () => {
1582
+ wsClients.delete(ws);
1583
+ });
1584
+ });
1585
+ httpServer.listen(options.port, () => {
1586
+ log3.component("sentinel-server").success("Server running", { url: `http://localhost:${options.port}` });
1587
+ log3.component("sentinel-ws").success("WebSocket ready", { url: `ws://localhost:${options.port}` });
1588
+ if (options.open) {
1589
+ import("open").then((openModule) => {
1590
+ openModule.default(`http://localhost:${options.port}`);
1591
+ log3.component("sentinel-server").info("Opened browser");
1592
+ }).catch(() => {
1593
+ log3.component("sentinel-server").warn("Could not open browser automatically");
1594
+ });
1595
+ }
1596
+ resolve();
1597
+ });
1598
+ httpServer.on("error", (err) => {
1599
+ if (err.code === "EADDRINUSE") {
1600
+ log3.component("sentinel-server").error("Port already in use", { port: options.port });
1601
+ } else {
1602
+ log3.component("sentinel-server").error("Server error", { error: err.message });
1603
+ }
1604
+ reject(err);
1605
+ });
1606
+ });
1607
+ }
1608
+ export {
1609
+ createApp,
1610
+ getSymbolCount,
1611
+ getSymbolsAtCommit,
1612
+ loadGitHistory,
1613
+ loadParadigmConfig,
1614
+ loadSymbolIndex,
1615
+ startServer
1616
+ };