@hasna/hooks 0.2.5 → 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);
@@ -4183,13 +4441,20 @@ function createHooksServer() {
4183
4441
  name: "@hasna/hooks",
4184
4442
  version: pkg.version
4185
4443
  });
4186
- server.tool("hooks_list", "List all available hooks, optionally filtered by category", { category: z.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security', 'Notifications', 'Context Management')") }, async ({ category }) => {
4444
+ server.tool("hooks_list", "List all available hooks, optionally filtered by category. Use compact:true to get minimal output (name+event+matcher only) \u2014 saves tokens.", {
4445
+ category: z.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security')"),
4446
+ compact: z.boolean().default(false).describe("Return minimal fields only: name, event, matcher. Reduces token usage.")
4447
+ }, async ({ category, compact }) => {
4448
+ const slim = (hooks) => compact ? hooks.map((h) => ({ name: h.name, event: h.event, matcher: h.matcher })) : hooks;
4187
4449
  if (category) {
4188
4450
  const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
4189
4451
  if (!cat) {
4190
4452
  return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown category: ${category}`, available: [...CATEGORIES] }) }] };
4191
4453
  }
4192
- return { content: [{ type: "text", text: JSON.stringify(getHooksByCategory(cat)) }] };
4454
+ return { content: [{ type: "text", text: JSON.stringify(slim(getHooksByCategory(cat))) }] };
4455
+ }
4456
+ if (compact) {
4457
+ return { content: [{ type: "text", text: JSON.stringify(slim(HOOKS)) }] };
4193
4458
  }
4194
4459
  const result = {};
4195
4460
  for (const cat of CATEGORIES) {
@@ -4197,9 +4462,13 @@ function createHooksServer() {
4197
4462
  }
4198
4463
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
4199
4464
  });
4200
- server.tool("hooks_search", "Search for hooks by name, description, or tags", { query: z.string().describe("Search query") }, async ({ query }) => {
4465
+ server.tool("hooks_search", "Search for hooks by name, description, or tags. Use compact:true for minimal output to save tokens.", {
4466
+ query: z.string().describe("Search query"),
4467
+ compact: z.boolean().default(false).describe("Return minimal fields only: name, event, matcher.")
4468
+ }, async ({ query, compact }) => {
4201
4469
  const results = searchHooks(query);
4202
- return { content: [{ type: "text", text: JSON.stringify(results) }] };
4470
+ const out = compact ? results.map((h) => ({ name: h.name, event: h.event, matcher: h.matcher })) : results;
4471
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
4203
4472
  });
4204
4473
  server.tool("hooks_info", "Get detailed information about a specific hook including install status", { name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')") }, async ({ name }) => {
4205
4474
  const meta = getHook(name);
@@ -4252,7 +4521,7 @@ function createHooksServer() {
4252
4521
  const settingsPath = getSettingsPath(scope);
4253
4522
  const issues = [];
4254
4523
  const healthy = [];
4255
- const settingsExist = existsSync3(settingsPath);
4524
+ const settingsExist = existsSync5(settingsPath);
4256
4525
  if (!settingsExist) {
4257
4526
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
4258
4527
  }
@@ -4265,13 +4534,13 @@ function createHooksServer() {
4265
4534
  continue;
4266
4535
  }
4267
4536
  const hookDir = getHookPath(name);
4268
- if (!existsSync3(join3(hookDir, "src", "hook.ts"))) {
4537
+ if (!existsSync5(join5(hookDir, "src", "hook.ts"))) {
4269
4538
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
4270
4539
  hookHealthy = false;
4271
4540
  }
4272
4541
  if (meta && settingsExist) {
4273
4542
  try {
4274
- const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
4543
+ const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
4275
4544
  const eventHooks = settings.hooks?.[meta.event] || [];
4276
4545
  const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
4277
4546
  const match = h.command?.match(/^hooks run (\w+)/);
@@ -4286,7 +4555,7 @@ function createHooksServer() {
4286
4555
  if (hookHealthy)
4287
4556
  healthy.push(name);
4288
4557
  }
4289
- return { content: [{ type: "text", text: JSON.stringify({ healthy, issues, registered, scope }) }] };
4558
+ return { content: [{ type: "text", text: JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }) }] };
4290
4559
  });
4291
4560
  server.tool("hooks_categories", "List all hook categories with counts", {}, async () => {
4292
4561
  const result = CATEGORIES.map((cat) => ({
@@ -4302,10 +4571,10 @@ function createHooksServer() {
4302
4571
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4303
4572
  }
4304
4573
  const hookPath = getHookPath(name);
4305
- const readmePath = join3(hookPath, "README.md");
4574
+ const readmePath = join5(hookPath, "README.md");
4306
4575
  let readme = "";
4307
- if (existsSync3(readmePath)) {
4308
- readme = readFileSync3(readmePath, "utf-8");
4576
+ if (existsSync5(readmePath)) {
4577
+ readme = readFileSync4(readmePath, "utf-8");
4309
4578
  }
4310
4579
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
4311
4580
  }
@@ -4339,7 +4608,7 @@ function createHooksServer() {
4339
4608
  const registered = getRegisteredHooks(scope);
4340
4609
  const result = registered.map((name) => {
4341
4610
  const meta = getHook(name);
4342
- return { name, event: meta?.event, version: meta?.version, description: meta?.description };
4611
+ return { name, event: meta?.event, matcher: meta?.matcher ?? "", version: meta?.version, description: meta?.description };
4343
4612
  });
4344
4613
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
4345
4614
  });
@@ -4354,8 +4623,8 @@ function createHooksServer() {
4354
4623
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4355
4624
  }
4356
4625
  const hookDir = getHookPath(name);
4357
- const hookScript = join3(hookDir, "src", "hook.ts");
4358
- if (!existsSync3(hookScript)) {
4626
+ const hookScript = join5(hookDir, "src", "hook.ts");
4627
+ if (!existsSync5(hookScript)) {
4359
4628
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook script not found: ${hookScript}` }) }] };
4360
4629
  }
