@hasna/logs 0.3.26 → 0.3.27

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 (130) hide show
  1. package/README.md +33 -10
  2. package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
  3. package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
  4. package/dashboard/dist/index.html +14 -0
  5. package/dist/cli/index.js +8511 -177
  6. package/dist/count-bmj4r2zb.js +10 -0
  7. package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
  8. package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
  9. package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
  10. package/dist/index-931pbyn5.js +141 -0
  11. package/dist/index-b5c72f1p.js +7 -0
  12. package/dist/{index-gc0zvs88.js → index-bnr19y0h.js} +596 -37
  13. package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
  14. package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
  15. package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
  16. package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
  17. package/dist/index-hq6kzaah.js +26 -0
  18. package/dist/index-j34f36wy.js +5672 -0
  19. package/dist/index-p4dbdzx4.js +1849 -0
  20. package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
  21. package/dist/index-t3x838zw.js +2583 -0
  22. package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
  23. package/dist/index.js +2982 -22
  24. package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
  25. package/dist/mcp/index.js +1473 -4286
  26. package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
  27. package/dist/server/index.js +2944 -417
  28. package/dist/storage.js +50 -0
  29. package/package.json +27 -8
  30. package/biome.json +0 -13
  31. package/bun.lock +0 -376
  32. package/dashboard/README.md +0 -73
  33. package/dashboard/bun.lock +0 -526
  34. package/dashboard/eslint.config.js +0 -23
  35. package/dashboard/index.html +0 -13
  36. package/dashboard/package.json +0 -32
  37. package/dashboard/src/App.css +0 -184
  38. package/dashboard/src/App.tsx +0 -49
  39. package/dashboard/src/api.ts +0 -33
  40. package/dashboard/src/assets/hero.png +0 -0
  41. package/dashboard/src/assets/react.svg +0 -1
  42. package/dashboard/src/assets/vite.svg +0 -1
  43. package/dashboard/src/index.css +0 -111
  44. package/dashboard/src/main.tsx +0 -10
  45. package/dashboard/src/pages/Alerts.tsx +0 -69
  46. package/dashboard/src/pages/Issues.tsx +0 -50
  47. package/dashboard/src/pages/Perf.tsx +0 -75
  48. package/dashboard/src/pages/Projects.tsx +0 -67
  49. package/dashboard/src/pages/Summary.tsx +0 -67
  50. package/dashboard/src/pages/Tail.tsx +0 -65
  51. package/dashboard/tsconfig.app.json +0 -28
  52. package/dashboard/tsconfig.json +0 -7
  53. package/dashboard/tsconfig.node.json +0 -26
  54. package/dashboard/vite.config.ts +0 -14
  55. package/dist/count-x3n7qg3c.js +0 -9
  56. package/dist/index-997bkzr2.js +0 -15
  57. package/dist/index-pen6t0yc.js +0 -10794
  58. package/sdk/package.json +0 -27
  59. package/sdk/src/index.ts +0 -143
  60. package/sdk/src/types.ts +0 -56
  61. package/src/cli/entrypoints.test.ts +0 -63
  62. package/src/cli/index.ts +0 -471
  63. package/src/db/index.test.ts +0 -33
  64. package/src/db/index.ts +0 -189
  65. package/src/db/migrations/001_alert_rules.ts +0 -21
  66. package/src/db/migrations/002_issues.ts +0 -21
  67. package/src/db/migrations/003_retention.ts +0 -15
  68. package/src/db/migrations/004_page_auth.ts +0 -13
  69. package/src/db/pg-migrations.ts +0 -167
  70. package/src/index.ts +0 -1
  71. package/src/lib/alerts.test.ts +0 -67
  72. package/src/lib/alerts.ts +0 -117
  73. package/src/lib/browser-script.test.ts +0 -35
  74. package/src/lib/browser-script.ts +0 -31
  75. package/src/lib/compare.test.ts +0 -52
  76. package/src/lib/compare.ts +0 -85
  77. package/src/lib/count.test.ts +0 -44
  78. package/src/lib/count.ts +0 -55
  79. package/src/lib/diagnose.test.ts +0 -55
  80. package/src/lib/diagnose.ts +0 -91
  81. package/src/lib/export.test.ts +0 -66
  82. package/src/lib/export.ts +0 -65
  83. package/src/lib/github.ts +0 -38
  84. package/src/lib/health.test.ts +0 -48
  85. package/src/lib/health.ts +0 -51
  86. package/src/lib/ingest.test.ts +0 -57
  87. package/src/lib/ingest.ts +0 -78
  88. package/src/lib/issues.test.ts +0 -79
  89. package/src/lib/issues.ts +0 -70
  90. package/src/lib/jobs.test.ts +0 -69
  91. package/src/lib/jobs.ts +0 -63
  92. package/src/lib/lighthouse.ts +0 -65
  93. package/src/lib/package-meta.test.ts +0 -43
  94. package/src/lib/package-meta.ts +0 -80
  95. package/src/lib/page-auth.test.ts +0 -54
  96. package/src/lib/page-auth.ts +0 -48
  97. package/src/lib/parse-time.test.ts +0 -37
  98. package/src/lib/parse-time.ts +0 -14
  99. package/src/lib/perf.test.ts +0 -45
  100. package/src/lib/perf.ts +0 -46
  101. package/src/lib/projects.test.ts +0 -73
  102. package/src/lib/projects.ts +0 -69
  103. package/src/lib/query.test.ts +0 -104
  104. package/src/lib/query.ts +0 -84
  105. package/src/lib/retention.test.ts +0 -42
  106. package/src/lib/retention.ts +0 -62
  107. package/src/lib/rotate.test.ts +0 -37
  108. package/src/lib/rotate.ts +0 -27
  109. package/src/lib/scanner.ts +0 -131
  110. package/src/lib/scheduler.ts +0 -63
  111. package/src/lib/session-context.ts +0 -28
  112. package/src/lib/summarize.test.ts +0 -38
  113. package/src/lib/summarize.ts +0 -23
  114. package/src/mcp/http.test.ts +0 -92
  115. package/src/mcp/http.ts +0 -135
  116. package/src/mcp/index.test.ts +0 -27
  117. package/src/mcp/index.ts +0 -444
  118. package/src/server/index.ts +0 -61
  119. package/src/server/routes/alerts.ts +0 -32
  120. package/src/server/routes/issues.ts +0 -43
  121. package/src/server/routes/jobs.ts +0 -32
  122. package/src/server/routes/logs.ts +0 -113
  123. package/src/server/routes/perf.ts +0 -23
  124. package/src/server/routes/projects.ts +0 -67
  125. package/src/server/routes/stream.ts +0 -43
  126. package/src/server/server.test.ts +0 -194
  127. package/src/types/index.ts +0 -119
  128. package/tsconfig.json +0 -22
  129. /package/dashboard/{public → dist}/favicon.svg +0 -0
  130. /package/dashboard/{public → dist}/icons.svg +0 -0
