@a-company/sentinel 0.2.0 → 3.5.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.
package/dist/mcp.js CHANGED
@@ -13,7 +13,7 @@ import initSqlJs from "sql.js";
13
13
  import { v4 as uuidv4 } from "uuid";
14
14
  import * as path from "path";
15
15
  import * as fs from "fs";
16
- var SCHEMA_VERSION = 2;
16
+ var SCHEMA_VERSION = 4;
17
17
  var DEFAULT_CONFIDENCE = {
18
18
  score: 50,
19
19
  timesMatched: 0,
@@ -133,6 +133,72 @@ var SentinelStorage = class {
133
133
  notes TEXT
134
134
  );
135
135
 
136
+ -- Structured logs table
137
+ CREATE TABLE IF NOT EXISTS logs (
138
+ id TEXT PRIMARY KEY,
139
+ timestamp TEXT NOT NULL,
140
+ level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
141
+ symbol TEXT NOT NULL,
142
+ symbol_type TEXT NOT NULL DEFAULT 'raw',
143
+ message TEXT NOT NULL,
144
+ data_json TEXT,
145
+ service TEXT NOT NULL,
146
+ session_id TEXT,
147
+ correlation_id TEXT,
148
+ duration_ms REAL,
149
+ environment TEXT
150
+ );
151
+
152
+ -- Service registry
153
+ CREATE TABLE IF NOT EXISTS services (
154
+ name TEXT PRIMARY KEY,
155
+ version TEXT,
156
+ pid INTEGER,
157
+ started_at TEXT NOT NULL,
158
+ last_seen_at TEXT NOT NULL,
159
+ environment TEXT,
160
+ metadata_json TEXT
161
+ );
162
+
163
+ -- Live app state snapshots (latest-wins per service+session)
164
+ CREATE TABLE IF NOT EXISTS app_state (
165
+ service TEXT NOT NULL,
166
+ session_id TEXT NOT NULL,
167
+ timestamp TEXT NOT NULL,
168
+ state_json TEXT NOT NULL,
169
+ active_flows_json TEXT,
170
+ active_gates_json TEXT,
171
+ PRIMARY KEY (service, session_id)
172
+ );
173
+
174
+ -- Metrics table
175
+ CREATE TABLE IF NOT EXISTS metrics (
176
+ id TEXT PRIMARY KEY,
177
+ timestamp TEXT NOT NULL,
178
+ name TEXT NOT NULL,
179
+ type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
180
+ value REAL NOT NULL,
181
+ tags_json TEXT DEFAULT '{}',
182
+ service TEXT NOT NULL,
183
+ environment TEXT
184
+ );
185
+
186
+ -- Traces table
187
+ CREATE TABLE IF NOT EXISTS traces (
188
+ trace_id TEXT NOT NULL,
189
+ span_id TEXT PRIMARY KEY,
190
+ parent_span_id TEXT,
191
+ service TEXT NOT NULL,
192
+ symbol TEXT NOT NULL,
193
+ operation TEXT NOT NULL,
194
+ start_time TEXT NOT NULL,
195
+ end_time TEXT,
196
+ duration_ms REAL,
197
+ status TEXT NOT NULL DEFAULT 'ok',
198
+ tags_json TEXT DEFAULT '{}',
199
+ log_ids_json TEXT DEFAULT '[]'
200
+ );
201
+
136
202
  -- Indexes
137
203
  CREATE INDEX IF NOT EXISTS idx_incidents_timestamp ON incidents(timestamp);
138
204
  CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status);
