@a-company/sentinel 0.2.0 → 3.6.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 = 5;
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,149 @@ 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
+ );
447
+ currentVersion = 4;
448
+ }
449
+ if (currentVersion < 5) {
450
+ try {
451
+ this.db.run(`
452
+ CREATE TABLE IF NOT EXISTS schemas (
453
+ id TEXT PRIMARY KEY,
454
+ version TEXT NOT NULL,
455
+ name TEXT NOT NULL,
456
+ description TEXT,
457
+ scope_json TEXT NOT NULL,
458
+ event_types_json TEXT NOT NULL,
459
+ causality_json TEXT,
460
+ visualization_json TEXT,
461
+ tags_json TEXT DEFAULT '[]',
462
+ registered_at TEXT NOT NULL,
463
+ updated_at TEXT NOT NULL
464
+ );
465
+
466
+ CREATE TABLE IF NOT EXISTS events (
467
+ id TEXT PRIMARY KEY,
468
+ schema_id TEXT NOT NULL,
469
+ event_type TEXT NOT NULL,
470
+ category TEXT NOT NULL,
471
+ timestamp TEXT NOT NULL,
472
+ scope_value TEXT,
473
+ scope_ordinal INTEGER,
474
+ session_id TEXT,
475
+ service TEXT NOT NULL,
476
+ data_json TEXT,
477
+ severity TEXT DEFAULT 'info',
478
+ parent_event_id TEXT,
479
+ depth INTEGER DEFAULT 0
480
+ );
481
+
482
+ CREATE INDEX IF NOT EXISTS idx_events_schema ON events(schema_id);
483
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
484
+ CREATE INDEX IF NOT EXISTS idx_events_scope ON events(schema_id, scope_value);
485
+ CREATE INDEX IF NOT EXISTS idx_events_scope_ord ON events(schema_id, scope_ordinal);
486
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
487
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
488
+ CREATE INDEX IF NOT EXISTS idx_events_service ON events(service);
489
+ `);
490
+ } catch {
491
+ }
492
+ this.db.run(
493
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '5')"
494
+ );
274
495
  }
275
496
  }
