@hasna/logs 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dashboard/README.md +73 -0
  2. package/dashboard/bun.lock +526 -0
  3. package/dashboard/eslint.config.js +23 -0
  4. package/dashboard/index.html +13 -0
  5. package/dashboard/package.json +32 -0
  6. package/dashboard/public/favicon.svg +1 -0
  7. package/dashboard/public/icons.svg +24 -0
  8. package/dashboard/src/App.css +184 -0
  9. package/dashboard/src/App.tsx +49 -0
  10. package/dashboard/src/api.ts +33 -0
  11. package/dashboard/src/assets/hero.png +0 -0
  12. package/dashboard/src/assets/react.svg +1 -0
  13. package/dashboard/src/assets/vite.svg +1 -0
  14. package/dashboard/src/index.css +111 -0
  15. package/dashboard/src/main.tsx +10 -0
  16. package/dashboard/src/pages/Alerts.tsx +69 -0
  17. package/dashboard/src/pages/Issues.tsx +50 -0
  18. package/dashboard/src/pages/Perf.tsx +75 -0
  19. package/dashboard/src/pages/Projects.tsx +67 -0
  20. package/dashboard/src/pages/Summary.tsx +67 -0
  21. package/dashboard/src/pages/Tail.tsx +65 -0
  22. package/dashboard/tsconfig.app.json +28 -0
  23. package/dashboard/tsconfig.json +7 -0
  24. package/dashboard/tsconfig.node.json +26 -0
  25. package/dashboard/vite.config.ts +14 -0
  26. package/dist/cli/index.js +116 -12
  27. package/dist/mcp/index.js +306 -100
  28. package/dist/server/index.js +592 -7
  29. package/package.json +12 -2
  30. package/sdk/package.json +3 -2
  31. package/sdk/src/index.ts +1 -1
  32. package/sdk/src/types.ts +56 -0
  33. package/src/cli/index.ts +114 -4
  34. package/src/db/index.ts +10 -0
  35. package/src/db/migrations/001_alert_rules.ts +21 -0
  36. package/src/db/migrations/002_issues.ts +21 -0
  37. package/src/db/migrations/003_retention.ts +15 -0
  38. package/src/db/migrations/004_page_auth.ts +13 -0
  39. package/src/lib/alerts.test.ts +67 -0
  40. package/src/lib/alerts.ts +117 -0
  41. package/src/lib/compare.test.ts +52 -0
  42. package/src/lib/compare.ts +85 -0
  43. package/src/lib/diagnose.test.ts +55 -0
  44. package/src/lib/diagnose.ts +76 -0
  45. package/src/lib/export.test.ts +66 -0
  46. package/src/lib/export.ts +65 -0
  47. package/src/lib/health.test.ts +48 -0
  48. package/src/lib/health.ts +51 -0
  49. package/src/lib/ingest.ts +25 -2
  50. package/src/lib/issues.test.ts +79 -0
  51. package/src/lib/issues.ts +70 -0
  52. package/src/lib/page-auth.test.ts +54 -0
  53. package/src/lib/page-auth.ts +48 -0
  54. package/src/lib/retention.test.ts +42 -0
  55. package/src/lib/retention.ts +62 -0
  56. package/src/lib/scanner.ts +21 -2
  57. package/src/lib/scheduler.ts +6 -0
  58. package/src/lib/session-context.ts +28 -0
  59. package/src/mcp/index.ts +133 -89
  60. package/src/server/index.ts +12 -1
  61. package/src/server/routes/alerts.ts +32 -0
  62. package/src/server/routes/issues.ts +43 -0
  63. package/src/server/routes/logs.ts +21 -0
  64. package/src/server/routes/projects.ts +25 -0
  65. package/src/server/routes/stream.ts +43 -0
package/dist/mcp/index.js CHANGED
@@ -1,27 +1,39 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  import {
4
+ createAlertRule,
4
5
  createPage,
5
6
  createProject,
7
+ deleteAlertRule,
6
8
  getDb,
7
9
  getLatestSnapshot,
8
- getLogContext,
9
10
  getPerfTrend,
10
11
  ingestLog,
12
+ listAlertRules,
13
+ listIssues,
11
14
  listPages,
12
15
  listProjects,
13
16
  scoreLabel,
14
- searchLogs,
15
17
  summarizeLogs,
18
+ updateIssueStatus
19
+ } from "../index-77ss2sf4.js";
20
+ import {
21
+ createJob,
22
+ listJobs
23
+ } from "../jobs-124e878j.js";
24
+ import {
25
+ getLogContext,
26
+ searchLogs,
16
27
  tailLogs
17
- } from "../index-zj6ymcv7.js";
28
+ } from "../query-0qv7fvzt.js";
29
+ import {
30
+ getHealth
31
+ } from "../health-f2qrebqc.js";
18
32
  import {
19
33
  __commonJS,
20
34
  __export,
21
- __toESM,
22
- createJob,
23
- listJobs
24
- } from "../index-4mnved04.js";
35
+ __toESM
36
+ } from "../index-g8dczzvv.js";
25
37
 
26
38
  // node_modules/ajv/dist/compile/codegen/code.js
