@hasna/logs 0.2.0 → 0.3.1

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.
@@ -0,0 +1,39 @@
1
+ // @bun
2
+ import {
3
+ __require
4
+ } from "./index-g8dczzvv.js";
5
+
6
+ // src/lib/health.ts
7
+ var startTime = Date.now();
8
+ function getHealth(db) {
9
+ const projects = db.prepare("SELECT COUNT(*) as c FROM projects").get().c;
10
+ const total_logs = db.prepare("SELECT COUNT(*) as c FROM logs").get().c;
11
+ const scheduler_jobs = db.prepare("SELECT COUNT(*) as c FROM scan_jobs WHERE enabled = 1").get().c;
12
+ const open_issues = db.prepare("SELECT COUNT(*) as c FROM issues WHERE status = 'open'").get().c;
13
+ const levelRows = db.prepare("SELECT level, COUNT(*) as c FROM logs GROUP BY level").all();
14
+ const logs_by_level = Object.fromEntries(levelRows.map((r) => [r.level, r.c]));
15
+ const oldest = db.prepare("SELECT MIN(timestamp) as t FROM logs").get();
16
+ const newest = db.prepare("SELECT MAX(timestamp) as t FROM logs").get();
17
+ let db_size_bytes = null;
18
+ try {
19
+ const dbPath = process.env.LOGS_DB_PATH;
20
+ if (dbPath) {
21
+ const { statSync } = __require("fs");
22
+ db_size_bytes = statSync(dbPath).size;
23
+ }
24
+ } catch {}
25
+ return {
26
+ status: "ok",
27
+ uptime_seconds: Math.floor((Date.now() - startTime) / 1000),
28
+ db_size_bytes,
29
+ projects,
30
+ total_logs,
31
+ logs_by_level,
32
+ oldest_log: oldest.t,
33
+ newest_log: newest.t,
34
+ scheduler_jobs,
35
+ open_issues
36
+ };
37
+ }
38
+
39
+ export { getHealth };
@@ -0,0 +1,22 @@
1
+ // @bun
2
+ import {
3
+ createJob,
4
+ createScanRun,
5
+ deleteJob,
6
+ finishScanRun,
7
+ getJob,
8
+ listJobs,
9
+ listScanRuns,
10
+ updateJob
11
+ } from "./index-3dr7d80h.js";
12
+ import"./index-g8dczzvv.js";
13
+ export {
14
+ updateJob,
15
+ listScanRuns,
16
+ listJobs,
17
+ getJob,
18
+ finishScanRun,
19
+ deleteJob,
20
+ createScanRun,
21
+ createJob
22
+ };
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-6y8pmes4.js";
3
6
  import {
4
7
  createAlertRule,
5
8
  createPage,
@@ -8,27 +11,31 @@ import {
8
11
  getDb,
9
12
  getLatestSnapshot,
10
13
  getPerfTrend,
14
+ ingestBatch,
11
15
  ingestLog,
12
16
  listAlertRules,
13
17
  listIssues,
14
18
  listPages,
15
19
  listProjects,
20
+ resolveProjectId,
16
21
  scoreLabel,
17
22
  summarizeLogs,
18
23
  updateIssueStatus
19
- } from "../index-77ss2sf4.js";
24
+ } from "../index-5tvnhvgr.js";
20
25
  import {
21
26
  createJob,
22
27
  listJobs
23
- } from "../jobs-124e878j.js";
28
+ } from "../index-3dr7d80h.js";
24
29
  import {
25
30
  getLogContext,
31
+ getLogContextFromId,
32
+ parseTime,
26
33
  searchLogs,
27
34
  tailLogs
28
- } from "../query-0qv7fvzt.js";
35
+ } from "../index-rbrsvsyh.js";
29
36
  import {
30
37
  getHealth
31
- } from "../health-f2qrebqc.js";
38
+ } from "../index-yb8yd4j6.js";
32
39
  import {
33
40
  __commonJS,
34
41
  __export,
@@ -6294,7 +6301,7 @@ var require_formats = __commonJS((exports) => {
6294
6301
  }
6295
6302
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6296
6303
  function getTime(strictTimeZone) {
6297
- return function time(str) {
6304
+ return function time3(str) {
6298
6305
  const matches = TIME.exec(str);
6299
6306
  if (!matches)
6300
6307
  return false;
@@ -28381,17 +28388,19 @@ class StdioServerTransport {
28381
28388
  }
28382
28389
 
28383
28390
  // 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(`
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(`
28387
28396
  SELECT message, COUNT(*) as count, service, MAX(timestamp) as last_seen
28388
28397
  FROM logs
28389
28398
  WHERE project_id = $p AND level IN ('error','fatal') AND timestamp >= $since
28390
28399
  GROUP BY message, service
28391
28400
  ORDER BY count DESC
28392
28401
  LIMIT 10
28393
- `).all({ $p: projectId, $since: window });
28394
- const error_rate_by_service = db.prepare(`
28402
+ `).all({ $p: projectId, $since: window }) : [];
28403
+ const error_rate_by_service = want("error_rate") ? db.prepare(`
28395
28404
  SELECT service,
28396
28405
  SUM(CASE WHEN level IN ('error','fatal') THEN 1 ELSE 0 END) as errors,
28397
28406
  SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) as warns,
@@ -28400,8 +28409,8 @@ function diagnose(db, projectId, since) {
28400
28409
  WHERE project_id = $p AND timestamp >= $since
28401
28410
  GROUP BY service
28402
28411
  ORDER BY errors DESC
28403
- `).all({ $p: projectId, $since: window });
28404
- const failing_pages = db.prepare(`
28412
+ `).all({ $p: projectId, $since: window }) : [];
28413
+ const failing_pages = want("failing_pages") ? db.prepare(`
28405
28414
  SELECT l.page_id, p.url, COUNT(*) as error_count
28406
28415
  FROM logs l
28407
28416
  JOIN pages p ON p.id = l.page_id
@@ -28409,8 +28418,8 @@ function diagnose(db, projectId, since) {
28409
28418
  GROUP BY l.page_id, p.url
28410
28419
  ORDER BY error_count DESC
28411
28420
  LIMIT 10
28412
- `).all({ $p: projectId, $since: window });
28413
- const perf_regressions = db.prepare(`
28421
+ `).all({ $p: projectId, $since: window }) : [];
28422
+ const perf_regressions = want("perf") ? db.prepare(`
28414
28423
  SELECT * FROM (
28415
28424
  SELECT
28416
28425
  cur.page_id,
@@ -28427,11 +28436,25 @@ function diagnose(db, projectId, since) {
28427
28436
  ) WHERE delta < -5 OR delta IS NULL
28428
28437
  ORDER BY delta ASC
28429
28438
  LIMIT 10
28430
- `).all({ $p: projectId });
28439
+ `).all({ $p: projectId }) : [];
28431
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);
28432
28442
  const topService = error_rate_by_service[0];
28443
+ const score = totalErrors === 0 ? "green" : totalErrors <= 10 ? "yellow" : "red";
28433
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).`;
28434
- 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
+ };
28435
28458
  }
28436
28459
 
28437
28460
  // src/lib/compare.ts
@@ -28517,48 +28540,71 @@ async function getSessionContext(db, sessionId) {
28517
28540
 
28518
28541
  // src/mcp/index.ts
28519
28542
  var db = getDb();
28520
- var server = new McpServer({ name: "logs", version: "0.1.0" });
28543
+ var server = new McpServer({ name: "logs", version: "0.3.0" });
28521
28544
  function applyBrief(rows, brief = true) {
28522
28545
  if (!brief)
28523
28546
  return rows;
28524
- 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;
28525
28560
  }
28526
28561
  var TOOLS = {
28527
- register_project: "Register a project (name, github_repo?, base_url?, description?)",
28528
- register_page: "Register a page URL (project_id, url, path?, name?)",
28529
- create_scan_job: "Schedule page scans (project_id, schedule, page_id?)",
28530
- log_push: "Push a log entry (level, message, project_id?, service?, trace_id?, metadata?)",
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",
28541
- list_pages: "List pages for a project (project_id)",
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"
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: "()" }
28551
28591
  };
28552
28592
  server.tool("search_tools", { query: exports_external.string() }, ({ query }) => {
28553
28593
  const q = query.toLowerCase();
28554
- const matches = Object.entries(TOOLS).filter(([k, v]) => k.includes(q) || v.toLowerCase().includes(q));
28555
- return { content: [{ type: "text", text: matches.map(([k, v]) => `${k}: ${v}`).join(`
28556
- `) || "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 }] };
28557
28598
  });
28558
28599
  server.tool("describe_tools", {}, () => ({
28559
- 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(`
28560
28601
  `) }]
28561
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
+ });
28562
28608
  server.tool("register_project", {
28563
28609
  name: exports_external.string(),
28564
28610
  github_repo: exports_external.string().optional(),
@@ -28570,12 +28616,12 @@ server.tool("register_page", {
28570
28616
  url: exports_external.string(),
28571
28617
  path: exports_external.string().optional(),
28572
28618
  name: exports_external.string().optional()
28573
- }, (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 })) }] }));
28574
28620
  server.tool("create_scan_job", {
28575
28621
  project_id: exports_external.string(),
28576
28622
  schedule: exports_external.string(),
28577
28623
  page_id: exports_external.string().optional()
28578
- }, (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 })) }] }));
28579
28625
  server.tool("log_push", {
28580
28626
  level: exports_external.enum(["debug", "info", "warn", "error", "fatal"]),
28581
28627
  message: exports_external.string(),
@@ -28587,9 +28633,22 @@ server.tool("log_push", {
28587
28633
  url: exports_external.string().optional(),
28588
28634
  metadata: exports_external.record(exports_external.unknown()).optional()
28589
28635
  }, (args) => {
28590
- const row = ingestLog(db, args);
28636
+ const row = ingestLog(db, { ...args, project_id: rp(args.project_id) });
28591
28637
  return { content: [{ type: "text", text: `Logged: ${row.id}` }] };
28592
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
+ });
28593
28652
  server.tool("log_search", {
28594
28653
  project_id: exports_external.string().optional(),
28595
28654
  page_id: exports_external.string().optional(),
@@ -28602,7 +28661,13 @@ server.tool("log_search", {
28602
28661
  limit: exports_external.number().optional(),
28603
28662
  brief: exports_external.boolean().optional()
28604
28663
  }, (args) => {
28605
- 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
+ });
28606
28671
  return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, args.brief !== false)) }] };