276
497
  /**
@@ -1068,181 +1289,1130 @@ var SentinelStorage = class {
1068
1289
  suggestedPattern: obj.suggested_pattern_id ? this.getPattern(obj.suggested_pattern_id) || void 0 : void 0
1069
1290
  };
1070
1291
  }
1071
- // ─── Practice Events ─────────────────────────────────────────────
1072
- recordPracticeEvent(input) {
1292
+ // ─── Practice Events ─────────────────────────────────────────────
1293
+ recordPracticeEvent(input) {
1294
+ this.initializeSync();
1295
+ const id = `PE-${uuidv4().substring(0, 8)}`;
1296
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1297
+ this.db.run(
1298
+ `INSERT INTO practice_events (
1299
+ id, timestamp, habit_id, habit_category, result,
1300
+ engineer, session_id, lore_entry_id, task_description,
1301
+ symbols_touched, files_modified, related_incident_id, notes
1302
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1303
+ [
1304
+ id,
1305
+ now,
1306
+ input.habitId,
1307
+ input.habitCategory,
1308
+ input.result,
1309
+ input.engineer,
1310
+ input.sessionId,
1311
+ input.loreEntryId || null,
1312
+ input.taskDescription || null,
1313
+ JSON.stringify(input.symbolsTouched || []),
1314
+ JSON.stringify(input.filesModified || []),
1315
+ input.relatedIncidentId || null,
1316
+ input.notes || null
1317
+ ]
1318
+ );
1319
+ this.save();
1320
+ return id;
1321
+ }
1322
+ getPracticeEvents(options = {}) {
1323
+ this.initializeSync();
1324
+ const { limit = 100, offset = 0 } = options;
1325
+ const conditions = [];
1326
+ const params = [];
1327
+ if (options.habitId) {
1328
+ conditions.push("habit_id = ?");
1329
+ params.push(options.habitId);
1330
+ }
1331
+ if (options.habitCategory) {
1332
+ conditions.push("habit_category = ?");
1333
+ params.push(options.habitCategory);
1334
+ }
1335
+ if (options.result) {
1336
+ conditions.push("result = ?");
1337
+ params.push(options.result);
1338
+ }
1339
+ if (options.engineer) {
1340
+ conditions.push("engineer = ?");
1341
+ params.push(options.engineer);
1342
+ }
1343
+ if (options.sessionId) {
1344
+ conditions.push("session_id = ?");
1345
+ params.push(options.sessionId);
1346
+ }
1347
+ if (options.dateFrom) {
1348
+ conditions.push("timestamp >= ?");
1349
+ params.push(options.dateFrom);
1350
+ }
1351
+ if (options.dateTo) {
1352
+ conditions.push("timestamp <= ?");
1353
+ params.push(options.dateTo);
1354
+ }
1355
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1356
+ const result = this.db.exec(
1357
+ `SELECT * FROM practice_events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1358
+ [...params, limit, offset]
1359
+ );
1360
+ if (result.length === 0) return [];
1361
+ return result[0].values.map(
1362
+ (row) => this.rowToPracticeEvent(result[0].columns, row)
1363
+ );
1364
+ }
1365
+ getPracticeEventCount(options = {}) {
1366
+ this.initializeSync();
1367
+ const conditions = [];
1368
+ const params = [];
1369
+ if (options.habitId) {
1370
+ conditions.push("habit_id = ?");
1371
+ params.push(options.habitId);
1372
+ }
1373
+ if (options.habitCategory) {
1374
+ conditions.push("habit_category = ?");
1375
+ params.push(options.habitCategory);
1376
+ }
1377
+ if (options.result) {
1378
+ conditions.push("result = ?");
1379
+ params.push(options.result);
1380
+ }
1381
+ if (options.engineer) {
1382
+ conditions.push("engineer = ?");
1383
+ params.push(options.engineer);
1384
+ }
1385
+ if (options.dateFrom) {
1386
+ conditions.push("timestamp >= ?");
1387
+ params.push(options.dateFrom);
1388
+ }
1389
+ if (options.dateTo) {
1390
+ conditions.push("timestamp <= ?");
1391
+ params.push(options.dateTo);
1392
+ }
1393
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1394
+ const result = this.db.exec(
1395
+ `SELECT COUNT(*) as count FROM practice_events ${whereClause}`,
1396
+ params
1397
+ );
1398
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1399
+ return result[0].values[0][0];
1400
+ }
1401
+ getComplianceRate(options = {}) {
1402
+ this.initializeSync();
1403
+ const conditions = [];
1404
+ const params = [];
1405
+ if (options.habitId) {
1406
+ conditions.push("habit_id = ?");
1407
+ params.push(options.habitId);
1408
+ }
1409
+ if (options.habitCategory) {
1410
+ conditions.push("habit_category = ?");
1411
+ params.push(options.habitCategory);
1412
+ }
1413
+ if (options.engineer) {
1414
+ conditions.push("engineer = ?");
1415
+ params.push(options.engineer);
1416
+ }
1417
+ if (options.dateFrom) {
1418
+ conditions.push("timestamp >= ?");
1419
+ params.push(options.dateFrom);
1420
+ }
1421
+ if (options.dateTo) {
1422
+ conditions.push("timestamp <= ?");
1423
+ params.push(options.dateTo);
1424
+ }
1425
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1426
+ const result = this.db.exec(
1427
+ `SELECT result, COUNT(*) as count
1428
+ FROM practice_events ${whereClause}
1429
+ GROUP BY result`,
1430
+ params
1431
+ );
1432
+ let followed = 0;
1433
+ let skipped = 0;
1434
+ let partial = 0;
1435
+ if (result.length > 0) {
1436
+ for (const row of result[0].values) {
1437
+ const r = row[0];
1438
+ const count = row[1];
1439
+ if (r === "followed") followed = count;
1440
+ else if (r === "skipped") skipped = count;
1441
+ else if (r === "partial") partial = count;
1442
+ }
1443
+ }
1444
+ const total = followed + skipped + partial;
1445
+ const rate = total > 0 ? (followed + partial * 0.5) / total * 100 : 100;
1446
+ return { total, followed, skipped, partial, rate: Math.round(rate) };
1447
+ }
1448
+ rowToPracticeEvent(columns, row) {
1449
+ const obj = {};
1450
+ columns.forEach((col, i) => {
1451
+ obj[col] = row[i];
1452
+ });
1453
+ return {
1454
+ id: obj.id,
1455
+ timestamp: obj.timestamp,
1456
+ habitId: obj.habit_id,
1457
+ habitCategory: obj.habit_category,
1458
+ result: obj.result,
1459
+ engineer: obj.engineer,
1460
+ sessionId: obj.session_id,
1461
+ loreEntryId: obj.lore_entry_id || void 0,
1462
+ taskDescription: obj.task_description || void 0,
1463
+ symbolsTouched: JSON.parse(obj.symbols_touched || "[]"),
1464
+ filesModified: JSON.parse(obj.files_modified || "[]"),
1465
+ relatedIncidentId: obj.related_incident_id || void 0,
1466
+ notes: obj.notes || void 0
1467
+ };
1468
+ }
1469
+ // ─── Structured Logs ─────────────────────────────────────────────
1470
+ inferSymbolType(symbol) {
1471
+ if (symbol.startsWith("#")) return "component";
1472
+ if (symbol.startsWith("^")) return "gate";
1473
+ if (symbol.startsWith("!")) return "signal";
1474
+ if (symbol.startsWith("$")) return "flow";
1475
+ if (symbol.startsWith("~")) return "aspect";
1476
+ return "raw";
1477
+ }
1478
+ insertLog(input) {
1479
+ this.initializeSync();
1480
+ const id = input.id || uuidv4();
1481
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1482
+ const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
1483
+ this.db.run(
1484
+ `INSERT INTO logs (
1485
+ id, timestamp, level, symbol, symbol_type, message, data_json,
1486
+ service, session_id, correlation_id, duration_ms, environment
1487
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1488
+ [
1489
+ id,
1490
+ timestamp,
1491
+ input.level,
1492
+ input.symbol,
1493
+ symbolType,
1494
+ input.message,
1495
+ input.data ? JSON.stringify(input.data) : null,
1496
+ input.service,
1497
+ input.sessionId || null,
1498
+ input.correlationId || null,
1499
+ input.durationMs ?? null,
1500
+ input.environment || null
1501
+ ]
1502
+ );
1503
+ this.save();
1504
+ return id;
1505
+ }
1506
+ insertLogBatch(entries) {
1507
+ this.initializeSync();
1508
+ let accepted = 0;
1509
+ const errors = [];
1510
+ for (const input of entries) {
1511
+ try {
1512
+ const id = input.id || uuidv4();
1513
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1514
+ const symbolType = input.symbolType || this.inferSymbolType(input.symbol);
1515
+ this.db.run(
1516
+ `INSERT INTO logs (
1517
+ id, timestamp, level, symbol, symbol_type, message, data_json,
1518
+ service, session_id, correlation_id, duration_ms, environment
1519
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1520
+ [
1521
+ id,
1522
+ timestamp,
1523
+ input.level,
1524
+ input.symbol,
1525
+ symbolType,
1526
+ input.message,
1527
+ input.data ? JSON.stringify(input.data) : null,
1528
+ input.service,
1529
+ input.sessionId || null,
1530
+ input.correlationId || null,
1531
+ input.durationMs ?? null,
1532
+ input.environment || null
1533
+ ]
1534
+ );
1535
+ accepted++;
1536
+ } catch (err) {
1537
+ errors.push(err instanceof Error ? err.message : String(err));
1538
+ }
1539
+ }
1540
+ this.save();
1541
+ return { accepted, errors };
1542
+ }
1543
+ queryLogs(options = {}) {
1544
+ this.initializeSync();
1545
+ const { limit = 100, offset = 0 } = options;
1546
+ const conditions = [];
1547
+ const params = [];
1548
+ if (options.level) {
1549
+ conditions.push("level = ?");
1550
+ params.push(options.level);
1551
+ }
1552
+ if (options.symbol) {
1553
+ conditions.push("symbol LIKE ?");
1554
+ params.push(`%${options.symbol}%`);
1555
+ }
1556
+ if (options.service) {
1557
+ conditions.push("service = ?");
1558
+ params.push(options.service);
1559
+ }
1560
+ if (options.sessionId) {
1561
+ conditions.push("session_id = ?");
1562
+ params.push(options.sessionId);
1563
+ }
1564
+ if (options.correlationId) {
1565
+ conditions.push("correlation_id = ?");
1566
+ params.push(options.correlationId);
1567
+ }
1568
+ if (options.search) {
1569
+ conditions.push("message LIKE ?");
1570
+ params.push(`%${options.search}%`);
1571
+ }
1572
+ if (options.since) {
1573
+ conditions.push("timestamp >= ?");
1574
+ params.push(options.since);
1575
+ }
1576
+ if (options.until) {
1577
+ conditions.push("timestamp <= ?");
1578
+ params.push(options.until);
1579
+ }
1580
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1581
+ const result = this.db.exec(
1582
+ `SELECT * FROM logs ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1583
+ [...params, limit, offset]
1584
+ );
1585
+ if (result.length === 0) return [];
1586
+ return result[0].values.map(
1587
+ (row) => this.rowToLogEntry(result[0].columns, row)
1588
+ );
1589
+ }
1590
+ getLogCount(options = {}) {
1591
+ this.initializeSync();
1592
+ const conditions = [];
1593
+ const params = [];
1594
+ if (options.level) {
1595
+ conditions.push("level = ?");
1596
+ params.push(options.level);
1597
+ }
1598
+ if (options.symbol) {
1599
+ conditions.push("symbol LIKE ?");
1600
+ params.push(`%${options.symbol}%`);
1601
+ }
1602
+ if (options.service) {
1603
+ conditions.push("service = ?");
1604
+ params.push(options.service);
1605
+ }
1606
+ if (options.since) {
1607
+ conditions.push("timestamp >= ?");
1608
+ params.push(options.since);
1609
+ }
1610
+ if (options.until) {
1611
+ conditions.push("timestamp <= ?");
1612
+ params.push(options.until);
1613
+ }
1614
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1615
+ const result = this.db.exec(
1616
+ `SELECT COUNT(*) as count FROM logs ${whereClause}`,
1617
+ params
1618
+ );
1619
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1620
+ return result[0].values[0][0];
1621
+ }
1622
+ pruneLogs(maxCount) {
1623
+ this.initializeSync();
1624
+ if (maxCount <= 0) return 0;
1625
+ const currentCount = this.getLogCount();
1626
+ if (currentCount <= maxCount) return 0;
1627
+ const deleteCount = currentCount - maxCount;
1628
+ this.db.run(
1629
+ `DELETE FROM logs WHERE id IN (
1630
+ SELECT id FROM logs ORDER BY timestamp ASC LIMIT ?
1631
+ )`,
1632
+ [deleteCount]
1633
+ );
1634
+ this.save();
1635
+ return deleteCount;
1636
+ }
1637
+ rowToLogEntry(columns, row) {
1638
+ const obj = {};
1639
+ columns.forEach((col, i) => {
1640
+ obj[col] = row[i];
1641
+ });
1642
+ return {
1643
+ id: obj.id,
1644
+ timestamp: obj.timestamp,
1645
+ level: obj.level,
1646
+ symbol: obj.symbol,
1647
+ symbolType: obj.symbol_type || "raw",
1648
+ message: obj.message,
1649
+ data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
1650
+ service: obj.service,
1651
+ sessionId: obj.session_id || void 0,
1652
+ correlationId: obj.correlation_id || void 0,
1653
+ durationMs: obj.duration_ms || void 0,
1654
+ environment: obj.environment || void 0
1655
+ };
1656
+ }
1657
+ // ─── Service Registry ──────────────────────────────────────────
1658
+ registerService(reg) {
1659
+ this.initializeSync();
1660
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1661
+ this.db.run(
1662
+ `INSERT INTO services (name, version, pid, started_at, last_seen_at, environment, metadata_json)
1663
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1664
+ ON CONFLICT(name) DO UPDATE SET
1665
+ version = excluded.version,
1666
+ pid = excluded.pid,
1667
+ last_seen_at = excluded.last_seen_at,
1668
+ environment = excluded.environment,
1669
+ metadata_json = excluded.metadata_json`,
1670
+ [
1671
+ reg.name,
1672
+ reg.version || null,
1673
+ reg.pid ?? null,
1674
+ now,
1675
+ now,
1676
+ reg.environment || null,
1677
+ reg.metadata ? JSON.stringify(reg.metadata) : null
1678
+ ]
1679
+ );
1680
+ this.save();
1681
+ }
1682
+ updateServiceLastSeen(name) {
1683
+ this.initializeSync();
1684
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1685
+ this.db.run(
1686
+ "UPDATE services SET last_seen_at = ? WHERE name = ?",
1687
+ [now, name]
1688
+ );
1689
+ this.save();
1690
+ }
1691
+ getServices() {
1692
+ this.initializeSync();
1693
+ const result = this.db.exec(
1694
+ "SELECT * FROM services ORDER BY last_seen_at DESC"
1695
+ );
1696
+ if (result.length === 0) return [];
1697
+ return result[0].values.map((row) => {
1698
+ const obj = {};
1699
+ result[0].columns.forEach((col, i) => {
1700
+ obj[col] = row[i];
1701
+ });
1702
+ return {
1703
+ name: obj.name,
1704
+ version: obj.version || void 0,
1705
+ pid: obj.pid || void 0,
1706
+ startedAt: obj.started_at,
1707
+ lastSeenAt: obj.last_seen_at,
1708
+ environment: obj.environment || void 0,
1709
+ metadata: obj.metadata_json ? JSON.parse(obj.metadata_json) : void 0
1710
+ };
1711
+ });
1712
+ }
1713
+ // ─── App State ──────────────────────────────────────────────────
1714
+ upsertAppState(state) {
1715
+ this.initializeSync();
1716
+ this.db.run(
1717
+ `INSERT INTO app_state (service, session_id, timestamp, state_json, active_flows_json, active_gates_json)
1718
+ VALUES (?, ?, ?, ?, ?, ?)
1719
+ ON CONFLICT(service, session_id) DO UPDATE SET
1720
+ timestamp = excluded.timestamp,
1721
+ state_json = excluded.state_json,
1722
+ active_flows_json = excluded.active_flows_json,
1723
+ active_gates_json = excluded.active_gates_json`,
1724
+ [
1725
+ state.service,
1726
+ state.sessionId,
1727
+ state.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1728
+ JSON.stringify(state.state),
1729
+ state.activeFlows ? JSON.stringify(state.activeFlows) : null,
1730
+ state.activeGates ? JSON.stringify(state.activeGates) : null
1731
+ ]
1732
+ );
1733
+ this.save();
1734
+ }
1735
+ getAppState(service, sessionId) {
1736
+ this.initializeSync();
1737
+ let query = "SELECT * FROM app_state WHERE service = ?";
1738
+ const params = [service];
1739
+ if (sessionId) {
1740
+ query += " AND session_id = ?";
1741
+ params.push(sessionId);
1742
+ }
1743
+ query += " ORDER BY timestamp DESC";
1744
+ const result = this.db.exec(query, params);
1745
+ if (result.length === 0) return [];
1746
+ return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
1747
+ }
1748
+ getAllAppStates() {
1749
+ this.initializeSync();
1750
+ const result = this.db.exec(
1751
+ "SELECT * FROM app_state ORDER BY timestamp DESC"
1752
+ );
1753
+ if (result.length === 0) return [];
1754
+ return result[0].values.map((row) => this.rowToAppState(result[0].columns, row));
1755
+ }
1756
+ rowToAppState(columns, row) {
1757
+ const obj = {};
1758
+ columns.forEach((col, i) => {
1759
+ obj[col] = row[i];
1760
+ });
1761
+ return {
1762
+ service: obj.service,
1763
+ sessionId: obj.session_id,
1764
+ timestamp: obj.timestamp,
1765
+ state: JSON.parse(obj.state_json),
1766
+ activeFlows: obj.active_flows_json ? JSON.parse(obj.active_flows_json) : void 0,
1767
+ activeGates: obj.active_gates_json ? JSON.parse(obj.active_gates_json) : void 0
1768
+ };
1769
+ }
1770
+ // ─── Metrics ───────────────────────────────────────────────────
1771
+ insertMetric(input) {
1772
+ this.initializeSync();
1773
+ const id = uuidv4();
1774
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1775
+ this.db.run(
1776
+ `INSERT INTO metrics (
1777
+ id, timestamp, name, type, value, tags_json, service, environment
1778
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1779
+ [
1780
+ id,
1781
+ timestamp,
1782
+ input.name,
1783
+ input.type,
1784
+ input.value,
1785
+ JSON.stringify(input.tags || {}),
1786
+ input.service,
1787
+ input.environment || null
1788
+ ]
1789
+ );
1790
+ this.save();
1791
+ return id;
1792
+ }
1793
+ insertMetricBatch(entries) {
1794
+ this.initializeSync();
1795
+ let accepted = 0;
1796
+ const errors = [];
1797
+ for (const input of entries) {
1798
+ try {
1799
+ const id = uuidv4();
1800
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1801
+ this.db.run(
1802
+ `INSERT INTO metrics (
1803
+ id, timestamp, name, type, value, tags_json, service, environment
1804
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1805
+ [
1806
+ id,
1807
+ timestamp,
1808
+ input.name,
1809
+ input.type,
1810
+ input.value,
1811
+ JSON.stringify(input.tags || {}),
1812
+ input.service,
1813
+ input.environment || null
1814
+ ]
1815
+ );
1816
+ accepted++;
1817
+ } catch (err) {
1818
+ errors.push(err instanceof Error ? err.message : String(err));
1819
+ }
1820
+ }
1821
+ this.save();
1822
+ return { accepted, errors };
1823
+ }
1824
+ queryMetrics(options = {}) {
1825
+ this.initializeSync();
1826
+ const { limit = 100, offset = 0 } = options;
1827
+ const conditions = [];
1828
+ const params = [];
1829
+ if (options.name) {
1830
+ conditions.push("name = ?");
1831
+ params.push(options.name);
1832
+ }
1833
+ if (options.type) {
1834
+ conditions.push("type = ?");
1835
+ params.push(options.type);
1836
+ }
1837
+ if (options.service) {
1838
+ conditions.push("service = ?");
1839
+ params.push(options.service);
1840
+ }
1841
+ if (options.tag) {
1842
+ const eqIdx = options.tag.indexOf("=");
1843
+ if (eqIdx > 0) {
1844
+ const tagKey = options.tag.substring(0, eqIdx);
1845
+ const tagValue = options.tag.substring(eqIdx + 1);
1846
+ conditions.push("tags_json LIKE ?");
1847
+ params.push(`%"${tagKey}":"${tagValue}"%`);
1848
+ }
1849
+ }
1850
+ if (options.since) {
1851
+ conditions.push("timestamp >= ?");
1852
+ params.push(options.since);
1853
+ }
1854
+ if (options.until) {
1855
+ conditions.push("timestamp <= ?");
1856
+ params.push(options.until);
1857
+ }
1858
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1859
+ const result = this.db.exec(
1860
+ `SELECT * FROM metrics ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1861
+ [...params, limit, offset]
1862
+ );
1863
+ if (result.length === 0) return [];
1864
+ return result[0].values.map(
1865
+ (row) => this.rowToMetricEntry(result[0].columns, row)
1866
+ );
1867
+ }
1868
+ getMetricCount(options = {}) {
1869
+ this.initializeSync();
1870
+ const conditions = [];
1871
+ const params = [];
1872
+ if (options.name) {
1873
+ conditions.push("name = ?");
1874
+ params.push(options.name);
1875
+ }
1876
+ if (options.type) {
1877
+ conditions.push("type = ?");
1878
+ params.push(options.type);
1879
+ }
1880
+ if (options.service) {
1881
+ conditions.push("service = ?");
1882
+ params.push(options.service);
1883
+ }
1884
+ if (options.tag) {
1885
+ const eqIdx = options.tag.indexOf("=");
1886
+ if (eqIdx > 0) {
1887
+ const tagKey = options.tag.substring(0, eqIdx);
1888
+ const tagValue = options.tag.substring(eqIdx + 1);
1889
+ conditions.push("tags_json LIKE ?");
1890
+ params.push(`%"${tagKey}":"${tagValue}"%`);
1891
+ }
1892
+ }
1893
+ if (options.since) {
1894
+ conditions.push("timestamp >= ?");
1895
+ params.push(options.since);
1896
+ }
1897
+ if (options.until) {
1898
+ conditions.push("timestamp <= ?");
1899
+ params.push(options.until);
1900
+ }
1901
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1902
+ const result = this.db.exec(
1903
+ `SELECT COUNT(*) as count FROM metrics ${whereClause}`,
1904
+ params
1905
+ );
1906
+ if (result.length === 0 || result[0].values.length === 0) return 0;
1907
+ return result[0].values[0][0];
1908
+ }
1909
+ aggregateMetric(name, options) {
1910
+ this.initializeSync();
1911
+ const conditions = ["name = ?"];
1912
+ const params = [name];
1913
+ if (options?.service) {
1914
+ conditions.push("service = ?");
1915
+ params.push(options.service);
1916
+ }
1917
+ if (options?.since) {
1918
+ conditions.push("timestamp >= ?");
1919
+ params.push(options.since);
1920
+ }
1921
+ if (options?.until) {
1922
+ conditions.push("timestamp <= ?");
1923
+ params.push(options.until);
1924
+ }
1925
+ const whereClause = `WHERE ${conditions.join(" AND ")}`;
1926
+ const result = this.db.exec(
1927
+ `SELECT COUNT(*) as count, SUM(value) as sum, MIN(value) as min, MAX(value) as max, AVG(value) as avg
1928
+ FROM metrics ${whereClause}`,
1929
+ params
1930
+ );
1931
+ if (result.length === 0 || result[0].values.length === 0) {
1932
+ return { name, count: 0, sum: 0, min: 0, max: 0, avg: 0 };
1933
+ }
1934
+ const row = result[0].values[0];
1935
+ return {
1936
+ name,
1937
+ count: row[0] || 0,
1938
+ sum: row[1] || 0,
1939
+ min: row[2] || 0,
1940
+ max: row[3] || 0,
1941
+ avg: row[4] || 0
1942
+ };
1943
+ }
1944
+ pruneMetrics(maxCount) {
1945
+ this.initializeSync();
1946
+ if (maxCount <= 0) return 0;
1947
+ const currentCount = this.getMetricCount();
1948
+ if (currentCount <= maxCount) return 0;
1949
+ const deleteCount = currentCount - maxCount;
1950
+ this.db.run(
1951
+ `DELETE FROM metrics WHERE id IN (
1952
+ SELECT id FROM metrics ORDER BY timestamp ASC LIMIT ?
1953
+ )`,
1954
+ [deleteCount]
1955
+ );
1956
+ this.save();
1957
+ return deleteCount;
1958
+ }
1959
+ rowToMetricEntry(columns, row) {
1960
+ const obj = {};
1961
+ columns.forEach((col, i) => {
1962
+ obj[col] = row[i];
1963
+ });
1964
+ return {
1965
+ id: obj.id,
1966
+ timestamp: obj.timestamp,
1967
+ name: obj.name,
1968
+ type: obj.type,
1969
+ value: obj.value,
1970
+ tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
1971
+ service: obj.service,
1972
+ environment: obj.environment || void 0
1973
+ };
1974
+ }
1975
+ // ─── Traces ───────────────────────────────────────────────────
1976
+ insertSpan(input) {
1977
+ this.initializeSync();
1978
+ const spanId = input.spanId || uuidv4();
1979
+ const startTime = input.startTime || (/* @__PURE__ */ new Date()).toISOString();
1980
+ this.db.run(
1981
+ `INSERT INTO traces (
1982
+ trace_id, span_id, parent_span_id, service, symbol, operation,
1983
+ start_time, end_time, duration_ms, status, tags_json, log_ids_json
1984
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1985
+ [
1986
+ input.traceId,
1987
+ spanId,
1988
+ input.parentSpanId || null,
1989
+ input.service,
1990
+ input.symbol,
1991
+ input.operation,
1992
+ startTime,
1993
+ input.endTime || null,
1994
+ input.durationMs ?? null,
1995
+ input.status || "ok",
1996
+ JSON.stringify(input.tags || {}),
1997
+ JSON.stringify(input.logIds || [])
1998
+ ]
1999
+ );
2000
+ this.save();
2001
+ return spanId;
2002
+ }
2003
+ getTrace(traceId) {
2004
+ this.initializeSync();
2005
+ const result = this.db.exec(
2006
+ "SELECT * FROM traces WHERE trace_id = ? ORDER BY start_time ASC",
2007
+ [traceId]
2008
+ );
2009
+ if (result.length === 0 || result[0].values.length === 0) return null;
2010
+ const spans = result[0].values.map(
2011
+ (row) => this.rowToTraceSpan(result[0].columns, row)
2012
+ );
2013
+ const services = [...new Set(spans.map((s) => s.service))];
2014
+ const startTimes = spans.map((s) => s.startTime);
2015
+ const endTimes = spans.filter((s) => s.endTime).map((s) => s.endTime);
2016
+ const startTime = startTimes.sort()[0];
2017
+ const endTime = endTimes.length > 0 ? endTimes.sort().reverse()[0] : startTime;
2018
+ const startMs = new Date(startTime).getTime();
2019
+ const endMs = new Date(endTime).getTime();
2020
+ const totalDurationMs = endMs - startMs;
2021
+ return {
2022
+ traceId,
2023
+ spans,
2024
+ services,
2025
+ totalDurationMs: totalDurationMs > 0 ? totalDurationMs : 0,
2026
+ startTime,
2027
+ endTime
2028
+ };
2029
+ }
2030
+ queryTraces(options = {}) {
2031
+ this.initializeSync();
2032
+ const conditions = [];
2033
+ const params = [];
2034
+ if (options.service) {
2035
+ conditions.push("service = ?");
2036
+ params.push(options.service);
2037
+ }
2038
+ if (options.symbol) {
2039
+ conditions.push("symbol = ?");
2040
+ params.push(options.symbol);
2041
+ }
2042
+ if (options.since) {
2043
+ conditions.push("start_time >= ?");
2044
+ params.push(options.since);
2045
+ }
2046
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2047
+ const traceLimit = Math.min(options.limit || 20, 20);
2048
+ const result = this.db.exec(
2049
+ `SELECT DISTINCT trace_id FROM traces ${whereClause} ORDER BY start_time DESC LIMIT ?`,
2050
+ [...params, traceLimit]
2051
+ );
2052
+ if (result.length === 0) return [];
2053
+ const traces = [];
2054
+ for (const row of result[0].values) {
2055
+ const traceId = row[0];
2056
+ const trace = this.getTrace(traceId);
2057
+ if (trace) {
2058
+ traces.push(trace);
2059
+ }
2060
+ }
2061
+ return traces;
2062
+ }
2063
+ rowToTraceSpan(columns, row) {
2064
+ const obj = {};
2065
+ columns.forEach((col, i) => {
2066
+ obj[col] = row[i];
2067
+ });
2068
+ return {
2069
+ traceId: obj.trace_id,
2070
+ spanId: obj.span_id,
2071
+ parentSpanId: obj.parent_span_id || void 0,
2072
+ service: obj.service,
2073
+ symbol: obj.symbol,
2074
+ operation: obj.operation,
2075
+ startTime: obj.start_time,
2076
+ endTime: obj.end_time || void 0,
2077
+ durationMs: obj.duration_ms || void 0,
2078
+ status: obj.status || "ok",
2079
+ tags: obj.tags_json ? JSON.parse(obj.tags_json) : {},
2080
+ logs: obj.log_ids_json ? JSON.parse(obj.log_ids_json) : []
2081
+ };
2082
+ }
2083
+ // ─── Schema Registry ─────────────────────────────────────────────
2084
+ registerSchema(schema) {
1073
2085
  this.initializeSync();
1074
- const id = `PE-${uuidv4().substring(0, 8)}`;
1075
2086
  const now = (/* @__PURE__ */ new Date()).toISOString();
1076
2087
  this.db.run(
1077
- `INSERT INTO practice_events (
1078
- id, timestamp, habit_id, habit_category, result,
1079
- engineer, session_id, lore_entry_id, task_description,
1080
- symbols_touched, files_modified, related_incident_id, notes
1081
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2088
+ `INSERT INTO schemas (
2089
+ id, version, name, description, scope_json, event_types_json,
2090
+ causality_json, visualization_json, tags_json, registered_at, updated_at
2091
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2092
+ ON CONFLICT(id) DO UPDATE SET
2093
+ version = excluded.version,
2094
+ name = excluded.name,
2095
+ description = excluded.description,
2096
+ scope_json = excluded.scope_json,
2097
+ event_types_json = excluded.event_types_json,
2098
+ causality_json = excluded.causality_json,
2099
+ visualization_json = excluded.visualization_json,
2100
+ tags_json = excluded.tags_json,
2101
+ updated_at = excluded.updated_at`,
1082
2102
  [
1083
- id,
2103
+ schema.id,
2104
+ schema.version,
2105
+ schema.name,
2106
+ schema.description || null,
2107
+ JSON.stringify(schema.scope),
2108
+ JSON.stringify(schema.eventTypes),
2109
+ schema.causality ? JSON.stringify(schema.causality) : null,
2110
+ schema.visualization ? JSON.stringify(schema.visualization) : null,
2111
+ JSON.stringify(schema.tags || []),
1084
2112
  now,
1085
- input.habitId,
1086
- input.habitCategory,
1087
- input.result,
1088
- input.engineer,
1089
- input.sessionId,
1090
- input.loreEntryId || null,
1091
- input.taskDescription || null,
1092
- JSON.stringify(input.symbolsTouched || []),
1093
- JSON.stringify(input.filesModified || []),
1094
- input.relatedIncidentId || null,
1095
- input.notes || null
2113
+ now
1096
2114
  ]
1097
2115
  );
1098
2116
  this.save();
1099
- return id;
2117
+ return {
2118
+ id: schema.id,
2119
+ version: schema.version,
2120
+ name: schema.name,
2121
+ description: schema.description,
2122
+ scope: schema.scope,
2123
+ eventTypes: schema.eventTypes,
2124
+ causality: schema.causality,
2125
+ visualization: schema.visualization,
2126
+ tags: schema.tags || [],
2127
+ registeredAt: now,
2128
+ updatedAt: now
2129
+ };
1100
2130
  }