27
39
  var require_code = __commonJS((exports) => {
@@ -6282,7 +6294,7 @@ var require_formats = __commonJS((exports) => {
6282
6294
  }
6283
6295
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6284
6296
  function getTime(strictTimeZone) {
6285
- return function time3(str) {
6297
+ return function time(str) {
6286
6298
  const matches = TIME.exec(str);
6287
6299
  if (!matches)
6288
6300
  return false;
@@ -10322,7 +10334,7 @@ __export(exports_core2, {
10322
10334
  safeDecode: () => safeDecode,
10323
10335
  registry: () => registry,
10324
10336
  regexes: () => exports_regexes,
10325
- process: () => process,
10337
+ process: () => process2,
10326
10338
  prettifyError: () => prettifyError,
10327
10339
  parseAsync: () => parseAsync,
10328
10340
  parse: () => parse,
@@ -20786,7 +20798,7 @@ function initializeContext(params) {
20786
20798
  external: params?.external ?? undefined
20787
20799
  };
20788
20800
  }
20789
- function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
20801
+ function process2(schema, ctx, _params = { path: [], schemaPath: [] }) {
20790
20802
  var _a2;
20791
20803
  const def = schema._zod.def;
20792
20804
  const seen = ctx.seen.get(schema);
@@ -20823,7 +20835,7 @@ function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
20823
20835
  if (parent) {
20824
20836
  if (!result.ref)
20825
20837
  result.ref = parent;
20826
- process(parent, ctx, params);
20838
+ process2(parent, ctx, params);
20827
20839
  ctx.seen.get(parent).isParent = true;
20828
20840
  }
20829
20841
  }
@@ -21099,14 +21111,14 @@ function isTransforming(_schema, _ctx) {
21099
21111
  }
21100
21112
  var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
21101
21113
  const ctx = initializeContext({ ...params, processors });
21102
- process(schema, ctx);
21114
+ process2(schema, ctx);
21103
21115
  extractDefs(ctx, schema);
21104
21116
  return finalize(ctx, schema);
21105
21117
  };
21106
21118
  var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
21107
21119
  const { libraryOptions, target } = params ?? {};
21108
21120
  const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
21109
- process(schema, ctx);
21121
+ process2(schema, ctx);
21110
21122
  extractDefs(ctx, schema);
21111
21123
  return finalize(ctx, schema);
21112
21124
  };
@@ -21357,7 +21369,7 @@ var arrayProcessor = (schema, ctx, _json, params) => {
21357
21369
  if (typeof maximum === "number")
21358
21370
  json.maxItems = maximum;
21359
21371
  json.type = "array";
21360
- json.items = process(def.element, ctx, { ...params, path: [...params.path, "items"] });
21372
+ json.items = process2(def.element, ctx, { ...params, path: [...params.path, "items"] });
21361
21373
  };
21362
21374
  var objectProcessor = (schema, ctx, _json, params) => {
21363
21375
  const json = _json;
@@ -21366,7 +21378,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
21366
21378
  json.properties = {};
21367
21379
  const shape = def.shape;
21368
21380
  for (const key in shape) {
21369
- json.properties[key] = process(shape[key], ctx, {
21381
+ json.properties[key] = process2(shape[key], ctx, {
21370
21382
  ...params,
21371
21383
  path: [...params.path, "properties", key]
21372
21384
  });
@@ -21389,7 +21401,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
21389
21401
  if (ctx.io === "output")
21390
21402
  json.additionalProperties = false;
21391
21403
  } else if (def.catchall) {
21392
- json.additionalProperties = process(def.catchall, ctx, {
21404
+ json.additionalProperties = process2(def.catchall, ctx, {
21393
21405
  ...params,
21394
21406
  path: [...params.path, "additionalProperties"]
21395
21407
  });
@@ -21398,7 +21410,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
21398
21410
  var unionProcessor = (schema, ctx, json, params) => {
21399
21411
  const def = schema._zod.def;
21400
21412
  const isExclusive = def.inclusive === false;
21401
- const options = def.options.map((x, i) => process(x, ctx, {
21413
+ const options = def.options.map((x, i) => process2(x, ctx, {
21402
21414
  ...params,
21403
21415
  path: [...params.path, isExclusive ? "oneOf" : "anyOf", i]
21404
21416
  }));
@@ -21410,11 +21422,11 @@ var unionProcessor = (schema, ctx, json, params) => {
21410
21422
  };
21411
21423
  var intersectionProcessor = (schema, ctx, json, params) => {
21412
21424
  const def = schema._zod.def;
21413
- const a = process(def.left, ctx, {
21425
+ const a = process2(def.left, ctx, {
21414
21426
  ...params,
21415
21427
  path: [...params.path, "allOf", 0]
21416
21428
  });
21417
- const b = process(def.right, ctx, {
21429
+ const b = process2(def.right, ctx, {
21418
21430
  ...params,
21419
21431
  path: [...params.path, "allOf", 1]
21420
21432
  });
@@ -21431,11 +21443,11 @@ var tupleProcessor = (schema, ctx, _json, params) => {
21431
21443
  json.type = "array";
21432
21444
  const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
21433
21445
  const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
21434
- const prefixItems = def.items.map((x, i) => process(x, ctx, {
21446
+ const prefixItems = def.items.map((x, i) => process2(x, ctx, {
21435
21447
  ...params,
21436
21448
  path: [...params.path, prefixPath, i]
21437
21449
  }));
21438
- const rest = def.rest ? process(def.rest, ctx, {
21450
+ const rest = def.rest ? process2(def.rest, ctx, {
21439
21451
  ...params,
21440
21452
  path: [...params.path, restPath, ...ctx.target === "openapi-3.0" ? [def.items.length] : []]
21441
21453
  }) : null;
@@ -21475,7 +21487,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
21475
21487
  const keyBag = keyType._zod.bag;
21476
21488
  const patterns = keyBag?.patterns;
21477
21489
  if (def.mode === "loose" && patterns && patterns.size > 0) {
21478
- const valueSchema = process(def.valueType, ctx, {
21490
+ const valueSchema = process2(def.valueType, ctx, {
21479
21491
  ...params,
21480
21492
  path: [...params.path, "patternProperties", "*"]
21481
21493
  });
@@ -21485,12 +21497,12 @@ var recordProcessor = (schema, ctx, _json, params) => {
21485
21497
  }
21486
21498
  } else {
21487
21499
  if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
21488
- json.propertyNames = process(def.keyType, ctx, {
21500
+ json.propertyNames = process2(def.keyType, ctx, {
21489
21501
  ...params,
21490
21502
  path: [...params.path, "propertyNames"]
21491
21503
  });
21492
21504
  }
21493
- json.additionalProperties = process(def.valueType, ctx, {
21505
+ json.additionalProperties = process2(def.valueType, ctx, {
21494
21506
  ...params,
21495
21507
  path: [...params.path, "additionalProperties"]
21496
21508
  });
@@ -21505,7 +21517,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
21505
21517
  };
21506
21518
  var nullableProcessor = (schema, ctx, json, params) => {
21507
21519
  const def = schema._zod.def;
21508
- const inner = process(def.innerType, ctx, params);
21520
+ const inner = process2(def.innerType, ctx, params);
21509
21521
  const seen = ctx.seen.get(schema);
21510
21522
  if (ctx.target === "openapi-3.0") {
21511
21523
  seen.ref = def.innerType;
@@ -21516,20 +21528,20 @@ var nullableProcessor = (schema, ctx, json, params) => {
21516
21528
  };
21517
21529
  var nonoptionalProcessor = (schema, ctx, _json, params) => {
21518
21530
  const def = schema._zod.def;
21519
- process(def.innerType, ctx, params);
21531
+ process2(def.innerType, ctx, params);
21520
21532
  const seen = ctx.seen.get(schema);
21521
21533
  seen.ref = def.innerType;
21522
21534
  };
21523
21535
  var defaultProcessor = (schema, ctx, json, params) => {
21524
21536
  const def = schema._zod.def;
21525
- process(def.innerType, ctx, params);
21537
+ process2(def.innerType, ctx, params);
21526
21538
  const seen = ctx.seen.get(schema);
21527
21539
  seen.ref = def.innerType;
21528
21540
  json.default = JSON.parse(JSON.stringify(def.defaultValue));
21529
21541
  };
21530
21542
  var prefaultProcessor = (schema, ctx, json, params) => {
21531
21543
  const def = schema._zod.def;
21532
- process(def.innerType, ctx, params);
21544
+ process2(def.innerType, ctx, params);
21533
21545
  const seen = ctx.seen.get(schema);
21534
21546
  seen.ref = def.innerType;
21535
21547
  if (ctx.io === "input")
@@ -21537,7 +21549,7 @@ var prefaultProcessor = (schema, ctx, json, params) => {
21537
21549
  };
21538
21550
  var catchProcessor = (schema, ctx, json, params) => {
21539
21551
  const def = schema._zod.def;
21540
- process(def.innerType, ctx, params);
21552
+ process2(def.innerType, ctx, params);
21541
21553
  const seen = ctx.seen.get(schema);
21542
21554
  seen.ref = def.innerType;
21543
21555
  let catchValue;
@@ -21551,32 +21563,32 @@ var catchProcessor = (schema, ctx, json, params) => {
21551
21563
  var pipeProcessor = (schema, ctx, _json, params) => {
21552
21564
  const def = schema._zod.def;
21553
21565
  const innerType = ctx.io === "input" ? def.in._zod.def.type === "transform" ? def.out : def.in : def.out;
21554
- process(innerType, ctx, params);
21566
+ process2(innerType, ctx, params);
21555
21567
  const seen = ctx.seen.get(schema);
21556
21568
  seen.ref = innerType;
21557
21569
  };
21558
21570
  var readonlyProcessor = (schema, ctx, json, params) => {
21559
21571
  const def = schema._zod.def;
21560
- process(def.innerType, ctx, params);
21572
+ process2(def.innerType, ctx, params);
21561
21573
  const seen = ctx.seen.get(schema);
21562
21574
  seen.ref = def.innerType;
21563
21575
  json.readOnly = true;
21564
21576
  };
21565
21577
  var promiseProcessor = (schema, ctx, _json, params) => {
21566
21578
  const def = schema._zod.def;
21567
- process(def.innerType, ctx, params);
21579
+ process2(def.innerType, ctx, params);
21568
21580
  const seen = ctx.seen.get(schema);
21569
21581
  seen.ref = def.innerType;
21570
21582
  };
21571
21583
  var optionalProcessor = (schema, ctx, _json, params) => {
21572
21584
  const def = schema._zod.def;
21573
- process(def.innerType, ctx, params);
21585
+ process2(def.innerType, ctx, params);
21574
21586
  const seen = ctx.seen.get(schema);
21575
21587
  seen.ref = def.innerType;
21576
21588
  };
21577
21589
  var lazyProcessor = (schema, ctx, _json, params) => {
21578
21590
  const innerType = schema._zod.innerType;
21579
- process(innerType, ctx, params);
21591
+ process2(innerType, ctx, params);
21580
21592
  const seen = ctx.seen.get(schema);
21581
21593
  seen.ref = innerType;
21582
21594
  };
@@ -21628,7 +21640,7 @@ function toJSONSchema(input, params) {
21628
21640
  const defs = {};
21629
21641
  for (const entry of registry2._idmap.entries()) {
21630
21642
  const [_, schema] = entry;
21631
- process(schema, ctx2);
21643
+ process2(schema, ctx2);
21632
21644
  }
21633
21645
  const schemas = {};
21634
21646
  const external = {
@@ -21651,7 +21663,7 @@ function toJSONSchema(input, params) {
21651
21663
  return { schemas };
21652
21664
  }
21653
21665
  const ctx = initializeContext({ ...params, processors: allProcessors });
21654
- process(input, ctx);
21666
+ process2(input, ctx);
21655
21667
  extractDefs(ctx, input);
21656
21668
  return finalize(ctx, input);
21657
21669
  }
@@ -21697,7 +21709,7 @@ class JSONSchemaGenerator {
21697
21709
  });
21698
21710
  }
21699
21711
  process(schema, _params = { path: [], schemaPath: [] }) {
21700
- return process(schema, this.ctx, _params);
21712
+ return process2(schema, this.ctx, _params);
21701
21713
  }
21702
21714
  emit(schema, _params) {
21703
21715
  if (_params) {
@@ -28278,7 +28290,7 @@ var EMPTY_COMPLETION_RESULT = {
28278
28290
  };
28279
28291
 
28280
28292
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
28281
- import process2 from "process";
28293
+ import process3 from "process";
28282
28294
 
28283
28295
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
28284
28296
  class ReadBuffer {
@@ -28312,7 +28324,7 @@ function serializeMessage(message) {
28312
28324
 
28313
28325
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
28314
28326
  class StdioServerTransport {
28315
- constructor(_stdin = process2.stdin, _stdout = process2.stdout) {
28327
+ constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
28316
28328
  this._stdin = _stdin;
28317
28329
  this._stdout = _stdout;
28318
28330
  this._readBuffer = new ReadBuffer;
@@ -28368,25 +28380,174 @@ class StdioServerTransport {
28368
28380
  }
28369
28381
  }
28370
28382
 
28383
+ // src/lib/diagnose.ts
28384
+ function diagnose(db, projectId, since) {
28385
+ const window = since ?? new Date(Date.now() - 24 * 3600 * 1000).toISOString();
28386
+ const top_errors = db.prepare(`
28387
+ SELECT message, COUNT(*) as count, service, MAX(timestamp) as last_seen
28388
+ FROM logs
28389
+ WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since
28390
+ GROUP BY message, service
28391
+ ORDER BY count DESC
28392
+ LIMIT 10
28393
+ `).all({ $p: projectId, $since: window });
28394
+ const error_rate_by_service = db.prepare(`
28395
+ SELECT service,
28396
+ SUM(CASE WHEN level IN ('error','fatal') THEN 1 ELSE 0 END) as errors,
28397
+ SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) as warns,
28398
+ COUNT(*) as total
28399
+ FROM logs
28400
+ WHERE project_id = $p AND timestamp >= $since
28401
+ GROUP BY service
28402
+ ORDER BY errors DESC
28403
+ `).all({ $p: projectId, $since: window });
28404
+ const failing_pages = db.prepare(`
28405
+ SELECT l.page_id, p.url, COUNT(*) as error_count
28406
+ FROM logs l
28407
+ JOIN pages p ON p.id = l.page_id
28408
+ WHERE l.project_id = $p AND l.level IN ('error','fatal') AND l.timestamp >= $since AND l.page_id IS NOT NULL
28409
+ GROUP BY l.page_id, p.url
28410
+ ORDER BY error_count DESC
28411
+ LIMIT 10
28412
+ `).all({ $p: projectId, $since: window });
28413
+ const perf_regressions = db.prepare(`
28414
+ SELECT * FROM (
28415
+ SELECT
28416
+ cur.page_id,
28417
+ p.url,
28418
+ cur.score as score_now,
28419
+ prev.score as score_prev,
28420
+ (cur.score - prev.score) as delta
28421
+ FROM performance_snapshots cur
28422
+ JOIN pages p ON p.id = cur.page_id
28423
+ LEFT JOIN performance_snapshots prev ON prev.page_id = cur.page_id AND prev.id != cur.id
28424
+ WHERE cur.project_id = $p
28425
+ AND cur.timestamp = (SELECT MAX(timestamp) FROM performance_snapshots WHERE page_id = cur.page_id)
28426
+ AND (prev.timestamp = (SELECT MAX(timestamp) FROM performance_snapshots WHERE page_id = cur.page_id AND id != cur.id) OR prev.id IS NULL)
28427
+ ) WHERE delta < -5 OR delta IS NULL
28428
+ ORDER BY delta ASC
28429
+ LIMIT 10
28430
+ `).all({ $p: projectId });
28431
+ const totalErrors = top_errors.reduce((s, e) => s + e.count, 0);
28432
+ const topService = error_rate_by_service[0];
28433
+ const summary = totalErrors === 0 ? "No errors in this window. All looks good." : `${totalErrors} error(s) detected. Worst service: ${topService?.service ?? "unknown"} (${topService?.errors ?? 0} errors). ${failing_pages.length} page(s) with errors. ${perf_regressions.length} perf regression(s).`;
28434
+ return { project_id: projectId, window, top_errors, error_rate_by_service, failing_pages, perf_regressions, summary };
28435
+ }
28436
+
28437
+ // src/lib/compare.ts
28438
+ function getErrorsByMessage(db, projectId, since, until) {
28439
+ return db.prepare(`
28440
+ SELECT message, service, COUNT(*) as count
28441
+ FROM logs
28442
+ WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since AND timestamp <= $until
28443
+ GROUP BY message, service
28444
+ `).all({ $p: projectId, $since: since, $until: until });
28445
+ }
28446
+ function getErrorsByService(db, projectId, since, until) {
28447
+ return db.prepare(`
28448
+ SELECT service, COUNT(*) as errors
28449
+ FROM logs
28450
+ WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since AND timestamp <= $until
28451
+ GROUP BY service
28452
+ `).all({ $p: projectId, $since: since, $until: until });
28453
+ }
28454
+ function compare(db, projectId, aSince, aUntil, bSince, bUntil) {
28455
+ const errorsA = getErrorsByMessage(db, projectId, aSince, aUntil);
28456
+ const errorsB = getErrorsByMessage(db, projectId, bSince, bUntil);
28457
+ const keyA = new Set(errorsA.map((e) => `${e.service}|${e.message}`));
28458
+ const keyB = new Set(errorsB.map((e) => `${e.service}|${e.message}`));
28459
+ const new_errors = errorsB.filter((e) => !keyA.has(`${e.service}|${e.message}`));
28460
+ const resolved_errors = errorsA.filter((e) => !keyB.has(`${e.service}|${e.message}`));
28461
+ const svcA = getErrorsByService(db, projectId, aSince, aUntil);
28462
+ const svcB = getErrorsByService(db, projectId, bSince, bUntil);
28463
+ const svcMapA = new Map(svcA.map((s) => [s.service, s.errors]));
28464
+ const svcMapB = new Map(svcB.map((s) => [s.service, s.errors]));
28465
+ const allSvcs = new Set([...svcMapA.keys(), ...svcMapB.keys()]);
28466
+ const error_delta_by_service = [...allSvcs].map((svc) => ({
28467
+ service: svc,
28468
+ errors_a: svcMapA.get(svc) ?? 0,
28469
+ errors_b: svcMapB.get(svc) ?? 0,
28470
+ delta: (svcMapB.get(svc) ?? 0) - (svcMapA.get(svc) ?? 0)
28471
+ })).sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
28472
+ const perf_delta_by_page = db.prepare(`
28473
+ SELECT
28474
+ pa.page_id, pg.url,
28475
+ pa.score as score_a,
28476
+ pb.score as score_b,
28477
+ (pb.score - pa.score) as delta
28478
+ FROM
28479
+ (SELECT page_id, AVG(score) as score FROM performance_snapshots WHERE project_id = $p AND timestamp >= $as AND timestamp <= $au GROUP BY page_id) pa
28480
+ JOIN pages pg ON pg.id = pa.page_id
28481
+ LEFT JOIN (SELECT page_id, AVG(score) as score FROM performance_snapshots WHERE project_id = $p AND timestamp >= $bs AND timestamp <= $bu GROUP BY page_id) pb ON pb.page_id = pa.page_id
28482
+ ORDER BY delta ASC
28483
+ `).all({ $p: projectId, $as: aSince, $au: aUntil, $bs: bSince, $bu: bUntil });
28484
+ const summary = [
28485
+ `${new_errors.length} new error type(s), ${resolved_errors.length} resolved.`,
28486
+ error_delta_by_service.filter((s) => s.delta > 0).map((s) => `${s.service ?? "unknown"}: +${s.delta}`).join(", ") || "No error increases."
28487
+ ].join(" ");
28488
+ return {
28489
+ project_id: projectId,
28490
+ window_a: { since: aSince, until: aUntil },
28491
+ window_b: { since: bSince, until: bUntil },
28492
+ new_errors,
28493
+ resolved_errors,
28494
+ error_delta_by_service,
28495
+ perf_delta_by_page,
28496
+ summary
28497
+ };
28498
+ }
28499
+
28500
+ // src/lib/session-context.ts
28501
+ async function getSessionContext(db, sessionId) {
28502
+ const logs = db.prepare("SELECT * FROM logs WHERE session_id = $s ORDER BY timestamp ASC").all({ $s: sessionId });
28503
+ const sessionsUrl = process.env.SESSIONS_URL;
28504
+ if (!sessionsUrl) {
28505
+ return { session_id: sessionId, logs };
28506
+ }
28507
+ try {
28508
+ const res = await fetch(`${sessionsUrl.replace(/\/$/, "")}/api/sessions/${sessionId}`);
28509
+ if (!res.ok)
28510
+ return { session_id: sessionId, logs };
28511
+ const session = await res.json();
28512
+ return { session_id: sessionId, logs, session };
28513
+ } catch (err) {
28514
+ return { session_id: sessionId, logs, error: String(err) };
28515
+ }
28516
+ }
28517
+
28371
28518
  // src/mcp/index.ts
28372
28519
  var db = getDb();
28373
- var server = new McpServer({ name: "logs", version: "0.0.1" });
28520
+ var server = new McpServer({ name: "logs", version: "0.1.0" });
28521
+ function applyBrief(rows, brief = true) {
28522
+ if (!brief)
28523
+ return rows;
28524
+ return rows.map((r) => ({ id: r.id, timestamp: r.timestamp, level: r.level, message: r.message, service: r.service }));
28525
+ }
28374
28526
  var TOOLS = {
28375
28527
  register_project: "Register a project (name, github_repo?, base_url?, description?)",
28376
- register_page: "Register a page URL to a project (project_id, url, path?, name?)",
28377
- create_scan_job: "Schedule headless page scans (project_id, schedule, page_id?)",
28528
+ register_page: "Register a page URL (project_id, url, path?, name?)",
28529
+ create_scan_job: "Schedule page scans (project_id, schedule, page_id?)",
28378
28530
  log_push: "Push a log entry (level, message, project_id?, service?, trace_id?, metadata?)",
28379
- log_search: "Search logs (project_id?, page_id?, level?, since?, until?, text?, limit?)",
28380
- log_tail: "Get N most recent logs (project_id?, n?)",
28381
- log_summary: "Error/warn counts by service/page (project_id?, since?)",
28382
- log_context: "All logs for a trace_id",
28383
- perf_snapshot: "Latest performance snapshot for a project/page (project_id, page_id?)",
28384
- perf_trend: "Performance over time (project_id, page_id?, since?, limit?)",
28385
- scan_status: "Last scan runs per project (project_id?)",
28386
- list_projects: "List all registered projects",
28531
+ log_search: "Search logs (project_id?, level?, since?, text?, brief?=true, limit?)",
28532
+ log_tail: "Recent logs (project_id?, n?, brief?=true)",
28533
+ log_summary: "Error/warn counts by service (project_id?, since?)",
28534
+ log_context: "All logs for a trace_id (trace_id, brief?=true)",
28535
+ log_diagnose: "Full diagnosis: top errors, failing pages, perf regressions (project_id, since?)",
28536
+ log_compare: "Compare two time windows for new/resolved errors and perf delta",
28537
+ perf_snapshot: "Latest perf snapshot (project_id, page_id?)",
28538
+ perf_trend: "Perf over time (project_id, page_id?, since?, limit?)",
28539
+ scan_status: "Last scan jobs (project_id?)",
28540
+ list_projects: "List all projects",
28387
28541
  list_pages: "List pages for a project (project_id)",
28388
- search_tools: "Search available tools by keyword (query)",
28389
- describe_tools: "List all tools with descriptions"
28542
+ list_issues: "List grouped error issues (project_id?, status?, limit?)",
28543
+ resolve_issue: "Update issue status (id, status: open|resolved|ignored)",
28544
+ create_alert_rule: "Create alert rule (project_id, name, level, threshold_count, window_seconds, webhook_url?)",
28545
+ list_alert_rules: "List alert rules (project_id?)",
28546
+ delete_alert_rule: "Delete alert rule (id)",
28547
+ log_session_context: "Logs + session metadata for a session_id (requires SESSIONS_URL env)",
28548
+ get_health: "Server health + DB stats",
28549
+ search_tools: "Search tools by keyword (query)",
28550
+ describe_tools: "List all tools"
28390
28551
  };
28391
28552
  server.tool("search_tools", { query: exports_external.string() }, ({ query }) => {
28392
28553
  const q = query.toLowerCase();
@@ -28394,37 +28555,27 @@ server.tool("search_tools", { query: exports_external.string() }, ({ query }) =>
28394
28555
  return { content: [{ type: "text", text: matches.map(([k, v]) => `${k}: ${v}`).join(`
28395
28556
  `) || "No matches" }] };
28396
28557
  });
28397
- server.tool("describe_tools", {}, () => {
28398
- const text = Object.entries(TOOLS).map(([k, v]) => `${k}: ${v}`).join(`
28399
- `);
28400
- return { content: [{ type: "text", text }] };
28401
- });
28558
+ server.tool("describe_tools", {}, () => ({
28559
+ content: [{ type: "text", text: Object.entries(TOOLS).map(([k, v]) => `${k}: ${v}`).join(`
28560
+ `) }]
28561
+ }));
28402
28562
  server.tool("register_project", {
28403
28563
  name: exports_external.string(),
28404
28564
  github_repo: exports_external.string().optional(),
28405
28565
  base_url: exports_external.string().optional(),
28406
28566
  description: exports_external.string().optional()
28407
- }, (args) => {
28408
- const project = createProject(db, args);
28409
- return { content: [{ type: "text", text: JSON.stringify(project) }] };
28410
- });
28567
+ }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createProject(db, args)) }] }));
28411
28568
  server.tool("register_page", {
28412
28569
  project_id: exports_external.string(),
28413
28570
  url: exports_external.string(),
28414
28571
  path: exports_external.string().optional(),
28415
28572
  name: exports_external.string().optional()
28416
- }, (args) => {
28417
- const page = createPage(db, args);
28418
- return { content: [{ type: "text", text: JSON.stringify(page) }] };
28419
- });
28573
+ }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createPage(db, args)) }] }));
28420
28574
  server.tool("create_scan_job", {
28421
28575
  project_id: exports_external.string(),
28422
28576
  schedule: exports_external.string(),
28423
28577
  page_id: exports_external.string().optional()
28424
- }, (args) => {
28425
- const job = createJob(db, args);
28426
- return { content: [{ type: "text", text: JSON.stringify(job) }] };
28427
- });
28578
+ }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createJob(db, args)) }] }));
28428
28579
  server.tool("log_push", {
28429
28580
  level: exports_external.enum(["debug", "info", "warn", "error", "fatal"]),
28430
28581
  message: exports_external.string(),
@@ -28448,60 +28599,115 @@ server.tool("log_search", {
28448
28599
  until: exports_external.string().optional(),
28449
28600
  text: exports_external.string().optional(),
28450
28601
  trace_id: exports_external.string().optional(),
28451
- limit: exports_external.number().optional()
28602
+ limit: exports_external.number().optional(),
28603
+ brief: exports_external.boolean().optional()
28452
28604
  }, (args) => {
28453
- const rows = searchLogs(db, {
28454
- ...args,
28455
- level: args.level ? args.level.split(",") : undefined
28456
- });
28457
- return { content: [{ type: "text", text: JSON.stringify(rows) }] };
28605
+ const rows = searchLogs(db, { ...args, level: args.level ? args.level.split(",") : undefined });
28606
+ return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, args.brief !== false)) }] };
28458
28607
  });
28459
28608
  server.tool("log_tail", {
28460
28609
  project_id: exports_external.string().optional(),
28461
- n: exports_external.number().optional()
28462
- }, ({ project_id, n }) => {
28610
+ n: exports_external.number().optional(),
28611
+ brief: exports_external.boolean().optional()
28612
+ }, ({ project_id, n, brief }) => {
28463
28613
  const rows = tailLogs(db, project_id, n ?? 50);
28464
- return { content: [{ type: "text", text: JSON.stringify(rows) }] };
28614
+ return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] };
28465
28615
  });
28466
28616
  server.tool("log_summary", {
28467
28617
  project_id: exports_external.string().optional(),
28468
28618
  since: exports_external.string().optional()
28469
- }, ({ project_id, since }) => {
28470
- const summary = summarizeLogs(db, project_id, since);
28471
- return { content: [{ type: "text", text: JSON.stringify(summary) }] };
28472
- });
28473
- server.tool("log_context", { trace_id: exports_external.string() }, ({ trace_id }) => {
28619
+ }, ({ project_id, since }) => ({
28620
+ content: [{ type: "text", text: JSON.stringify(summarizeLogs(db, project_id, since)) }]
28621
+ }));
28622
+ server.tool("log_context", {
28623
+ trace_id: exports_external.string(),
28624
+ brief: exports_external.boolean().optional()
28625
+ }, ({ trace_id, brief }) => {
28474
28626
  const rows = getLogContext(db, trace_id);
28475
- return { content: [{ type: "text", text: JSON.stringify(rows) }] };
28627
+ return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] };
28476
28628
  });
28629
+ server.tool("log_diagnose", {
28630
+ project_id: exports_external.string(),
28631
+ since: exports_external.string().optional()
28632
+ }, ({ project_id, since }) => ({
28633
+ content: [{ type: "text", text: JSON.stringify(diagnose(db, project_id, since)) }]
28634
+ }));
28635
+ server.tool("log_compare", {
28636
+ project_id: exports_external.string(),
28637
+ a_since: exports_external.string(),
28638
+ a_until: exports_external.string(),
28639
+ b_since: exports_external.string(),
28640
+ b_until: exports_external.string()
28641
+ }, ({ project_id, a_since, a_until, b_since, b_until }) => ({
28642
+ content: [{ type: "text", text: JSON.stringify(compare(db, project_id, a_since, a_until, b_since, b_until)) }]
28643
+ }));
28477
28644
  server.tool("perf_snapshot", {
28478
28645
  project_id: exports_external.string(),
28479
28646
  page_id: exports_external.string().optional()
28480
28647
  }, ({ project_id, page_id }) => {
28481
28648
  const snap = getLatestSnapshot(db, project_id, page_id);
28482
- const label = snap ? scoreLabel(snap.score) : "unknown";
28483
- return { content: [{ type: "text", text: JSON.stringify({ ...snap, label }) }] };
28649
+ return { content: [{ type: "text", text: JSON.stringify(snap ? { ...snap, label: scoreLabel(snap.score) } : null) }] };
28484
28650
  });
28485
28651
  server.tool("perf_trend", {
28486
28652
  project_id: exports_external.string(),
28487
28653
  page_id: exports_external.string().optional(),
28488
28654
  since: exports_external.string().optional(),
28489
28655
  limit: exports_external.number().optional()
28490
- }, ({ project_id, page_id, since, limit }) => {
28491
- const trend = getPerfTrend(db, project_id, page_id, since, limit ?? 50);
28492
- return { content: [{ type: "text", text: JSON.stringify(trend) }] };
28493
- });
28656
+ }, ({ project_id, page_id, since, limit }) => ({
28657
+ content: [{ type: "text", text: JSON.stringify(getPerfTrend(db, project_id, page_id, since, limit ?? 50)) }]
28658
+ }));
28494
28659
  server.tool("scan_status", {
28495
28660
  project_id: exports_external.string().optional()
28496
- }, ({ project_id }) => {
28497
- const jobs = listJobs(db, project_id);
28498
- return { content: [{ type: "text", text: JSON.stringify(jobs) }] };
28499
- });
28500
- server.tool("list_projects", {}, () => {
28501
- return { content: [{ type: "text", text: JSON.stringify(listProjects(db)) }] };
28502
- });
28503
- server.tool("list_pages", { project_id: exports_external.string() }, ({ project_id }) => {
28504
- return { content: [{ type: "text", text: JSON.stringify(listPages(db, project_id)) }] };
28505
- });
28661
+ }, ({ project_id }) => ({
28662
+ content: [{ type: "text", text: JSON.stringify(listJobs(db, project_id)) }]
28663
+ }));
28664
+ server.tool("list_projects", {}, () => ({
28665
+ content: [{ type: "text", text: JSON.stringify(listProjects(db)) }]
28666
+ }));
28667
+ server.tool("list_pages", { project_id: exports_external.string() }, ({ project_id }) => ({
28668
+ content: [{ type: "text", text: JSON.stringify(listPages(db, project_id)) }]
28669
+ }));
28670
+ server.tool("list_issues", {
28671
+ project_id: exports_external.string().optional(),
28672
+ status: exports_external.string().optional(),
28673
+ limit: exports_external.number().optional()
28674
+ }, ({ project_id, status, limit }) => ({
28675
+ content: [{ type: "text", text: JSON.stringify(listIssues(db, project_id, status, limit ?? 50)) }]
28676
+ }));
28677
+ server.tool("resolve_issue", {
28678
+ id: exports_external.string(),
28679
+ status: exports_external.enum(["open", "resolved", "ignored"])
28680
+ }, ({ id, status }) => ({
28681
+ content: [{ type: "text", text: JSON.stringify(updateIssueStatus(db, id, status)) }]
28682
+ }));
28683
+ server.tool("create_alert_rule", {
28684
+ project_id: exports_external.string(),
28685
+ name: exports_external.string(),
28686
+ level: exports_external.string().optional(),
28687
+ service: exports_external.string().optional(),
28688
+ threshold_count: exports_external.number().optional(),
28689
+ window_seconds: exports_external.number().optional(),
28690
+ action: exports_external.enum(["webhook", "log"]).optional(),
28691
+ webhook_url: exports_external.string().optional()
28692
+ }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createAlertRule(db, args)) }] }));
28693
+ server.tool("list_alert_rules", {
28694
+ project_id: exports_external.string().optional()
28695
+ }, ({ project_id }) => ({
28696
+ content: [{ type: "text", text: JSON.stringify(listAlertRules(db, project_id)) }]
28697
+ }));
28698
+ server.tool("delete_alert_rule", { id: exports_external.string() }, ({ id }) => {
28699
+ deleteAlertRule(db, id);
28700
+ return { content: [{ type: "text", text: "deleted" }] };
28701
+ });
28702
+ server.tool("log_session_context", {
28703
+ session_id: exports_external.string(),
28704
+ brief: exports_external.boolean().optional()
28705
+ }, async ({ session_id, brief }) => {
28706
+ const ctx = await getSessionContext(db, session_id);
28707
+ return { content: [{ type: "text", text: JSON.stringify({ ...ctx, logs: applyBrief(ctx.logs, brief !== false) }) }] };
28708
+ });
28709
+ server.tool("get_health", {}, () => ({
28710
+ content: [{ type: "text", text: JSON.stringify(getHealth(db)) }]
28711
+ }));
28506
28712
  var transport = new StdioServerTransport;
28507
28713
  await server.connect(transport);