28607
28672
  });
28608
28673
  server.tool("log_tail", {
@@ -28610,27 +28675,55 @@ server.tool("log_tail", {
28610
28675
  n: exports_external.number().optional(),
28611
28676
  brief: exports_external.boolean().optional()
28612
28677
  }, ({ project_id, n, brief }) => {
28613
- const rows = tailLogs(db, project_id, n ?? 50);
28678
+ const rows = tailLogs(db, rp(project_id), n ?? 50);
28614
28679
  return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] };
28615
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
+ });
28616
28703
  server.tool("log_summary", {
28617
28704
  project_id: exports_external.string().optional(),
28618
28705
  since: exports_external.string().optional()
28619
28706
  }, ({ project_id, since }) => ({
28620
- 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)) }]
28621
28708
  }));
28622
28709
  server.tool("log_context", {
28623
28710
  trace_id: exports_external.string(),
28624
28711
  brief: exports_external.boolean().optional()
28625
- }, ({ trace_id, brief }) => {
28626
- const rows = getLogContext(db, trace_id);
28627
- return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] };
28628
- });
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
+ }));
28629
28721
  server.tool("log_diagnose", {
28630
28722
  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)) }]
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)) }]
28634
28727
  }));
28635
28728
  server.tool("log_compare", {
28636
28729
  project_id: exports_external.string(),
@@ -28639,13 +28732,20 @@ server.tool("log_compare", {
28639
28732
  b_since: exports_external.string(),
28640
28733
  b_until: exports_external.string()
28641
28734
  }, ({ 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)) }]
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)) }]
28643
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
+ });
28644
28744
  server.tool("perf_snapshot", {
28645
28745
  project_id: exports_external.string(),
28646
28746
  page_id: exports_external.string().optional()
28647
28747
  }, ({ project_id, page_id }) => {
28648
- const snap = getLatestSnapshot(db, project_id, page_id);
28748
+ const snap = getLatestSnapshot(db, rp(project_id) ?? project_id, page_id);
28649
28749
  return { content: [{ type: "text", text: JSON.stringify(snap ? { ...snap, label: scoreLabel(snap.score) } : null) }] };
28650
28750
  });