1101
- getPracticeEvents(options = {}) {
2131
+ getSchema(id) {
2132
+ this.initializeSync();
2133
+ const result = this.db.exec("SELECT * FROM schemas WHERE id = ?", [id]);
2134
+ if (result.length === 0 || result[0].values.length === 0) return null;
2135
+ return this.rowToSchema(result[0].columns, result[0].values[0]);
2136
+ }
2137
+ listSchemas() {
2138
+ this.initializeSync();
2139
+ const result = this.db.exec("SELECT * FROM schemas ORDER BY name ASC");
2140
+ if (result.length === 0) return [];
2141
+ return result[0].values.map(
2142
+ (row) => this.rowToSchema(result[0].columns, row)
2143
+ );
2144
+ }
2145
+ rowToSchema(columns, row) {
2146
+ const obj = {};
2147
+ columns.forEach((col, i) => {
2148
+ obj[col] = row[i];
2149
+ });
2150
+ return {
2151
+ id: obj.id,
2152
+ version: obj.version,
2153
+ name: obj.name,
2154
+ description: obj.description || void 0,
2155
+ scope: JSON.parse(obj.scope_json),
2156
+ eventTypes: JSON.parse(obj.event_types_json),
2157
+ causality: obj.causality_json ? JSON.parse(obj.causality_json) : void 0,
2158
+ visualization: obj.visualization_json ? JSON.parse(obj.visualization_json) : void 0,
2159
+ tags: JSON.parse(obj.tags_json || "[]"),
2160
+ registeredAt: obj.registered_at,
2161
+ updatedAt: obj.updated_at
2162
+ };
2163
+ }
2164
+ // ─── Generic Events ────────────────────────────────────────────
2165
+ insertEventBatch(schemaId, service, inputs) {
2166
+ this.initializeSync();
2167
+ const schema = this.getSchema(schemaId);
2168
+ const typeMap = /* @__PURE__ */ new Map();
2169
+ if (schema) {
2170
+ for (const et of schema.eventTypes) {
2171
+ typeMap.set(et.type, {
2172
+ category: et.category,
2173
+ severity: et.severity || "info"
2174
+ });
2175
+ }
2176
+ }
2177
+ let accepted = 0;
2178
+ const errors = [];
2179
+ for (const input of inputs) {
2180
+ try {
2181
+ const id = input.id || uuidv4();
2182
+ const timestamp = input.timestamp || (/* @__PURE__ */ new Date()).toISOString();
2183
+ const resolved = typeMap.get(input.type);
2184
+ const category = resolved?.category || "unknown";
2185
+ const severity = input.severity || resolved?.severity || "info";
2186
+ const scopeValue = input.scopeValue != null ? String(input.scopeValue) : null;
2187
+ const scopeOrdinal = typeof input.scopeValue === "number" ? input.scopeValue : null;
2188
+ this.db.run(
2189
+ `INSERT INTO events (
2190
+ id, schema_id, event_type, category, timestamp, scope_value,
2191
+ scope_ordinal, session_id, service, data_json, severity,
2192
+ parent_event_id, depth
2193
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2194
+ [
2195
+ id,
2196
+ schemaId,
2197
+ input.type,
2198
+ category,
2199
+ timestamp,
2200
+ scopeValue,
2201
+ scopeOrdinal,
2202
+ input.sessionId || null,
2203
+ service,
2204
+ input.data ? JSON.stringify(input.data) : null,
2205
+ severity,
2206
+ input.parentEventId || null,
2207
+ input.depth ?? 0
2208
+ ]
2209
+ );
2210
+ accepted++;
2211
+ } catch (err) {
2212
+ errors.push(err instanceof Error ? err.message : String(err));
2213
+ }
2214
+ }
2215
+ this.save();
2216
+ return { accepted, errors };
2217
+ }
2218
+ queryEvents(options = {}) {
1102
2219
  this.initializeSync();
1103
2220
  const { limit = 100, offset = 0 } = options;
1104
2221
  const conditions = [];
1105
2222
  const params = [];
1106
- if (options.habitId) {
1107
- conditions.push("habit_id = ?");
1108
- params.push(options.habitId);
2223
+ if (options.schemaId) {
2224
+ conditions.push("schema_id = ?");
2225
+ params.push(options.schemaId);
1109
2226
  }
1110
- if (options.habitCategory) {
1111
- conditions.push("habit_category = ?");
1112
- params.push(options.habitCategory);
2227
+ if (options.eventType) {
2228
+ conditions.push("event_type = ?");
2229
+ params.push(options.eventType);
1113
2230
  }
1114
- if (options.result) {
1115
- conditions.push("result = ?");
1116
- params.push(options.result);
2231
+ if (options.category) {
2232
+ conditions.push("category = ?");
2233
+ params.push(options.category);
1117
2234
  }
1118
- if (options.engineer) {
1119
- conditions.push("engineer = ?");
1120
- params.push(options.engineer);
2235
+ if (options.service) {
2236
+ conditions.push("service = ?");
2237
+ params.push(options.service);
1121
2238
  }
1122
2239
  if (options.sessionId) {
1123
2240
  conditions.push("session_id = ?");
1124
2241
  params.push(options.sessionId);
1125
2242
  }
1126
- if (options.dateFrom) {
2243
+ if (options.scopeValue) {
2244
+ conditions.push("scope_value = ?");
2245
+ params.push(options.scopeValue);
2246
+ }
2247
+ if (options.scopeFrom) {
2248
+ conditions.push("scope_value >= ?");
2249
+ params.push(options.scopeFrom);
2250
+ }
2251
+ if (options.scopeTo) {
2252
+ conditions.push("scope_value <= ?");
2253
+ params.push(options.scopeTo);
2254
+ }
2255
+ if (options.severity) {
2256
+ conditions.push("severity = ?");
2257
+ params.push(options.severity);
2258
+ }
2259
+ if (options.since) {
1127
2260
  conditions.push("timestamp >= ?");
1128
- params.push(options.dateFrom);
2261
+ params.push(options.since);
1129
2262
  }
1130
- if (options.dateTo) {
2263
+ if (options.until) {
1131
2264
  conditions.push("timestamp <= ?");
1132
- params.push(options.dateTo);
2265
+ params.push(options.until);
2266
+ }
2267
+ if (options.search) {
2268
+ conditions.push("data_json LIKE ?");
2269
+ params.push(`%${options.search}%`);
1133
2270
  }
1134
2271
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1135
2272
  const result = this.db.exec(
1136
- `SELECT * FROM practice_events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
2273
+ `SELECT * FROM events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ?`,
1137
2274
  [...params, limit, offset]
1138
2275
  );
1139
2276
  if (result.length === 0) return [];
1140
2277
  return result[0].values.map(
1141
- (row) => this.rowToPracticeEvent(result[0].columns, row)
2278
+ (row) => this.rowToGenericEvent(result[0].columns, row)
1142
2279
  );
1143
2280
  }
1144
- getPracticeEventCount(options = {}) {
2281
+ queryEventsByScope(schemaId, scopeValue) {
1145
2282
  this.initializeSync();
1146
- const conditions = [];
1147
- const params = [];
1148
- if (options.habitId) {
1149
- conditions.push("habit_id = ?");
1150
- params.push(options.habitId);
1151
- }
1152
- if (options.habitCategory) {
1153
- conditions.push("habit_category = ?");
1154
- params.push(options.habitCategory);
1155
- }
1156
- if (options.result) {
1157
- conditions.push("result = ?");
1158
- params.push(options.result);
1159
- }
1160
- if (options.engineer) {
1161
- conditions.push("engineer = ?");
1162
- params.push(options.engineer);
1163
- }
1164
- if (options.dateFrom) {
1165
- conditions.push("timestamp >= ?");
1166
- params.push(options.dateFrom);
1167
- }
1168
- if (options.dateTo) {
1169
- conditions.push("timestamp <= ?");
1170
- params.push(options.dateTo);
2283
+ const result = this.db.exec(
2284
+ `SELECT * FROM events
2285
+ WHERE schema_id = ? AND scope_value = ?
2286
+ ORDER BY timestamp ASC`,
2287
+ [schemaId, scopeValue]
2288
+ );
2289
+ if (result.length === 0) return [];
2290
+ return result[0].values.map(
2291
+ (row) => this.rowToGenericEvent(result[0].columns, row)
2292
+ );
2293
+ }
2294
+ getEventScopes(schemaId, options = {}) {
2295
+ this.initializeSync();
2296
+ const { limit = 100, offset = 0 } = options;
2297
+ const conditions = ["schema_id = ?"];
2298
+ const params = [schemaId];
2299
+ if (options.sessionId) {
2300
+ conditions.push("session_id = ?");
2301
+ params.push(options.sessionId);
1171
2302
  }
1172
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2303
+ const whereClause = `WHERE ${conditions.join(" AND ")}`;
1173
2304
  const result = this.db.exec(
1174
- `SELECT COUNT(*) as count FROM practice_events ${whereClause}`,
1175
- params
2305
+ `SELECT
2306
+ scope_value,
2307
+ MIN(scope_ordinal) as scope_ordinal,
2308
+ COUNT(*) as event_count,
2309
+ MIN(timestamp) as first_timestamp,
2310
+ MAX(timestamp) as last_timestamp
2311
+ FROM events
2312
+ ${whereClause}
2313
+ AND scope_value IS NOT NULL
2314
+ GROUP BY scope_value
2315
+ ORDER BY MIN(COALESCE(scope_ordinal, 0)) DESC, MIN(timestamp) DESC
2316
+ LIMIT ? OFFSET ?`,
2317
+ [...params, limit, offset]
1176
2318
  );
1177
- if (result.length === 0 || result[0].values.length === 0) return 0;
1178
- return result[0].values[0][0];
2319
+ if (result.length === 0) return [];
2320
+ const scopes = [];
2321
+ for (const row of result[0].values) {
2322
+ const scopeValue = row[0];
2323
+ const scopeOrdinal = row[1] != null ? row[1] : void 0;
2324
+ const eventCount = row[2];
2325
+ const firstTimestamp = row[3];
2326
+ const lastTimestamp = row[4];
2327
+ const catResult = this.db.exec(
2328
+ `SELECT category, COUNT(*) as count FROM events
2329
+ WHERE schema_id = ? AND scope_value = ?
2330
+ GROUP BY category`,
2331
+ [schemaId, scopeValue]
2332
+ );
2333
+ const categories = {};
2334
+ if (catResult.length > 0) {
2335
+ for (const catRow of catResult[0].values) {
2336
+ categories[catRow[0]] = catRow[1];
2337
+ }
2338
+ }
2339
+ scopes.push({
2340
+ scopeValue,
2341
+ scopeOrdinal,
2342
+ eventCount,
2343
+ categories,
2344
+ firstTimestamp,
2345
+ lastTimestamp
2346
+ });
2347
+ }
2348
+ return scopes;
1179
2349
  }
1180
- getComplianceRate(options = {}) {
2350
+ getEventCount(options = {}) {
1181
2351
  this.initializeSync();
1182
2352
  const conditions = [];
1183
2353
  const params = [];
1184
- if (options.habitId) {
1185
- conditions.push("habit_id = ?");
1186
- params.push(options.habitId);
2354
+ if (options.schemaId) {
2355
+ conditions.push("schema_id = ?");
2356
+ params.push(options.schemaId);
1187
2357
  }
1188
- if (options.habitCategory) {
1189
- conditions.push("habit_category = ?");
1190
- params.push(options.habitCategory);
2358
+ if (options.eventType) {
2359
+ conditions.push("event_type = ?");
2360
+ params.push(options.eventType);
1191
2361
  }
1192
- if (options.engineer) {
1193
- conditions.push("engineer = ?");
1194
- params.push(options.engineer);
2362
+ if (options.service) {
2363
+ conditions.push("service = ?");
2364
+ params.push(options.service);
1195
2365
  }
1196
- if (options.dateFrom) {
2366
+ if (options.since) {
1197
2367
  conditions.push("timestamp >= ?");
1198
- params.push(options.dateFrom);
2368
+ params.push(options.since);
1199
2369
  }
1200
- if (options.dateTo) {
2370
+ if (options.until) {
1201
2371
  conditions.push("timestamp <= ?");
1202
- params.push(options.dateTo);
2372
+ params.push(options.until);
1203
2373
  }
1204
2374
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1205
2375
  const result = this.db.exec(
1206
- `SELECT result, COUNT(*) as count
1207
- FROM practice_events ${whereClause}
1208
- GROUP BY result`,
2376
+ `SELECT COUNT(*) as count FROM events ${whereClause}`,
1209
2377
  params
1210
2378
  );
1211
- let followed = 0;
1212
- let skipped = 0;
1213
- let partial = 0;
1214
- if (result.length > 0) {
1215
- for (const row of result[0].values) {
1216
- const r = row[0];
1217
- const count = row[1];
1218
- if (r === "followed") followed = count;
1219
- else if (r === "skipped") skipped = count;
1220
- else if (r === "partial") partial = count;
1221
- }
1222
- }
1223
- const total = followed + skipped + partial;
1224
- const rate = total > 0 ? (followed + partial * 0.5) / total * 100 : 100;
1225
- return { total, followed, skipped, partial, rate: Math.round(rate) };
2379
+ if (result.length === 0 || result[0].values.length === 0) return 0;
2380
+ return result[0].values[0][0];
1226
2381
  }
1227
- rowToPracticeEvent(columns, row) {
2382
+ pruneEvents(maxCount) {
2383
+ this.initializeSync();
2384
+ if (maxCount <= 0) return 0;
2385
+ const currentCount = this.getEventCount();
2386
+ if (currentCount <= maxCount) return 0;
2387
+ const deleteCount = currentCount - maxCount;
2388
+ this.db.run(
2389
+ `DELETE FROM events WHERE id IN (
2390
+ SELECT id FROM events ORDER BY timestamp ASC LIMIT ?
2391
+ )`,
2392
+ [deleteCount]
2393
+ );
2394
+ this.save();
2395
+ return deleteCount;
2396
+ }
2397
+ rowToGenericEvent(columns, row) {
1228
2398
  const obj = {};
1229
2399
  columns.forEach((col, i) => {
1230
2400
  obj[col] = row[i];
1231
2401
  });
1232
2402
  return {
1233
2403
  id: obj.id,
2404
+ schemaId: obj.schema_id,
2405
+ eventType: obj.event_type,
2406
+ category: obj.category,
1234
2407
  timestamp: obj.timestamp,
1235
- habitId: obj.habit_id,
1236
- habitCategory: obj.habit_category,
1237
- result: obj.result,
1238
- engineer: obj.engineer,
1239
- sessionId: obj.session_id,
1240
- loreEntryId: obj.lore_entry_id || void 0,
1241
- taskDescription: obj.task_description || void 0,
1242
- symbolsTouched: JSON.parse(obj.symbols_touched || "[]"),
1243
- filesModified: JSON.parse(obj.files_modified || "[]"),
1244
- relatedIncidentId: obj.related_incident_id || void 0,
1245
- notes: obj.notes || void 0
2408
+ scopeValue: obj.scope_value || void 0,
2409
+ scopeOrdinal: obj.scope_ordinal != null ? obj.scope_ordinal : void 0,
2410
+ sessionId: obj.session_id || void 0,
2411
+ service: obj.service,
2412
+ data: obj.data_json ? JSON.parse(obj.data_json) : void 0,
2413
+ severity: obj.severity || "info",
2414
+ parentEventId: obj.parent_event_id || void 0,
2415
+ depth: obj.depth || 0
1246
2416
  };
1247
2417
  }
1248
2418
  close() {
@@ -1902,7 +3072,7 @@ var PatternSuggester = class {
1902
3072
  },
1903
3073
  resolution: {
1904
3074
  description: incident.resolution?.notes || "Resolution approach TBD",
1905
- strategy: "fix-code",
3075
+ strategy: this.inferStrategy([incident]),
1906
3076
  priority: "medium"
1907
3077
  },
1908
3078
  source: "suggested",
@@ -1917,6 +3087,7 @@ var PatternSuggester = class {
1917
3087
  suggestFromGroup(group) {
1918
3088
  const baseId = `group-${group.id.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
1919
3089
  const symbols = this.buildSymbolCriteria(group.commonSymbols);
3090
+ const groupIncidents = group.incidents.slice(0, 20).map((id) => this.storage.getIncident(id)).filter((i) => i != null);
1920
3091
  const pattern = {
1921
3092
  id: baseId,
1922
3093
  name: group.name || `Pattern from group ${group.id}`,
@@ -1927,7 +3098,7 @@ var PatternSuggester = class {
1927
3098
  },
1928
3099
  resolution: {
1929
3100
  description: "Resolution approach TBD based on grouped incidents",
1930
- strategy: "fix-code",
3101
+ strategy: groupIncidents.length > 0 ? this.inferStrategy(groupIncidents) : "fix-code",
1931
3102
  priority: this.getPriorityFromCount(group.count)
1932
3103
  },
1933
3104
  source: "suggested",
@@ -2226,21 +3397,38 @@ var PatternSuggester = class {
2226
3397
  return false;
2227
3398
  }
2228
3399
  /**
2229
- * Infer resolution strategy from incidents
3400
+ * Infer resolution strategy from incident error patterns and context.
3401
+ * Uses keyword heuristics across all incident messages to pick the
3402
+ * most likely resolution approach.
2230
3403
  */
2231
3404
  inferStrategy(incidents) {
2232
3405
  const messages = incidents.map((i) => i.error.message.toLowerCase());
2233
- if (messages.some((m) => m.includes("timeout") || m.includes("network"))) {
3406
+ const hasKeyword = (keywords) => messages.some((m) => keywords.some((k) => m.includes(k)));
3407
+ if (hasKeyword(["revert", "rollback", "regression", "broke after deploy", "since deploy"])) {
3408
+ return "rollback";
3409
+ }
3410
+ if (hasKeyword(["config", "environment variable", "env var", "missing key", "secret", "credential"])) {
3411
+ return "config-change";
3412
+ }
3413
+ if (hasKeyword(["out of memory", "oom", "heap", "memory limit", "capacity", "too many connections", "pool exhausted"])) {
3414
+ return "scale-up";
3415
+ }
3416
+ if (hasKeyword(["timeout", "network", "econnrefused", "econnreset", "dns", "socket hang up"])) {
2234
3417
  return "retry";
2235
3418
  }
2236
- if (messages.some(
2237
- (m) => m.includes("validation") || m.includes("invalid") || m.includes("required")
2238
- )) {
3419
+ if (hasKeyword(["unavailable", "service down", "circuit breaker", "fallback", "503", "502"])) {
3420
+ return "fallback";
3421
+ }
3422
+ if (hasKeyword(["validation", "invalid", "required", "constraint", "duplicate", "not found", "404"])) {
2239
3423
  return "fix-data";
2240
3424
  }
2241
- if (messages.some((m) => m.includes("permission") || m.includes("403"))) {
3425
+ if (hasKeyword(["permission", "forbidden", "403", "401", "unauthorized", "access denied"])) {
2242
3426
  return "escalate";
2243
3427
  }
3428
+ const uniqueTypes = new Set(incidents.map((i) => i.error.type).filter(Boolean));
3429
+ if (uniqueTypes.size > 2) {
3430
+ return "investigate";
3431
+ }
2244
3432
  return "fix-code";
2245
3433
  }
2246
3434
  /**
@@ -2497,6 +3685,101 @@ function getToolsList() {
2497
3685
  minOccurrences: { type: "number", description: "Min similar incidents for suggestion" }
2498
3686
  }
2499
3687
  }
3688
+ },
3689
+ // ─── Observability Tools ──────────────────────────────────────
3690
+ {
3691
+ name: "sentinel_logs",
3692
+ description: "Query structured logs from connected apps. Filters by level, symbol, service, search text, time range.",
3693
+ annotations: { readOnlyHint: true, destructiveHint: false },
3694
+ inputSchema: {
3695
+ type: "object",
3696
+ properties: {
3697
+ level: { type: "string", enum: ["debug", "info", "warn", "error"], description: "Filter by log level" },
3698
+ symbol: { type: "string", description: "Filter by symbol (partial match)" },
3699
+ service: { type: "string", description: "Filter by service name" },
3700
+ search: { type: "string", description: "Search in log messages" },
3701
+ since: { type: "string", description: "ISO timestamp \u2014 logs after this time" },
3702
+ sessionId: { type: "string", description: "Filter by session ID" },
3703
+ correlationId: { type: "string", description: "Filter by correlation ID" },
3704
+ limit: { type: "number", description: "Max results (default: 50)" }
3705
+ }
3706
+ }
3707
+ },
3708
+ {
3709
+ name: "sentinel_services",
3710
+ description: "List all registered services with version, environment, and last-seen time.",
3711
+ annotations: { readOnlyHint: true, destructiveHint: false },
3712
+ inputSchema: {
3713
+ type: "object",
3714
+ properties: {}
3715
+ }
3716
+ },
3717
+ {
3718
+ name: "sentinel_app_state",
3719
+ description: "Get live app state snapshots. Shows current state, active flows, and held gates for connected services.",
3720
+ annotations: { readOnlyHint: true, destructiveHint: false },
3721
+ inputSchema: {
3722
+ type: "object",
3723
+ properties: {
3724
+ service: { type: "string", description: "Filter by service name" }
3725
+ }
3726
+ }
3727
+ },
3728
+ {
3729
+ name: "sentinel_validate_symbol",
3730
+ description: "Check if a symbol has been used in logs. Returns usage count and suggestions.",
3731
+ annotations: { readOnlyHint: true, destructiveHint: false },
3732
+ inputSchema: {
3733
+ type: "object",
3734
+ properties: {
3735
+ symbol: { type: "string", description: "Symbol to validate (e.g., #checkout, ^auth)" }
3736
+ },
3737
+ required: ["symbol"]
3738
+ }
3739
+ },
3740
+ {
3741
+ name: "sentinel_flow_activity",
3742
+ description: "Get recent flow events \u2014 which flow nodes were hit, in what order, by which service.",
3743
+ annotations: { readOnlyHint: true, destructiveHint: false },
3744
+ inputSchema: {
3745
+ type: "object",
3746
+ properties: {
3747
+ flowId: { type: "string", description: "Filter by flow ID (e.g., $checkout-flow)" },
3748
+ service: { type: "string", description: "Filter by service name" },
3749
+ since: { type: "string", description: "ISO timestamp \u2014 events after this time" }
3750
+ }
3751
+ }
3752
+ },
3753
+ {
3754
+ name: "sentinel_metrics",
3755
+ description: "Query metrics (counters, gauges, histograms) from connected apps. Supports filtering and aggregation.",
3756
+ annotations: { readOnlyHint: true, destructiveHint: false },
3757
+ inputSchema: {
3758
+ type: "object",
3759
+ properties: {
3760
+ name: { type: "string", description: "Metric name filter" },
3761
+ type: { type: "string", enum: ["counter", "gauge", "histogram"], description: "Metric type filter" },
3762
+ service: { type: "string", description: "Service name filter" },
3763
+ since: { type: "string", description: "ISO timestamp \u2014 metrics after this time" },
3764
+ aggregate: { type: "boolean", description: "If true and name is provided, return aggregation instead of raw data" },
3765
+ limit: { type: "number", description: "Max results (default: 50)" }
3766
+ }
3767
+ }
3768
+ },
3769
+ {
3770
+ name: "sentinel_traces",
3771
+ description: "Query distributed traces across services. Shows span trees with timing, status, and service hops.",
3772
+ annotations: { readOnlyHint: true, destructiveHint: false },
3773
+ inputSchema: {
3774
+ type: "object",
3775
+ properties: {
3776
+ traceId: { type: "string", description: "Get a specific trace by ID" },
3777
+ service: { type: "string", description: "Filter by service name" },
3778
+ symbol: { type: "string", description: "Filter by symbol" },
3779
+ since: { type: "string", description: "ISO timestamp \u2014 traces after this time" },
3780
+ limit: { type: "number", description: "Max traces (default: 10, max: 20)" }
3781
+ }
3782
+ }
2500
3783
  }
2501
3784
  ];
2502
3785
  }
@@ -2725,6 +4008,137 @@ async function handleTool(name, args) {
2725
4008
  2
2726
4009
  );
2727
4010
  }
4011
+ // ─── Observability Tools ──────────────────────────────────────
4012
+ case "sentinel_logs": {
4013
+ const { level, symbol, service, search, since, sessionId, correlationId, limit = 50 } = args;
4014
+ const logs = store.queryLogs({
4015
+ level,
4016
+ symbol,
4017
+ service,
4018
+ search,
4019
+ since,
4020
+ sessionId,
4021
+ correlationId,
4022
+ limit
4023
+ });
4024
+ const total = store.getLogCount({ level, symbol, service, since });
4025
+ return JSON.stringify({
4026
+ count: logs.length,
4027
+ total,
4028
+ logs: logs.map((l) => ({
4029
+ timestamp: l.timestamp,
4030
+ level: l.level,
4031
+ symbol: l.symbol,
4032
+ service: l.service,
4033
+ message: l.message,
4034
+ data: l.data,
4035
+ sessionId: l.sessionId,
4036
+ correlationId: l.correlationId,
4037
+ durationMs: l.durationMs
4038
+ }))
4039
+ }, null, 2);
4040
+ }
4041
+ case "sentinel_services": {
4042
+ const services = store.getServices();
4043
+ return JSON.stringify({
4044
+ count: services.length,
4045
+ services: services.map((s) => ({
4046
+ name: s.name,
4047
+ version: s.version,
4048
+ environment: s.environment,
4049
+ lastSeen: s.lastSeenAt,
4050
+ startedAt: s.startedAt,
4051
+ pid: s.pid
4052
+ }))
4053
+ }, null, 2);
4054
+ }
4055
+ case "sentinel_app_state": {
4056
+ const { service: svc } = args;
4057
+ const states = svc ? store.getAppState(svc) : store.getAllAppStates();
4058
+ return JSON.stringify({
4059
+ states: states.map((s) => ({
4060
+ service: s.service,
4061
+ sessionId: s.sessionId,
4062
+ state: s.state,
4063
+ activeFlows: s.activeFlows,
4064
+ activeGates: s.activeGates,
4065
+ timestamp: s.timestamp
4066
+ }))
4067
+ }, null, 2);
4068
+ }
4069
+ case "sentinel_validate_symbol": {
4070
+ const { symbol: sym } = args;
4071
+ const logCount = store.getLogCount({ symbol: sym });
4072
+ return JSON.stringify({
4073
+ symbol: sym,
4074
+ usedInLogs: logCount > 0,
4075
+ logCount,
4076
+ 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.`
4077
+ }, null, 2);
4078
+ }
4079
+ case "sentinel_flow_activity": {
4080
+ const { flowId, service: flowSvc, since: flowSince } = args;
4081
+ const flowLogs = store.queryLogs({ symbol: flowId, service: flowSvc, since: flowSince, limit: 100 });
4082
+ const flowEvents = flowLogs.filter((l) => ["flow", "signal", "gate"].includes(l.symbolType)).map((l) => ({
4083
+ timestamp: l.timestamp,
4084
+ symbol: l.symbol,
4085
+ symbolType: l.symbolType,
4086
+ service: l.service,
4087
+ message: l.message,
4088
+ level: l.level
4089
+ }));
4090
+ return JSON.stringify({ count: flowEvents.length, events: flowEvents }, null, 2);
4091
+ }
4092
+ case "sentinel_metrics": {
4093
+ const { name: metricName, type: metricType, service: metricSvc, since: metricSince, aggregate, limit: metricLimit } = args;
4094
+ if (aggregate && metricName) {
4095
+ const agg = store.aggregateMetric(metricName, { service: metricSvc, since: metricSince });
4096
+ return JSON.stringify(agg, null, 2);
4097
+ }
4098
+ const metrics = store.queryMetrics({
4099
+ name: metricName,
4100
+ type: metricType,
4101
+ service: metricSvc,
4102
+ since: metricSince,
4103
+ limit: Math.min(metricLimit || 50, 100)
4104
+ });
4105
+ return JSON.stringify({
4106
+ count: metrics.length,
4107
+ metrics: metrics.map((m) => ({
4108
+ timestamp: m.timestamp,
4109
+ name: m.name,
4110
+ type: m.type,
4111
+ value: m.value,
4112
+ tags: m.tags,
4113
+ service: m.service
4114
+ }))
4115
+ }, null, 2);
4116
+ }
4117
+ case "sentinel_traces": {
4118
+ const { traceId: tid, service: traceSvc, symbol: traceSym, since: traceSince, limit: traceLimit } = args;
4119
+ if (tid) {
4120
+ const trace = store.getTrace(tid);
4121
+ if (!trace) return JSON.stringify({ error: "Trace not found" });
4122
+ return JSON.stringify(trace, null, 2);
4123
+ }
4124
+ const traces = store.queryTraces({
4125
+ service: traceSvc,
4126
+ symbol: traceSym,
4127
+ since: traceSince,
4128
+ limit: Math.min(traceLimit || 10, 20)
4129
+ });
4130
+ return JSON.stringify({
4131
+ count: traces.length,
4132
+ traces: traces.map((t) => ({
4133
+ traceId: t.traceId,
4134
+ services: t.services,
4135
+ spanCount: t.spans.length,
4136
+ totalDurationMs: t.totalDurationMs,
4137
+ startTime: t.startTime,
4138
+ endTime: t.endTime
4139
+ }))
4140
+ }, null, 2);
4141
+ }
2728
4142
  default:
2729
4143
  return JSON.stringify({ error: `Unknown tool: ${name}` });
2730
4144
  }