4361
4630
  let hookInput = { ...input };
@@ -4426,6 +4695,203 @@ function createHooksServer() {
4426
4695
  const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
4427
4696
  return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
4428
4697
  });
4698
+ server.tool("hooks_context", "Get full agent context in one call: installed hooks (with event+matcher), active profile, settings path, and doctor status. Call this once at session start instead of making 4 separate calls.", {
4699
+ scope: z.enum(["global", "project"]).default("global").describe("Scope to inspect"),
4700
+ profile: z.string().optional().describe("Agent profile ID to include in context")
4701
+ }, async ({ scope, profile }) => {
4702
+ const settingsPath = getSettingsPath(scope);
4703
+ const registered = getRegisteredHooks(scope);
4704
+ const hooks = registered.map((name) => {
4705
+ const meta = getHook(name);
4706
+ return { name, event: meta?.event, matcher: meta?.matcher ?? "", version: meta?.version };
4707
+ });
4708
+ const issues = [];
4709
+ for (const name of registered) {
4710
+ if (!hookExists(name)) {
4711
+ issues.push({ hook: name, issue: "Hook not found in package", severity: "error" });
4712
+ }
4713
+ }
4714
+ const healthy = issues.length === 0;
4715
+ const ctx = {
4716
+ scope,
4717
+ settings_path: settingsPath,
4718
+ settings_exists: existsSync5(settingsPath),
4719
+ registered_hooks: hooks,
4720
+ hook_count: hooks.length,
4721
+ healthy,
4722
+ issues,
4723
+ version: pkg.version
4724
+ };
4725
+ if (profile) {
4726
+ const p = getProfile(profile);
4727
+ ctx.profile = p ?? null;
4728
+ }
4729
+ return { content: [{ type: "text", text: JSON.stringify(ctx) }] };
4730
+ });
4731
+ server.tool("hooks_preview", "Simulate which installed PreToolUse hooks would fire for a given tool call and what decision each returns. Use this to understand your hook environment before taking an action.", {
4732
+ tool_name: z.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
4733
+ tool_input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
4734
+ scope: z.enum(["global", "project"]).default("global").describe("Scope to check"),
4735
+ timeout_ms: z.number().default(5000).describe("Per-hook timeout in milliseconds")
4736
+ }, async ({ tool_name, tool_input, scope, timeout_ms }) => {
4737
+ const registered = getRegisteredHooks(scope);
4738
+ const matchingHooks = registered.filter((name) => {
4739
+ const meta = getHook(name);
4740
+ if (!meta || meta.event !== "PreToolUse")
4741
+ return false;
4742
+ if (!meta.matcher)
4743
+ return true;
4744
+ try {
4745
+ return new RegExp(meta.matcher).test(tool_name);
4746
+ } catch {
4747
+ return false;
4748
+ }
4749
+ });
4750
+ if (matchingHooks.length === 0) {
4751
+ return { content: [{ type: "text", text: JSON.stringify({ tool_name, matching_hooks: [], result: "no_hooks_match", decision: "approve" }) }] };
4752
+ }
4753
+ const input = { tool_name, tool_input };
4754
+ const results = await Promise.all(matchingHooks.map(async (name) => {
4755
+ const hookDir = getHookPath(name);
4756
+ const hookScript = join5(hookDir, "src", "hook.ts");
4757
+ if (!existsSync5(hookScript))
4758
+ return { name, decision: "approve", error: "script not found" };
4759
+ const proc = Bun.spawn(["bun", "run", hookScript], {
4760
+ stdin: new Response(JSON.stringify(input)),
4761
+ stdout: "pipe",
4762
+ stderr: "pipe",
4763
+ env: process.env
4764
+ });
4765
+ const timeout = new Promise((r) => setTimeout(() => r(null), timeout_ms));
4766
+ const res = await Promise.race([
4767
+ Promise.all([new Response(proc.stdout).text(), proc.exited]).then(([stdout]) => ({ stdout, timedOut: false })),
4768
+ timeout.then(() => {
4769
+ proc.kill();
4770
+ return { stdout: "", timedOut: true };
4771
+ })
4772
+ ]);
4773
+ if (res.timedOut)
4774
+ return { name, decision: "approve", timedOut: true };
4775
+ let output = {};
4776
+ try {
4777
+ output = JSON.parse(res.stdout);
4778
+ } catch {}
4779
+ return { name, decision: output.decision ?? "approve", reason: output.reason, raw: output };
4780
+ }));
4781
+ const blocked = results.find((r) => r.decision === "block");
4782
+ return {
4783
+ content: [{
4784
+ type: "text",
4785
+ text: JSON.stringify({
4786
+ tool_name,
4787
+ matching_hooks: matchingHooks,
4788
+ results,
4789
+ decision: blocked ? "block" : "approve",
4790
+ blocked_by: blocked?.name ?? null,
4791
+ blocked_reason: blocked?.reason ?? null
4792
+ })
4793
+ }]
4794
+ };
4795
+ });
4796
+ server.tool("hooks_setup", "Single-shot agent onboarding: create an agent profile + install recommended hooks in one call. Ideal for agents setting up hooks at session start.", {
4797
+ agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
4798
+ name: z.string().optional().describe("Optional display name for the agent"),
4799
+ hooks: z.array(z.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
4800
+ scope: z.enum(["global", "project"]).default("global").describe("Install scope")
4801
+ }, async ({ agent_type, name, hooks, scope }) => {
4802
+ const profile = createProfile({ agent_type, name });
4803
+ const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
4804
+ const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id }));
4805
+ const installed = results.filter((r) => r.success).map((r) => r.hook);
4806
+ const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
4807
+ return {
4808
+ content: [{
4809
+ type: "text",
4810
+ text: JSON.stringify({ profile, installed, failed, scope, run_with: `hooks run <name> --profile ${profile.agent_id}` })
4811
+ }]
4812
+ };
4813
+ });
4814
+ server.tool("hooks_batch_run", "Run multiple hooks in parallel in a single call. Returns all results at once \u2014 more efficient than N separate hooks_run calls.", {
4815
+ hooks: z.array(z.object({
4816
+ name: z.string().describe("Hook name"),
4817
+ input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input JSON")
4818
+ })).describe("List of hooks to run with their inputs"),
4819
+ timeout_ms: z.number().default(1e4).describe("Per-hook timeout in milliseconds")
4820
+ }, async ({ hooks, timeout_ms }) => {
4821
+ const results = await Promise.all(hooks.map(async ({ name, input }) => {
4822
+ const meta = getHook(name);
4823
+ if (!meta)
4824
+ return { name, error: `Hook '${name}' not found` };
4825
+ const hookScript = join5(getHookPath(name), "src", "hook.ts");
4826
+ if (!existsSync5(hookScript))
4827
+ return { name, error: "script not found" };
4828
+ const proc = Bun.spawn(["bun", "run", hookScript], {
4829
+ stdin: new Response(JSON.stringify(input)),
4830
+ stdout: "pipe",
4831
+ stderr: "pipe",
4832
+ env: process.env
4833
+ });
4834
+ const timeout = new Promise((r) => setTimeout(() => r(null), timeout_ms));
4835
+ const res = await Promise.race([
4836
+ Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]).then(([stdout, stderr, exitCode]) => ({ stdout, stderr, exitCode, timedOut: false })),
4837
+ timeout.then(() => {
4838
+ proc.kill();
4839
+ return { stdout: "", stderr: "", exitCode: -1, timedOut: true };
4840
+ })
4841
+ ]);
4842
+ let output = {};
4843
+ try {
4844
+ output = JSON.parse(res.stdout);
4845
+ } catch {
4846
+ output = res.stdout ? { raw: res.stdout } : {};
4847
+ }
4848
+ return { name, output, exitCode: res.exitCode, ...res.timedOut ? { timedOut: true } : {} };
4849
+ }));
4850
+ return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
4851
+ });
4852
+ server.tool("hooks_disable", "Temporarily disable a registered hook without removing it. Stores disabled list in settings under hooks.__disabled.", {
4853
+ name: z.string().describe("Hook name to disable"),
4854
+ scope: z.enum(["global", "project"]).default("global").describe("Scope")
4855
+ }, async ({ name, scope }) => {
4856
+ const settingsPath = getSettingsPath(scope);
4857
+ let settings = {};
4858
+ try {
4859
+ if (existsSync5(settingsPath))
4860
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
4861
+ } catch {}
4862
+ if (!settings.hooks)
4863
+ settings.hooks = {};
4864
+ const disabled = settings.hooks.__disabled ?? [];
4865
+ if (!disabled.includes(name))
4866
+ disabled.push(name);
4867
+ settings.hooks.__disabled = disabled;
4868
+ const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4 } = await import("fs");
4869
+ const { dirname: dirname3 } = await import("path");
4870
+ mkdirSync4(dirname3(settingsPath), { recursive: true });
4871
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
4872
+ `);
4873
+ return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
4874
+ });
4875
+ server.tool("hooks_enable", "Re-enable a previously disabled hook.", {
4876
+ name: z.string().describe("Hook name to enable"),
4877
+ scope: z.enum(["global", "project"]).default("global").describe("Scope")
4878
+ }, async ({ name, scope }) => {
4879
+ const settingsPath = getSettingsPath(scope);
4880
+ let settings = {};
4881
+ try {
4882
+ if (existsSync5(settingsPath))
4883
+ settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
4884
+ } catch {}
4885
+ if (settings.hooks?.__disabled) {
4886
+ settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
4887
+ if (settings.hooks.__disabled.length === 0)
4888
+ delete settings.hooks.__disabled;
4889
+ const { writeFileSync: writeFileSync3 } = await import("fs");
4890
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + `
4891
+ `);
4892
+ }
4893
+ return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
4894
+ });
4429
4895
  server.tool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
4430
4896
  agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
4431
4897
  name: z.string().optional().describe("Optional display name for the agent")
@@ -4437,6 +4903,104 @@ function createHooksServer() {
4437
4903
  const profiles = listProfiles();
4438
4904
  return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
4439
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
+ });
4440
5004
  return server;
4441
5005
  }
4442
5006
  async function startSSEServer(port = MCP_PORT) {
@@ -4482,7 +5046,7 @@ var init_server = __esm(() => {
4482
5046
  init_installer();
4483
5047
  init_profiles();
4484
5048
  __dirname3 = dirname2(fileURLToPath2(import.meta.url));
4485
- pkg = JSON.parse(readFileSync3(join3(__dirname3, "..", "..", "package.json"), "utf-8"));
5049
+ pkg = JSON.parse(readFileSync4(join5(__dirname3, "..", "..", "package.json"), "utf-8"));
4486
5050
  });
4487
5051
 
4488
5052
  // src/cli/index.tsx
@@ -4506,8 +5070,8 @@ var {
4506
5070
 
4507
5071
  // src/cli/index.tsx
4508
5072
  import chalk2 from "chalk";
4509
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
4510
- 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";
4511
5075
  import { fileURLToPath as fileURLToPath3 } from "url";
4512
5076
 
4513
5077
  // src/cli/components/App.tsx
@@ -5685,8 +6249,8 @@ init_installer();
5685
6249
  init_profiles();
5686
6250
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
5687
6251
  var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
5688
- var pkgPath = existsSync4(join4(__dirname4, "..", "package.json")) ? join4(__dirname4, "..", "package.json") : join4(__dirname4, "..", "..", "package.json");
5689
- 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"));
5690
6254
  var program2 = new Command;
5691
6255
  function resolveScope(options) {
5692
6256
  if (options.project)
@@ -5756,8 +6320,8 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
5756
6320
  process.exit(1);
5757
6321
  }
5758
6322
  const hookDir = getHookPath(hook);
5759
- const hookScript = join4(hookDir, "src", "hook.ts");
5760
- if (!existsSync4(hookScript)) {
6323
+ const hookScript = join6(hookDir, "src", "hook.ts");
6324
+ if (!existsSync6(hookScript)) {
5761
6325
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
5762
6326
  process.exit(1);
5763
6327
  }
@@ -6057,7 +6621,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
6057
6621
  const settingsPath = getSettingsPath(scope);
6058
6622
  const issues = [];
6059
6623
  const healthy = [];
6060
- const settingsExist = existsSync4(settingsPath);
6624
+ const settingsExist = existsSync6(settingsPath);
6061
6625
  if (!settingsExist) {
6062
6626
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
6063
6627
  }
@@ -6071,14 +6635,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
6071
6635
  continue;
6072
6636
  }
6073
6637
  const hookDir = getHookPath(name);
6074
- const hookScript = join4(hookDir, "src", "hook.ts");
6075
- if (!existsSync4(hookScript)) {
6638
+ const hookScript = join6(hookDir, "src", "hook.ts");
6639
+ if (!existsSync6(hookScript)) {
6076
6640
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
6077
6641
  hookHealthy = false;
6078
6642
  }
6079
6643
  if (meta && settingsExist) {
6080
6644
  try {
6081
- const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
6645
+ const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
6082
6646
  const eventHooks = settings.hooks?.[meta.event] || [];
6083
6647
  const found = eventHooks.some((entry) => entry.hooks?.some((h) => {
6084
6648
  const match = h.command?.match(/^hooks run (\w+)/);
@@ -6095,7 +6659,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
6095
6659
  }
6096
6660
  }
6097
6661
  if (options.json) {
6098
- console.log(JSON.stringify({ healthy, issues, registered, scope }));
6662
+ console.log(JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }));
6099
6663
  return;
6100
6664
  }
6101
6665
  console.log(chalk2.bold(`
@@ -6176,10 +6740,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
6176
6740
  return;
6177
6741
  }