28651
28751
  server.tool("perf_trend", {
@@ -28654,25 +28754,25 @@ server.tool("perf_trend", {
28654
28754
  since: exports_external.string().optional(),
28655
28755
  limit: exports_external.number().optional()
28656
28756
  }, ({ project_id, page_id, since, limit }) => ({
28657
- 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)) }]
28658
28758
  }));
28659
28759
  server.tool("scan_status", {
28660
28760
  project_id: exports_external.string().optional()
28661
28761
  }, ({ project_id }) => ({
28662
- content: [{ type: "text", text: JSON.stringify(listJobs(db, project_id)) }]
28762
+ content: [{ type: "text", text: JSON.stringify(listJobs(db, rp(project_id))) }]
28663
28763
  }));
28664
28764
  server.tool("list_projects", {}, () => ({
28665
28765
  content: [{ type: "text", text: JSON.stringify(listProjects(db)) }]
28666
28766
  }));
28667
28767
  server.tool("list_pages", { project_id: exports_external.string() }, ({ project_id }) => ({
28668
- content: [{ type: "text", text: JSON.stringify(listPages(db, project_id)) }]
28768
+ content: [{ type: "text", text: JSON.stringify(listPages(db, rp(project_id) ?? project_id)) }]
28669
28769
  }));
28670
28770
  server.tool("list_issues", {
28671
28771
  project_id: exports_external.string().optional(),
28672
28772
  status: exports_external.string().optional(),
28673
28773
  limit: exports_external.number().optional()
28674
28774
  }, ({ project_id, status, limit }) => ({
28675
- 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)) }]
28676
28776
  }));
