@hasna/logs 0.1.0 → 0.3.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 (45) 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 +80 -9
  27. package/dist/mcp/index.js +217 -96
  28. package/dist/server/index.js +307 -7
  29. package/package.json +3 -1
  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 +70 -4
  34. package/src/lib/count.test.ts +44 -0
  35. package/src/lib/count.ts +45 -0
  36. package/src/lib/diagnose.ts +26 -11
  37. package/src/lib/parse-time.test.ts +37 -0
  38. package/src/lib/parse-time.ts +14 -0
  39. package/src/lib/projects.ts +10 -0
  40. package/src/lib/query.ts +10 -2
  41. package/src/lib/session-context.ts +28 -0
  42. package/src/lib/summarize.ts +2 -1
  43. package/src/mcp/index.ts +138 -59
  44. package/src/server/index.ts +4 -1
  45. package/src/server/routes/logs.ts +28 -1
package/dist/mcp/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ import {
4
+ countLogs
5
+ } from "../index-n8qd55mt.js";
3
6
  import {
4
7
  createAlertRule,
5
8
  createPage,
@@ -7,23 +10,29 @@ import {
7
10
  deleteAlertRule,
8
11
  getDb,
9
12
  getLatestSnapshot,
10
- getLogContext,
11
13
  getPerfTrend,
14
+ ingestBatch,
12
15
  ingestLog,
13
16
  listAlertRules,
14
17
  listIssues,
15
18
  listPages,
16
19
  listProjects,
20
+ resolveProjectId,
17
21
  scoreLabel,
18
- searchLogs,
19
22
  summarizeLogs,
20
- tailLogs,
21
23
  updateIssueStatus
22
- } from "../index-qmsvtxax.js";
24
+ } from "../index-34xx795x.js";
23
25
  import {
24
26
  createJob,
25
27
  listJobs
26
28
  } from "../jobs-124e878j.js";
29
+ import {
30
+ getLogContext,
31
+ getLogContextFromId,
32
+ parseTime,
33
+ searchLogs,
34
+ tailLogs
35
+ } from "../query-d5b0chp4.js";
27
36
  import {
28
37
  getHealth
29
38
  } from "../health-f2qrebqc.js";
@@ -10332,7 +10341,7 @@ __export(exports_core2, {
10332
10341
  safeDecode: () => safeDecode,
10333
10342
  registry: () => registry,
10334
10343
  regexes: () => exports_regexes,
10335
- process: () => process,
10344
+ process: () => process2,
10336
10345
  prettifyError: () => prettifyError,
10337
10346
  parseAsync: () => parseAsync,
10338
10347
  parse: () => parse,
@@ -20796,7 +20805,7 @@ function initializeContext(params) {
20796
20805
  external: params?.external ?? undefined
20797
20806
  };
20798
20807
  }
20799
- function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
20808
+ function process2(schema, ctx, _params = { path: [], schemaPath: [] }) {
20800
20809
  var _a2;
20801
20810
  const def = schema._zod.def;
20802
20811
  const seen = ctx.seen.get(schema);
@@ -20833,7 +20842,7 @@ function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
20833
20842
  if (parent) {
20834
20843
  if (!result.ref)
20835
20844
  result.ref = parent;
20836
- process(parent, ctx, params);
20845
+ process2(parent, ctx, params);
20837
20846
  ctx.seen.get(parent).isParent = true;
20838
20847
  }
20839
20848
  }
@@ -21109,14 +21118,14 @@ function isTransforming(_schema, _ctx) {
21109
21118
  }
21110
21119
  var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
21111
21120
  const ctx = initializeContext({ ...params, processors });
21112
- process(schema, ctx);
21121
+ process2(schema, ctx);
21113
21122
  extractDefs(ctx, schema);
21114
21123
  return finalize(ctx, schema);
21115
21124
  };
21116
21125
  var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
21117
21126
  const { libraryOptions, target } = params ?? {};
21118
21127
  const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
21119
- process(schema, ctx);
21128
+ process2(schema, ctx);
21120
21129
  extractDefs(ctx, schema);
21121
21130
  return finalize(ctx, schema);
21122
21131
  };
@@ -21367,7 +21376,7 @@ var arrayProcessor = (schema, ctx, _json, params) => {
21367
21376
  if (typeof maximum === "number")
21368
21377
  json.maxItems = maximum;
21369
21378
  json.type = "array";
21370
- json.items = process(def.element, ctx, { ...params, path: [...params.path, "items"] });
21379
+ json.items = process2(def.element, ctx, { ...params, path: [...params.path, "items"] });
21371
21380
  };