@@ -142,6 +208,18 @@ var SentinelStorage = class {
142
208
  CREATE INDEX IF NOT EXISTS idx_practice_events_habit_id ON practice_events(habit_id);
143
209
  CREATE INDEX IF NOT EXISTS idx_practice_events_engineer ON practice_events(engineer);
144
210
  CREATE INDEX IF NOT EXISTS idx_practice_events_session_id ON practice_events(session_id);
211
+ CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
212
+ CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
213
+ CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
214
+ CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
215
+ CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
216
+ CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
217
+ CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
218
+ CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
219
+ CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
220
+ CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
221
+ CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
222
+ CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
145
223
  `);
146
224
  this.db.run(
147
225
  "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
@@ -271,6 +349,101 @@ var SentinelStorage = class {
271
349
  this.db.run(
272
350
  "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '2')"
273
351
  );
352
+ currentVersion = 2;
353
+ }
354
+ if (currentVersion < 3) {
355
+ try {
356
+ this.db.run(`
357
+ CREATE TABLE IF NOT EXISTS logs (
358
+ id TEXT PRIMARY KEY,
359
+ timestamp TEXT NOT NULL,
360
+ level TEXT NOT NULL CHECK (level IN ('debug','info','warn','error')),
361
+ symbol TEXT NOT NULL,
362
+ symbol_type TEXT NOT NULL DEFAULT 'raw',
363
+ message TEXT NOT NULL,
364
+ data_json TEXT,
365
+ service TEXT NOT NULL,
366
+ session_id TEXT,
367
+ correlation_id TEXT,
368
+ duration_ms REAL,
369
+ environment TEXT
370
+ );
371
+
372
+ CREATE TABLE IF NOT EXISTS services (
373
+ name TEXT PRIMARY KEY,
374
+ version TEXT,
375
+ pid INTEGER,
376
+ started_at TEXT NOT NULL,
377
+ last_seen_at TEXT NOT NULL,
378
+ environment TEXT,
379
+ metadata_json TEXT
380
+ );
381
+
382
+ CREATE TABLE IF NOT EXISTS app_state (
383
+ service TEXT NOT NULL,
384
+ session_id TEXT NOT NULL,
385
+ timestamp TEXT NOT NULL,
386
+ state_json TEXT NOT NULL,
387
+ active_flows_json TEXT,
388
+ active_gates_json TEXT,
389
+ PRIMARY KEY (service, session_id)
390
+ );
391
+
392
+ CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
393
+ CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
394
+ CREATE INDEX IF NOT EXISTS idx_logs_symbol ON logs(symbol);
395
+ CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service);
396
+ CREATE INDEX IF NOT EXISTS idx_logs_session_id ON logs(session_id);
397
+ CREATE INDEX IF NOT EXISTS idx_logs_correlation_id ON logs(correlation_id);
398
+ `);
399
+ } catch {
400
+ }
401
+ this.db.run(
402
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '3')"
403
+ );
404
+ currentVersion = 3;
405
+ }
406
+ if (currentVersion < 4) {
407
+ try {
408
+ this.db.run(`
409
+ CREATE TABLE IF NOT EXISTS metrics (
410
+ id TEXT PRIMARY KEY,
411
+ timestamp TEXT NOT NULL,
412
+ name TEXT NOT NULL,
413
+ type TEXT NOT NULL CHECK (type IN ('counter','gauge','histogram')),
414
+ value REAL NOT NULL,
415
+ tags_json TEXT DEFAULT '{}',
416
+ service TEXT NOT NULL,
417
+ environment TEXT
418
+ );
419
+
420
+ CREATE TABLE IF NOT EXISTS traces (
421
+ trace_id TEXT NOT NULL,
422
+ span_id TEXT PRIMARY KEY,
423
+ parent_span_id TEXT,
424
+ service TEXT NOT NULL,
425
+ symbol TEXT NOT NULL,
426
+ operation TEXT NOT NULL,
427
+ start_time TEXT NOT NULL,
428
+ end_time TEXT,
429
+ duration_ms REAL,
430
+ status TEXT NOT NULL DEFAULT 'ok',
431
+ tags_json TEXT DEFAULT '{}',
432
+ log_ids_json TEXT DEFAULT '[]'
433
+ );
434
+
435
+ CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
436
+ CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name);
437
+ CREATE INDEX IF NOT EXISTS idx_metrics_service ON metrics(service);
438
+ CREATE INDEX IF NOT EXISTS idx_traces_trace_id ON traces(trace_id);
439
+ CREATE INDEX IF NOT EXISTS idx_traces_service ON traces(service);
440
+ CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time);
441
+ `);
442
+ } catch {
443
+ }
444
+ this.db.run(
445
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '4')"
446
+ );
274
447
  }
275
448
  }
276
449
  /**
@@ -1245,6 +1418,620 @@ var SentinelStorage = class {
1245
1418
  notes: obj.notes || void 0
1246
1419
  };
1247
1420
  }
1421
+ // ─── Structured Logs ─────────────────────────────────────────────
1422
+ inferSymbolType(symbol) {
1423
+ if (symbol.startsWith("#")) return "component";
1424
+ if (symbol.startsWith("^")) return "gate";
1425
+ if (symbol.startsWith("!")) return "signal";
1426
+ if (symbol.startsWith("$")) return "flow";
1427
+ if (symbol.startsWith("~")) return "aspect";
1428
+ return "raw";
1429
+ }
1430
+ insertLog(input) {
1431
+ this.initializeSync();
1432
+ const id = input.id || uuidv4();
1433
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1434
+ const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
1435
+ this.db.run(
1436
+ `INSERT INTO logs (
1437
+ id, timestamp, level, symbol, symbol_type, message, data_json,
1438
+ service, session_id, correlation_id, duration_ms, environment
1439
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1440
+ [
1441
+ id,
1442
+ timestamp,
1443
+ input.level,
1444
+ input.symbol,
1445
+ symbolType,
1446
+ input.message,
1447
+ input.data ? JSON.stringify(input.data) : null,
1448
+ input.service,
1449
+ input.sessionId || null,
1450
+ input.correlationId || null,
1451
+ input.durationMs ?? null,
1452
+ input.environment || null
1453
+ ]
1454
+ );
1455
+ this.save();
1456
+ return id;
1457
+ }
1458
+ insertLogBatch(entries) {
1459
+ this.initializeSync();
1460
+ let accepted = 0;
1461
+ const errors = [];
1462
+ for (const input of entries) {
1463
+ try {
1464
+ const id = input.id || uuidv4();
1465
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1466
+ const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
1467
+ this.db.run(
1468
+ `INSERT INTO logs (
1469
+ id, timestamp, level, symbol, symbol_type, message, data_json,
1470
+ service, session_id, correlation_id, duration_ms, environment
1471
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1472
+ [
1473
+ id,
1474
+ timestamp,
1475
+ input.level,
1476
+ input.symbol,
1477
+ symbolType,
1478
+ input.message,
1479
+ input.data ? JSON.stringify(input.data) : null,
1480
+ input.service,
1481
+ input.sessionId || null,
1482
+ input.correlationId || null,
1483
+ input.durationMs ?? null,
1484
+ input.environment || null
1485
+ ]
1486
+ );
1487
+ accepted++;
1488
+ } catch (err) {
1489
+ errors.push(err instanceof Error ? err.message : String(err));
1490
+ }
1491
+ }
1492
+ this.save();
1493
+ return { accepted, errors };
1494
+ }
1495
+ queryLogs(options = {}) {
1496
+ this.initializeSync();
1497
+ const { limit = 100, offset = 0 } = options;
1498
+ const conditions = [];
1499
+ const params = [];
1500
+ if (options.level) {
1501
+ conditions.push("level = ?");
1502
+ params.push(options.level);
1503
+ }
1504
+ if (options.symbol) {
1505
+ conditions.push("symbol LIKE ?");
1506
+ params.push(`%${options.symbol}%`);
1507
+ }
1508
+ if (options.service) {
1509
+ conditions.push("service = ?");
1510
+ params.push(options.service);
1511
+ }
1512
+ if (options.sessionId) {
1513
+ conditions.push("session_id = ?");
1514
+ params.push(options.sessionId);
1515
+ }
1516
+ if (options.correlationId) {
1517
+ conditions.push("correlation_id = ?");
1518
+ params.push(options.correlationId);
1519
+ }
1520
+ if (options.search) {
1521
+ conditions.push("message LIKE ?");
1522
+ params.push(`%${options.search}%`);
1523
+ }
1524
+ if (options.since) {
1525
+ conditions.push("timestamp >= ?");
1526
+ params.push(options.since);
1527
+ }
1528
+ if (options.until) {
1529
+ conditions.push("timestamp <= ?");
1530
+ params.push(options.until);
1531
+ }
1532
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1533
+ const result = this.db.exec(
1534
+ `SELECT * FROM logs ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1535
+ [...params, limit, offset]
1536
+ );
1537
+ if (result.length === 0) return [];
1538
+ return result[0].values.map(
1539
+ (row) => this.rowToLogEntry(result[0].columns, row)
1540
+ );
1541
+ }
1542
+ getLogCount(options = {}) {
1543
+ this.initializeSync();
1544
+ const conditions = [];
1545
+ const params = [];
1546
+ if (options.level) {
1547
+ conditions.push("level = ?");
1548
+ params.push(options.level);
1549
+ }
1550
+ if (options.symbol) {
1551
+ conditions.push("symbol LIKE ?");
1552
+ params.push(`%${options.symbol}%`);
1553
+ }
1554
+ if (options.service) {
1555
+ conditions.push("service = ?");
1556
+ params.push(options.service);
1557
+ }
1558
+ if (options.since) {
1559
+ conditions.push("timestamp >= ?");
1560
+ params.push(options.since);
1561
+ }
1562
+ if (options.until) {
1563
+ conditions.push("timestamp <= ?");
1564
+ params.push(options.until);
1565
+ }
1566
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1567
+ const result = this.db.exec(
1568
+ `SELECT COUNT(*) as count FROM logs ${whereClause}`,
1569
+ params
1570
+ );
1571
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1572
+ return result[0].values[0][0];
1573
+ }
1574
+ pruneLogs(maxCount) {
1575
+ this.initializeSync();
1576
+ if (maxCount <= 0) return 0;
1577
+ const currentCount = this.getLogCount();
1578
+ if (currentCount <= maxCount) return 0;
1579
+ const deleteCount = currentCount - maxCount;
1580
+ this.db.run(
1581
+ `DELETE FROM logs WHERE id IN (
1582
+ SELECT id FROM logs ORDER BY timestamp ASC LIMIT ?
1583
+ )`,
1584
+ [deleteCount]
1585
+ );
1586
+ this.save();
1587
+ return deleteCount;
1588
+ }
1589
+ rowToLogEntry(columns, row) {
1590
+ const obj = {};
1591
+ columns.forEach((col, i) => {
1592
+ obj[col] = row[i];
1593
+ });
1594
+ return {
1595
+ id: obj.id,
1596
+ timestamp: obj.timestamp,
1597
+ level: obj.level,
1598
+ symbol: obj.symbol,
1599
+ symbolType: obj.symbol_type || "raw",
1600
+ message: obj.message,
1601
+ data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
1602
+ service: obj.service,
1603
+ sessionId: obj.session_id || void 0,
1604
+ correlationId: obj.correlation_id || void 0,
1605
+ durationMs: obj.duration_ms || void 0,
1606
+ environment: obj.environment || void 0
1607
+ };
1608
+ }
1609
+ // ─── Service Registry ──────────────────────────────────────────
1610
+ registerService(reg) {
1611
+ this.initializeSync();
1612
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1613
+ this.db.run(
1614
+ `INSERT INTO services (name, version, pid, started_at, last_seen_at, environment, metadata_json)
1615
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1616
+ ON CONFLICT(name) DO UPDATE SET
1617
+ version = excluded.version,
1618
+ pid = excluded.pid,
1619
+ last_seen_at = excluded.last_seen_at,
1620
+ environment = excluded.environment,
1621
+ metadata_json = excluded.metadata_json`,
1622
+ [
1623
+ reg.name,
1624
+ reg.version || null,
1625
+ reg.pid ?? null,
1626
+ now,
1627
+ now,
1628
+ reg.environment || null,
1629
+ reg.metadata ? JSON.stringify(reg.metadata) : null
1630
+ ]
1631
+ );
1632
+ this.save();
1633
+ }
1634
+ updateServiceLastSeen(name) {
1635
+ this.initializeSync();
1636
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1637
+ this.db.run(
1638
+ "UPDATE services SET last_seen_at = ? WHERE name = ?",
1639
+ [now, name]
1640
+ );
1641
+ this.save();
1642
+ }
1643
+ getServices() {
1644
+ this.initializeSync();
1645
+ const result = this.db.exec(
1646
+ "SELECT * FROM services ORDER BY last_seen_at DESC"
1647
+ );
1648
+ if (result.length === 0) return [];
1649
+ return result[0].values.map((row) => {
1650
+ const obj = {};
1651
+ result[0].columns.forEach((col, i) => {
1652
+ obj[col] = row[i];
1653
+ });
1654
+ return {
1655
+ name: obj.name,
1656
+ version: obj.version || void 0,
1657
+ pid: obj.pid || void 0,
1658
+ startedAt: obj.started_at,
1659
+ lastSeenAt: obj.last_seen_at,
1660
+ environment: obj.environment || void 0,
1661
+ metadata: obj.metadata_json ? JSON.parse(obj.metadata_json) : void 0
1662
+ };
1663
+ });
1664
+ }
1665
+ // ─── App State ──────────────────────────────────────────────────
1666
+ upsertAppState(state) {
1667
+ this.initializeSync();
1668
+ this.db.run(
1669
+ `INSERT INTO app_state (service, session_id, timestamp, state_json, active_flows_json, active_gates_json)
1670
+ VALUES (?, ?, ?, ?, ?, ?)
1671
+ ON CONFLICT(service, session_id) DO UPDATE SET
1672
+ timestamp = excluded.timestamp,
1673
+ state_json = excluded.state_json,
1674
+ active_flows_json = excluded.active_flows_json,
1675
+ active_gates_json = excluded.active_gates_json`,
1676
+ [
1677
+ state.service,
1678
+ state.sessionId,
1679
+ state.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1680
+ JSON.stringify(state.state),
1681
+ state.activeFlows ? JSON.stringify(state.activeFlows) : null,
1682
+ state.activeGates ? JSON.stringify(state.activeGates) : null
1683
+ ]
1684
+ );
1685
+ this.save();
1686
+ }
1687
+ getAppState(service, sessionId) {
1688
+ this.initializeSync();
1689
+ let query = "SELECT * FROM app_state WHERE service = ?";
1690
+ const params = [service];
1691
+ if (sessionId) {
1692
+ query += " AND session_id = ?";
1693
+ params.push(sessionId);
1694
+ }
1695
+ query += " ORDER BY timestamp DESC";
1696
+ const result = this.db.exec(query, params);
1697
+ if (result.length === 0) return [];
1698
+ return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
1699
+ }
1700
+ getAllAppStates() {
1701
+ this.initializeSync();
1702
+ const result = this.db.exec(
1703
+ "SELECT * FROM app_state ORDER BY timestamp DESC"
1704
+ );
1705
+ if (result.length === 0) return [];
1706
+ return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
1707
+ }
1708
+ rowToAppState(columns, row) {
1709
+ const obj = {};
1710
+ columns.forEach((col, i) => {
1711
+ obj[col] = row[i];
1712
+ });
1713
+ return {
1714
+ service: obj.service,
1715
+ sessionId: obj.session_id,
1716
+ timestamp: obj.timestamp,
1717
+ state: JSON.parse(obj.state_json),
1718
+ activeFlows: obj.active_flows_json ? JSON.parse(obj.active_flows_json) : void 0,
1719
+ activeGates: obj.active_gates_json ? JSON.parse(obj.active_gates_json) : void 0
1720
+ };
1721
+ }
1722
+ // ─── Metrics ───────────────────────────────────────────────────
1723
+ insertMetric(input) {
1724
+ this.initializeSync();
1725
+ const id = uuidv4();
1726
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1727
+ this.db.run(
1728
+ `INSERT INTO metrics (
1729
+ id, timestamp, name, type, value, tags_json, service, environment
1730
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1731
+ [
1732
+ id,
1733
+ timestamp,
1734
+ input.name,
1735
+ input.type,
1736
+ input.value,
1737
+ JSON.stringify(input.tags || {}),
1738
+ input.service,
1739
+ input.environment || null
1740
+ ]
1741
+ );
1742
+ this.save();
1743
+ return id;
1744
+ }
1745
+ insertMetricBatch(entries) {
1746
+ this.initializeSync();
1747
+ let accepted = 0;
1748
+ const errors = [];
1749
+ for (const input of entries) {
1750
+ try {
1751
+ const id = uuidv4();
1752
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1753
+ this.db.run(
1754
+ `INSERT INTO metrics (
1755
+ id, timestamp, name, type, value, tags_json, service, environment
1756
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1757
+ [
1758
+ id,
1759
+ timestamp,
1760
+ input.name,
1761
+ input.type,
1762
+ input.value,
1763
+ JSON.stringify(input.tags || {}),
1764
+ input.service,
1765
+ input.environment || null
1766
+ ]
1767
+ );
1768
+ accepted++;
1769
+ } catch (err) {
1770
+ errors.push(err instanceof Error ? err.message : String(err));
1771
+ }
1772
+ }
1773
+ this.save();
1774
+ return { accepted, errors };
1775
+ }
1776
+ queryMetrics(options = {}) {
1777
+ this.initializeSync();
1778
+ const { limit = 100, offset = 0 } = options;
1779
+ const conditions = [];
1780
+ const params = [];
1781
+ if (options.name) {
1782
+ conditions.push("name = ?");
1783
+ params.push(options.name);
1784
+ }
1785
+ if (options.type) {
1786
+ conditions.push("type = ?");
1787
+ params.push(options.type);
1788
+ }
1789
+ if (options.service) {
1790
+ conditions.push("service = ?");
1791
+ params.push(options.service);
1792
+ }
1793
+ if (options.tag) {
1794
+ const eqIdx = options.tag.indexOf("=");
1795
+ if (eqIdx > 0) {
1796
+ const tagKey = options.tag.substring(0, eqIdx);
1797
+ const tagValue = options.tag.substring(eqIdx + 1);
1798
+ conditions.push("tags_json LIKE ?");
1799
+ params.push(`%"${tagKey}":"${tagValue}"%`);
1800
+ }
1801
+ }
1802
+ if (options.since) {
1803
+ conditions.push("timestamp >= ?");
1804
+ params.push(options.since);
1805
+ }
1806
+ if (options.until) {
1807
+ conditions.push("timestamp <= ?");
1808
+ params.push(options.until);
1809
+ }
1810
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1811
+ const result = this.db.exec(
1812
+ `SELECT * FROM metrics ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1813
+ [...params, limit, offset]
1814
+ );
1815
+ if (result.length === 0) return [];
1816
+ return result[0].values.map(
1817
+ (row) => this.rowToMetricEntry(result[0].columns, row)
1818
+ );
1819
+ }
1820
+ getMetricCount(options = {}) {
1821
+ this.initializeSync();
1822
+ const conditions = [];
1823
+ const params = [];
1824
+ if (options.name) {
1825
+ conditions.push("name = ?");
1826
+ params.push(options.name);
1827
+ }
1828
+ if (options.type) {
1829
+ conditions.push("type = ?");
1830
+ params.push(options.type);
1831
+ }
1832
+ if (options.service) {
1833
+ conditions.push("service = ?");
1834
+ params.push(options.service);
1835
+ }
1836
+ if (options.tag) {
1837
+ const eqIdx = options.tag.indexOf("=");
1838
+ if (eqIdx > 0) {
1839
+ const tagKey = options.tag.substring(0, eqIdx);
1840
+ const tagValue = options.tag.substring(eqIdx + 1);
1841
+ conditions.push("tags_json LIKE ?");
1842
+ params.push(`%"${tagKey}":"${tagValue}"%`);
1843
+ }
1844
+ }
1845
+ if (options.since) {
1846
+ conditions.push("timestamp >= ?");
1847
+ params.push(options.since);
1848
+ }
1849
+ if (options.until) {
1850
+ conditions.push("timestamp <= ?");
1851
+ params.push(options.until);
1852
+ }
1853
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1854
+ const result = this.db.exec(
1855
+ `SELECT COUNT(*) as count FROM metrics ${whereClause}`,
1856
+ params
1857
+ );
1858
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1859
+ return result[0].values[0][0];
1860
+ }
1861
+ aggregateMetric(name, options) {
1862
+ this.initializeSync();
1863
+ const conditions = ["name = ?"];
1864
+ const params = [name];
1865
+ if (options?.service) {
1866
+ conditions.push("service = ?");
1867
+ params.push(options.service);
1868
+ }
1869
+ if (options?.since) {
1870
+ conditions.push("timestamp >= ?");
1871
+ params.push(options.since);
1872
+ }
1873
+ if (options?.until) {
1874
+ conditions.push("timestamp <= ?");
1875
+ params.push(options.until);
1876
+ }
1877
+ const whereClause = `WHERE ${conditions.join(" AND ")}`;
1878
+ const result = this.db.exec(
1879
+ `SELECT COUNT(*) as count, SUM(value) as sum, MIN(value) as min, MAX(value) as max, AVG(value) as avg
1880
+ FROM metrics ${whereClause}`,
1881
+ params
1882
+ );
1883
+ if (result.length === 0 || result[0].values.length === 0) {
1884
+ return { name, count: 0, sum: 0, min: 0, max: 0, avg: 0 };
1885
+ }
1886
+ const row = result[0].values[0];
1887
+ return {
1888
+ name,
1889
+ count: row[0] || 0,
1890
+ sum: row[1] || 0,
1891
+ min: row[2] || 0,
1892
+ max: row[3] || 0,
1893
+ avg: row[4] || 0
1894
+ };
1895
+ }
1896
+ pruneMetrics(maxCount) {
1897
+ this.initializeSync();
1898
+ if (maxCount <= 0) return 0;
1899
+ const currentCount = this.getMetricCount();
1900
+ if (currentCount <= maxCount) return 0;
1901
+ const deleteCount = currentCount - maxCount;
1902
+ this.db.run(
1903
+ `DELETE FROM metrics WHERE id IN (
1904
+ SELECT id FROM metrics ORDER BY timestamp ASC LIMIT ?
1905
+ )`,
1906
+ [deleteCount]
1907
+ );
1908
+ this.save();
1909
+ return deleteCount;
1910
+ }
1911
+ rowToMetricEntry(columns, row) {
1912
+ const obj = {};
1913
+ columns.forEach((col, i) => {
1914
+ obj[col] = row[i];
1915
+ });
1916
+ return {
1917
+ id: obj.id,
1918
+ timestamp: obj.timestamp,
1919
+ name: obj.name,
1920
+ type: obj.type,
1921
+ value: obj.value,
1922
+ tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
1923
+ service: obj.service,
1924
+ environment: obj.environment || void 0
1925
+ };
1926
+ }
1927
+ // ─── Traces ───────────────────────────────────────────────────
1928
+ insertSpan(input) {
1929
+ this.initializeSync();
1930
+ const spanId = input.spanId || uuidv4();
1931
+ const startTime = input.startTime || (/* @__PURE__ */ new Date()).toISOString();
1932
+ this.db.run(
1933
+ `INSERT INTO traces (
1934
+ trace_id, span_id, parent_span_id, service, symbol, operation,
1935
+ start_time, end_time, duration_ms, status, tags_json, log_ids_json
1936
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1937
+ [
1938
+ input.traceId,
1939
+ spanId,
1940
+ input.parentSpanId || null,
1941
+ input.service,
1942
+ input.symbol,
1943
+ input.operation,
1944
+ startTime,
1945
+ input.endTime || null,
1946
+ input.durationMs ?? null,
1947
+ input.status || "ok",
1948
+ JSON.stringify(input.tags || {}),
1949
+ JSON.stringify(input.logIds || [])
1950
+ ]
1951
+ );
1952
+ this.save();
1953
+ return spanId;
1954
+ }
1955
+ getTrace(traceId) {
1956
+ this.initializeSync();
1957
+ const result = this.db.exec(
1958
+ "SELECT * FROM traces WHERE trace_id = ? ORDER BY start_time ASC",
1959
+ [traceId]
1960
+ );
1961
+ if (result.length === 0 || result[0].values.length === 0) return null;
1962
+ const spans = result[0].values.map(
1963
+ (row) => this.rowToTraceSpan(result[0].columns, row)
1964
+ );
1965
+ const services = [...new Set(spans.map((s) => s.service))];
1966
+ const startTimes = spans.map((s) => s.startTime);
1967
+ const endTimes = spans.filter((s) => s.endTime).map((s) => s.endTime);
1968
+ const startTime = startTimes.sort()[0];
1969
+ const endTime = endTimes.length > 0 ? endTimes.sort().reverse()[0] : startTime;
1970
+ const startMs = new Date(startTime).getTime();
1971
+ const endMs = new Date(endTime).getTime();
1972
+ const totalDurationMs = endMs - startMs;
1973
+ return {
1974
+ traceId,
1975
+ spans,
1976
+ services,
1977
+ totalDurationMs: totalDurationMs > 0 ? totalDurationMs : 0,
1978
+ startTime,
1979
+ endTime
1980
+ };
1981
+ }
1982
+ queryTraces(options = {}) {
1983
+ this.initializeSync();
1984
+ const conditions = [];
1985
+ const params = [];
1986
+ if (options.service) {
1987
+ conditions.push("service = ?");
1988
+ params.push(options.service);
1989
+ }
1990
+ if (options.symbol) {
1991
+ conditions.push("symbol = ?");
1992
+ params.push(options.symbol);
1993
+ }
1994
+ if (options.since) {
1995
+ conditions.push("start_time >= ?");
1996
+ params.push(options.since);
1997
+ }
1998
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1999
+ const traceLimit = Math.min(options.limit || 20, 20);
2000
+ const result = this.db.exec(
2001
+ `SELECT DISTINCT trace_id FROM traces ${whereClause} ORDER BY start_time DESC LIMIT ?`,
2002
+ [...params, traceLimit]
2003
+ );
2004
+ if (result.length === 0) return [];
2005
+ const traces = [];
2006
+ for (const row of result[0].values) {
2007
+ const traceId = row[0];
2008
+ const trace = this.getTrace(traceId);
2009
+ if (trace) {
2010
+ traces.push(trace);
2011
+ }
2012
+ }
2013
+ return traces;
2014
+ }
2015
+ rowToTraceSpan(columns, row) {
2016
+ const obj = {};
2017
+ columns.forEach((col, i) => {
2018
+ obj[col] = row[i];
2019
+ });
2020
+ return {
2021
+ traceId: obj.trace_id,
2022
+ spanId: obj.span_id,
2023
+ parentSpanId: obj.parent_span_id || void 0,
2024
+ service: obj.service,
2025
+ symbol: obj.symbol,
2026
+ operation: obj.operation,
2027
+ startTime: obj.start_time,
2028
+ endTime: obj.end_time || void 0,
2029
+ durationMs: obj.duration_ms || void 0,
2030
+ status: obj.status || "ok",
2031
+ tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
2032
+ logs: obj.log_ids_json ? JSON.parse(obj.log_ids_json) : []
2033
+ };
2034
+ }
1248
2035
  close() {
1249
2036
  if (this.db) {
1250
2037
  this.save();
@@ -1902,7 +2689,7 @@ var PatternSuggester = class {
1902
2689
  },
1903
2690
  resolution: {
1904
2691
  description: incident.resolution?.notes || "Resolution approach TBD",
1905
- strategy: "fix-code",
2692
+ strategy: this.inferStrategy([incident]),
1906
2693
  priority: "medium"
1907
2694
  },
1908
2695
  source: "suggested",
@@ -1917,6 +2704,7 @@ var PatternSuggester = class {
1917
2704
  suggestFromGroup(group) {
1918
2705
  const baseId = `group-${group.id.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
1919
2706
  const symbols = this.buildSymbolCriteria(group.commonSymbols);
2707
+ const groupIncidents = group.incidents.slice(0, 20).map((id) => this.storage.getIncident(id)).filter((i) => i != null);
1920
2708
  const pattern = {
1921
2709
  id: baseId,
1922
2710
  name: group.name || `Pattern from group ${group.id}`,
@@ -1927,7 +2715,7 @@ var PatternSuggester = class {
1927
2715
  },
1928
2716
  resolution: {
1929
2717
  description: "Resolution approach TBD based on grouped incidents",
1930
- strategy: "fix-code",
2718
+ strategy: groupIncidents.length > 0 ? this.inferStrategy(groupIncidents) : "fix-code",
1931
2719
  priority: this.getPriorityFromCount(group.count)
1932
2720
  },
1933
2721
  source: "suggested",
@@ -2226,21 +3014,38 @@ var PatternSuggester = class {
2226
3014
  return false;
2227
3015
  }
2228
3016
  /**
2229
- * Infer resolution strategy from incidents
3017
+ * Infer resolution strategy from incident error patterns and context.
3018
+ * Uses keyword heuristics across all incident messages to pick the
3019
+ * most likely resolution approach.
2230
3020
  */
2231
3021
  inferStrategy(incidents) {
2232
3022
  const messages = incidents.map((i) => i.error.message.toLowerCase());
2233
- if (messages.some((m) => m.includes("timeout") || m.includes("network"))) {
3023
+ const hasKeyword = (keywords) => messages.some((m) => keywords.some((k) => m.includes(k)));
3024
+ if (hasKeyword(["revert", "rollback", "regression", "broke after deploy", "since deploy"])) {
3025
+ return "rollback";
3026
+ }
3027
+ if (hasKeyword(["config", "environment variable", "env var", "missing key", "secret", "credential"])) {
3028
+ return "config-change";
3029
+ }
3030
+ if (hasKeyword(["out of memory", "oom", "heap", "memory limit", "capacity", "too many connections", "pool exhausted"])) {
3031
+ return "scale-up";
3032
+ }
3033
+ if (hasKeyword(["timeout", "network", "econnrefused", "econnreset", "dns", "socket hang up"])) {
2234
3034
  return "retry";
2235
3035
  }
2236
- if (messages.some(
2237
- (m) => m.includes("validation") || m.includes("invalid") || m.includes("required")
2238
- )) {
3036
+ if (hasKeyword(["unavailable", "service down", "circuit breaker", "fallback", "503", "502"])) {
3037
+ return "fallback";
3038
+ }
3039
+ if (hasKeyword(["validation", "invalid", "required", "constraint", "duplicate", "not found", "404"])) {
2239
3040
  return "fix-data";
2240
3041
  }
2241
- if (messages.some((m) => m.includes("permission") || m.includes("403"))) {
3042
+ if (hasKeyword(["permission", "forbidden", "403", "401", "unauthorized", "access denied"])) {
2242
3043
  return "escalate";
2243
3044
  }
3045
+ const uniqueTypes = new Set(incidents.map((i) => i.error.type).filter(Boolean));
3046
+ if (uniqueTypes.size > 2) {
3047
+ return "investigate";
3048
+ }
2244
3049
  return "fix-code";
2245
3050
  }
2246
3051
  /**
@@ -2497,6 +3302,101 @@ function getToolsList() {
2497
3302
  minOccurrences: { type: "number", description: "Min similar incidents for suggestion" }
2498
3303
  }
2499
3304
  }
3305
+ },
3306
+ // ─── Observability Tools ──────────────────────────────────────
3307
+ {
3308
+ name: "sentinel_logs",
3309
+ description: "Query structured logs from connected apps. Filters by level, symbol, service, search text, time range.",
3310
+ annotations: { readOnlyHint: true, destructiveHint: false },
3311
+ inputSchema: {
3312
+ type: "object",
3313
+ properties: {
3314
+ level: { type: "string", enum: ["debug", "info", "warn", "error"], description: "Filter by log level" },
3315
+ symbol: { type: "string", description: "Filter by symbol (partial match)" },
3316
+ service: { type: "string", description: "Filter by service name" },
3317
+ search: { type: "string", description: "Search in log messages" },
3318
+ since: { type: "string", description: "ISO timestamp \u2014 logs after this time" },
3319
+ sessionId: { type: "string", description: "Filter by session ID" },
3320
+ correlationId: { type: "string", description: "Filter by correlation ID" },
3321
+ limit: { type: "number", description: "Max results (default: 50)" }
3322
+ }
3323
+ }
3324
+ },
3325
+ {
3326
+ name: "sentinel_services",
3327
+ description: "List all registered services with version, environment, and last-seen time.",
3328
+ annotations: { readOnlyHint: true, destructiveHint: false },
3329
+ inputSchema: {
3330
+ type: "object",
3331
+ properties: {}
3332
+ }
3333
+ },
3334
+ {
3335
+ name: "sentinel_app_state",
3336
+ description: "Get live app state snapshots. Shows current state, active flows, and held gates for connected services.",
3337
+ annotations: { readOnlyHint: true, destructiveHint: false },
3338
+ inputSchema: {
3339
+ type: "object",
3340
+ properties: {
3341
+ service: { type: "string", description: "Filter by service name" }
3342
+ }
3343
+ }
3344
+ },
3345
+ {
3346
+ name: "sentinel_validate_symbol",
3347
+ description: "Check if a symbol has been used in logs. Returns usage count and suggestions.",
3348
+ annotations: { readOnlyHint: true, destructiveHint: false },
3349
+ inputSchema: {
3350
+ type: "object",
3351
+ properties: {
3352
+ symbol: { type: "string", description: "Symbol to validate (e.g., #checkout, ^auth)" }
3353
+ },
3354
+ required: ["symbol"]
3355
+ }
3356
+ },
3357
+ {
3358
+ name: "sentinel_flow_activity",
3359
+ description: "Get recent flow events \u2014 which flow nodes were hit, in what order, by which service.",
3360
+ annotations: { readOnlyHint: true, destructiveHint: false },
3361
+ inputSchema: {
3362
+ type: "object",
3363
+ properties: {
3364
+ flowId: { type: "string", description: "Filter by flow ID (e.g., $checkout-flow)" },
3365
+ service: { type: "string", description: "Filter by service name" },
3366
+ since: { type: "string", description: "ISO timestamp \u2014 events after this time" }
3367
+ }
3368
+ }
3369
+ },
3370
+ {
3371
+ name: "sentinel_metrics",
3372
+ description: "Query metrics (counters, gauges, histograms) from connected apps. Supports filtering and aggregation.",
3373
+ annotations: { readOnlyHint: true, destructiveHint: false },
3374
+ inputSchema: {
3375
+ type: "object",
3376
+ properties: {
3377
+ name: { type: "string", description: "Metric name filter" },
3378
+ type: { type: "string", enum: ["counter", "gauge", "histogram"], description: "Metric type filter" },
3379
+ service: { type: "string", description: "Service name filter" },
3380
+ since: { type: "string", description: "ISO timestamp \u2014 metrics after this time" },
3381
+ aggregate: { type: "boolean", description: "If true and name is provided, return aggregation instead of raw data" },
3382
+ limit: { type: "number", description: "Max results (default: 50)" }
3383
+ }
3384
+ }
3385
+ },
3386
+ {
3387
+ name: "sentinel_traces",
3388
+ description: "Query distributed traces across services. Shows span trees with timing, status, and service hops.",
3389
+ annotations: { readOnlyHint: true, destructiveHint: false },
3390
+ inputSchema: {
3391
+ type: "object",
3392
+ properties: {
3393
+ traceId: { type: "string", description: "Get a specific trace by ID" },
3394
+ service: { type: "string", description: "Filter by service name" },
3395
+ symbol: { type: "string", description: "Filter by symbol" },
3396
+ since: { type: "string", description: "ISO timestamp \u2014 traces after this time" },
3397
+ limit: { type: "number", description: "Max traces (default: 10, max: 20)" }
3398
+ }
3399
+ }
2500
3400
  }
2501
3401
  ];
2502
3402
  }
@@ -2725,6 +3625,137 @@ async function handleTool(name, args) {
2725
3625
  2
2726
3626
  );
2727
3627
  }
3628
+ // ─── Observability Tools ──────────────────────────────────────
3629
+ case "sentinel_logs": {
3630
+ const { level, symbol, service, search, since, sessionId, correlationId, limit = 50 } = args;
3631
+ const logs = store.queryLogs({
3632
+ level,
3633
+ symbol,
3634
+ service,
3635
+ search,
3636
+ since,
3637
+ sessionId,
3638
+ correlationId,
3639
+ limit
3640
+ });
3641
+ const total = store.getLogCount({ level, symbol, service, since });
3642
+ return JSON.stringify({
3643
+ count: logs.length,
3644
+ total,
3645
+ logs: logs.map((l) => ({
3646
+ timestamp: l.timestamp,
3647
+ level: l.level,
3648
+ symbol: l.symbol,
3649
+ service: l.service,
3650
+ message: l.message,
3651
+ data: l.data,
3652
+ sessionId: l.sessionId,
3653
+ correlationId: l.correlationId,
3654
+ durationMs: l.durationMs
3655
+ }))
3656
+ }, null, 2);
3657
+ }
3658
+ case "sentinel_services": {
3659
+ const services = store.getServices();
3660
+ return JSON.stringify({
3661
+ count: services.length,
3662
+ services: services.map((s) => ({
3663
+ name: s.name,
3664
+ version: s.version,
3665
+ environment: s.environment,
3666
+ lastSeen: s.lastSeenAt,
3667
+ startedAt: s.startedAt,
3668
+ pid: s.pid
3669
+ }))
3670
+ }, null, 2);
3671
+ }
3672
+ case "sentinel_app_state": {
3673
+ const { service: svc } = args;
3674
+ const states = svc ? store.getAppState(svc) : store.getAllAppStates();
3675
+ return JSON.stringify({
3676
+ states: states.map((s) => ({
3677
+ service: s.service,
3678
+ sessionId: s.sessionId,
3679
+ state: s.state,
3680
+ activeFlows: s.activeFlows,
3681
+ activeGates: s.activeGates,
3682
+ timestamp: s.timestamp
3683
+ }))
3684
+ }, null, 2);
3685
+ }
3686
+ case "sentinel_validate_symbol": {
3687
+ const { symbol: sym } = args;
3688
+ const logCount = store.getLogCount({ symbol: sym });
3689
+ return JSON.stringify({
3690
+ symbol: sym,
3691
+ usedInLogs: logCount > 0,
3692
+ logCount,
3693
+ tip: logCount === 0 ? "This symbol has not appeared in any logs. It may be a typo or unused." : `This symbol has been used in ${logCount} log entries.`
3694
+ }, null, 2);
3695
+ }
3696
+ case "sentinel_flow_activity": {
3697
+ const { flowId, service: flowSvc, since: flowSince } = args;
3698
+ const flowLogs = store.queryLogs({ symbol: flowId, service: flowSvc, since: flowSince, limit: 100 });
3699
+ const flowEvents = flowLogs.filter((l) => ["flow", "signal", "gate"].includes(l.symbolType)).map((l) => ({
3700
+ timestamp: l.timestamp,
3701
+ symbol: l.symbol,
3702
+ symbolType: l.symbolType,
3703
+ service: l.service,
3704
+ message: l.message,
3705
+ level: l.level
3706
+ }));
3707
+ return JSON.stringify({ count: flowEvents.length, events: flowEvents }, null, 2);
3708
+ }
3709
+ case "sentinel_metrics": {
3710
+ const { name: metricName, type: metricType, service: metricSvc, since: metricSince, aggregate, limit: metricLimit } = args;
3711
+ if (aggregate && metricName) {
3712
+ const agg = store.aggregateMetric(metricName, { service: metricSvc, since: metricSince });
3713
+ return JSON.stringify(agg, null, 2);
3714
+ }
3715
+ const metrics = store.queryMetrics({
3716
+ name: metricName,
3717
+ type: metricType,
3718
+ service: metricSvc,
3719
+ since: metricSince,
3720
+ limit: Math.min(metricLimit || 50, 100)
3721
+ });
3722
+ return JSON.stringify({
3723
+ count: metrics.length,
3724
+ metrics: metrics.map((m) => ({
3725
+ timestamp: m.timestamp,
3726
+ name: m.name,
3727
+ type: m.type,
3728
+ value: m.value,
3729
+ tags: m.tags,
3730
+ service: m.service
3731
+ }))
3732
+ }, null, 2);
3733
+ }
3734
+ case "sentinel_traces": {
3735
+ const { traceId: tid, service: traceSvc, symbol: traceSym, since: traceSince, limit: traceLimit } = args;
3736
+ if (tid) {
3737
+ const trace = store.getTrace(tid);
3738
+ if (!trace) return JSON.stringify({ error: "Trace not found" });
3739
+ return JSON.stringify(trace, null, 2);
3740
+ }
3741
+ const traces = store.queryTraces({
3742
+ service: traceSvc,
3743
+ symbol: traceSym,
3744
+ since: traceSince,
3745
+ limit: Math.min(traceLimit || 10, 20)
3746
+ });
3747
+ return JSON.stringify({
3748
+ count: traces.length,
3749
+ traces: traces.map((t) => ({
3750
+ traceId: t.traceId,
3751
+ services: t.services,
3752
+ spanCount: t.spans.length,
3753
+ totalDurationMs: t.totalDurationMs,
3754
+ startTime: t.startTime,
3755
+ endTime: t.endTime
3756
+ }))
3757
+ }, null, 2);
3758
+ }
2728
3759
  default:
2729
3760
  return JSON.stringify({ error: `Unknown tool: ${name}` });
2730
3761
  }