@hasna/logs 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/sdk/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob } from "../../src/types/index.ts"
1
+ import type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob } from "./types.ts"
2
2
 
3
3
  export type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob }
4
4
 
@@ -0,0 +1,56 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal"
2
+ export type LogSource = "sdk" | "script" | "scanner"
3
+
4
+ export interface LogEntry {
5
+ level: LogLevel
6
+ message: string
7
+ project_id?: string
8
+ page_id?: string
9
+ source?: LogSource
10
+ service?: string
11
+ trace_id?: string
12
+ session_id?: string
13
+ agent?: string
14
+ url?: string
15
+ stack_trace?: string
16
+ metadata?: Record<string, unknown>
17
+ }
18
+
19
+ export interface LogRow {
20
+ id: string; timestamp: string; project_id: string | null; page_id: string | null
21
+ level: LogLevel; source: LogSource; service: string | null; message: string
22
+ trace_id: string | null; session_id: string | null; agent: string | null
23
+ url: string | null; stack_trace: string | null; metadata: string | null
24
+ }
25
+
26
+ export interface Project {
27
+ id: string; name: string; github_repo: string | null; base_url: string | null
28
+ description: string | null; created_at: string
29
+ }
30
+
31
+ export interface Page {
32
+ id: string; project_id: string; url: string; path: string
33
+ name: string | null; last_scanned_at: string | null; created_at: string
34
+ }
35
+
36
+ export interface ScanJob {
37
+ id: string; project_id: string; page_id: string | null
38
+ schedule: string; enabled: number; last_run_at: string | null; created_at: string
39
+ }
40
+
41
+ export interface PerformanceSnapshot {
42
+ id: string; timestamp: string; project_id: string; page_id: string | null
43
+ url: string; lcp: number | null; fcp: number | null; cls: number | null
44
+ tti: number | null; ttfb: number | null; score: number | null; raw_audit: string | null
45
+ }
46
+
47
+ export interface LogQuery {
48
+ project_id?: string; page_id?: string; level?: LogLevel | LogLevel[]
49
+ service?: string; since?: string; until?: string; text?: string
50
+ trace_id?: string; limit?: number; offset?: number; fields?: string[]
51
+ }
52
+
53
+ export interface LogSummary {
54
+ project_id: string | null; service: string | null; page_id: string | null
55
+ level: LogLevel; count: number; latest: string
56
+ }
package/src/cli/index.ts CHANGED
@@ -151,6 +151,58 @@ program.command("scan")
151
151
  console.log("Scan complete.")
152
152
  })
153
153
 
154
+ // ── logs watch ────────────────────────────────────────────
155
+ program.command("watch")
156
+ .description("Stream new logs in real time with color coding")
157
+ .option("--project <id>")
158
+ .option("--level <levels>", "Comma-separated levels")
159
+ .option("--service <name>")
160
+ .action(async (opts) => {
161
+ const db = getDb()
162
+ const { searchLogs } = await import("../lib/query.ts")
163
+
164
+ const COLORS: Record<string, string> = {
165
+ debug: "\x1b[90m", info: "\x1b[36m", warn: "\x1b[33m", error: "\x1b[31m", fatal: "\x1b[35m",
166
+ }
167
+ const RESET = "\x1b[0m"
168
+ const BOLD = "\x1b[1m"
169
+
170
+ let lastTimestamp = new Date().toISOString()
171
+ let errorCount = 0
172
+ let warnCount = 0
173
+
174
+ process.stdout.write(`\x1b[2J\x1b[H`) // clear screen
175
+ console.log(`${BOLD}@hasna/logs watch${RESET} — Ctrl+C to exit\n`)
176
+
177
+ const poll = () => {
178
+ const rows = searchLogs(db, {
179
+ project_id: opts.project,
180
+ level: opts.level ? (opts.level.split(",") as LogLevel[]) : undefined,
181
+ service: opts.service,
182
+ since: lastTimestamp,
183
+ limit: 100,
184
+ }).reverse()
185
+
186
+ for (const row of rows) {
187
+ if (row.timestamp <= lastTimestamp) continue
188
+ lastTimestamp = row.timestamp
189
+ if (row.level === "error" || row.level === "fatal") errorCount++
190
+ if (row.level === "warn") warnCount++
191
+ const color = COLORS[row.level] ?? ""
192
+ const ts = row.timestamp.slice(11, 19)
193
+ const svc = (row.service ?? "-").padEnd(12)
194
+ const lvl = row.level.toUpperCase().padEnd(5)
195
+ console.log(`${color}${ts} ${BOLD}${lvl}${RESET}${color} ${svc} ${row.message}${RESET}`)
196
+ }
197
+
198
+ // Update terminal title with counts
199
+ process.stdout.write(`\x1b]2;logs: ${errorCount}E ${warnCount}W\x07`)
200
+ }
201
+
202
+ const interval = setInterval(poll, 500)
203
+ process.on("SIGINT", () => { clearInterval(interval); console.log(`\n\nErrors: ${errorCount} Warnings: ${warnCount}`); process.exit(0) })
204
+ })
205
+
154
206
  // ── logs export ───────────────────────────────────────────
155
207
  program.command("export")
156
208
  .description("Export logs to JSON or CSV")
@@ -203,11 +255,25 @@ program.command("mcp")
203
255
  .option("--gemini", "Install into Gemini")