21372
21381
  var objectProcessor = (schema, ctx, _json, params) => {
21373
21382
  const json = _json;
@@ -21376,7 +21385,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
21376
21385
  json.properties = {};
21377
21386
  const shape = def.shape;
21378
21387
  for (const key in shape) {
21379
- json.properties[key] = process(shape[key], ctx, {
21388
+ json.properties[key] = process2(shape[key], ctx, {
21380
21389
  ...params,
21381
21390
  path: [...params.path, "properties", key]
21382
21391
  });
@@ -21399,7 +21408,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
21399
21408
  if (ctx.io === "output")
21400
21409
  json.additionalProperties = false;
21401
21410
  } else if (def.catchall) {
21402
- json.additionalProperties = process(def.catchall, ctx, {
21411
+ json.additionalProperties = process2(def.catchall, ctx, {
21403
21412
  ...params,
21404
21413
  path: [...params.path, "additionalProperties"]
21405
21414
  });
@@ -21408,7 +21417,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
21408
21417
  var unionProcessor = (schema, ctx, json, params) => {
21409
21418
  const def = schema._zod.def;
21410
21419
  const isExclusive = def.inclusive === false;
21411
- const options = def.options.map((x, i) => process(x, ctx, {
21420
+ const options = def.options.map((x, i) => process2(x, ctx, {
21412
21421
  ...params,
21413
21422
  path: [...params.path, isExclusive ? "oneOf" : "anyOf", i]
21414
21423
  }));
@@ -21420,11 +21429,11 @@ var unionProcessor = (schema, ctx, json, params) => {
21420
21429
  };
21421
21430
  var intersectionProcessor = (schema, ctx, json, params) => {
21422
21431
  const def = schema._zod.def;
21423
- const a = process(def.left, ctx, {
21432
+ const a = process2(def.left, ctx, {
21424
21433
  ...params,
21425
21434
  path: [...params.path, "allOf", 0]
21426
21435
  });
21427
- const b = process(def.right, ctx, {
21436
+ const b = process2(def.right, ctx, {
21428
21437
  ...params,
21429
21438
  path: [...params.path, "allOf", 1]
21430
21439
  });
@@ -21441,11 +21450,11 @@ var tupleProcessor = (schema, ctx, _json, params) => {
21441
21450
  json.type = "array";
21442
21451
  const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
21443
21452
  const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
21444
- const prefixItems = def.items.map((x, i) => process(x, ctx, {
21453
+ const prefixItems = def.items.map((x, i) => process2(x, ctx, {
21445
21454
  ...params,
21446
21455
  path: [...params.path, prefixPath, i]
21447
21456
  }));
21448
- const rest = def.rest ? process(def.rest, ctx, {
21457
+ const rest = def.rest ? process2(def.rest, ctx, {
21449
21458
  ...params,
21450
21459
  path: [...params.path, restPath, ...ctx.target === "openapi-3.0" ? [def.items.length] : []]
21451
21460
  }) : null;
@@ -21485,7 +21494,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
21485
21494
  const keyBag = keyType._zod.bag;
21486
21495
  const patterns = keyBag?.patterns;
21487
21496
  if (def.mode === "loose" && patterns && patterns.size > 0) {
21488
- const valueSchema = process(def.valueType, ctx, {
21497
+ const valueSchema = process2(def.valueType, ctx, {
21489
21498
  ...params,
21490
21499
  path: [...params.path, "patternProperties", "*"]
21491
21500
  });
@@ -21495,12 +21504,12 @@ var recordProcessor = (schema, ctx, _json, params) => {
21495
21504
  }
21496
21505
  } else {
21497
21506
  if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
21498
- json.propertyNames = process(def.keyType, ctx, {
21507
+ json.propertyNames = process2(def.keyType, ctx, {
21499
21508
  ...params,
21500
21509
  path: [...params.path, "propertyNames"]
21501
21510
  });
21502
21511
  }
21503
- json.additionalProperties = process(def.valueType, ctx, {
21512
+ json.additionalProperties = process2(def.valueType, ctx, {
21504
21513
  ...params,
21505
21514
  path: [...params.path, "additionalProperties"]
21506
21515
  });
@@ -21515,7 +21524,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
21515
21524
  };
21516
21525
  var nullableProcessor = (schema, ctx, json, params) => {
21517
21526
  const def = schema._zod.def;
21518
- const inner = process(def.innerType, ctx, params);
21527
+ const inner = process2(def.innerType, ctx, params);
21519
21528
  const seen = ctx.seen.get(schema);
21520
21529
  if (ctx.target === "openapi-3.0") {
21521
21530
  seen.ref = def.innerType;
@@ -21526,20 +21535,20 @@ var nullableProcessor = (schema, ctx, json, params) => {
21526
21535
  };
21527
21536
  var nonoptionalProcessor = (schema, ctx, _json, params) => {
21528
21537
  const def = schema._zod.def;
21529
- process(def.innerType, ctx, params);
21538
+ process2(def.innerType, ctx, params);
21530
21539
  const seen = ctx.seen.get(schema);
21531
21540
  seen.ref = def.innerType;
21532
21541
  };
21533
21542
  var defaultProcessor = (schema, ctx, json, params) => {
21534
21543
  const def = schema._zod.def;
21535
- process(def.innerType, ctx, params);
21544
+ process2(def.innerType, ctx, params);
21536
21545
  const seen = ctx.seen.get(schema);
21537
21546
  seen.ref = def.innerType;
21538
21547
  json.default = JSON.parse(JSON.stringify(def.defaultValue));
21539
21548
  };
21540
21549
  var prefaultProcessor = (schema, ctx, json, params) => {
21541
21550
  const def = schema._zod.def;
21542
- process(def.innerType, ctx, params);
21551
+ process2(def.innerType, ctx, params);
21543
21552
  const seen = ctx.seen.get(schema);
21544
21553
  seen.ref = def.innerType;
21545
21554
  if (ctx.io === "input")
@@ -21547,7 +21556,7 @@ var prefaultProcessor = (schema, ctx, json, params) => {
21547
21556
  };
21548
21557
  var catchProcessor = (schema, ctx, json, params) => {
21549
21558
  const def = schema._zod.def;
21550
- process(def.innerType, ctx, params);
21559
+ process2(def.innerType, ctx, params);
21551
21560
  const seen = ctx.seen.get(schema);
21552
21561
  seen.ref = def.innerType;
21553
21562
  let catchValue;
@@ -21561,32 +21570,32 @@ var catchProcessor = (schema, ctx, json, params) => {
21561
21570
  var pipeProcessor = (schema, ctx, _json, params) => {
21562
21571
  const def = schema._zod.def;
21563
21572
  const innerType = ctx.io === "input" ? def.in._zod.def.type === "transform" ? def.out : def.in : def.out;
21564
- process(innerType, ctx, params);
21573
+ process2(innerType, ctx, params);
21565
21574
  const seen = ctx.seen.get(schema);
21566
21575
  seen.ref = innerType;
21567
21576
  };
21568
21577
  var readonlyProcessor = (schema, ctx, json, params) => {
21569
21578
  const def = schema._zod.def;
21570
- process(def.innerType, ctx, params);
21579
+ process2(def.innerType, ctx, params);
21571
21580
  const seen = ctx.seen.get(schema);
21572
21581
  seen.ref = def.innerType;
21573
21582
  json.readOnly = true;
21574
21583
  };
21575
21584
  var promiseProcessor = (schema, ctx, _json, params) => {
21576
21585
  const def = schema._zod.def;
21577
- process(def.innerType, ctx, params);
21586
+ process2(def.innerType, ctx, params);
21578
21587
  const seen = ctx.seen.get(schema);
21579
21588
  seen.ref = def.innerType;
21580
21589
  };
21581
21590
  var optionalProcessor = (schema, ctx, _json, params) => {
21582
21591
  const def = schema._zod.def;
21583
- process(def.innerType, ctx, params);
21592
+ process2(def.innerType, ctx, params);
21584
21593
  const seen = ctx.seen.get(schema);
21585
21594
  seen.ref = def.innerType;
21586
21595
  };
21587
21596
  var lazyProcessor = (schema, ctx, _json, params) => {
21588
21597
  const innerType = schema._zod.innerType;
21589
- process(innerType, ctx, params);
21598
+ process2(innerType, ctx, params);
21590
21599
  const seen = ctx.seen.get(schema);
21591
21600
  seen.ref = innerType;
21592
21601
  };
@@ -21638,7 +21647,7 @@ function toJSONSchema(input, params) {
21638
21647
  const defs = {};
21639
21648
  for (const entry of registry2._idmap.entries()) {
21640
21649
  const [_, schema] = entry;
21641
- process(schema, ctx2);
21650
+ process2(schema, ctx2);
21642
21651
  }
21643
21652
  const schemas = {};
21644
21653
  const external = {
@@ -21661,7 +21670,7 @@ function toJSONSchema(input, params) {
21661
21670
  return { schemas };
21662
21671
  }
21663
21672
  const ctx = initializeContext({ ...params, processors: allProcessors });
21664
- process(input, ctx);
21673
+ process2(input, ctx);
21665
21674
  extractDefs(ctx, input);
21666
21675
  return finalize(ctx, input);
21667
21676
  }
@@ -21707,7 +21716,7 @@ class JSONSchemaGenerator {
21707
21716
  });
21708
21717
  }
21709
21718
  process(schema, _params = { path: [], schemaPath: [] }) {
21710
- return process(schema, this.ctx, _params);
21719
+ return process2(schema, this.ctx, _params);
21711
21720
  }
21712
21721
  emit(schema, _params) {
21713
21722
  if (_params) {
@@ -28288,7 +28297,7 @@ var EMPTY_COMPLETION_RESULT = {
28288
28297
  };
28289
28298
 
28290
28299
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
28291
- import process2 from "process";
28300
+ import process3 from "process";
28292
28301
 
28293
28302
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
28294
28303
  class ReadBuffer {
@@ -28322,7 +28331,7 @@ function serializeMessage(message) {
28322
28331
 
28323
28332
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
28324
28333
  class StdioServerTransport {
28325
- constructor(_stdin = process2.stdin, _stdout = process2.stdout) {
28334
+ constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
28326
28335
  this._stdin = _stdin;
28327
28336
  this._stdout = _stdout;
28328
28337
  this._readBuffer = new ReadBuffer;
@@ -28379,17 +28388,19 @@ class StdioServerTransport {
28379
28388
  }
28380
28389
 
28381
28390
  // src/lib/diagnose.ts
28382
- function diagnose(db, projectId, since) {
28383
- const window = since ?? new Date(Date.now() - 24 * 3600 * 1000).toISOString();
28384
- const top_errors = db.prepare(`
28391
+ function diagnose(db, projectId, since, include) {
28392
+ const window = parseTime(since) ?? since ?? new Date(Date.now() - 24 * 3600 * 1000).toISOString();
28393
+ const all = !include || include.length === 0;
28394
+ const want = (k) => all || include.includes(k);
28395
+ const top_errors = want("top_errors") ? db.prepare(`
28385
28396
  SELECT message, COUNT(*) as count, service, MAX(timestamp) as last_seen
28386
28397
  FROM logs
28387
28398
  WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since
28388
28399
  GROUP BY message, service
28389
28400
  ORDER BY count DESC
28390
28401
  LIMIT 10
28391
- `).all({ $p: projectId, $since: window });
28392
- const error_rate_by_service = db.prepare(`
28402
+ `).all({ $p: projectId, $since: window }) : [];
28403
+ const error_rate_by_service = want("error_rate") ? db.prepare(`
28393
28404
  SELECT service,
28394
28405
  SUM(CASE WHEN level IN ('error','fatal') THEN 1 ELSE 0 END) as errors,
28395
28406
  SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) as warns,
@@ -28398,8 +28409,8 @@ function diagnose(db, projectId, since) {
28398
28409
  WHERE project_id = $p AND timestamp >= $since
28399
28410
  GROUP BY service
28400
28411
  ORDER BY errors DESC
28401
- `).all({ $p: projectId, $since: window });
28402
- const failing_pages = db.prepare(`
28412
+ `).all({ $p: projectId, $since: window }) : [];
28413
+ const failing_pages = want("failing_pages") ? db.prepare(`
28403
28414
  SELECT l.page_id, p.url, COUNT(*) as error_count
28404
28415
  FROM logs l
28405
28416
  JOIN pages p ON p.id = l.page_id
@@ -28407,8 +28418,8 @@ function diagnose(db, projectId, since) {
28407
28418
  GROUP BY l.page_id, p.url
28408
28419
  ORDER BY error_count DESC
28409
28420
  LIMIT 10
28410
- `).all({ $p: projectId, $since: window });
28411
- const perf_regressions = db.prepare(`
28421
+ `).all({ $p: projectId, $since: window }) : [];
28422
+ const perf_regressions = want("perf") ? db.prepare(`
28412
28423
  SELECT * FROM (
28413
28424
  SELECT
28414
28425
  cur.page_id,
@@ -28425,11 +28436,25 @@ function diagnose(db, projectId, since) {
28425
28436
  ) WHERE delta < -5 OR delta IS NULL
28426
28437
  ORDER BY delta ASC
28427
28438
  LIMIT 10
28428
- `).all({ $p: projectId });
28439
+ `).all({ $p: projectId }) : [];
28429
28440
  const totalErrors = top_errors.reduce((s, e) => s + e.count, 0);
28441
+ const totalWarns = error_rate_by_service.reduce((s, r) => s + r.warns, 0);
28430
28442
  const topService = error_rate_by_service[0];
28443
+ const score = totalErrors === 0 ? "green" : totalErrors <= 10 ? "yellow" : "red";
28431
28444
  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).`;
28432
- return { project_id: projectId, window, top_errors, error_rate_by_service, failing_pages, perf_regressions, summary };
28445
+ return {
28446
+ project_id: projectId,
28447
+ window,
28448
+ score,
28449
+ error_count: totalErrors,
28450
+ warn_count: totalWarns,
28451
+ has_perf_regression: perf_regressions.length > 0,
28452
+ top_errors,
28453
+ error_rate_by_service,
28454
+ failing_pages,
28455
+ perf_regressions,
28456
+ summary
28457
+ };
28433
28458
  }
28434
28459
 
28435
28460
  // src/lib/compare.ts
@@ -28495,49 +28520,91 @@ function compare(db, projectId, aSince, aUntil, bSince, bUntil) {
28495
28520
  };
28496
28521
  }
28497
28522
 
28523
+ // src/lib/session-context.ts
28524
+ async function getSessionContext(db, sessionId) {
28525
+ const logs = db.prepare("SELECT * FROM logs WHERE session_id = $s ORDER BY timestamp ASC").all({ $s: sessionId });
28526
+ const sessionsUrl = process.env.SESSIONS_URL;
28527
+ if (!sessionsUrl) {
28528
+ return { session_id: sessionId, logs };
28529
+ }
28530
+ try {
28531
+ const res = await fetch(`${sessionsUrl.replace(/\/$/, "")}/api/sessions/${sessionId}`);
28532
+ if (!res.ok)
28533
+ return { session_id: sessionId, logs };
28534
+ const session = await res.json();
28535
+ return { session_id: sessionId, logs, session };
28536
+ } catch (err) {
28537
+ return { session_id: sessionId, logs, error: String(err) };
28538
+ }
28539
+ }
28540
+
28498
28541
  // src/mcp/index.ts
28499
28542
  var db = getDb();
28500
- var server = new McpServer({ name: "logs", version: "0.1.0" });
28543
+ var server = new McpServer({ name: "logs", version: "0.3.0" });
28501
28544
  function applyBrief(rows, brief = true) {
28502
28545
  if (!brief)
28503
28546
  return rows;
28504
- return rows.map((r) => ({ id: r.id, timestamp: r.timestamp, level: r.level, message: r.message, service: r.service }));
28547
+ return rows.map((r) => ({
28548
+ id: r.id,
28549
+ timestamp: r.timestamp,
28550
+ level: r.level,
28551
+ message: r.message,
28552
+ service: r.service,
28553
+ age_seconds: Math.floor((Date.now() - new Date(r.timestamp).getTime()) / 1000)
28554
+ }));
28555
+ }
28556
+ function rp(idOrName) {
28557
+ if (!idOrName)
28558
+ return;
28559
+ return resolveProjectId(db, idOrName) ?? idOrName;
28505
28560
  }
28506
28561
  var TOOLS = {
28507
- register_project: "Register a project (name, github_repo?, base_url?, description?)",
28508
- register_page: "Register a page URL (project_id, url, path?, name?)",
28509
- create_scan_job: "Schedule page scans (project_id, schedule, page_id?)",
28510
- log_push: "Push a log entry (level, message, project_id?, service?, trace_id?, metadata?)",
28511
- log_search: "Search logs (project_id?, level?, since?, text?, brief?=true, limit?)",
28512
- log_tail: "Recent logs (project_id?, n?, brief?=true)",
28513
- log_summary: "Error/warn counts by service (project_id?, since?)",
28514
- log_context: "All logs for a trace_id (trace_id, brief?=true)",
28515
- log_diagnose: "Full diagnosis: top errors, failing pages, perf regressions (project_id, since?)",
28516
- log_compare: "Compare two time windows for new/resolved errors and perf delta",
28517
- perf_snapshot: "Latest perf snapshot (project_id, page_id?)",
28518
- perf_trend: "Perf over time (project_id, page_id?, since?, limit?)",
28519
- scan_status: "Last scan jobs (project_id?)",
28520
- list_projects: "List all projects",
28521
- list_pages: "List pages for a project (project_id)",
28522
- list_issues: "List grouped error issues (project_id?, status?, limit?)",
28523
- resolve_issue: "Update issue status (id, status: open|resolved|ignored)",
28524
- create_alert_rule: "Create alert rule (project_id, name, level, threshold_count, window_seconds, webhook_url?)",
28525
- list_alert_rules: "List alert rules (project_id?)",
28526
- delete_alert_rule: "Delete alert rule (id)",
28527
- get_health: "Server health + DB stats",
28528
- search_tools: "Search tools by keyword (query)",
28529
- describe_tools: "List all tools"
28562
+ register_project: { desc: "Register a project", params: "(name, github_repo?, base_url?, description?)" },
28563
+ register_page: { desc: "Register a page URL to a project", params: "(project_id, url, path?, name?)" },
28564
+ create_scan_job: { desc: "Schedule headless page scans", params: "(project_id, schedule, page_id?)" },
28565
+ resolve_project: { desc: "Resolve project name to ID", params: "(name)" },
28566
+ log_push: { desc: "Push a single log entry", params: "(level, message, project_id?, service?, trace_id?, metadata?)" },
28567
+ log_push_batch: { desc: "Push multiple log entries in one call", params: "(entries: Array<{level, message, project_id?, service?, trace_id?}>)" },
28568
+ log_search: { desc: "Search logs", params: "(project_id?, level?, since?, until?, text?, service?, limit?=100, brief?=true)" },
28569
+ log_tail: { desc: "Get N most recent logs", params: "(project_id?, n?=50, brief?=true)" },
28570
+ log_count: { desc: "Count logs \u2014 zero token cost, pure signal", params: "(project_id?, service?, level?, since?, until?)" },
28571
+ log_recent_errors: { desc: "Shortcut: recent errors + fatals", params: "(project_id?, since?='1h', limit?=20)" },
28572
+ log_summary: { desc: "Error/warn counts by service", params: "(project_id?, since?)" },
28573
+ log_context: { desc: "All logs for a trace_id", params: "(trace_id, brief?=true)" },
28574
+ log_context_from_id: { desc: "Trace context from a log ID (no trace_id needed)", params: "(log_id, brief?=true)" },
28575
+ log_diagnose: { desc: "Full diagnosis: score, top errors, failing pages, perf regressions", params: "(project_id, since?='24h', include?=['top_errors','error_rate','failing_pages','perf'])" },
28576
+ log_compare: { desc: "Diff two time windows for new/resolved errors", params: "(project_id, a_since, a_until, b_since, b_until)" },
28577
+ log_session_context: { desc: "Logs + session metadata for a session_id", params: "(session_id, brief?=true)" },
28578
+ perf_snapshot: { desc: "Latest performance snapshot", params: "(project_id, page_id?)" },
28579
+ perf_trend: { desc: "Performance over time", params: "(project_id, page_id?, since?, limit?=50)" },
28580
+ scan_status: { desc: "Last scan jobs", params: "(project_id?)" },
28581
+ list_projects: { desc: "List all projects", params: "()" },
28582
+ list_pages: { desc: "List pages for a project", params: "(project_id)" },
28583
+ list_issues: { desc: "List grouped error issues", params: "(project_id?, status?, limit?=50)" },
28584
+ resolve_issue: { desc: "Update issue status", params: "(id, status: open|resolved|ignored)" },
28585
+ create_alert_rule: { desc: "Create alert rule", params: "(project_id, name, level?, threshold_count?, window_seconds?, webhook_url?)" },
28586
+ list_alert_rules: { desc: "List alert rules", params: "(project_id?)" },
28587
+ delete_alert_rule: { desc: "Delete alert rule", params: "(id)" },
28588
+ get_health: { desc: "Server health + DB stats", params: "()" },
28589
+ search_tools: { desc: "Search tools by keyword \u2014 returns names, descriptions, param signatures", params: "(query)" },
28590
+ describe_tools: { desc: "List all tools with descriptions and param signatures", params: "()" }
28530
28591
  };
28531
28592
  server.tool("search_tools", { query: exports_external.string() }, ({ query }) => {
28532
28593
  const q = query.toLowerCase();
28533
- const matches = Object.entries(TOOLS).filter(([k, v]) => k.includes(q) || v.toLowerCase().includes(q));
28534
- return { content: [{ type: "text", text: matches.map(([k, v]) => `${k}: ${v}`).join(`
28535
- `) || "No matches" }] };
28594
+ const matches = Object.entries(TOOLS).filter(([k, v]) => k.includes(q) || v.desc.toLowerCase().includes(q));
28595
+ const text = matches.map(([k, v]) => `${k}${v.params} \u2014 ${v.desc}`).join(`
28596
+ `) || "No matches";
28597
+ return { content: [{ type: "text", text }] };
28536
28598
  });
28537
28599
  server.tool("describe_tools", {}, () => ({
28538
- content: [{ type: "text", text: Object.entries(TOOLS).map(([k, v]) => `${k}: ${v}`).join(`
28600
+ content: [{ type: "text", text: Object.entries(TOOLS).map(([k, v]) => `${k}${v.params} \u2014 ${v.desc}`).join(`
28539
28601
  `) }]
28540
28602
  }));
28603
+ server.tool("resolve_project", { name: exports_external.string() }, ({ name }) => {
28604
+ const id = resolveProjectId(db, name);
28605
+ const project = id ? db.prepare("SELECT * FROM projects WHERE id = $id").get({ $id: id }) : null;
28606
+ return { content: [{ type: "text", text: JSON.stringify(project ?? { error: `Project '${name}' not found` }) }] };
28607
+ });
28541
28608
  server.tool("register_project", {
28542
28609
  name: exports_external.string(),
28543
28610
  github_repo: exports_external.string().optional(),
@@ -28549,12 +28616,12 @@ server.tool("register_page", {
28549
28616
  url: exports_external.string(),
28550
28617
  path: exports_external.string().optional(),
28551
28618
  name: exports_external.string().optional()
28552
- }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createPage(db, args)) }] }));
28619
+ }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createPage(db, { ...args, project_id: rp(args.project_id) ?? args.project_id })) }] }));
28553
28620
  server.tool("create_scan_job", {
28554
28621
  project_id: exports_external.string(),
28555
28622
  schedule: exports_external.string(),
28556
28623
  page_id: exports_external.string().optional()
28557
- }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createJob(db, args)) }] }));
28624
+ }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createJob(db, { ...args, project_id: rp(args.project_id) ?? args.project_id })) }] }));
28558
28625
  server.tool("log_push", {
28559
28626
  level: exports_external.enum(["debug", "info", "warn", "error", "fatal"]),
28560
28627
  message: exports_external.string(),
@@ -28566,9 +28633,22 @@ server.tool("log_push", {
28566
28633
  url: exports_external.string().optional(),
28567
28634
  metadata: exports_external.record(exports_external.unknown()).optional()
28568
28635
  }, (args) => {
28569
- const row = ingestLog(db, args);
28636
+ const row = ingestLog(db, { ...args, project_id: rp(args.project_id) });
28570
28637
  return { content: [{ type: "text", text: `Logged: ${row.id}` }] };
28571
28638
  });
28639
+ server.tool("log_push_batch", {
28640
+ entries: exports_external.array(exports_external.object({
28641
+ level: exports_external.enum(["debug", "info", "warn", "error", "fatal"]),
28642
+ message: exports_external.string(),
28643
+ project_id: exports_external.string().optional(),
28644
+ service: exports_external.string().optional(),
28645
+ trace_id: exports_external.string().optional(),
28646
+ metadata: exports_external.record(exports_external.unknown()).optional()
28647
+ }))
28648
+ }, ({ entries }) => {
28649
+ const rows = ingestBatch(db, entries.map((e) => ({ ...e, project_id: rp(e.project_id) })));
28650
+ return { content: [{ type: "text", text: `Logged ${rows.length} entries` }] };
28651
+ });
28572
28652
  server.tool("log_search", {
28573
28653
  project_id: exports_external.string().optional(),
28574
28654
  page_id: exports_external.string().optional(),
@@ -28581,7 +28661,13 @@ server.tool("log_search", {
28581
28661
  limit: exports_external.number().optional(),
28582
28662
  brief: exports_external.boolean().optional()
28583
28663
  }, (args) => {
28584
- const rows = searchLogs(db, { ...args, level: args.level ? args.level.split(",") : undefined });
28664
+ const rows = searchLogs(db, {
28665
+ ...args,
28666
+ project_id: rp(args.project_id),
28667
+ level: args.level ? args.level.split(",") : undefined,
28668
+ since: parseTime(args.since) ?? args.since,
28669
+ until: parseTime(args.until) ?? args.until
28670
+ });
28585
28671
  return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, args.brief !== false)) }] };
28586
28672
  });
28587
28673
  server.tool("log_tail", {
@@ -28589,27 +28675,55 @@ server.tool("log_tail", {
28589
28675
  n: exports_external.number().optional(),
28590
28676
  brief: exports_external.boolean().optional()
28591
28677
  }, ({ project_id, n, brief }) => {
28592
- const rows = tailLogs(db, project_id, n ?? 50);
28678
+ const rows = tailLogs(db, rp(project_id), n ?? 50);
28593
28679
  return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] };
28594
28680
  });
28681
+ server.tool("log_count", {
28682
+ project_id: exports_external.string().optional(),
28683
+ service: exports_external.string().optional(),
28684
+ level: exports_external.string().optional(),
28685
+ since: exports_external.string().optional(),
28686
+ until: exports_external.string().optional()
28687
+ }, (args) => ({
28688
+ content: [{ type: "text", text: JSON.stringify(countLogs(db, { ...args, project_id: rp(args.project_id) })) }]
28689
+ }));
28690
+ server.tool("log_recent_errors", {
28691
+ project_id: exports_external.string().optional(),
28692
+ since: exports_external.string().optional(),
28693
+ limit: exports_external.number().optional()
28694
+ }, ({ project_id, since, limit }) => {
28695
+ const rows = searchLogs(db, {
28696
+ project_id: rp(project_id),
28697
+ level: ["error", "fatal"],
28698
+ since: parseTime(since ?? "1h"),
28699
+ limit: limit ?? 20
28700
+ });
28701
+ return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, true)) }] };
28702
+ });
28595
28703
  server.tool("log_summary", {
28596
28704
  project_id: exports_external.string().optional(),
28597
28705
  since: exports_external.string().optional()
28598
28706
  }, ({ project_id, since }) => ({
28599
- content: [{ type: "text", text: JSON.stringify(summarizeLogs(db, project_id, since)) }]
28707
+ content: [{ type: "text", text: JSON.stringify(summarizeLogs(db, rp(project_id), parseTime(since) ?? since)) }]
28600
28708
  }));
28601
28709
  server.tool("log_context", {
28602
28710
  trace_id: exports_external.string(),
28603
28711
  brief: exports_external.boolean().optional()
28604
- }, ({ trace_id, brief }) => {
28605
- const rows = getLogContext(db, trace_id);
28606
- return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] };
28607
- });
28712
+ }, ({ trace_id, brief }) => ({
28713
+ content: [{ type: "text", text: JSON.stringify(applyBrief(getLogContext(db, trace_id), brief !== false)) }]
28714
+ }));
28715
+ server.tool("log_context_from_id", {
28716
+ log_id: exports_external.string(),
28717
+ brief: exports_external.boolean().optional()
28718
+ }, ({ log_id, brief }) => ({
28719
+ content: [{ type: "text", text: JSON.stringify(applyBrief(getLogContextFromId(db, log_id), brief !== false)) }]
28720
+ }));
28608
28721
  server.tool("log_diagnose", {
28609
28722
  project_id: exports_external.string(),
28610
- since: exports_external.string().optional()
28611
- }, ({ project_id, since }) => ({
28612
- content: [{ type: "text", text: JSON.stringify(diagnose(db, project_id, since)) }]
28723
+ since: exports_external.string().optional(),
28724
+ include: exports_external.array(exports_external.enum(["top_errors", "error_rate", "failing_pages", "perf"])).optional()
28725
+ }, ({ project_id, since, include }) => ({
28726
+ content: [{ type: "text", text: JSON.stringify(diagnose(db, rp(project_id) ?? project_id, since, include)) }]
28613
28727
  }));
28614
28728
  server.tool("log_compare", {
28615
28729
  project_id: exports_external.string(),
@@ -28618,13 +28732,20 @@ server.tool("log_compare", {
28618
28732
  b_since: exports_external.string(),
28619
28733
  b_until: exports_external.string()
28620
28734
  }, ({ project_id, a_since, a_until, b_since, b_until }) => ({
28621
- content: [{ type: "text", text: JSON.stringify(compare(db, project_id, a_since, a_until, b_since, b_until)) }]
28735
+ content: [{ type: "text", text: JSON.stringify(compare(db, rp(project_id) ?? project_id, parseTime(a_since) ?? a_since, parseTime(a_until) ?? a_until, parseTime(b_since) ?? b_since, parseTime(b_until) ?? b_until)) }]
28622
28736
  }));
28737
+ server.tool("log_session_context", {
28738
+ session_id: exports_external.string(),
28739
+ brief: exports_external.boolean().optional()
28740
+ }, async ({ session_id, brief }) => {
28741
+ const ctx = await getSessionContext(db, session_id);
28742
+ return { content: [{ type: "text", text: JSON.stringify({ ...ctx, logs: applyBrief(ctx.logs, brief !== false) }) }] };
28743
+ });
28623
28744
  server.tool("perf_snapshot", {
28624
28745
  project_id: exports_external.string(),
28625
28746
  page_id: exports_external.string().optional()
28626
28747
  }, ({ project_id, page_id }) => {
28627
- const snap = getLatestSnapshot(db, project_id, page_id);
28748
+ const snap = getLatestSnapshot(db, rp(project_id) ?? project_id, page_id);
28628
28749
  return { content: [{ type: "text", text: JSON.stringify(snap ? { ...snap, label: scoreLabel(snap.score) } : null) }] };
28629
28750
  });
28630
28751
  server.tool("perf_trend", {
@@ -28633,25 +28754,25 @@ server.tool("perf_trend", {
28633
28754
  since: exports_external.string().optional(),
28634
28755
  limit: exports_external.number().optional()
28635
28756
  }, ({ project_id, page_id, since, limit }) => ({
28636
- content: [{ type: "text", text: JSON.stringify(getPerfTrend(db, project_id, page_id, since, limit ?? 50)) }]
28757
+ content: [{ type: "text", text: JSON.stringify(getPerfTrend(db, rp(project_id) ?? project_id, page_id, parseTime(since) ?? since, limit ?? 50)) }]
28637
28758
  }));
28638
28759
  server.tool("scan_status", {
28639
28760
  project_id: exports_external.string().optional()
28640
28761
  }, ({ project_id }) => ({
28641
- content: [{ type: "text", text: JSON.stringify(listJobs(db, project_id)) }]
28762
+ content: [{ type: "text", text: JSON.stringify(listJobs(db, rp(project_id))) }]
28642
28763
  }));
28643
28764
  server.tool("list_projects", {}, () => ({
28644
28765
  content: [{ type: "text", text: JSON.stringify(listProjects(db)) }]
28645
28766
  }));
28646
28767
  server.tool("list_pages", { project_id: exports_external.string() }, ({ project_id }) => ({
28647
- content: [{ type: "text", text: JSON.stringify(listPages(db, project_id)) }]
28768
+ content: [{ type: "text", text: JSON.stringify(listPages(db, rp(project_id) ?? project_id)) }]
28648
28769
  }));
28649
28770
  server.tool("list_issues", {
28650
28771
  project_id: exports_external.string().optional(),
28651
28772
  status: exports_external.string().optional(),
28652
28773
  limit: exports_external.number().optional()
28653
28774
  }, ({ project_id, status, limit }) => ({
28654
- content: [{ type: "text", text: JSON.stringify(listIssues(db, project_id, status, limit ?? 50)) }]
28775
+ content: [{ type: "text", text: JSON.stringify(listIssues(db, rp(project_id), status, limit ?? 50)) }]
28655
28776
  }));
28656
28777
  server.tool("resolve_issue", {
28657
28778
  id: exports_external.string(),
@@ -28668,11 +28789,11 @@ server.tool("create_alert_rule", {
28668
28789
  window_seconds: exports_external.number().optional(),
28669
28790
  action: exports_external.enum(["webhook", "log"]).optional(),
28670
28791
  webhook_url: exports_external.string().optional()
28671
- }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createAlertRule(db, args)) }] }));
28792
+ }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createAlertRule(db, { ...args, project_id: rp(args.project_id) ?? args.project_id })) }] }));
28672
28793
  server.tool("list_alert_rules", {
28673
28794
  project_id: exports_external.string().optional()
28674
28795
  }, ({ project_id }) => ({
28675
- content: [{ type: "text", text: JSON.stringify(listAlertRules(db, project_id)) }]
28796
+ content: [{ type: "text", text: JSON.stringify(listAlertRules(db, rp(project_id))) }]
28676
28797
  }));
28677
28798
  server.tool("delete_alert_rule", { id: exports_external.string() }, ({ id }) => {
28678
28799
  deleteAlertRule(db, id);