28677
28777
  server.tool("resolve_issue", {
28678
28778
  id: exports_external.string(),
@@ -28689,23 +28789,16 @@ server.tool("create_alert_rule", {
28689
28789
  window_seconds: exports_external.number().optional(),
28690
28790
  action: exports_external.enum(["webhook", "log"]).optional(),
28691
28791
  webhook_url: exports_external.string().optional()
28692
- }, (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 })) }] }));
28693
28793
  server.tool("list_alert_rules", {
28694
28794
  project_id: exports_external.string().optional()
28695
28795
  }, ({ project_id }) => ({
28696
- content: [{ type: "text", text: JSON.stringify(listAlertRules(db, project_id)) }]
28796
+ content: [{ type: "text", text: JSON.stringify(listAlertRules(db, rp(project_id))) }]
28697
28797
  }));
28698
28798
  server.tool("delete_alert_rule", { id: exports_external.string() }, ({ id }) => {
28699
28799
  deleteAlertRule(db, id);
28700
28800
  return { content: [{ type: "text", text: "deleted" }] };
28701
28801
  });
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
28802
  server.tool("get_health", {}, () => ({
28710
28803
  content: [{ type: "text", text: JSON.stringify(getHealth(db)) }]
28711
28804
  }));
@@ -0,0 +1,14 @@
1
+ // @bun
2
+ import {
3
+ getLogContext,
4
+ getLogContextFromId,
5
+ searchLogs,
6
+ tailLogs
7
+ } from "./index-rbrsvsyh.js";
8
+ import"./index-g8dczzvv.js";
9
+ export {
10
+ tailLogs,
11
+ searchLogs,
12
+ getLogContextFromId,
13
+ getLogContext
14
+ };
@@ -6,7 +6,10 @@ import {
6
6
  setPageAuth,
7
7
  setRetentionPolicy,
8
8
  startScheduler
9
- } from "../index-2p6ynjet.js";
9
+ } from "../index-wbsq8qjd.js";
10
+ import {
11
+ countLogs
12
+ } from "../index-6y8pmes4.js";
10
13
  import {
11
14
  createAlertRule,
12
15
  createPage,
@@ -23,29 +26,31 @@ import {
23
26
  listIssues,
24
27
  listPages,
25
28
  listProjects,
29
+ resolveProjectId,
26
30
  summarizeLogs,
27
31
  updateAlertRule,
28
32
  updateIssueStatus,
29
33
  updateProject
30
- } from "../index-77ss2sf4.js";
34
+ } from "../index-5tvnhvgr.js";
31
35
  import {
32
36
  createJob,
33
37
  deleteJob,
34
38
  listJobs,
35
39
  updateJob
36
- } from "../jobs-124e878j.js";
40
+ } from "../index-3dr7d80h.js";
37
41
  import {
38
42
  getLogContext,
43
+ parseTime,
39
44
  searchLogs,
40
45
  tailLogs
41
- } from "../query-0qv7fvzt.js";
46
+ } from "../index-rbrsvsyh.js";
42
47
  import {
43
48
  exportToCsv,
44
49
  exportToJson
45
- } from "../export-yjaw2sr3.js";
50
+ } from "../index-eh9bkbpa.js";
46
51
  import {
47
52
  getHealth
48
- } from "../health-f2qrebqc.js";
53
+ } from "../index-yb8yd4j6.js";
49
54
  import"../index-g8dczzvv.js";