204
256
  .action(async (opts) => {
205
257
  if (opts.claude || opts.codex || opts.gemini) {
206
- const bin = process.execPath
207
- const script = new URL(import.meta.url).pathname
258
+ const { execSync } = await import("node:child_process")
259
+ // Resolve the MCP binary path — works from both source and dist
260
+ const selfPath = process.argv[1] ?? new URL(import.meta.url).pathname
261
+ const mcpBin = selfPath.replace(/cli\/index\.(ts|js)$/, "mcp/index.$1")
262
+ const runtime = process.execPath // bun or node
263
+
208
264
  if (opts.claude) {
209
- const { execSync } = await import("node:child_process")
210
- execSync(`claude mcp add --transport stdio --scope user logs -- ${bin} ${script} mcp`, { stdio: "inherit" })
265
+ const cmd = `claude mcp add --transport stdio --scope user logs -- ${runtime} ${mcpBin}`
266
+ console.log(`Running: ${cmd}`)
267
+ execSync(cmd, { stdio: "inherit" })
268
+ console.log("✓ Installed logs-mcp into Claude Code")
269
+ }
270
+ if (opts.codex) {
271
+ const config = `[mcp_servers.logs]\ncommand = "${runtime}"\nargs = ["${mcpBin}"]`
272
+ console.log("Add to ~/.codex/config.toml:\n\n" + config)
273
+ }
274
+ if (opts.gemini) {
275
+ const config = JSON.stringify({ mcpServers: { logs: { command: runtime, args: [mcpBin] } } }, null, 2)
276
+ console.log("Add to ~/.gemini/settings.json mcpServers:\n\n" + config)
211
277
  }
212
278
  return
213
279
  }
@@ -0,0 +1,28 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { LogRow } from "../types/index.ts"
3
+
4
+ export interface SessionContext {
5
+ session_id: string
6
+ logs: LogRow[]
7
+ session?: Record<string, unknown>
8
+ error?: string
9
+ }
10
+
11
+ export async function getSessionContext(db: Database, sessionId: string): Promise<SessionContext> {
12
+ const logs = db.prepare("SELECT * FROM logs WHERE session_id = $s ORDER BY timestamp ASC")
13
+ .all({ $s: sessionId }) as LogRow[]
14
+
15
+ const sessionsUrl = process.env.SESSIONS_URL
16
+ if (!sessionsUrl) {
17
+ return { session_id: sessionId, logs }
18
+ }
19
+
20
+ try {
21
+ const res = await fetch(`${sessionsUrl.replace(/\/$/, "")}/api/sessions/${sessionId}`)
22
+ if (!res.ok) return { session_id: sessionId, logs }
23
+ const session = await res.json() as Record<string, unknown>
24
+ return { session_id: sessionId, logs, session }
25
+ } catch (err) {
26
+ return { session_id: sessionId, logs, error: String(err) }
27
+ }
28
+ }
package/src/mcp/index.ts CHANGED
@@ -14,6 +14,7 @@ import { listIssues, updateIssueStatus } from "../lib/issues.ts"
14
14
  import { diagnose } from "../lib/diagnose.ts"
15
15
  import { compare } from "../lib/compare.ts"
16
16
  import { getHealth } from "../lib/health.ts"
17
+ import { getSessionContext } from "../lib/session-context.ts"
17
18
  import type { LogLevel, LogRow } from "../types/index.ts"
18
19
 
19
20
  const db = getDb()
@@ -47,6 +48,7 @@ const TOOLS: Record<string, string> = {
47
48
  create_alert_rule: "Create alert rule (project_id, name, level, threshold_count, window_seconds, webhook_url?)",
48
49
  list_alert_rules: "List alert rules (project_id?)",
49
50
  delete_alert_rule: "Delete alert rule (id)",
51
+ log_session_context: "Logs + session metadata for a session_id (requires SESSIONS_URL env)",
50
52
  get_health: "Server health + DB stats",
51
53
  search_tools: "Search tools by keyword (query)",
52
54
  describe_tools: "List all tools",
@@ -191,6 +193,14 @@ server.tool("delete_alert_rule", { id: z.string() }, ({ id }) => {
191
193
  return { content: [{ type: "text", text: "deleted" }] }
192
194
  })
193
195
 
196
+ server.tool("log_session_context", {
197
+ session_id: z.string(),
198
+ brief: z.boolean().optional(),
199
+ }, async ({ session_id, brief }) => {
200
+ const ctx = await getSessionContext(db, session_id)
201
+ return { content: [{ type: "text", text: JSON.stringify({ ...ctx, logs: applyBrief(ctx.logs, brief !== false) }) }] }
202
+ })
203
+
194
204
  server.tool("get_health", {}, () => ({
195
205
  content: [{ type: "text", text: JSON.stringify(getHealth(db)) }]
196
206
  }))
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Hono } from "hono"
3
3
  import { cors } from "hono/cors"
4
+ import { serveStatic } from "hono/bun"
4
5
  import { getDb } from "../db/index.ts"
5
6
  import { getBrowserScript } from "../lib/browser-script.ts"
6
7
  import { getHealth } from "../lib/health.ts"
@@ -37,7 +38,9 @@ app.route("/api/issues", issuesRoutes(db))
37
38
  app.route("/api/perf", perfRoutes(db))
38
39
 
39
40
  app.get("/health", (c) => c.json(getHealth(db)))
40
- app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok" }))
41
+ app.get("/dashboard", (c) => c.redirect("/dashboard/"))
42
+ app.use("/dashboard/*", serveStatic({ root: "./dashboard/dist", rewriteRequestPath: (p) => p.replace(/^\/dashboard/, "") }))
43
+ app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok", dashboard: `http://localhost:${PORT}/dashboard/` }))
41
44
 
42
45
  // Start scheduler
43
46
  startScheduler(db)