6178
6742
  const hookPath = getHookPath(hook);
6179
- const readmePath = join4(hookPath, "README.md");
6743
+ const readmePath = join6(hookPath, "README.md");
6180
6744
  let readme = "";
6181
- if (existsSync4(readmePath)) {
6182
- readme = readFileSync4(readmePath, "utf-8");
6745
+ if (existsSync6(readmePath)) {
6746
+ readme = readFileSync5(readmePath, "utf-8");
6183
6747
  }
6184
6748
  if (options.json) {
6185
6749
  console.log(JSON.stringify({ ...meta, readme }));
@@ -6361,9 +6925,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
6361
6925
  if (file === "-") {
6362
6926
  raw = await new Response(Bun.stdin.stream()).text();
6363
6927
  } else {
6364
- const { readFileSync: readFileSync5 } = await import("fs");
6928
+ const { readFileSync: readFileSync6 } = await import("fs");
6365
6929
  try {
6366
- raw = readFileSync5(file, "utf-8");
6930
+ raw = readFileSync6(file, "utf-8");
6367
6931
  } catch {
6368
6932
  if (options.json) {
6369
6933
  console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
@@ -6394,6 +6958,154 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
6394
6958
  console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
6395
6959
  }
6396
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
+ });
6397
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) => {
6398
7110
  if (options.stdio) {
6399
7111
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));