@hasna/hooks 0.2.6 → 0.2.7

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/bin/index.js CHANGED
@@ -2147,7 +2147,7 @@ var init_registry = __esm(() => {
2147
2147
  {
2148
2148
  name: "sessionlog",
2149
2149
  displayName: "Session Log",
2150
- description: "Logs every tool call to .claude/session-log-<date>.jsonl",
2150
+ description: "Logs every tool call to SQLite (~/.hooks/hooks.db)",
2151
2151
  version: "0.1.0",
2152
2152
  category: "Observability",
2153
2153
  event: "PostToolUse",
@@ -2157,7 +2157,7 @@ var init_registry = __esm(() => {
2157
2157
  {
2158
2158
  name: "commandlog",
2159
2159
  displayName: "Command Log",
2160
- description: "Logs every bash command Claude runs to .claude/commands.log",
2160
+ description: "Logs every Bash command to SQLite (~/.hooks/hooks.db)",
2161
2161
  version: "0.1.0",
2162
2162
  category: "Observability",
2163
2163
  event: "PostToolUse",
@@ -2167,7 +2167,7 @@ var init_registry = __esm(() => {
2167
2167
  {
2168
2168
  name: "costwatch",
2169
2169
  displayName: "Cost Watch",
2170
- description: "Estimates session token usage and warns when budget threshold is exceeded",
2170
+ description: "Estimates session token usage, persists cost history to SQLite, and warns on budget overrun",
2171
2171
  version: "0.1.0",
2172
2172
  category: "Observability",
2173
2173
  event: "Stop",
@@ -2177,7 +2177,7 @@ var init_registry = __esm(() => {
2177
2177
  {
2178
2178
  name: "errornotify",
2179
2179
  displayName: "Error Notify",
2180
- description: "Detects tool failures and logs errors to .claude/errors.log",
2180
+ description: "Detects tool failures and logs errors to SQLite (~/.hooks/hooks.db)",
2181
2181
  version: "0.1.0",
2182
2182
  category: "Observability",
2183
2183
  event: "PostToolUse",
@@ -4152,6 +4152,264 @@ var init_profiles = __esm(() => {
4152
4152
  PROFILES_DIR = join2(homedir2(), ".hooks", "profiles");
4153
4153
  });
4154
4154
 
4155
+ // src/db/schema.ts
4156
+ var CREATE_HOOK_EVENTS_TABLE = `
4157
+ CREATE TABLE IF NOT EXISTS hook_events (
4158
+ id TEXT PRIMARY KEY,
4159
+ timestamp TEXT NOT NULL,
4160
+ session_id TEXT NOT NULL,
4161
+ hook_name TEXT NOT NULL,
4162
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification')),
4163
+ tool_name TEXT,
4164
+ tool_input TEXT,
4165
+ result TEXT CHECK (result IN ('continue', 'block', NULL)),
4166
+ error TEXT,
4167
+ duration_ms INTEGER,
4168
+ project_dir TEXT,
4169
+ metadata TEXT
4170
+ )
4171
+ `, CREATE_INDEXES;
4172
+ var init_schema = __esm(() => {
4173
+ CREATE_INDEXES = [
4174
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (timestamp)`,
4175
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_session_id ON hook_events (session_id)`,
4176
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name)`,
4177
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_event_type ON hook_events (event_type)`,
4178
+ `CREATE INDEX IF NOT EXISTS idx_hook_events_errors ON hook_events (timestamp) WHERE error IS NOT NULL`
4179
+ ];
4180
+ });
4181
+
4182
+ // src/db/migrations/001_initial.ts
4183
+ function up(db) {
4184
+ db.exec(CREATE_HOOK_EVENTS_TABLE);
4185
+ for (const idx of CREATE_INDEXES) {
4186
+ db.exec(idx);
4187
+ }
4188
+ }
4189
+ var init_001_initial = __esm(() => {
4190
+ init_schema();
4191
+ });
4192
+
4193
+ // src/db/migrations/index.ts
4194
+ function ensureMigrationsTable(db) {
4195
+ db.exec(`
4196
+ CREATE TABLE IF NOT EXISTS schema_migrations (
4197
+ version TEXT PRIMARY KEY,
4198
+ applied_at TEXT NOT NULL
4199
+ )
4200
+ `);
4201
+ }
4202
+ function getApplied(db) {
4203
+ const rows = db.query("SELECT version FROM schema_migrations").all();
4204
+ return new Set(rows.map((r) => r.version));
4205
+ }
4206
+ function runMigrations(db) {
4207
+ ensureMigrationsTable(db);
4208
+ const applied = getApplied(db);
4209
+ for (const migration of MIGRATIONS) {
4210
+ if (applied.has(migration.version))
4211
+ continue;
4212
+ migration.up(db);
4213
+ db.run("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", [
4214
+ migration.version,
4215
+ new Date().toISOString()
4216
+ ]);
4217
+ }
4218
+ }
4219
+ var MIGRATIONS;
4220
+ var init_migrations = __esm(() => {
4221
+ init_001_initial();
4222
+ MIGRATIONS = [{ version: "001_initial", up }];
4223
+ });
4224
+
4225
+ // src/db/legacy-import.ts
4226
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
4227
+ import { join as join3 } from "path";
4228
+ import { homedir as homedir3 } from "os";
4229
+ function ensureMetaTable(db) {
4230
+ db.exec(`
4231
+ CREATE TABLE IF NOT EXISTS _meta (
4232
+ key TEXT PRIMARY KEY,
4233
+ value TEXT NOT NULL
4234
+ )
4235
+ `);
4236
+ }
4237
+ function isAlreadyDone(db) {
4238
+ ensureMetaTable(db);
4239
+ const row = db.query("SELECT value FROM _meta WHERE key = ?").get(META_KEY);
4240
+ return row?.value === "1";
4241
+ }
4242
+ function markDone(db) {
4243
+ db.run("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)", [META_KEY, "1"]);
4244
+ }
4245
+ function nanoid() {
4246
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
4247
+ }
4248
+ function importJsonlFile(db, filePath) {
4249
+ let count = 0;
4250
+ try {
4251
+ const lines = readFileSync3(filePath, "utf-8").split(`
4252
+ `).filter(Boolean);
4253
+ for (const line of lines) {
4254
+ try {
4255
+ const entry = JSON.parse(line);
4256
+ db.run(`INSERT OR IGNORE INTO hook_events
4257
+ (id, timestamp, session_id, hook_name, event_type, tool_name, tool_input, project_dir)
4258
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
4259
+ nanoid(),
4260
+ entry.timestamp ?? new Date().toISOString(),
4261
+ entry.session_id ?? "legacy",
4262
+ "sessionlog",
4263
+ "PostToolUse",
4264
+ entry.tool_name ?? null,
4265
+ entry.tool_input ? String(entry.tool_input).slice(0, 500) : null,
4266
+ null
4267
+ ]);
4268
+ count++;
4269
+ } catch {}
4270
+ }
4271
+ } catch {}
4272
+ return count;
4273
+ }
4274
+ function importErrorsLog(db, filePath) {
4275
+ let count = 0;
4276
+ try {
4277
+ const lines = readFileSync3(filePath, "utf-8").split(`
4278
+ `).filter(Boolean);
4279
+ const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
4280
+ for (const line of lines) {
4281
+ try {
4282
+ const m = line.match(linePattern);
4283
+ if (!m)
4284
+ continue;
4285
+ const [, timestamp, sessionPrefix, , errorMsg] = m;
4286
+ db.run(`INSERT OR IGNORE INTO hook_events
4287
+ (id, timestamp, session_id, hook_name, event_type, error)
4288
+ VALUES (?, ?, ?, ?, ?, ?)`, [
4289
+ nanoid(),
4290
+ timestamp,
4291
+ sessionPrefix ? `legacy-${sessionPrefix}` : "legacy",
4292
+ "errornotify",
4293
+ "PostToolUse",
4294
+ errorMsg.slice(0, 500)
4295
+ ]);
4296
+ count++;
4297
+ } catch {}
4298
+ }
4299
+ } catch {}
4300
+ return count;
4301
+ }
4302
+ function runLegacyImport(db) {
4303
+ try {
4304
+ if (isAlreadyDone(db))
4305
+ return;
4306
+ let total = 0;
4307
+ const claudeProjectsDir = join3(homedir3(), ".claude", "projects");
4308
+ if (existsSync3(claudeProjectsDir)) {
4309
+ try {
4310
+ const projectDirs = readdirSync2(claudeProjectsDir);
4311
+ for (const dir of projectDirs) {
4312
+ const projectDir = join3(claudeProjectsDir, dir);
4313
+ try {
4314
+ const files = readdirSync2(projectDir);
4315
+ for (const file of files) {
4316
+ if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
4317
+ total += importJsonlFile(db, join3(projectDir, file));
4318
+ }
4319
+ if (file === "errors.log") {
4320
+ total += importErrorsLog(db, join3(projectDir, file));
4321
+ }
4322
+ }
4323
+ } catch {}
4324
+ }
4325
+ } catch {}
4326
+ }
4327
+ markDone(db);
4328
+ if (total > 0) {
4329
+ process.stderr.write(`[hooks] Imported ${total} legacy log entries into SQLite.
4330
+ `);
4331
+ }
4332
+ } catch (err) {
4333
+ process.stderr.write(`[hooks] Legacy import failed (non-fatal): ${err}
4334
+ `);
4335
+ }
4336
+ }
4337
+ var META_KEY = "legacy_import_done";
4338
+ var init_legacy_import = () => {};
4339
+
4340
+ // src/db/retention.ts
4341
+ function runRetention(db, days) {
4342
+ const envDays = parseInt(process.env.HOOKS_RETENTION_DAYS ?? "30");
4343
+ const retentionDays = days ?? (isNaN(envDays) || envDays <= 0 ? 30 : envDays);
4344
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
4345
+ try {
4346
+ db.run("DELETE FROM hook_events WHERE timestamp < ?", [cutoff]);
4347
+ const changes = db.query("SELECT changes() as changes").get()?.changes ?? 0;
4348
+ return changes;
4349
+ } catch {
4350
+ return 0;
4351
+ }
4352
+ }
4353
+
4354
+ // src/db/index.ts
4355
+ var exports_db = {};
4356
+ __export(exports_db, {
4357
+ getDbPath: () => getDbPath,
4358
+ getDb: () => getDb,
4359
+ createTestDb: () => createTestDb,
4360
+ closeDb: () => closeDb
4361
+ });
4362
+ import { Database } from "bun:sqlite";
4363
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
4364
+ import { join as join4 } from "path";
4365
+ import { homedir as homedir4 } from "os";
4366
+ function getDbPath() {
4367
+ if (process.env.HOOKS_DB_PATH) {
4368
+ return process.env.HOOKS_DB_PATH;
4369
+ }
4370
+ const dataDir = process.env.HOOKS_DATA_DIR ?? join4(homedir4(), ".hooks");
4371
+ return join4(dataDir, "hooks.db");
4372
+ }
4373
+ function ensureDir(dbPath) {
4374
+ const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
4375
+ if (dir && !existsSync4(dir)) {
4376
+ mkdirSync3(dir, { recursive: true });
4377
+ }
4378
+ }
4379
+ function getDb() {
4380
+ if (instance)
4381
+ return instance;
4382
+ const dbPath = getDbPath();
4383
+ const isNew = dbPath === ":memory:" || !existsSync4(dbPath);
4384
+ ensureDir(dbPath);
4385
+ instance = new Database(dbPath);
4386
+ instance.exec("PRAGMA journal_mode=WAL");
4387
+ instance.exec("PRAGMA foreign_keys=ON");
4388
+ runMigrations(instance);
4389
+ runRetention(instance);
4390
+ if (isNew) {
4391
+ runLegacyImport(instance);
4392
+ }
4393
+ return instance;
4394
+ }
4395
+ function closeDb() {
4396
+ if (instance) {
4397
+ instance.close();
4398
+ instance = null;
4399
+ }
4400
+ }
4401
+ function createTestDb() {
4402
+ const db = new Database(":memory:");
4403
+ db.exec("PRAGMA journal_mode=WAL");
4404
+ db.exec("PRAGMA foreign_keys=ON");
4405
+ return db;
4406
+ }
4407
+ var instance = null;
4408
+ var init_db = __esm(() => {
4409
+ init_migrations();
4410
+ init_legacy_import();
4411
+ });
4412
+
4155
4413
  // src/mcp/server.ts
4156
4414
  var exports_server = {};
4157
4415
  __export(exports_server, {
@@ -4165,8 +4423,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4165
4423
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4166
4424
  import { z } from "zod";
4167
4425
  import { createServer } from "http";
4168
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4169
- import { join as join3, dirname as dirname2 } from "path";
4426
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
4427
+ import { join as join5, dirname as dirname2 } from "path";
4170
4428
  import { fileURLToPath as fileURLToPath2 } from "url";
4171
4429
  function formatInstallResults(results, extra) {
4172
4430
  const installed = results.filter((r) => r.success).map((r) => r.hook);
@@ -4263,7 +4521,7 @@ function createHooksServer() {
4263
4521
  const settingsPath = getSettingsPath(scope);
4264
4522
  const issues = [];
4265
4523
  const healthy = [];
4266
- const settingsExist = existsSync3(settingsPath);
4524
+ const settingsExist = existsSync5(settingsPath);
4267
4525
  if (!settingsExist) {
4268
4526
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
4269
4527
  }
@@ -4276,13 +4534,13 @@ function createHooksServer() {
4276
4534
  continue;
4277
4535
  }
4278
4536
  const hookDir = getHookPath(name);
4279
- if (!existsSync3(join3(hookDir, "src", "hook.ts"))) {
4537
+ if (!existsSync5(join5(hookDir, "src", "hook.ts"))) {
4280
4538
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
4281
4539
  hookHealthy = false;
4282
4540
  }
4283
4541
  if (meta && settingsExist) {
4284
4542
  try {
4285
- const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
4543
+ const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
4286
4544
  const eventHooks = settings.hooks?.[meta.event] || [];
4287
4545
  const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
4288
4546
  const match = h.command?.match(/^hooks run (\w+)/);
@@ -4313,10 +4571,10 @@ function createHooksServer() {
4313
4571
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4314
4572
  }
4315
4573
  const hookPath = getHookPath(name);
4316
- const readmePath = join3(hookPath, "README.md");
4574
+ const readmePath = join5(hookPath, "README.md");
4317
4575
  let readme = "";
4318
- if (existsSync3(readmePath)) {
4319
- readme = readFileSync3(readmePath, "utf-8");
4576
+ if (existsSync5(readmePath)) {
4577
+ readme = readFileSync4(readmePath, "utf-8");
4320
4578
  }
4321
4579
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
4322
4580
  }
@@ -4365,8 +4623,8 @@ function createHooksServer() {
4365
4623
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4366
4624
  }
4367
4625
  const hookDir = getHookPath(name);
4368
- const hookScript = join3(hookDir, "src", "hook.ts");
4369
- if (!existsSync3(hookScript)) {
4626
+ const hookScript = join5(hookDir, "src", "hook.ts");
4627
+ if (!existsSync5(hookScript)) {
4370
4628
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
4371
4629
  }
4372
4630
  let hookInput = { ...input };
@@ -4457,7 +4715,7 @@ function createHooksServer() {
4457
4715
  const ctx = {
4458
4716
  scope,
4459
4717
  settings_path: settingsPath,
4460
- settings_exists: existsSync3(settingsPath),
4718
+ settings_exists: existsSync5(settingsPath),
4461
4719
  registered_hooks: hooks,
4462
4720
  hook_count: hooks.length,
4463
4721
  healthy,
@@ -4495,8 +4753,8 @@ function createHooksServer() {
4495
4753
  const input = { tool_name, tool_input };
4496
4754
  const results = await Promise.all(matchingHooks.map(async (name) => {
4497
4755
  const hookDir = getHookPath(name);
4498
- const hookScript = join3(hookDir, "src", "hook.ts");
4499
- if (!existsSync3(hookScript))
4756
+ const hookScript = join5(hookDir, "src", "hook.ts");
4757
+ if (!existsSync5(hookScript))
4500
4758
  return { name, decision: "approve", error: "script not found" };
4501
4759
  const proc = Bun.spawn(["bun", "run", hookScript], {
4502
4760
  stdin: new Response(JSON.stringify(input)),
@@ -4564,8 +4822,8 @@ function createHooksServer() {
4564
4822
  const meta = getHook(name);
4565
4823
  if (!meta)
4566
4824
  return { name, error: `Hook '${name}' not found` };
4567
- const hookScript = join3(getHookPath(name), "src", "hook.ts");
4568
- if (!existsSync3(hookScript))
4825
+ const hookScript = join5(getHookPath(name), "src", "hook.ts");
4826
+ if (!existsSync5(hookScript))
4569
4827
  return { name, error: "script not found" };
4570
4828
  const proc = Bun.spawn(["bun", "run", hookScript], {
4571
4829
  stdin: new Response(JSON.stringify(input)),
@@ -4598,8 +4856,8 @@ function createHooksServer() {
4598
4856
  const settingsPath = getSettingsPath(scope);
4599
4857
  let settings = {};
4600
4858
  try {
4601
- if (existsSync3(settingsPath))
4602
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
4859
+ if (existsSync5(settingsPath))
4860
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
4603
4861
  } catch {}
4604
4862
  if (!settings.hooks)
4605
4863
  settings.hooks = {};
@@ -4607,9 +4865,9 @@ function createHooksServer() {
4607
4865
  if (!disabled.includes(name))
4608
4866
  disabled.push(name);
4609
4867
  settings.hooks.__disabled = disabled;
4610
- const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = await import("fs");
4868
+ const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4 } = await import("fs");
4611
4869
  const { dirname: dirname3 } = await import("path");
4612
- mkdirSync3(dirname3(settingsPath), { recursive: true });
4870
+ mkdirSync4(dirname3(settingsPath), { recursive: true });
4613
4871
  writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
4614
4872
  `);
4615
4873
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -4621,8 +4879,8 @@ function createHooksServer() {
4621
4879
  const settingsPath = getSettingsPath(scope);
4622
4880
  let settings = {};
4623
4881
  try {
4624
- if (existsSync3(settingsPath))
4625
- settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
4882
+ if (existsSync5(settingsPath))
4883
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
4626
4884
  } catch {}
4627
4885
  if (settings.hooks?.__disabled) {
4628
4886
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
@@ -4645,6 +4903,104 @@ function createHooksServer() {
4645
4903
  const profiles = listProfiles();
4646
4904
  return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
4647
4905
  });
4906
+ server.tool("hooks_log_list", "List hook events from SQLite (~/.hooks/hooks.db). Filter by hook name, session ID, or time range.", {
4907
+ hook_name: z.string().optional().describe("Filter by hook name (e.g. 'sessionlog', 'costwatch')"),
4908
+ session_id: z.string().optional().describe("Filter by session ID prefix"),
4909
+ limit: z.number().default(50).describe("Max number of events to return"),
4910
+ since: z.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from")
4911
+ }, async ({ hook_name, session_id, limit, since }) => {
4912
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
4913
+ const db = getDb2();
4914
+ function parseDuration(s) {
4915
+ const m = s.match(/^(\d+)(s|m|h|d)$/);
4916
+ if (!m)
4917
+ return null;
4918
+ const n = parseInt(m[1]);
4919
+ const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
4920
+ return new Date(Date.now() - n * ms).toISOString();
4921
+ }
4922
+ let sql = "SELECT * FROM hook_events WHERE 1=1";
4923
+ const params = [];
4924
+ if (hook_name) {
4925
+ sql += " AND hook_name = ?";
4926
+ params.push(hook_name);
4927
+ }
4928
+ if (session_id) {
4929
+ sql += " AND session_id LIKE ?";
4930
+ params.push(`${session_id}%`);
4931
+ }
4932
+ if (since) {
4933
+ const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
4934
+ if (ts) {
4935
+ sql += " AND timestamp >= ?";
4936
+ params.push(ts);
4937
+ }
4938
+ }
4939
+ sql += " ORDER BY timestamp DESC LIMIT ?";
4940
+ params.push(limit);
4941
+ const rows = db.query(sql).all(...params);
4942
+ return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
4943
+ });
4944
+ server.tool("hooks_log_tail", "Show the most recent hook events from SQLite.", {
4945
+ n: z.number().default(20).describe("Number of most recent events to return")
4946
+ }, async ({ n }) => {
4947
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
4948
+ const db = getDb2();
4949
+ const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(n);
4950
+ return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
4951
+ });
4952
+ server.tool("hooks_log_errors", "Show hook events that contain errors, optionally filtered by time range.", {
4953
+ since: z.string().default("24h").describe("Duration string (e.g. '1h', '30m', '7d') or ISO timestamp"),
4954
+ limit: z.number().default(50).describe("Max number of error events to return")
4955
+ }, async ({ since, limit }) => {
4956
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
4957
+ const db = getDb2();
4958
+ function parseDuration(s) {
4959
+ const m = s.match(/^(\d+)(s|m|h|d)$/);
4960
+ if (!m)
4961
+ return s;
4962
+ const n = parseInt(m[1]);
4963
+ const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
4964
+ return new Date(Date.now() - n * ms).toISOString();
4965
+ }
4966
+ const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
4967
+ const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, limit);
4968
+ return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
4969
+ });
4970
+ server.tool("hooks_log_summary", "Summarize hook execution: counts per hook, error rates, and recent activity.", {
4971
+ since: z.string().default("24h").describe("Duration string (e.g. '1h', '24h', '7d') or ISO timestamp")
4972
+ }, async ({ since }) => {
4973
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
4974
+ const db = getDb2();
4975
+ function parseDuration(s) {
4976
+ const m = s.match(/^(\d+)(s|m|h|d)$/);
4977
+ if (!m)
4978
+ return s;
4979
+ const n = parseInt(m[1]);
4980
+ const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
4981
+ return new Date(Date.now() - n * ms).toISOString();
4982
+ }
4983
+ const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
4984
+ const totals = db.query("SELECT hook_name, COUNT(*) as total, SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) as errors FROM hook_events WHERE timestamp >= ? GROUP BY hook_name ORDER BY total DESC").all(ts);
4985
+ const summary = totals.map((r) => ({
4986
+ hook_name: r.hook_name,
4987
+ total: r.total,
4988
+ errors: r.errors,
4989
+ error_rate: r.total > 0 ? (r.errors / r.total * 100).toFixed(1) + "%" : "0%"
4990
+ }));
4991
+ const grandTotal = totals.reduce((s, r) => s + r.total, 0);
4992
+ const grandErrors = totals.reduce((s, r) => s + r.errors, 0);
4993
+ return {
4994
+ content: [{
4995
+ type: "text",
4996
+ text: JSON.stringify({
4997
+ since: ts,
4998
+ hooks: summary,
4999
+ totals: { events: grandTotal, errors: grandErrors, hooks_active: totals.length }
5000
+ })
5001
+ }]
5002
+ };
5003
+ });
4648
5004
  return server;
4649
5005
  }
4650
5006
  async function startSSEServer(port = MCP_PORT) {
@@ -4690,7 +5046,7 @@ var init_server = __esm(() => {
4690
5046
  init_installer();
4691
5047
  init_profiles();
4692
5048
  __dirname3 = dirname2(fileURLToPath2(import.meta.url));
4693
- pkg = JSON.parse(readFileSync3(join3(__dirname3, "..", "..", "package.json"), "utf-8"));
5049
+ pkg = JSON.parse(readFileSync4(join5(__dirname3, "..", "..", "package.json"), "utf-8"));
4694
5050
  });
4695
5051
 
4696
5052
  // src/cli/index.tsx
@@ -4714,8 +5070,8 @@ var {
4714
5070
 
4715
5071
  // src/cli/index.tsx
4716
5072
  import chalk2 from "chalk";
4717
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
4718
- import { join as join4, dirname as dirname3 } from "path";
5073
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
5074
+ import { join as join6, dirname as dirname3 } from "path";
4719
5075
  import { fileURLToPath as fileURLToPath3 } from "url";
4720
5076
 
4721
5077
  // src/cli/components/App.tsx
@@ -5893,8 +6249,8 @@ init_installer();
5893
6249
  init_profiles();
5894
6250
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
5895
6251
  var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
5896
- var pkgPath = existsSync4(join4(__dirname4, "..", "package.json")) ? join4(__dirname4, "..", "package.json") : join4(__dirname4, "..", "..", "package.json");
5897
- var pkg2 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
6252
+ var pkgPath = existsSync6(join6(__dirname4, "..", "package.json")) ? join6(__dirname4, "..", "package.json") : join6(__dirname4, "..", "..", "package.json");
6253
+ var pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
5898
6254
  var program2 = new Command;
5899
6255
  function resolveScope(options) {
5900
6256
  if (options.project)
@@ -5964,8 +6320,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
5964
6320
  process.exit(1);
5965
6321
  }
5966
6322
  const hookDir = getHookPath(hook);
5967
- const hookScript = join4(hookDir, "src", "hook.ts");
5968
- if (!existsSync4(hookScript)) {
6323
+ const hookScript = join6(hookDir, "src", "hook.ts");
6324
+ if (!existsSync6(hookScript)) {
5969
6325
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
5970
6326
  process.exit(1);
5971
6327
  }
@@ -6265,7 +6621,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
6265
6621
  const settingsPath = getSettingsPath(scope);
6266
6622
  const issues = [];
6267
6623
  const healthy = [];
6268
- const settingsExist = existsSync4(settingsPath);
6624
+ const settingsExist = existsSync6(settingsPath);
6269
6625
  if (!settingsExist) {
6270
6626
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
6271
6627
  }
@@ -6279,14 +6635,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
6279
6635
  continue;
6280
6636
  }
6281
6637
  const hookDir = getHookPath(name);
6282
- const hookScript = join4(hookDir, "src", "hook.ts");
6283
- if (!existsSync4(hookScript)) {
6638
+ const hookScript = join6(hookDir, "src", "hook.ts");
6639
+ if (!existsSync6(hookScript)) {
6284
6640
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
6285
6641
  hookHealthy = false;
6286
6642
  }
6287
6643
  if (meta && settingsExist) {
6288
6644
  try {
6289
- const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
6645
+ const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
6290
6646
  const eventHooks = settings.hooks?.[meta.event] || [];
6291
6647
  const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
6292
6648
  const match = h.command?.match(/^hooks run (\w+)/);
@@ -6384,10 +6740,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
6384
6740
  return;
6385
6741
  }
6386
6742
  const hookPath = getHookPath(hook);
6387
- const readmePath = join4(hookPath, "README.md");
6743
+ const readmePath = join6(hookPath, "README.md");
6388
6744
  let readme = "";
6389
- if (existsSync4(readmePath)) {
6390
- readme = readFileSync4(readmePath, "utf-8");
6745
+ if (existsSync6(readmePath)) {
6746
+ readme = readFileSync5(readmePath, "utf-8");
6391
6747
  }
6392
6748
  if (options.json) {
6393
6749
  console.log(JSON.stringify({ ...meta, readme }));
@@ -6569,9 +6925,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
6569
6925
  if (file === "-") {
6570
6926
  raw = await new Response(Bun.stdin.stream()).text();
6571
6927
  } else {
6572
- const { readFileSync: readFileSync5 } = await import("fs");
6928
+ const { readFileSync: readFileSync6 } = await import("fs");
6573
6929
  try {
6574
- raw = readFileSync5(file, "utf-8");
6930
+ raw = readFileSync6(file, "utf-8");
6575
6931
  } catch {
6576
6932
  if (options.json) {
6577
6933
  console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
@@ -6602,6 +6958,154 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
6602
6958
  console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
6603
6959
  }
6604
6960
  });
6961
+ var logCmd = program2.command("log").description("Query hook event logs from SQLite (~/.hooks/hooks.db)");
6962
+ logCmd.command("list").description("List hook events").option("--hook <name>", "Filter by hook name").option("--session <id>", "Filter by session ID").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
6963
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
6964
+ const db = getDb2();
6965
+ const limit = parseInt(options.limit) || 50;
6966
+ let sql = "SELECT * FROM hook_events WHERE 1=1";
6967
+ const params = [];
6968
+ if (options.hook) {
6969
+ sql += " AND hook_name = ?";
6970
+ params.push(options.hook);
6971
+ }
6972
+ if (options.session) {
6973
+ sql += " AND session_id LIKE ?";
6974
+ params.push(`${options.session}%`);
6975
+ }
6976
+ sql += " ORDER BY timestamp DESC LIMIT ?";
6977
+ params.push(String(limit));
6978
+ const rows = db.query(sql).all(...params);
6979
+ if (options.json) {
6980
+ console.log(JSON.stringify(rows, null, 2));
6981
+ return;
6982
+ }
6983
+ if (rows.length === 0) {
6984
+ console.log(chalk2.dim("No events found."));
6985
+ return;
6986
+ }
6987
+ console.log(chalk2.bold(`
6988
+ Hook Events (${rows.length})
6989
+ `));
6990
+ for (const row of rows) {
6991
+ const ts = row.timestamp.slice(0, 19).replace("T", " ");
6992
+ const err = row.error ? chalk2.red(` ERR: ${row.error.slice(0, 60)}`) : "";
6993
+ const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
6994
+ console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
6995
+ }
6996
+ console.log();
6997
+ });
6998
+ logCmd.command("search <text>").description("Search hook events by tool_input or error text").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (text, options) => {
6999
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
7000
+ const db = getDb2();
7001
+ const limit = parseInt(options.limit) || 50;
7002
+ const q = `%${text}%`;
7003
+ const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
7004
+ if (options.json) {
7005
+ console.log(JSON.stringify(rows, null, 2));
7006
+ return;
7007
+ }
7008
+ if (rows.length === 0) {
7009
+ console.log(chalk2.dim(`No events matching "${text}".`));
7010
+ return;
7011
+ }
7012
+ console.log(chalk2.bold(`
7013
+ Search results for "${text}" (${rows.length})
7014
+ `));
7015
+ for (const row of rows) {
7016
+ const ts = row.timestamp.slice(0, 19).replace("T", " ");
7017
+ const snippet = (row.tool_input || row.error || "").slice(0, 80);
7018
+ console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
7019
+ }
7020
+ console.log();
7021
+ });
7022
+ logCmd.command("tail").description("Show most recent hook events").option("-n <n>", "Number of rows", "20").option("-j, --json", "Output as JSON", false).action(async (options) => {
7023
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
7024
+ const db = getDb2();
7025
+ const limit = parseInt(options.n) || 20;
7026
+ const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
7027
+ if (options.json) {
7028
+ console.log(JSON.stringify(rows, null, 2));
7029
+ return;
7030
+ }
7031
+ if (rows.length === 0) {
7032
+ console.log(chalk2.dim("No events yet."));
7033
+ return;
7034
+ }
7035
+ console.log(chalk2.bold(`
7036
+ Last ${rows.length} events
7037
+ `));
7038
+ for (const row of rows) {
7039
+ const ts = row.timestamp.slice(0, 19).replace("T", " ");
7040
+ const err = row.error ? chalk2.red(` \u2717 ${row.error.slice(0, 60)}`) : "";
7041
+ const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
7042
+ console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
7043
+ }
7044
+ console.log();
7045
+ });
7046
+ logCmd.command("errors").description("Show hook events that contain errors").option("--since <duration>", "Only show errors since this duration (e.g. 1h, 30m, 7d)", "24h").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
7047
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
7048
+ const db = getDb2();
7049
+ const limit = parseInt(options.limit) || 50;
7050
+ function parseDuration(s) {
7051
+ const m = s.match(/^(\d+)(s|m|h|d)$/);
7052
+ if (!m)
7053
+ return 86400000;
7054
+ const n = parseInt(m[1]);
7055
+ switch (m[2]) {
7056
+ case "s":
7057
+ return n * 1000;
7058
+ case "m":
7059
+ return n * 60 * 1000;
7060
+ case "h":
7061
+ return n * 60 * 60 * 1000;
7062
+ case "d":
7063
+ return n * 24 * 60 * 60 * 1000;
7064
+ default:
7065
+ return 86400000;
7066
+ }
7067
+ }
7068
+ const since = new Date(Date.now() - parseDuration(options.since)).toISOString();
7069
+ const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(since, limit);
7070
+ if (options.json) {
7071
+ console.log(JSON.stringify(rows, null, 2));
7072
+ return;
7073
+ }
7074
+ if (rows.length === 0) {
7075
+ console.log(chalk2.dim(`No errors in the last ${options.since}.`));
7076
+ return;
7077
+ }
7078
+ console.log(chalk2.bold(`
7079
+ Errors (last ${options.since}, ${rows.length} found)
7080
+ `));
7081
+ for (const row of rows) {
7082
+ const ts = row.timestamp.slice(0, 19).replace("T", " ");
7083
+ console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(row.error.slice(0, 100))}`);
7084
+ }
7085
+ console.log();
7086
+ });
7087
+ logCmd.command("clear").description("Delete hook event logs").option("--hook <name>", "Only delete events for this hook").option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
7088
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
7089
+ const db = getDb2();
7090
+ const countRow = options.hook ? db.query("SELECT COUNT(*) as n FROM hook_events WHERE hook_name = ?").get(options.hook) : db.query("SELECT COUNT(*) as n FROM hook_events").get();
7091
+ const count = countRow?.n ?? 0;
7092
+ if (count === 0) {
7093
+ console.log(chalk2.dim("Nothing to clear."));
7094
+ return;
7095
+ }
7096
+ if (!options.yes) {
7097
+ const scope = options.hook ? `hook "${options.hook}"` : "all hooks";
7098
+ console.log(chalk2.yellow(`About to delete ${count} event(s) for ${scope}.`));
7099
+ console.log(chalk2.dim("Re-run with --yes to confirm."));
7100
+ return;
7101
+ }
7102
+ if (options.hook) {
7103
+ db.run("DELETE FROM hook_events WHERE hook_name = ?", [options.hook]);
7104
+ } else {
7105
+ db.run("DELETE FROM hook_events");
7106
+ }
7107
+ console.log(chalk2.green(`\u2713 Cleared ${count} event(s).`));
7108
+ });
6605
7109
  program2.command("mcp").option("-s, --stdio", "Use stdio transport (for agent MCP integration)", false).option("-p, --port <port>", "Port for SSE transport", "39427").description("Start MCP server for AI agent integration").action(async (options) => {
6606
7110
  if (options.stdio) {
6607
7111
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));