50
55
 
51
56
  // node_modules/hono/dist/compose.js
@@ -1835,7 +1840,7 @@ var serveStatic = (options) => {
1835
1840
 
1836
1841
  // node_modules/hono/dist/adapter/bun/serve-static.js
1837
1842
  var serveStatic2 = (options) => {
1838
- return async function serveStatic2(c, next) {
1843
+ return async function serveStatic22(c, next) {
1839
1844
  const getContent = async (path) => {
1840
1845
  const file = Bun.file(path);
1841
1846
  return await file.exists() ? file : null;
@@ -2097,9 +2102,29 @@ function logsRoutes(db) {
2097
2102
  });
2098
2103
  app.get("/summary", (c) => {
2099
2104
  const { project_id, since } = c.req.query();
2100
- const summary = summarizeLogs(db, project_id || undefined, since || undefined);
2105
+ const summary = summarizeLogs(db, resolveProjectId(db, project_id) || undefined, parseTime(since) || since || undefined);
2101
2106
  return c.json(summary);
2102
2107
  });
2108
+ app.get("/count", (c) => {
2109
+ const { project_id, service, level, since, until } = c.req.query();
2110
+ return c.json(countLogs(db, {
2111
+ project_id: resolveProjectId(db, project_id) || undefined,
2112
+ service: service || undefined,
2113
+ level: level || undefined,
2114
+ since: since || undefined,
2115
+ until: until || undefined
2116
+ }));
2117
+ });
2118
+ app.get("/recent-errors", (c) => {
2119
+ const { project_id, since, limit } = c.req.query();
2120
+ const rows = searchLogs(db, {
2121
+ project_id: resolveProjectId(db, project_id) || undefined,
2122
+ level: ["error", "fatal"],
2123
+ since: parseTime(since || "1h"),
2124
+ limit: limit ? Number(limit) : 20
2125
+ });
2126
+ return c.json(rows.map((r) => ({ id: r.id, timestamp: r.timestamp, level: r.level, message: r.message, service: r.service, age_seconds: Math.floor((Date.now() - new Date(r.timestamp).getTime()) / 1000) })));
2127
+ });
2103
2128
  app.get("/:trace_id/context", (c) => {
2104
2129
  const rows = getLogContext(db, c.req.param("trace_id"));
2105
2130
  return c.json(rows);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/logs",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Log aggregation + browser script + headless page scanner + performance monitoring for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { createTestDb } from "../db/index.ts"
3
+ import { ingestBatch } from "./ingest.ts"
4
+ import { countLogs } from "./count.ts"
5
+
6
+ describe("countLogs", () => {
7
+ it("counts all logs", () => {
8
+ const db = createTestDb()
9
+ ingestBatch(db, [{ level: "error", message: "e" }, { level: "warn", message: "w" }, { level: "info", message: "i" }])
10
+ const c = countLogs(db, {})
11
+ expect(c.total).toBe(3)
12
+ expect(c.errors).toBe(1)
13
+ expect(c.warns).toBe(1)
14
+ expect(c.fatals).toBe(0)
15
+ })
16
+
17
+ it("filters by project", () => {
18
+ const db = createTestDb()
19
+ const p = db.prepare("INSERT INTO projects (name) VALUES ('app') RETURNING id").get() as { id: string }
20
+ ingestBatch(db, [{ level: "error", message: "e", project_id: p.id }, { level: "error", message: "e2" }])
21
+ const c = countLogs(db, { project_id: p.id })
22
+ expect(c.total).toBe(1)
23
+ })
24
+
25
+ it("filters by service", () => {
26
+ const db = createTestDb()
27
+ ingestBatch(db, [{ level: "error", message: "e", service: "api" }, { level: "error", message: "e2", service: "db" }])
28
+ expect(countLogs(db, { service: "api" }).total).toBe(1)
29
+ })
30
+
31
+ it("returns zero counts for empty db", () => {
32
+ const c = countLogs(createTestDb(), {})
33
+ expect(c.total).toBe(0)
34
+ expect(c.errors).toBe(0)
35
+ expect(c.by_level).toEqual({})
36
+ })
37
+
38
+ it("accepts relative since", () => {
39
+ const db = createTestDb()
40
+ ingestBatch(db, [{ level: "error", message: "recent" }])
41
+ const c = countLogs(db, { since: "1h" })
42
+ expect(c.total).toBe(1)
43
+ })
44
+ })