package/src/mcp/index.ts DELETED
@@ -1,444 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
- import { registerCloudTools } from "@hasna/cloud"
5
- import { z } from "zod"
6
- import { getDb } from "../db/index.ts"
7
- import { exitIfMetadataRequest, PACKAGE_VERSION } from "../lib/package-meta.ts"
8
- import { ingestBatch, ingestLog } from "../lib/ingest.ts"
9
- import { getLogContext, getLogContextFromId, searchLogs, tailLogs } from "../lib/query.ts"
10
- import { summarizeLogs } from "../lib/summarize.ts"
11
- import { countLogs } from "../lib/count.ts"
12
- import { createJob, listJobs } from "../lib/jobs.ts"
13
- import { createPage, createProject, listPages, listProjects, resolveProjectId } from "../lib/projects.ts"
14
- import { getLatestSnapshot, getPerfTrend, scoreLabel } from "../lib/perf.ts"
15
- import { createAlertRule, deleteAlertRule, listAlertRules } from "../lib/alerts.ts"
16
- import { listIssues, updateIssueStatus } from "../lib/issues.ts"
17
- import { diagnose } from "../lib/diagnose.ts"
18
- import { exportToJson, exportToCsv } from "../lib/export.ts"
19
- import { compare } from "../lib/compare.ts"
20
- import { getHealth } from "../lib/health.ts"
21
- import { getSessionContext } from "../lib/session-context.ts"
22
- import { parseTime } from "../lib/parse-time.ts"
23
- import type { LogLevel, LogRow } from "../types/index.ts"
24
-
25
- exitIfMetadataRequest({
26
- name: "logs-mcp",
27
- description: "Start the @hasna/logs MCP server (stdio by default).",
28
- options: [
29
- " --http Serve MCP over Streamable HTTP (127.0.0.1)",
30
- " --port <number> HTTP port (default: 8820, env: MCP_HTTP_PORT)",
31
- ],
32
- })
33
-
34
- const db = getDb()
35
-
36
- // --- in-memory agent registry (module-level for shared HTTP process) ---
37
- interface _LogsAgent { id: string; name: string; session_id?: string; last_seen_at: string; project_id?: string }
38
- const _logsAgents = new Map<string, _LogsAgent>()
39
-
40
- export function buildServer(): McpServer {
41
- const server = new McpServer({ name: "logs", version: PACKAGE_VERSION })
42
-
43
- const BRIEF_FIELDS: (keyof LogRow)[] = ["id", "timestamp", "level", "message", "service"]
44
-
45
- function applyBrief(rows: LogRow[], brief = true): unknown[] {
46
- if (!brief) return rows
47
- return rows.map(r => ({
48
- id: r.id,
49
- timestamp: r.timestamp,
50
- level: r.level,
51
- message: r.message,
52
- service: r.service,
53
- age_seconds: Math.floor((Date.now() - new Date(r.timestamp).getTime()) / 1000),
54
- }))
55
- }
56
-
57
- function rp(idOrName?: string): string | undefined {
58
- if (!idOrName) return undefined
59
- return resolveProjectId(db, idOrName) ?? idOrName
60
- }
61
-
62
- // Tool registry with param signatures for discoverability
63
- const TOOLS: Record<string, { desc: string; params: string }> = {
64
- register_project: { desc: "Register a project", params: "(name, github_repo?, base_url?, description?)" },
65
- register_page: { desc: "Register a page URL to a project", params: "(project_id, url, path?, name?)" },
66
- create_scan_job: { desc: "Schedule headless page scans", params: "(project_id, schedule, page_id?)" },
67
- resolve_project: { desc: "Resolve project name to ID", params: "(name)" },
68
- log_push: { desc: "Push a single log entry", params: "(level, message, project_id?, service?, trace_id?, metadata?)" },
69
- log_push_batch: { desc: "Push multiple log entries in one call", params: "(entries: Array<{level, message, project_id?, service?, trace_id?}>)" },
70
- log_search: { desc: "Search logs", params: "(project_id?, level?, since?, until?, text?, service?, limit?=100, brief?=true)" },
71
- log_tail: { desc: "Get N most recent logs", params: "(project_id?, n?=50, brief?=true)" },
72
- log_count: { desc: "Count logs — zero token cost, pure signal", params: "(project_id?, service?, level?, since?, until?)" },
73
- log_recent_errors: { desc: "Shortcut: recent errors + fatals", params: "(project_id?, since?='1h', limit?=20)" },
74
- log_summary: { desc: "Error/warn counts by service", params: "(project_id?, since?)" },
75
- log_context: { desc: "All logs for a trace_id", params: "(trace_id, brief?=true)" },
76
- log_context_from_id: { desc: "Trace context from a log ID (no trace_id needed)", params: "(log_id, brief?=true)" },
77
- log_export: { desc: "Export matching logs as JSON or CSV", params: "(project_id?, format?='json', since?, until?, level?, service?, limit?=100000)" },
78
- 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'])" },
79
- log_compare: { desc: "Diff two time windows for new/resolved errors", params: "(project_id, a_since, a_until, b_since, b_until)" },
80
- log_session_context: { desc: "Logs + session metadata for a session_id", params: "(session_id, brief?=true)" },
81
- perf_snapshot: { desc: "Latest performance snapshot", params: "(project_id, page_id?)" },
82
- perf_trend: { desc: "Performance over time", params: "(project_id, page_id?, since?, limit?=50)" },
83
- scan_status: { desc: "Last scan jobs", params: "(project_id?)" },
84
- list_projects: { desc: "List all projects", params: "()" },
85
- list_pages: { desc: "List pages for a project", params: "(project_id)" },
86
- list_issues: { desc: "List grouped error issues", params: "(project_id?, status?, limit?=50)" },
87
- resolve_issue: { desc: "Update issue status", params: "(id, status: open|resolved|ignored)" },
88
- create_alert_rule: { desc: "Create alert rule", params: "(project_id, name, level?, threshold_count?, window_seconds?, webhook_url?)" },
89
- list_alert_rules: { desc: "List alert rules", params: "(project_id?)" },
90
- delete_alert_rule: { desc: "Delete alert rule", params: "(id)" },
91
- get_health: { desc: "Server health + DB stats", params: "()" },
92
- log_stats: { desc: "Aggregate DB-level log statistics for a project", params: "(project_id?)" },
93
- search_tools: { desc: "Search tools by keyword — returns names, descriptions, param signatures", params: "(query)" },
94
- describe_tools: { desc: "List all tools with descriptions and param signatures", params: "()" },
95
- }
96
-
97
- // Fellow agents: keep MCP registrations behind this helper so descriptions and schemas stay aligned with the current SDK.
98
- function registerTool(
99
- name: keyof typeof TOOLS,
100
- schema: Record<string, z.ZodTypeAny>,
101
- handler: (...args: any[]) => any,
102
- ) {
103
- return server.tool(name, TOOLS[name].desc, schema, handler)
104
- }
105
-
106
- registerTool("search_tools", { query: z.string() }, ({ query }) => {
107
- const q = query.toLowerCase()
108
- const matches = Object.entries(TOOLS).filter(([k, v]) => k.includes(q) || v.desc.toLowerCase().includes(q))
109
- const text = matches.map(([k, v]) => `${k}${v.params} — ${v.desc}`).join("\n") || "No matches"
110
- return { content: [{ type: "text", text }] }
111
- })
112
-
113
- registerTool("describe_tools", {}, () => ({
114
- content: [{ type: "text", text: Object.entries(TOOLS).map(([k, v]) => `${k}${v.params} — ${v.desc}`).join("\n") }]
115
- }))
116
-
117
- registerTool("resolve_project", { name: z.string() }, ({ name }) => {
118
- const id = resolveProjectId(db, name)
119
- const project = id ? db.prepare("SELECT * FROM projects WHERE id = $id").get({ $id: id }) : null
120
- return { content: [{ type: "text", text: JSON.stringify(project ?? { error: `Project '${name}' not found` }) }] }
121
- })
122
-
123
- registerTool("register_project", {
124
- name: z.string(), github_repo: z.string().optional(), base_url: z.string().optional(), description: z.string().optional(),
125
- }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createProject(db, args)) }] }))
126
-
127
- registerTool("register_page", {
128
- project_id: z.string(), url: z.string(), path: z.string().optional(), name: z.string().optional(),
129
- }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createPage(db, { ...args, project_id: rp(args.project_id) ?? args.project_id })) }] }))
130
-
131
- registerTool("create_scan_job", {
132
- project_id: z.string(), schedule: z.string(), page_id: z.string().optional(),
133
- }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createJob(db, { ...args, project_id: rp(args.project_id) ?? args.project_id })) }] }))
134
-
135
- registerTool("log_push", {
136
- level: z.enum(["debug", "info", "warn", "error", "fatal"]),
137
- message: z.string(),
138
- project_id: z.string().optional(), service: z.string().optional(),
139
- trace_id: z.string().optional(), session_id: z.string().optional(),
140
- agent: z.string().optional(), url: z.string().optional(),
141
- metadata: z.record(z.string(), z.unknown()).optional(),
142
- }, (args) => {
143
- const row = ingestLog(db, { ...args, project_id: rp(args.project_id) })
144
- return { content: [{ type: "text", text: `Logged: ${row.id}` }] }
145
- })
146
-
147
- registerTool("log_push_batch", {
148
- entries: z.array(z.object({
149
- level: z.enum(["debug", "info", "warn", "error", "fatal"]),
150
- message: z.string(),
151
- project_id: z.string().optional(), service: z.string().optional(),
152
- trace_id: z.string().optional(), metadata: z.record(z.string(), z.unknown()).optional(),
153
- })),
154
- trace_id: z.string().optional().describe("Shared trace_id applied to all entries that don't have their own trace_id"),
155
- project_id: z.string().optional().describe("Shared project_id applied to all entries (individual entry project_id takes precedence)"),
156
- }, ({ entries, trace_id, project_id }) => {
157
- const mapped = entries.map(e => ({
158
- ...e,
159
- project_id: rp(e.project_id ?? project_id),
160
- }))
161
- const rows = ingestBatch(db, mapped, trace_id)
162
- return { content: [{ type: "text", text: `Logged ${rows.length} entries${trace_id ? ` (trace: ${trace_id})` : ''}` }] }
163
- })
164
-
165
- registerTool("log_search", {
166
- project_id: z.string().optional(), page_id: z.string().optional(),
167
- level: z.string().optional(), service: z.string().optional(),
168
- since: z.string().optional(), until: z.string().optional(),
169
- text: z.string().optional(), trace_id: z.string().optional(),
170
- limit: z.number().optional(), brief: z.boolean().optional(),
171
- }, (args) => {
172
- const rows = searchLogs(db, {
173
- ...args,
174
- project_id: rp(args.project_id),
175
- level: args.level ? (args.level.split(",") as LogLevel[]) : undefined,
176
- since: parseTime(args.since) ?? args.since,
177
- until: parseTime(args.until) ?? args.until,
178
- })
179
- return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, args.brief !== false)) }] }
180
- })
181
-
182
- registerTool("log_tail", {
183
- project_id: z.string().optional(), n: z.number().optional(), brief: z.boolean().optional(),
184
- }, ({ project_id, n, brief }) => {
185
- const rows = tailLogs(db, rp(project_id), n ?? 50)
186
- return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, brief !== false)) }] }
187
- })
188
-
189
- registerTool("log_count", {
190
- project_id: z.string().optional(), service: z.string().optional(),
191
- level: z.string().optional(), since: z.string().optional(), until: z.string().optional(),
192
- group_by: z.enum(["level", "service"]).optional().describe("Return breakdown by 'level' or 'service' in addition to totals"),
193
- }, (args) => ({
194
- content: [{ type: "text", text: JSON.stringify(countLogs(db, { ...args, project_id: rp(args.project_id) })) }]
195
- }))
196
-
197
- registerTool("log_recent_errors", {
198
- project_id: z.string().optional(), since: z.string().optional(), limit: z.number().optional(),
199
- }, ({ project_id, since, limit }) => {
200
- const rows = searchLogs(db, {
201
- project_id: rp(project_id),
202
- level: ["error", "fatal"],
203
- since: parseTime(since ?? "1h"),
204
- limit: limit ?? 20,
205
- })
206
- return { content: [{ type: "text", text: JSON.stringify(applyBrief(rows, true)) }] }
207
- })
208
-
209
- registerTool("log_summary", {
210
- project_id: z.string().optional(), since: z.string().optional(),
211
- }, ({ project_id, since }) => ({
212
- content: [{ type: "text", text: JSON.stringify(summarizeLogs(db, rp(project_id), parseTime(since) ?? since)) }]
213
- }))
214
-
215
- registerTool("log_context", {
216
- trace_id: z.string(), brief: z.boolean().optional(),
217
- }, ({ trace_id, brief }) => ({
218
- content: [{ type: "text", text: JSON.stringify(applyBrief(getLogContext(db, trace_id), brief !== false)) }]
219
- }))
220
-
221
- registerTool("log_context_from_id", {
222
- log_id: z.string(),
223
- brief: z.boolean().optional(),
224
- window: z.number().int().min(0).optional().describe("Return N logs before and after the target log's timestamp (in addition to trace context)"),
225
- }, ({ log_id, brief, window }) => ({
226
- content: [{ type: "text", text: JSON.stringify(applyBrief(getLogContextFromId(db, log_id, window ?? 0), brief !== false)) }]
227
- }))
228
-
229
- registerTool("log_export", {
230
- project_id: z.string().optional().describe("Project name or ID"),
231
- format: z.enum(["json", "csv"]).optional().default("json").describe("Output format"),
232
- since: z.string().optional().describe("Since time (1h, 24h, 7d, ISO)"),
233
- until: z.string().optional(),
234
- level: z.array(z.string()).optional().describe("Filter by levels"),
235
- service: z.string().optional(),
236
- limit: z.number().optional().default(100000),
237
- }, (args) => {
238
- const chunks: string[] = []
239
- const write = (s: string) => { chunks.push(s); return true }
240
- const options = {
241
- project_id: rp(args.project_id),
242
- level: args.level as never,
243
- service: args.service,
244
- since: args.since,
245
- until: args.until,
246
- limit: args.limit ?? 100000,
247
- }
248
- if (args.format === "csv") exportToCsv(db, options, write)
249
- else exportToJson(db, options, write)
250
- return { content: [{ type: "text" as const, text: chunks.join("") }] }
251
- })
252
-
253
- registerTool("log_diagnose", {
254
- project_id: z.string(),
255
- since: z.string().optional(),
256
- include: z.array(z.enum(["top_errors", "error_rate", "failing_pages", "perf"])).optional(),
257
- }, ({ project_id, since, include }) => ({
258
- content: [{ type: "text", text: JSON.stringify(diagnose(db, rp(project_id) ?? project_id, since, include)) }]
259
- }))
260
-
261
- registerTool("log_compare", {
262
- project_id: z.string(),
263
- a_since: z.string(), a_until: z.string(),
264
- b_since: z.string(), b_until: z.string(),
265
- }, ({ project_id, a_since, a_until, b_since, b_until }) => ({
266
- content: [{ type: "text", text: JSON.stringify(compare(db, rp(project_id) ?? project_id,
267
- parseTime(a_since) ?? a_since, parseTime(a_until) ?? a_until,
268
- parseTime(b_since) ?? b_since, parseTime(b_until) ?? b_until)) }]
269
- }))
270
-
271
- registerTool("log_session_context", {
272
- session_id: z.string(), brief: z.boolean().optional(),
273
- }, async ({ session_id, brief }) => {
274
- const ctx = await getSessionContext(db, session_id)
275
- return { content: [{ type: "text", text: JSON.stringify({ ...ctx, logs: applyBrief(ctx.logs, brief !== false) }) }] }
276
- })
277
-
278
- registerTool("perf_snapshot", {
279
- project_id: z.string(), page_id: z.string().optional(),
280
- }, ({ project_id, page_id }) => {
281
- const snap = getLatestSnapshot(db, rp(project_id) ?? project_id, page_id)
282
- return { content: [{ type: "text", text: JSON.stringify(snap ? { ...snap, label: scoreLabel(snap.score) } : null) }] }
283
- })
284
-
285
- registerTool("perf_trend", {
286
- project_id: z.string(), page_id: z.string().optional(), since: z.string().optional(), limit: z.number().optional(),
287
- }, ({ project_id, page_id, since, limit }) => ({
288
- content: [{ type: "text", text: JSON.stringify(getPerfTrend(db, rp(project_id) ?? project_id, page_id, parseTime(since) ?? since, limit ?? 50)) }]
289
- }))
290
-
291
- registerTool("scan_status", {
292
- project_id: z.string().optional(),
293
- }, ({ project_id }) => ({
294
- content: [{ type: "text", text: JSON.stringify(listJobs(db, rp(project_id))) }]
295
- }))
296
-
297
- registerTool("list_projects", {}, () => ({
298
- content: [{ type: "text", text: JSON.stringify(listProjects(db)) }]
299
- }))
300
-
301
- registerTool("list_pages", { project_id: z.string() }, ({ project_id }) => ({
302
- content: [{ type: "text", text: JSON.stringify(listPages(db, rp(project_id) ?? project_id)) }]
303
- }))
304
-
305
- registerTool("list_issues", {
306
- project_id: z.string().optional(), status: z.string().optional(), limit: z.number().optional(),
307
- }, ({ project_id, status, limit }) => ({
308
- content: [{ type: "text", text: JSON.stringify(listIssues(db, rp(project_id), status, limit ?? 50)) }]
309
- }))
310
-
311
- registerTool("resolve_issue", {
312
- id: z.string(), status: z.enum(["open", "resolved", "ignored"]),
313
- }, ({ id, status }) => ({
314
- content: [{ type: "text", text: JSON.stringify(updateIssueStatus(db, id, status)) }]
315
- }))
316
-
317
- registerTool("create_alert_rule", {
318
- project_id: z.string(), name: z.string(),
319
- level: z.string().optional(), service: z.string().optional(),
320
- threshold_count: z.number().optional(), window_seconds: z.number().optional(),
321
- action: z.enum(["webhook", "log"]).optional(), webhook_url: z.string().optional(),
322
- }, (args) => ({ content: [{ type: "text", text: JSON.stringify(createAlertRule(db, { ...args, project_id: rp(args.project_id) ?? args.project_id })) }] }))
323
-
324
- registerTool("list_alert_rules", {
325
- project_id: z.string().optional(),
326
- }, ({ project_id }) => ({
327
- content: [{ type: "text", text: JSON.stringify(listAlertRules(db, rp(project_id))) }]
328
- }))
329
-
330
- registerTool("delete_alert_rule", { id: z.string() }, ({ id }) => {
331
- deleteAlertRule(db, id)
332
- return { content: [{ type: "text", text: "deleted" }] }
333
- })
334
-
335
- registerTool("get_health", {}, () => ({
336
- content: [{ type: "text", text: JSON.stringify(getHealth(db)) }]
337
- }))
338
-
339
- registerTool("log_stats", {
340
- project_id: z.string().optional().describe("Project name or ID (scope stats to a project)"),
341
- }, (args) => {
342
- const projectId = rp(args.project_id)
343
- const pFilter = projectId ? `WHERE project_id = ?` : ""
344
- const pAnd = projectId ? `AND project_id = ?` : ""
345
- const pParam = projectId ? [projectId] : []
346
-
347
- const total = (db.query(`SELECT COUNT(*) as c FROM logs ${pFilter}`).get(...pParam) as { c: number }).c
348
- const oldest = (db.query(`SELECT MIN(timestamp) as t FROM logs ${pFilter}`).get(...pParam) as { t: string | null }).t
349
- const newest = (db.query(`SELECT MAX(timestamp) as t FROM logs ${pFilter}`).get(...pParam) as { t: string | null }).t
350
- const byLevel = db.query(`SELECT level, COUNT(*) as c FROM logs ${pFilter} GROUP BY level ORDER BY c DESC`).all(...pParam) as { level: string; c: number }[]
351
- const topServices = db.query(`SELECT COALESCE(service, '-') as service, COUNT(*) as c FROM logs ${pFilter} GROUP BY service ORDER BY c DESC LIMIT 5`).all(...pParam) as { service: string; c: number }[]
352
- const days = db.query(`SELECT strftime('%Y-%m-%d', timestamp) as day, COUNT(*) as c FROM logs WHERE timestamp >= datetime('now', '-7 days') ${pAnd} GROUP BY day ORDER BY day`).all(...pParam) as { day: string; c: number }[]
353
- const errors = (byLevel.find(r => r.level === "error")?.c ?? 0) + (byLevel.find(r => r.level === "fatal")?.c ?? 0)
354
- const error_rate_pct = total > 0 ? parseFloat(((errors / total) * 100).toFixed(2)) : 0
355
- return {
356
- content: [{ type: "text" as const, text: JSON.stringify({ total, oldest, newest, by_level: Object.fromEntries(byLevel.map(r => [r.level, r.c])), top_services: topServices, last_7_days: days, error_rate_pct }) }]
357
- }
358
- })
359
-
360
- server.tool(
361
- "send_feedback",
362
- "Send feedback about this service",
363
- {
364
- message: z.string(),
365
- email: z.string().optional(),
366
- category: z.enum(["bug", "feature", "general"]).optional(),
367
- },
368
- async (params) => {
369
- try {
370
- db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [
371
- params.message, params.email || null, params.category || "general", PACKAGE_VERSION,
372
- ])
373
- return { content: [{ type: "text" as const, text: "Feedback saved. Thank you!" }] }
374
- } catch (e) {
375
- return { content: [{ type: "text" as const, text: String(e) }], isError: true }
376
- }
377
- },
378
- )
379
-
380
- // --- Agent Tools ---
381
-
382
- server.tool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
383
- name: z.string(),
384
- session_id: z.string().optional(),
385
- }, async (params) => {
386
- const existing = [..._logsAgents.values()].find(a => a.name === params.name)
387
- if (existing) { existing.last_seen_at = new Date().toISOString(); if (params.session_id) existing.session_id = params.session_id; return { content: [{ type: "text" as const, text: JSON.stringify(existing) }] } }
388
- const id = Math.random().toString(36).slice(2, 10)
389
- const ag: _LogsAgent = { id, name: params.name, session_id: params.session_id, last_seen_at: new Date().toISOString() }
390
- _logsAgents.set(id, ag)
391
- return { content: [{ type: "text" as const, text: JSON.stringify(ag) }] }
392
- })
393
-
394
- server.tool("heartbeat", "Update last_seen_at to signal agent is active.", {
395
- agent_id: z.string(),
396
- }, async (params) => {
397
- const ag = _logsAgents.get(params.agent_id)
398
- if (!ag) return { content: [{ type: "text" as const, text: `Agent not found: ${params.agent_id}` }], isError: true }
399
- ag.last_seen_at = new Date().toISOString()
400
- return { content: [{ type: "text" as const, text: JSON.stringify({ agent_id: ag.id, last_seen_at: ag.last_seen_at }) }] }
401
- })
402
-
403
- server.tool("set_focus", "Set active project context for this agent session.", {
404
- agent_id: z.string(),
405
- project_id: z.string().optional(),
406
- }, async (params) => {
407
- const ag = _logsAgents.get(params.agent_id)
408
- if (!ag) return { content: [{ type: "text" as const, text: `Agent not found: ${params.agent_id}` }], isError: true }
409
- ag.project_id = params.project_id
410
- return { content: [{ type: "text" as const, text: JSON.stringify({ agent_id: ag.id, project_id: ag.project_id ?? null }) }] }
411
- })
412
-
413
- server.tool("list_agents", "List all registered agents.", {}, async () => {
414
- return { content: [{ type: "text" as const, text: JSON.stringify([..._logsAgents.values()]) }] }
415
- })
416
-
417
- registerCloudTools(server, "logs")
418
- return server
419
- }
420
-
421
- async function main(): Promise<void> {
422
- const { isStdioMode, resolveMcpHttpPort, startMcpHttpServer } = await import("./http.ts")
423
-
424
- if (isStdioMode()) {
425
- const server = buildServer()
426
- const transport = new StdioServerTransport()
427
- await server.connect(transport)
428
- return
429
- }
430
-
431
- // Default: shared Streamable HTTP server (one process per MCP, many agents).
432
- const handle = await startMcpHttpServer(buildServer, {
433
- port: resolveMcpHttpPort(),
434
- })
435
- process.on("SIGINT", () => void handle.close().finally(() => process.exit(0)))
436
- process.on("SIGTERM", () => void handle.close().finally(() => process.exit(0)))
437
- }
438
-
439
- if (import.meta.main) {
440
- main().catch((err) => {
441
- console.error(err)
442
- process.exit(1)
443
- })
444
- }
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { Hono } from "hono"
3
- import { cors } from "hono/cors"
4
- import { serveStatic } from "hono/bun"
5
- import { getDb } from "../db/index.ts"
6
- import { getBrowserScript } from "../lib/browser-script.ts"
7
- import { getHealth } from "../lib/health.ts"
8
- import { exitIfMetadataRequest, readOptionValue } from "../lib/package-meta.ts"
9
- import { startScheduler } from "../lib/scheduler.ts"
10
- import { alertsRoutes } from "./routes/alerts.ts"
11
- import { issuesRoutes } from "./routes/issues.ts"
12
- import { jobsRoutes } from "./routes/jobs.ts"
13
- import { logsRoutes } from "./routes/logs.ts"
14
- import { perfRoutes } from "./routes/perf.ts"
15
- import { projectsRoutes } from "./routes/projects.ts"
16
- import { streamRoutes } from "./routes/stream.ts"
17
-
18
- exitIfMetadataRequest({
19
- name: "logs-serve",
20
- description: "Start the @hasna/logs REST API server.",
21
- options: [" -p, --port <n> Port to listen on (default: LOGS_PORT or 3460)"],
22
- })
23
-
24
- const portArg = readOptionValue(["--port", "-p"])
25
- const PORT = Number(portArg ?? process.env.LOGS_PORT ?? 3460)
26
- const db = getDb()
27
- const app = new Hono()
28
-
29
- app.use("*", cors())
30
-
31
- // Browser tracking script
32
- app.get("/script.js", (c) => {
33
- const host = `${c.req.header("x-forwarded-proto") ?? "http"}://${c.req.header("host") ?? `localhost:${PORT}`}`
34
- c.header("Content-Type", "application/javascript")
35
- c.header("Cache-Control", "public, max-age=300")
36
- return c.text(getBrowserScript(host))
37
- })
38
-
39
- // API routes
40
- app.route("/api/logs", logsRoutes(db))
41
- app.route("/api/logs/stream", streamRoutes(db))
42
- app.route("/api/projects", projectsRoutes(db))
43
- app.route("/api/jobs", jobsRoutes(db))
44
- app.route("/api/alerts", alertsRoutes(db))
45
- app.route("/api/issues", issuesRoutes(db))
46
- app.route("/api/perf", perfRoutes(db))
47
-
48
- app.get("/health", (c) => c.json(getHealth(db)))
49
- app.get("/dashboard", (c) => c.redirect("/dashboard/"))
50
- app.use("/dashboard/*", serveStatic({ root: "./dashboard/dist", rewriteRequestPath: (p) => p.replace(/^\/dashboard/, "") }))
51
- app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok", dashboard: `http://localhost:${PORT}/dashboard/` }))
52
-
53
- // Start scheduler
54
- startScheduler(db)
55
-
56
- console.log(`@hasna/logs server running on http://localhost:${PORT}`)
57
-
58
- export default {
59
- port: PORT,
60
- fetch: app.fetch,
61
- }
@@ -1,32 +0,0 @@
1
- import { Hono } from "hono"
2
- import type { Database } from "bun:sqlite"
3
- import { createAlertRule, deleteAlertRule, listAlertRules, updateAlertRule } from "../../lib/alerts.ts"
4
-
5
- export function alertsRoutes(db: Database) {
6
- const app = new Hono()
7
-
8
- app.post("/", async (c) => {
9
- const body = await c.req.json()
10
- if (!body.project_id || !body.name) return c.json({ error: "project_id and name required" }, 422)
11
- return c.json(createAlertRule(db, body), 201)
12
- })
13
-
14
- app.get("/", (c) => {
15
- const { project_id } = c.req.query()
16
- return c.json(listAlertRules(db, project_id || undefined))
17
- })
18
-
19
- app.put("/:id", async (c) => {
20
- const body = await c.req.json()
21
- const updated = updateAlertRule(db, c.req.param("id"), body)
22
- if (!updated) return c.json({ error: "not found" }, 404)
23
- return c.json(updated)
24
- })
25
-
26
- app.delete("/:id", (c) => {
27
- deleteAlertRule(db, c.req.param("id"))
28
- return c.json({ deleted: true })
29
- })
30
-
31
- return app
32
- }
@@ -1,43 +0,0 @@
1
- import { Hono } from "hono"
2
- import type { Database } from "bun:sqlite"
3
- import { getIssue, listIssues, updateIssueStatus } from "../../lib/issues.ts"
4
- import { searchLogs } from "../../lib/query.ts"
5
-
6
- export function issuesRoutes(db: Database) {
7
- const app = new Hono()
8
-
9
- app.get("/", (c) => {
10
- const { project_id, status, limit } = c.req.query()
11
- return c.json(listIssues(db, project_id || undefined, status || undefined, limit ? Number(limit) : 50))
12
- })
13
-
14
- app.get("/:id", (c) => {
15
- const issue = getIssue(db, c.req.param("id"))
16
- if (!issue) return c.json({ error: "not found" }, 404)
17
- return c.json(issue)
18
- })
19
-
20
- app.get("/:id/logs", (c) => {
21
- const issue = getIssue(db, c.req.param("id"))
22
- if (!issue) return c.json({ error: "not found" }, 404)
23
- // Search logs matching this issue's fingerprint via service+level
24
- const rows = searchLogs(db, {
25
- project_id: issue.project_id ?? undefined,
26
- level: issue.level as "error",
27
- service: issue.service ?? undefined,
28
- text: issue.message_template.slice(0, 50),
29
- limit: 50,
30
- })
31
- return c.json(rows)
32
- })
33
-
34
- app.put("/:id", async (c) => {
35
- const { status } = await c.req.json() as { status: "open" | "resolved" | "ignored" }
36
- if (!["open", "resolved", "ignored"].includes(status)) return c.json({ error: "invalid status" }, 422)
37
- const updated = updateIssueStatus(db, c.req.param("id"), status)
38
- if (!updated) return c.json({ error: "not found" }, 404)
39
- return c.json(updated)
40
- })
41
-
42
- return app
43
- }
@@ -1,32 +0,0 @@
1
- import { Hono } from "hono"
2
- import type { Database } from "bun:sqlite"
3
- import { createJob, deleteJob, listJobs, updateJob } from "../../lib/jobs.ts"
4
-
5
- export function jobsRoutes(db: Database) {
6
- const app = new Hono()
7
-
8
- app.post("/", async (c) => {
9
- const body = await c.req.json()
10
- if (!body.project_id || !body.schedule) return c.json({ error: "project_id and schedule are required" }, 422)
11
- return c.json(createJob(db, body), 201)
12
- })
13
-
14
- app.get("/", (c) => {
15
- const { project_id } = c.req.query()
16
- return c.json(listJobs(db, project_id || undefined))
17
- })
18
-
19
- app.put("/:id", async (c) => {
20
- const body = await c.req.json()
21
- const updated = updateJob(db, c.req.param("id"), body)
22
- if (!updated) return c.json({ error: "not found" }, 404)
23
- return c.json(updated)
24
- })
25
-
26
- app.delete("/:id", (c) => {
27
- deleteJob(db, c.req.param("id"))
28
- return c.json({ deleted: true })
29
- })
30
-
31
- return app
32
- }