@hasna/hooks 0.3.10 → 0.4.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/bin/index.js CHANGED
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
861
861
  this._exitCallback = (err) => {
862
862
  if (err.code !== "commander.executeSubCommandAsync") {
863
863
  throw err;
864
- } else {}
864
+ }
865
865
  };
866
866
  }
867
867
  return this;
@@ -5346,41 +5346,83 @@ function formatInstallResults(results, extra) {
5346
5346
  }]
5347
5347
  };
5348
5348
  }
5349
+ function truncateText(value, max = 160) {
5350
+ const text = String(value ?? "").replace(/\s+/g, " ").trim();
5351
+ if (text.length <= max)
5352
+ return text;
5353
+ return `${text.slice(0, Math.max(0, max - 3))}...`;
5354
+ }
5355
+ function boundedLimit(value, fallback, max) {
5356
+ if (!Number.isFinite(value) || !value || value <= 0)
5357
+ return fallback;
5358
+ return Math.min(Math.floor(value), max);
5359
+ }
5360
+ function compactHook(hook) {
5361
+ return {
5362
+ name: hook.name,
5363
+ event: hook.event,
5364
+ matcher: hook.matcher,
5365
+ category: hook.category
5366
+ };
5367
+ }
5368
+ function compactHookResult(hooks, limit, detail) {
5369
+ const visible = hooks.slice(0, limit).map(compactHook);
5370
+ const omitted = Math.max(0, hooks.length - visible.length);
5371
+ return {
5372
+ hooks: visible,
5373
+ count: visible.length,
5374
+ total: hooks.length,
5375
+ omitted,
5376
+ hint: omitted > 0 ? `Use limit, compact:false, or ${detail} for more detail.` : `Use compact:false or ${detail} for full details.`
5377
+ };
5378
+ }
5379
+ function compactEvent(row) {
5380
+ return {
5381
+ id: row.id,
5382
+ timestamp: row.timestamp,
5383
+ hook_name: row.hook_name,
5384
+ tool_name: row.tool_name ?? undefined,
5385
+ session_id: row.session_id ? String(row.session_id).slice(0, 12) : undefined,
5386
+ error: row.error ? truncateText(row.error, 180) : undefined,
5387
+ tool_input_preview: row.tool_input ? truncateText(row.tool_input, 180) : undefined
5388
+ };
5389
+ }
5349
5390
  function createHooksServer() {
5350
5391
  const server = new McpServer({
5351
5392
  name: "@hasna/hooks",
5352
5393
  version: pkg.version
5353
5394
  });
5354
- 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.", {
5395
+ const defineTool = (name, description, schema, handler) => server.tool(name, description, schema, handler);
5396
+ defineTool("hooks_list", "List available hooks. Compact by default; pass compact:false for full HookMeta objects.", {
5355
5397
  category: z.string().optional().describe("Filter by category name (e.g. 'Git Safety', 'Code Quality', 'Security')"),
5356
- compact: z.boolean().default(false).describe("Return minimal fields only: name, event, matcher. Reduces token usage.")
5357
- }, async ({ category, compact }) => {
5358
- const slim = (hooks) => compact ? hooks.map((h) => ({ name: h.name, event: h.event, matcher: h.matcher })) : hooks;
5398
+ compact: z.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
5399
+ limit: z.number().default(25).describe("Max compact rows to return")
5400
+ }, async ({ category, compact, limit }) => {
5401
+ const maxRows = boundedLimit(limit, 25, 100);
5359
5402
  if (category) {
5360
5403
  const cat = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
5361
5404
  if (!cat) {
5362
5405
  return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown category: ${category}`, available: [...CATEGORIES] }) }] };
5363
5406
  }
5364
- return { content: [{ type: "text", text: JSON.stringify(slim(getHooksByCategory(cat))) }] };
5365
- }
5366
- if (compact) {
5367
- return { content: [{ type: "text", text: JSON.stringify(slim(HOOKS)) }] };
5407
+ const hooks = getHooksByCategory(cat);
5408
+ return { content: [{ type: "text", text: JSON.stringify(compact ? compactHookResult(hooks, maxRows, "hooks_info") : hooks) }] };
5368
5409
  }
5369
5410
  const result = {};
5370
5411
  for (const cat of CATEGORIES) {
5371
5412
  result[cat] = getHooksByCategory(cat);
5372
5413
  }
5373
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
5414
+ return { content: [{ type: "text", text: JSON.stringify(compact ? compactHookResult(HOOKS, maxRows, "hooks_info") : result) }] };
5374
5415
  });
5375
- server.tool("hooks_search", "Search for hooks by name, description, or tags. Use compact:true for minimal output to save tokens.", {
5416
+ defineTool("hooks_search", "Search hooks. Compact by default; pass compact:false for full HookMeta objects.", {
5376
5417
  query: z.string().describe("Search query"),
5377
- compact: z.boolean().default(false).describe("Return minimal fields only: name, event, matcher.")
5378
- }, async ({ query, compact }) => {
5418
+ compact: z.boolean().default(true).describe("Return compact summaries by default. Set false for full fields."),
5419
+ limit: z.number().default(10).describe("Max compact rows to return")
5420
+ }, async ({ query, compact, limit }) => {
5379
5421
  const results = searchHooks(query);
5380
- const out = compact ? results.map((h) => ({ name: h.name, event: h.event, matcher: h.matcher })) : results;
5422
+ const out = compact ? compactHookResult(results, boundedLimit(limit, 10, 100), "hooks_info") : results;
5381
5423
  return { content: [{ type: "text", text: JSON.stringify(out) }] };
5382
5424
  });
5383
- 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 }) => {
5425
+ defineTool("hooks_info", "Get detailed information about a specific hook including install status", { name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')") }, async ({ name }) => {
5384
5426
  const meta = getHook(name);
5385
5427
  if (!meta) {
5386
5428
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
@@ -5389,7 +5431,7 @@ function createHooksServer() {
5389
5431
  const projectInstalled = getRegisteredHooks("project").includes(meta.name);
5390
5432
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }) }] };
5391
5433
  });
5392
- server.tool("hooks_install", "Install one or more hooks by registering them in agent settings", {
5434
+ defineTool("hooks_install", "Install one or more hooks by registering them in agent settings", {
5393
5435
  hooks: z.array(z.string()).describe("Hook names to install"),
5394
5436
  scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
5395
5437
  overwrite: z.boolean().default(false).describe("Overwrite if already installed"),
@@ -5398,7 +5440,7 @@ function createHooksServer() {
5398
5440
  const results = hooks.map((name) => installHook(name, { scope, overwrite, profile }));
5399
5441
  return formatInstallResults(results, { scope, profile });
5400
5442
  });
5401
- server.tool("hooks_install_category", "Install all hooks in a category", {
5443
+ defineTool("hooks_install_category", "Install all hooks in a category", {
5402
5444
  category: z.string().describe("Category name"),
5403
5445
  scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
5404
5446
  overwrite: z.boolean().default(false).describe("Overwrite if already installed")
@@ -5411,21 +5453,21 @@ function createHooksServer() {
5411
5453
  const results = hooks.map((name) => installHook(name, { scope, overwrite }));
5412
5454
  return formatInstallResults(results, { category: cat, scope });
5413
5455
  });
5414
- server.tool("hooks_install_all", "Install all available hooks", {
5456
+ defineTool("hooks_install_all", "Install all available hooks", {
5415
5457
  scope: z.enum(["global", "project"]).default("global").describe("Install scope"),
5416
5458
  overwrite: z.boolean().default(false).describe("Overwrite if already installed")
5417
5459
  }, async ({ scope, overwrite }) => {
5418
5460
  const results = HOOKS.map((h) => installHook(h.name, { scope, overwrite }));
5419
5461
  return formatInstallResults(results, { scope });
5420
5462
  });
5421
- server.tool("hooks_remove", "Remove (unregister) a hook from agent settings", {
5463
+ defineTool("hooks_remove", "Remove (unregister) a hook from agent settings", {
5422
5464
  name: z.string().describe("Hook name to remove"),
5423
5465
  scope: z.enum(["global", "project"]).default("global").describe("Scope to remove from")
5424
5466
  }, async ({ name, scope }) => {
5425
5467
  const removed = removeHook(name, scope);
5426
5468
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, removed, scope }) }] };
5427
5469
  });
5428
- server.tool("hooks_doctor", "Check health of installed hooks \u2014 verifies hook source exists, settings are correct", {
5470
+ defineTool("hooks_doctor", "Check health of installed hooks \u2014 verifies hook source exists, settings are correct", {
5429
5471
  scope: z.enum(["global", "project"]).default("global").describe("Scope to check")
5430
5472
  }, async ({ scope }) => {
5431
5473
  const settingsPath = getSettingsPath(scope);
@@ -5467,14 +5509,17 @@ function createHooksServer() {
5467
5509
  }
5468
5510
  return { content: [{ type: "text", text: JSON.stringify({ healthy: issues.length === 0, healthy_hooks: healthy, issues, registered, scope }) }] };
5469
5511
  });
5470
- server.tool("hooks_categories", "List all hook categories with counts", {}, async () => {
5512
+ defineTool("hooks_categories", "List all hook categories with counts", {}, async () => {
5471
5513
  const result = CATEGORIES.map((cat) => ({
5472
5514
  name: cat,
5473
5515
  count: getHooksByCategory(cat).length
5474
5516
  }));
5475
5517
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
5476
5518
  });
5477
- server.tool("hooks_docs", "Get documentation \u2014 general overview or README for a specific hook", { name: z.string().optional().describe("Hook name for specific docs, omit for general docs") }, async ({ name }) => {
5519
+ defineTool("hooks_docs", "Get documentation. Hook README content is summarized by default; pass verbose:true for the full README.", {
5520
+ name: z.string().optional().describe("Hook name for specific docs, omit for general docs"),
5521
+ verbose: z.boolean().default(false).describe("Return full hook README when true")
5522
+ }, async ({ name, verbose }) => {
5478
5523
  if (name) {
5479
5524
  const meta = getHook(name);
5480
5525
  if (!meta) {
@@ -5486,7 +5531,24 @@ function createHooksServer() {
5486
5531
  if (existsSync6(readmePath)) {
5487
5532
  readme = readFileSync4(readmePath, "utf-8");
5488
5533
  }
5489
- return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
5534
+ if (verbose) {
5535
+ return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
5536
+ }
5537
+ return {
5538
+ content: [{
5539
+ type: "text",
5540
+ text: JSON.stringify({
5541
+ ...compactHook(meta),
5542
+ displayName: meta.displayName,
5543
+ version: meta.version,
5544
+ description: truncateText(meta.description, 220),
5545
+ readme_preview: truncateText(readme, 500),
5546
+ readme_lines: readme ? readme.split(`
5547
+ `).length : 0,
5548
+ hint: "Call hooks_docs with verbose:true for the full README or hooks_info for metadata."
5549
+ })
5550
+ }]
5551
+ };
5490
5552
  }
5491
5553
  return {
5492
5554
  content: [{
@@ -5514,17 +5576,39 @@ function createHooksServer() {
5514
5576
  }]
5515
5577
  };
5516
5578
  });
5517
- server.tool("hooks_registered", "Get list of currently registered hooks for a scope", {
5518
- scope: z.enum(["global", "project"]).default("global").describe("Scope to check")
5519
- }, async ({ scope }) => {
5579
+ defineTool("hooks_registered", "Get currently registered hooks for a scope. Compact by default.", {
5580
+ scope: z.enum(["global", "project"]).default("global").describe("Scope to check"),
5581
+ compact: z.boolean().default(true).describe("Return compact summaries by default. Set false for descriptions."),
5582
+ limit: z.number().default(25).describe("Max compact rows to return")
5583
+ }, async ({ scope, compact, limit }) => {
5520
5584
  const registered = getRegisteredHooks(scope);
5521
5585
  const result = registered.map((name) => {
5522
5586
  const meta = getHook(name);
5523
5587
  return { name, event: meta?.event, matcher: meta?.matcher ?? "", version: meta?.version, description: meta?.description };
5524
5588
  });
5525
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
5589
+ if (!compact)
5590
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
5591
+ const maxRows = boundedLimit(limit, 25, 100);
5592
+ const visible = result.slice(0, maxRows).map((hook) => ({
5593
+ name: hook.name,
5594
+ event: hook.event,
5595
+ matcher: hook.matcher,
5596
+ version: hook.version
5597
+ }));
5598
+ return {
5599
+ content: [{
5600
+ type: "text",
5601
+ text: JSON.stringify({
5602
+ hooks: visible,
5603
+ count: visible.length,
5604
+ total: result.length,
5605
+ omitted: Math.max(0, result.length - visible.length),
5606
+ hint: "Use compact:false for descriptions or hooks_info for one hook."
5607
+ })
5608
+ }]
5609
+ };
5526
5610
  });
5527
- server.tool("hooks_run", "Execute a hook programmatically with the given input and return its output", {
5611
+ defineTool("hooks_run", "Execute a hook programmatically with the given input and return its output", {
5528
5612
  name: z.string().describe("Hook name (e.g. 'gitguard', 'checkpoint')"),
5529
5613
  input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input as JSON object (HookInput)"),
5530
5614
  profile: z.string().optional().describe("Agent profile ID to inject into hook input"),
@@ -5588,7 +5672,7 @@ function createHooksServer() {
5588
5672
  }]
5589
5673
  };
5590
5674
  });
5591
- server.tool("hooks_update", "Re-register installed hooks to pick up new package version (reinstalls with overwrite)", {
5675
+ defineTool("hooks_update", "Re-register installed hooks to pick up new package version (reinstalls with overwrite)", {
5592
5676
  hooks: z.array(z.string()).optional().describe("Hook names to update (omit to update all installed hooks)"),
5593
5677
  scope: z.enum(["global", "project"]).default("global").describe("Scope to update")
5594
5678
  }, async ({ hooks, scope }) => {
@@ -5607,10 +5691,11 @@ function createHooksServer() {
5607
5691
  const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
5608
5692
  return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
5609
5693
  });
5610
- 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.", {
5694
+ defineTool("hooks_context", "Get compact agent context in one call: installed hooks, active profile summary, settings path, and doctor status.", {
5611
5695
  scope: z.enum(["global", "project"]).default("global").describe("Scope to inspect"),
5612
- profile: z.string().optional().describe("Agent profile ID to include in context")
5613
- }, async ({ scope, profile }) => {
5696
+ profile: z.string().optional().describe("Agent profile ID to include in context"),
5697
+ verbose: z.boolean().default(false).describe("Include full profile preferences/details when true")
5698
+ }, async ({ scope, profile, verbose }) => {
5614
5699
  const settingsPath = getSettingsPath(scope);
5615
5700
  const registered = getRegisteredHooks(scope);
5616
5701
  const hooks = registered.map((name) => {
@@ -5636,11 +5721,13 @@ function createHooksServer() {
5636
5721
  };
5637
5722
  if (profile) {
5638
5723
  const p = getProfile(profile);
5639
- ctx.profile = p ?? null;
5724
+ ctx.profile = p && !verbose ? { agent_id: p.agent_id, agent_type: p.agent_type, name: p.name } : p ?? null;
5725
+ if (p && !verbose)
5726
+ ctx.profile_hint = "Use verbose:true for profile preferences.";
5640
5727
  }
5641
5728
  return { content: [{ type: "text", text: JSON.stringify(ctx) }] };
5642
5729
  });
5643
- 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.", {
5730
+ defineTool("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.", {
5644
5731
  tool_name: z.string().describe("Tool name to simulate (e.g. 'Bash', 'Write', 'Edit')"),
5645
5732
  tool_input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Tool input to pass to matching hooks"),
5646
5733
  scope: z.enum(["global", "project"]).default("global").describe("Scope to check"),
@@ -5705,7 +5792,7 @@ function createHooksServer() {
5705
5792
  }]
5706
5793
  };
5707
5794
  });
5708
- 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.", {
5795
+ defineTool("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.", {
5709
5796
  agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
5710
5797
  name: z.string().optional().describe("Optional display name for the agent"),
5711
5798
  hooks: z.array(z.string()).optional().describe("Hook names to install (omit for sensible defaults: gitguard, checkpoint, checktests, protectfiles)"),
@@ -5723,7 +5810,7 @@ function createHooksServer() {
5723
5810
  }]
5724
5811
  };
5725
5812
  });
5726
- 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.", {
5813
+ defineTool("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.", {
5727
5814
  hooks: z.array(z.object({
5728
5815
  name: z.string().describe("Hook name"),
5729
5816
  input: z.record(z.string(), z.unknown()).default(() => ({})).describe("Hook input JSON")
@@ -5761,7 +5848,7 @@ function createHooksServer() {
5761
5848
  }));
5762
5849
  return { content: [{ type: "text", text: JSON.stringify({ results, count: results.length }) }] };
5763
5850
  });
5764
- server.tool("hooks_disable", "Temporarily disable a registered hook without removing it. Stores disabled list in settings under hooks.__disabled.", {
5851
+ defineTool("hooks_disable", "Temporarily disable a registered hook without removing it. Stores disabled list in settings under hooks.__disabled.", {
5765
5852
  name: z.string().describe("Hook name to disable"),
5766
5853
  scope: z.enum(["global", "project"]).default("global").describe("Scope")
5767
5854
  }, async ({ name, scope }) => {
@@ -5784,7 +5871,7 @@ function createHooksServer() {
5784
5871
  `);
5785
5872
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
5786
5873
  });
5787
- server.tool("hooks_enable", "Re-enable a previously disabled hook.", {
5874
+ defineTool("hooks_enable", "Re-enable a previously disabled hook.", {
5788
5875
  name: z.string().describe("Hook name to enable"),
5789
5876
  scope: z.enum(["global", "project"]).default("global").describe("Scope")
5790
5877
  }, async ({ name, scope }) => {
@@ -5804,25 +5891,50 @@ function createHooksServer() {
5804
5891
  }
5805
5892
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
5806
5893
  });
5807
- server.tool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
5894
+ defineTool("hooks_init", "Register a new agent profile \u2014 returns a unique agent_id for use with hook installation and execution", {
5808
5895
  agent_type: z.enum(["claude", "gemini", "custom"]).default("claude").describe("Type of AI agent"),
5809
5896
  name: z.string().optional().describe("Optional display name for the agent")
5810
5897
  }, async ({ agent_type, name }) => {
5811
5898
  const profile = createProfile({ agent_type, name });
5812
5899
  return { content: [{ type: "text", text: JSON.stringify(profile) }] };
5813
5900
  });
5814
- server.tool("hooks_profiles", "List all registered agent profiles", {}, async () => {
5901
+ defineTool("hooks_profiles", "List registered agent profiles. Compact by default.", {
5902
+ compact: z.boolean().default(true).describe("Return compact summaries by default. Set false for full profiles."),
5903
+ limit: z.number().default(25).describe("Max compact rows to return")
5904
+ }, async ({ compact, limit }) => {
5815
5905
  const profiles = listProfiles();
5816
- return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
5906
+ if (!compact)
5907
+ return { content: [{ type: "text", text: JSON.stringify(profiles) }] };
5908
+ const maxRows = boundedLimit(limit, 25, 100);
5909
+ const visible = profiles.slice(0, maxRows).map((profile) => ({
5910
+ agent_id: profile.agent_id,
5911
+ agent_type: profile.agent_type,
5912
+ name: profile.name,
5913
+ last_seen_at: profile.last_seen_at
5914
+ }));
5915
+ return {
5916
+ content: [{
5917
+ type: "text",
5918
+ text: JSON.stringify({
5919
+ profiles: visible,
5920
+ count: visible.length,
5921
+ total: profiles.length,
5922
+ omitted: Math.max(0, profiles.length - visible.length),
5923
+ hint: "Use compact:false for full profile preferences."
5924
+ })
5925
+ }]
5926
+ };
5817
5927
  });
5818
- server.tool("hooks_log_list", "List hook events from SQLite (~/.hasna/hooks/hooks.db). Filter by hook name, session ID, or time range.", {
5928
+ defineTool("hooks_log_list", "List hook events from SQLite. Compact summaries by default; set compact:false for full event rows.", {
5819
5929
  hook_name: z.string().optional().describe("Filter by hook name (e.g. 'sessionlog', 'costwatch')"),
5820
5930
  session_id: z.string().optional().describe("Filter by session ID prefix"),
5821
- limit: z.number().default(50).describe("Max number of events to return"),
5822
- since: z.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from")
5823
- }, async ({ hook_name, session_id, limit, since }) => {
5931
+ limit: z.number().optional().describe("Max number of events to return. Defaults to 20 compact rows or 50 full rows."),
5932
+ since: z.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from"),
5933
+ compact: z.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
5934
+ }, async ({ hook_name, session_id, limit, since, compact }) => {
5824
5935
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
5825
5936
  const db = getDb2();
5937
+ const maxRows = boundedLimit(limit, compact ? 20 : 50, compact ? 100 : 500);
5826
5938
  function parseDuration(s) {
5827
5939
  const m = s.match(/^(\d+)(s|m|h|d)$/);
5828
5940
  if (!m)
@@ -5849,24 +5961,48 @@ function createHooksServer() {
5849
5961
  }
5850
5962
  }
5851
5963
  sql += " ORDER BY timestamp DESC LIMIT ?";
5852
- params.push(limit);
5964
+ params.push(maxRows);
5853
5965
  const rows = db.query(sql).all(...params);
5854
- return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
5966
+ return {
5967
+ content: [{
5968
+ type: "text",
5969
+ text: JSON.stringify({
5970
+ events: compact ? rows.map(compactEvent) : rows,
5971
+ count: rows.length,
5972
+ compact,
5973
+ hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
5974
+ })
5975
+ }]
5976
+ };
5855
5977
  });
5856
- server.tool("hooks_log_tail", "Show the most recent hook events from SQLite.", {
5857
- n: z.number().default(20).describe("Number of most recent events to return")
5858
- }, async ({ n }) => {
5978
+ defineTool("hooks_log_tail", "Show recent hook events from SQLite. Compact summaries by default.", {
5979
+ n: z.number().default(20).describe("Number of most recent events to return"),
5980
+ compact: z.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
5981
+ }, async ({ n, compact }) => {
5859
5982
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
5860
5983
  const db = getDb2();
5861
- const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(n);
5862
- return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
5984
+ const maxRows = boundedLimit(n, 20, compact ? 100 : 500);
5985
+ const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
5986
+ return {
5987
+ content: [{
5988
+ type: "text",
5989
+ text: JSON.stringify({
5990
+ events: compact ? rows.map(compactEvent) : rows,
5991
+ count: rows.length,
5992
+ compact,
5993
+ hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
5994
+ })
5995
+ }]
5996
+ };
5863
5997
  });
5864
- server.tool("hooks_log_errors", "Show hook events that contain errors, optionally filtered by time range.", {
5998
+ defineTool("hooks_log_errors", "Show hook events that contain errors. Compact summaries by default.", {
5865
5999
  since: z.string().default("24h").describe("Duration string (e.g. '1h', '30m', '7d') or ISO timestamp"),
5866
- limit: z.number().default(50).describe("Max number of error events to return")
5867
- }, async ({ since, limit }) => {
6000
+ limit: z.number().optional().describe("Max number of error events to return. Defaults to 20 compact rows or 50 full rows."),
6001
+ compact: z.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
6002
+ }, async ({ since, limit, compact }) => {
5868
6003
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
5869
6004
  const db = getDb2();
6005
+ const maxRows = boundedLimit(limit, compact ? 20 : 50, compact ? 100 : 500);
5870
6006
  function parseDuration(s) {
5871
6007
  const m = s.match(/^(\d+)(s|m|h|d)$/);
5872
6008
  if (!m)
@@ -5876,10 +6012,20 @@ function createHooksServer() {
5876
6012
  return new Date(Date.now() - n * ms).toISOString();
5877
6013
  }
5878
6014
  const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
5879
- const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, limit);
5880
- return { content: [{ type: "text", text: JSON.stringify({ events: rows, count: rows.length }) }] };
6015
+ const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, maxRows);
6016
+ return {
6017
+ content: [{
6018
+ type: "text",
6019
+ text: JSON.stringify({
6020
+ events: compact ? rows.map(compactEvent) : rows,
6021
+ count: rows.length,
6022
+ compact,
6023
+ hint: compact ? "Use compact:false for full tool_input/output fields." : undefined
6024
+ })
6025
+ }]
6026
+ };
5881
6027
  });
5882
- server.tool("hooks_log_summary", "Summarize hook execution: counts per hook, error rates, and recent activity.", {
6028
+ defineTool("hooks_log_summary", "Summarize hook execution: counts per hook, error rates, and recent activity.", {
5883
6029
  since: z.string().default("24h").describe("Duration string (e.g. '1h', '24h', '7d') or ISO timestamp")
5884
6030
  }, async ({ since }) => {
5885
6031
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
@@ -5913,11 +6059,11 @@ function createHooksServer() {
5913
6059
  }]
5914
6060
  };
5915
6061
  });
5916
- server.tool("storage_status", "Show hooks storage sync configuration and local sync history.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(getStorageStatus()) }] }));
5917
- server.tool("storage_push", "Push local hook data to storage PostgreSQL.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(params.tables ? { tables: params.tables } : undefined)) }] }));
5918
- server.tool("storage_pull", "Pull hook data from storage PostgreSQL to local SQLite.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(params.tables ? { tables: params.tables } : undefined)) }] }));
5919
- server.tool("storage_sync", "Bidirectional hooks storage sync: pull then push.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(params.tables ? { tables: params.tables } : undefined)) }] }));
5920
- server.tool("send_feedback", "Send feedback about this service", {
6062
+ defineTool("storage_status", "Show hooks storage sync configuration and local sync history.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(getStorageStatus()) }] }));
6063
+ defineTool("storage_push", "Push local hook data to storage PostgreSQL.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(params.tables ? { tables: params.tables } : undefined)) }] }));
6064
+ defineTool("storage_pull", "Pull hook data from storage PostgreSQL to local SQLite.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(params.tables ? { tables: params.tables } : undefined)) }] }));
6065
+ defineTool("storage_sync", "Bidirectional hooks storage sync: pull then push.", { tables: z.array(z.string()).optional() }, async (params) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(params.tables ? { tables: params.tables } : undefined)) }] }));
6066
+ defineTool("send_feedback", "Send feedback about this service", {
5921
6067
  message: z.string(),
5922
6068
  email: z.string().optional(),
5923
6069
  category: z.enum(["bug", "feature", "general"]).optional()
@@ -5936,7 +6082,7 @@ function createHooksServer() {
5936
6082
  return { content: [{ type: "text", text: String(e) }], isError: true };
5937
6083
  }
5938
6084
  });
5939
- server.tool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
6085
+ defineTool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
5940
6086
  name: z.string(),
5941
6087
  session_id: z.string().optional()
5942
6088
  }, async (params) => {
@@ -5952,7 +6098,7 @@ function createHooksServer() {
5952
6098
  _hooksAgents.set(id, ag);
5953
6099
  return { content: [{ type: "text", text: JSON.stringify(ag) }] };
5954
6100
  });
5955
- server.tool("heartbeat", "Update last_seen_at to signal agent is active.", {
6101
+ defineTool("heartbeat", "Update last_seen_at to signal agent is active.", {
5956
6102
  agent_id: z.string()
5957
6103
  }, async (params) => {
5958
6104
  const ag = _hooksAgents.get(params.agent_id);
@@ -5961,7 +6107,7 @@ function createHooksServer() {
5961
6107
  ag.last_seen_at = new Date().toISOString();
5962
6108
  return { content: [{ type: "text", text: JSON.stringify({ agent_id: ag.id, last_seen_at: ag.last_seen_at }) }] };
5963
6109
  });
5964
- server.tool("set_focus", "Set active project context for this agent session.", {
6110
+ defineTool("set_focus", "Set active project context for this agent session.", {
5965
6111
  agent_id: z.string(),
5966
6112
  project_id: z.string().optional()
5967
6113
  }, async (params) => {
@@ -5971,8 +6117,32 @@ function createHooksServer() {
5971
6117
  ag.project_id = params.project_id;
5972
6118
  return { content: [{ type: "text", text: JSON.stringify({ agent_id: ag.id, project_id: ag.project_id ?? null }) }] };
5973
6119
  });
5974
- server.tool("list_agents", "List all registered agents.", {}, async () => {
5975
- return { content: [{ type: "text", text: JSON.stringify([..._hooksAgents.values()]) }] };
6120
+ defineTool("list_agents", "List registered agents. Compact by default.", {
6121
+ compact: z.boolean().default(true).describe("Return compact agent summaries by default. Set false for full records."),
6122
+ limit: z.number().default(25).describe("Max compact rows to return")
6123
+ }, async ({ compact, limit }) => {
6124
+ const agents = [..._hooksAgents.values()];
6125
+ if (!compact)
6126
+ return { content: [{ type: "text", text: JSON.stringify(agents) }] };
6127
+ const maxRows = boundedLimit(limit, 25, 100);
6128
+ const visible = agents.slice(0, maxRows).map((agent) => ({
6129
+ id: agent.id,
6130
+ name: agent.name,
6131
+ project_id: agent.project_id,
6132
+ last_seen_at: agent.last_seen_at
6133
+ }));
6134
+ return {
6135
+ content: [{
6136
+ type: "text",
6137
+ text: JSON.stringify({
6138
+ agents: visible,
6139
+ count: visible.length,
6140
+ total: agents.length,
6141
+ omitted: Math.max(0, agents.length - visible.length),
6142
+ hint: "Use compact:false for session_id and full records."
6143
+ })
6144
+ }]
6145
+ };
5976
6146
  });
5977
6147
  return server;
5978
6148
  }
@@ -6100,6 +6270,7 @@ var init_http = () => {};
6100
6270
 
6101
6271
  // node_modules/@hasna/events/dist/commander.js
6102
6272
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
6273
+ import { Buffer as Buffer2 } from "buffer";
6103
6274
  import { existsSync } from "fs";
6104
6275
  import { homedir } from "os";
6105
6276
  import { join } from "path";
@@ -6204,6 +6375,9 @@ function channelMatchesEvent(channel, event) {
6204
6375
  }
6205
6376
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
6206
6377
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
6378
+ var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
6379
+ var DEFAULT_EVENT_PAGE_LIMIT = 100;
6380
+ var MAX_EVENT_PAGE_LIMIT = 1000;
6207
6381
  function getEventsDataDir(override) {
6208
6382
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
6209
6383
  }
@@ -6217,11 +6391,13 @@ function getActiveEventsDirEnv() {
6217
6391
 
6218
6392
  class JsonEventsStore {
6219
6393
  dataDir;
6394
+ runtime;
6220
6395
  channelsPath;
6221
6396
  eventsPath;
6222
6397
  deliveriesPath;
6223
6398
  constructor(dataDir = getEventsDataDir()) {
6224
6399
  this.dataDir = dataDir;
6400
+ this.runtime = localJsonRuntime(dataDir);
6225
6401
  this.channelsPath = join(dataDir, "channels.json");
6226
6402
  this.eventsPath = join(dataDir, "events.json");
6227
6403
  this.deliveriesPath = join(dataDir, "deliveries.json");
@@ -6269,13 +6445,58 @@ class JsonEventsStore {
6269
6445
  await this.writeJson(this.eventsPath, events);
6270
6446
  return event;
6271
6447
  }
6272
- async listEvents() {
6448
+ async appendEventOnce(event, options = {}) {
6273
6449
  await this.init();
6274
- return this.readJson(this.eventsPath, []);
6450
+ const events = await this.readJson(this.eventsPath, []);
6451
+ const dedupe = options.dedupe !== false;
6452
+ if (dedupe) {
6453
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
6454
+ if (existing) {
6455
+ return {
6456
+ event: existing,
6457
+ stored: false,
6458
+ deduped: true,
6459
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
6460
+ };
6461
+ }
6462
+ }
6463
+ events.push(event);
6464
+ await this.writeJson(this.eventsPath, events);
6465
+ return {
6466
+ event,
6467
+ stored: true,
6468
+ deduped: false,
6469
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
6470
+ };
6471
+ }
6472
+ async listEvents(options = {}) {
6473
+ await this.init();
6474
+ const events = await this.readJson(this.eventsPath, []);
6475
+ return queryEvents(events, options);
6476
+ }
6477
+ async listEventsPage(options = {}) {
6478
+ await this.init();
6479
+ const events = await this.readJson(this.eventsPath, []);
6480
+ const queried = queryEvents(events, {
6481
+ eventId: options.eventId,
6482
+ source: options.source,
6483
+ type: options.type
6484
+ });
6485
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
6486
+ const limit = normalizeEventPageLimit(options.limit);
6487
+ const pageEvents = queried.slice(offset, offset + limit);
6488
+ const nextOffset = offset + pageEvents.length;
6489
+ const hasMore = nextOffset < queried.length;
6490
+ return {
6491
+ events: pageEvents,
6492
+ cursor: options.cursor,
6493
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
6494
+ hasMore
6495
+ };
6275
6496
  }
6276
6497
  async findEventByIdentity(identity) {
6277
6498
  const events = await this.listEvents();
6278
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6499
+ return findEventByIdentity(events, identity);
6279
6500
  }
6280
6501
  async appendDelivery(result) {
6281
6502
  await this.init();
@@ -6326,6 +6547,83 @@ class JsonEventsStore {
6326
6547
  });
6327
6548
  }
6328
6549
  }
6550
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
6551
+ return {
6552
+ mode: "local-files",
6553
+ name: "json-events-store",
6554
+ remote: false,
6555
+ localFiles: true,
6556
+ localSqlite: false,
6557
+ postgres: false,
6558
+ s3: false,
6559
+ aws: false,
6560
+ durable: true,
6561
+ idempotency: "best-effort-local",
6562
+ replayCursors: true,
6563
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
6564
+ };
6565
+ }
6566
+ function encodeLocalJsonEventCursor(offset, options = {}) {
6567
+ if (!Number.isInteger(offset) || offset < 0)
6568
+ throw new Error(`Invalid event cursor offset: ${offset}`);
6569
+ const payload = {
6570
+ offset,
6571
+ eventId: options.eventId,
6572
+ source: options.source,
6573
+ type: options.type
6574
+ };
6575
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
6576
+ }
6577
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
6578
+ if (!cursor)
6579
+ return 0;
6580
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
6581
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6582
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
6583
+ let payload;
6584
+ try {
6585
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
6586
+ } catch {
6587
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6588
+ }
6589
+ const offset = payload.offset;
6590
+ if (!Number.isInteger(offset) || offset < 0)
6591
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
6592
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
6593
+ assertCursorFilter("source", payload.source, options.source);
6594
+ assertCursorFilter("type", payload.type, options.type);
6595
+ return offset;
6596
+ }
6597
+ function normalizeEventPageLimit(limit) {
6598
+ if (limit === undefined)
6599
+ return DEFAULT_EVENT_PAGE_LIMIT;
6600
+ if (!Number.isInteger(limit) || limit < 1)
6601
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
6602
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
6603
+ }
6604
+ function queryEvents(events, options) {
6605
+ let rows = events;
6606
+ if (options.eventId)
6607
+ rows = rows.filter((event) => event.id === options.eventId);
6608
+ if (options.source)
6609
+ rows = rows.filter((event) => event.source === options.source);
6610
+ if (options.type)
6611
+ rows = rows.filter((event) => event.type === options.type);
6612
+ if (options.cursor) {
6613
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
6614
+ rows = rows.slice(offset);
6615
+ }
6616
+ if (options.limit !== undefined)
6617
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
6618
+ return rows;
6619
+ }
6620
+ function assertCursorFilter(name, cursorValue, optionValue) {
6621
+ if (cursorValue !== optionValue)
6622
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
6623
+ }
6624
+ function findEventByIdentity(events, identity) {
6625
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
6626
+ }
6329
6627
  async function getEventsStatus(dataDir) {
6330
6628
  const store = new JsonEventsStore(dataDir);
6331
6629
  await store.init();
@@ -6342,6 +6640,7 @@ async function getEventsStatus(dataDir) {
6342
6640
  service: "events",
6343
6641
  schemaVersion: "1.0",
6344
6642
  dataDir: store.dataDir,
6643
+ storage: store.runtime,
6345
6644
  env: {
6346
6645
  primary: HASNA_EVENTS_DIR_ENV,
6347
6646
  fallback: HASNA_EVENTS_HOME_ENV,
@@ -6528,6 +6827,51 @@ function createDeliveryResult(event, channel, attempts) {
6528
6827
  completedAt: attempts.at(-1)?.completedAt ?? now()
6529
6828
  };
6530
6829
  }
6830
+
6831
+ class EventValidationError extends Error {
6832
+ eventType;
6833
+ issues;
6834
+ constructor(eventType, issues) {
6835
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
6836
+ super(`Event validation failed for type "${eventType}": ${detail}`);
6837
+ this.name = "EventValidationError";
6838
+ this.eventType = eventType;
6839
+ this.issues = issues;
6840
+ }
6841
+ }
6842
+
6843
+ class EventTypeCatalog {
6844
+ definitions = new Map;
6845
+ register(definition) {
6846
+ this.definitions.set(definition.type, definition);
6847
+ return this;
6848
+ }
6849
+ unregister(type) {
6850
+ return this.definitions.delete(type);
6851
+ }
6852
+ has(type) {
6853
+ return this.definitions.has(type);
6854
+ }
6855
+ get(type) {
6856
+ return this.definitions.get(type);
6857
+ }
6858
+ list() {
6859
+ return [...this.definitions.values()];
6860
+ }
6861
+ validateEvent(event) {
6862
+ const definition = this.definitions.get(event.type);
6863
+ if (!definition)
6864
+ return { ok: true };
6865
+ return definition.validate(event.data, event);
6866
+ }
6867
+ assertEventValid(event) {
6868
+ const result = this.validateEvent(event);
6869
+ if (!result.ok) {
6870
+ throw new EventValidationError(event.type, result.issues);
6871
+ }
6872
+ }
6873
+ }
6874
+ var defaultEventTypeCatalog = new EventTypeCatalog;
6531
6875
  function createEvent(input) {
6532
6876
  return {
6533
6877
  id: input.id ?? randomUUID2(),
@@ -6548,10 +6892,14 @@ class EventsClient {
6548
6892
  store;
6549
6893
  redactors;
6550
6894
  transportOptions;
6895
+ catalog;
6896
+ validateCatalogTypes;
6551
6897
  constructor(options = {}) {
6552
6898
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
6553
6899
  this.redactors = options.redactors ?? [];
6554
6900
  this.transportOptions = { fetchImpl: options.fetchImpl };
6901
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
6902
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
6555
6903
  }
6556
6904
  async addChannel(input) {
6557
6905
  const timestamp = new Date().toISOString();
@@ -6569,18 +6917,40 @@ class EventsClient {
6569
6917
  }
6570
6918
  async emit(input, options = {}) {
6571
6919
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
6572
- if (options.dedupe !== false) {
6573
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
6574
- if (existing) {
6575
- return { event: existing, deliveries: [], deduped: true };
6576
- }
6577
- }
6578
- await this.store.appendEvent(event);
6579
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
6580
- return { event, deliveries, deduped: false };
6581
- }
6582
- async listEvents() {
6583
- return this.store.listEvents();
6920
+ if (options.validate ?? this.validateCatalogTypes) {
6921
+ this.catalog.assertEventValid(event);
6922
+ }
6923
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
6924
+ if (append.deduped) {
6925
+ return { event: append.event, deliveries: [], deduped: true };
6926
+ }
6927
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
6928
+ return { event: append.event, deliveries, deduped: false };
6929
+ }
6930
+ async listEvents(options = {}) {
6931
+ if (Object.keys(options).length === 0)
6932
+ return this.store.listEvents();
6933
+ return queryClientEvents(await this.store.listEvents(), options);
6934
+ }
6935
+ async listEventsPage(options = {}) {
6936
+ if (this.store.listEventsPage)
6937
+ return this.store.listEventsPage(options);
6938
+ const events = queryClientEvents(await this.store.listEvents(), {
6939
+ eventId: options.eventId,
6940
+ source: options.source,
6941
+ type: options.type
6942
+ });
6943
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
6944
+ const limit = normalizeEventPageLimit(options.limit);
6945
+ const pageEvents = events.slice(offset, offset + limit);
6946
+ const nextOffset = offset + pageEvents.length;
6947
+ const hasMore = nextOffset < events.length;
6948
+ return {
6949
+ events: pageEvents,
6950
+ cursor: options.cursor,
6951
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
6952
+ hasMore
6953
+ };
6584
6954
  }
6585
6955
  async listDeliveries() {
6586
6956
  return this.store.listDeliveries();
@@ -6648,22 +7018,37 @@ class EventsClient {
6648
7018
  return result;
6649
7019
  }
6650
7020
  async replay(options = {}) {
6651
- const events = (await this.store.listEvents()).filter((event) => {
6652
- if (options.eventId && event.id !== options.eventId)
6653
- return false;
6654
- if (options.source && event.source !== options.source)
6655
- return false;
6656
- if (options.type && event.type !== options.type)
6657
- return false;
6658
- return true;
6659
- });
7021
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
6660
7022
  if (options.dryRun)
6661
- return { events, deliveries: [] };
7023
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
6662
7024
  const deliveries = [];
6663
- for (const event of events) {
7025
+ for (const event of page.events) {
6664
7026
  deliveries.push(...await this.deliver(event));
6665
7027
  }
6666
- return { events, deliveries };
7028
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
7029
+ }
7030
+ async appendEvent(event, options) {
7031
+ if (this.store.appendEventOnce) {
7032
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
7033
+ }
7034
+ if (options.dedupe) {
7035
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
7036
+ if (existing) {
7037
+ return {
7038
+ event: existing,
7039
+ stored: false,
7040
+ deduped: true,
7041
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
7042
+ };
7043
+ }
7044
+ }
7045
+ const stored = await this.store.appendEvent(event);
7046
+ return {
7047
+ event: stored,
7048
+ stored: true,
7049
+ deduped: false,
7050
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
7051
+ };
6667
7052
  }
6668
7053
  async applyRedaction(event, channel) {
6669
7054
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -6740,6 +7125,20 @@ function setPath(input, path, replacement) {
6740
7125
  if (last && last in cursor)
6741
7126
  cursor[last] = replacement;
6742
7127
  }
7128
+ function queryClientEvents(events, options) {
7129
+ let rows = events;
7130
+ if (options.eventId)
7131
+ rows = rows.filter((event) => event.id === options.eventId);
7132
+ if (options.source)
7133
+ rows = rows.filter((event) => event.source === options.source);
7134
+ if (options.type)
7135
+ rows = rows.filter((event) => event.type === options.type);
7136
+ if (options.cursor)
7137
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
7138
+ if (options.limit !== undefined)
7139
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
7140
+ return rows;
7141
+ }
6743
7142
  function normalizeTime(value) {
6744
7143
  if (!value)
6745
7144
  return new Date().toISOString();
@@ -6822,6 +7221,7 @@ function parseMatcherExpression(value, label) {
6822
7221
  negated: false
6823
7222
  };
6824
7223
  }
7224
+ var DEFAULT_EVENT_LIST_LIMIT = 100;
6825
7225
  function parseJsonObject(value, fallback) {
6826
7226
  if (!value)
6827
7227
  return fallback;
@@ -6854,6 +7254,14 @@ function print(value, json, text) {
6854
7254
  else
6855
7255
  console.log(text);
6856
7256
  }
7257
+ function fail(error, json) {
7258
+ const message = error instanceof Error ? error.message : String(error);
7259
+ if (json)
7260
+ console.log(JSON.stringify({ error: message }, null, 2));
7261
+ else
7262
+ console.error(message);
7263
+ process.exitCode = 1;
7264
+ }
6857
7265
  function hasJsonOption(options) {
6858
7266
  return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
6859
7267
  }
@@ -6908,26 +7316,36 @@ function registerChannelCommands(program, options) {
6908
7316
  print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
6909
7317
  });
6910
7318
  channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
6911
- const result = await createClient(options).testChannel(id, {
6912
- source: actionOptions.source ?? options.source,
6913
- type: actionOptions.type,
6914
- subject: actionOptions.subject ?? id,
6915
- message: actionOptions.message,
6916
- data: parseJsonObject(actionOptions.data, { test: true }),
6917
- metadata: parseJsonObject(actionOptions.metadata, {})
6918
- }, { honorFilters: actionOptions.honorFilters });
6919
- print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
7319
+ const json = wantsJson(actionOptions, command);
7320
+ try {
7321
+ const result = await createClient(options).testChannel(id, {
7322
+ source: actionOptions.source ?? options.source,
7323
+ type: actionOptions.type,
7324
+ subject: actionOptions.subject ?? id,
7325
+ message: actionOptions.message,
7326
+ data: parseJsonObject(actionOptions.data, { test: true }),
7327
+ metadata: parseJsonObject(actionOptions.metadata, {})
7328
+ }, { honorFilters: actionOptions.honorFilters });
7329
+ print(result, json, `${result.status}: ${result.channelId}`);
7330
+ } catch (error) {
7331
+ fail(error, json);
7332
+ }
6920
7333
  });
6921
7334
  channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
6922
- const result = await createClient(options).matchChannel(id, {
6923
- source: actionOptions.source ?? options.source,
6924
- type: actionOptions.type,
6925
- subject: actionOptions.subject ?? id,
6926
- message: actionOptions.message,
6927
- data: parseJsonObject(actionOptions.data, { test: true }),
6928
- metadata: parseJsonObject(actionOptions.metadata, {})
6929
- });
6930
- print(result, wantsJson(actionOptions, command), `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
7335
+ const json = wantsJson(actionOptions, command);
7336
+ try {
7337
+ const result = await createClient(options).matchChannel(id, {
7338
+ source: actionOptions.source ?? options.source,
7339
+ type: actionOptions.type,
7340
+ subject: actionOptions.subject ?? id,
7341
+ message: actionOptions.message,
7342
+ data: parseJsonObject(actionOptions.data, { test: true }),
7343
+ metadata: parseJsonObject(actionOptions.metadata, {})
7344
+ });
7345
+ print(result, json, `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
7346
+ } catch (error) {
7347
+ fail(error, json);
7348
+ }
6931
7349
  });
6932
7350
  return channels;
6933
7351
  }
@@ -6946,7 +7364,8 @@ function registerEventCommands(program, options) {
6946
7364
  }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
6947
7365
  print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
6948
7366
  });
6949
- events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
7367
+ const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
7368
+ events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
6950
7369
  let rows = await createClient(options).listEvents();
6951
7370
  if (actionOptions.source)
6952
7371
  rows = rows.filter((event) => event.source === actionOptions.source);
@@ -6965,14 +7384,16 @@ function registerEventCommands(program, options) {
6965
7384
  for (const event of rows)
6966
7385
  console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
6967
7386
  });
6968
- events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
7387
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
6969
7388
  const result = await createClient(options).replay({
6970
7389
  eventId: actionOptions.id,
6971
7390
  source: actionOptions.source,
6972
7391
  type: actionOptions.type,
7392
+ cursor: actionOptions.cursor,
7393
+ limit: actionOptions.limit,
6973
7394
  dryRun: actionOptions.dryRun
6974
7395
  });
6975
- print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
7396
+ print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
6976
7397
  });
6977
7398
  return events;
6978
7399
  }
@@ -6990,6 +7411,10 @@ function collectValues(value, previous) {
6990
7411
  previous.push(value);
6991
7412
  return previous;
6992
7413
  }
7414
+ function replaySummary(events, deliveries, nextCursor) {
7415
+ const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
7416
+ return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
7417
+ }
6993
7418
 
6994
7419
  // src/cli/index.tsx
6995
7420
  import { render } from "ink";
@@ -8208,6 +8633,39 @@ function resolveTarget(options) {
8208
8633
  return "all";
8209
8634
  return "claude";
8210
8635
  }
8636
+ function parseLimit(value, fallback, max) {
8637
+ const parsed = value ? parseInt(value, 10) : fallback;
8638
+ if (!Number.isFinite(parsed) || parsed <= 0)
8639
+ return fallback;
8640
+ return Math.min(parsed, max);
8641
+ }
8642
+ function truncateText2(value, max = 96) {
8643
+ const text = (value ?? "").replace(/\s+/g, " ").trim();
8644
+ if (text.length <= max)
8645
+ return text;
8646
+ return `${text.slice(0, Math.max(0, max - 3))}...`;
8647
+ }
8648
+ function readmePreview(readme, max = 280) {
8649
+ const blocks = readme.split(/\n\s*\n/).map((part) => part.trim()).filter(Boolean).filter((part) => !part.startsWith("#")).filter((part) => !part.startsWith("```")).filter((part) => !/^\[!\[/.test(part));
8650
+ const preview = blocks[0] ?? readme.split(`
8651
+ `).map((line) => line.trim()).find((line) => line && !line.startsWith("#"));
8652
+ return preview ? truncateText2(preview, max) : undefined;
8653
+ }
8654
+ function hookSummaryLine(hook, options = {}) {
8655
+ const matcher = hook.matcher ? ` ${hook.matcher}` : "";
8656
+ const description = options.verbose ? ` - ${truncateText2(hook.description, 110)}` : "";
8657
+ return ` ${chalk2.cyan(hook.name.padEnd(17))} ${chalk2.dim(`[${hook.event}${matcher}]`)} ${chalk2.dim(hook.category)}${description}`;
8658
+ }
8659
+ function printDisclosureHint(hidden, detailCommand, options = {}) {
8660
+ const rowControls = options.includeAll ? "--limit, --all, --verbose" : "--limit, --verbose";
8661
+ if (hidden > 0) {
8662
+ console.log(chalk2.dim(`
8663
+ Showing a compact subset. ${hidden} more hidden; use ${rowControls}, or ${detailCommand}.`));
8664
+ } else {
8665
+ console.log(chalk2.dim(`
8666
+ Use --verbose or ${detailCommand} for details.`));
8667
+ }
8668
+ }
8211
8669
  function editDistance(a, b) {
8212
8670
  const m = a.length, n = b.length;
8213
8671
  const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
@@ -8417,8 +8875,9 @@ Installing hooks (${scope}, ${target})...
8417
8875
  console.log(chalk2.dim(`
8418
8876
  Registered in ${settingsFile}`));
8419
8877
  });
8420
- program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
8878
+ program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
8421
8879
  const scope = resolveScope(options);
8880
+ const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
8422
8881
  if (options.registered || options.installed) {
8423
8882
  const target = options.target === "gemini" ? "gemini" : options.target === "codewith" ? "codewith" : "claude";
8424
8883
  const registered = getRegisteredHooksForTarget(scope, target);
@@ -8433,13 +8892,18 @@ program2.command("list").alias("ls").option("-c, --category <category>", "Filter
8433
8892
  console.log(chalk2.dim(`No hooks registered (${scope}, ${target})`));
8434
8893
  return;
8435
8894
  }
8895
+ const visible2 = registered.slice(0, limit);
8436
8896
  console.log(chalk2.bold(`
8437
- Registered hooks \u2014 ${scope}/${target} (${registered.length}):
8897
+ Registered hooks \u2014 ${scope}/${target} (${registered.length}, showing ${visible2.length}):
8438
8898
  `));
8439
- for (const name of registered) {
8899
+ for (const name of visible2) {
8440
8900
  const meta = getHook(name);
8441
- console.log(` ${chalk2.cyan(name)} ${chalk2.dim(`[${meta?.event || "unknown"}]`)} - ${meta?.description || ""}`);
8901
+ if (meta)
8902
+ console.log(hookSummaryLine(meta, { verbose: options.verbose }));
8903
+ else
8904
+ console.log(` ${chalk2.cyan(name)} ${chalk2.dim("[unknown]")}`);
8442
8905
  }
8906
+ printDisclosureHint(registered.length - visible2.length, "hooks info <name>", { includeAll: true });
8443
8907
  return;
8444
8908
  }
8445
8909
  if (options.category) {
@@ -8458,12 +8922,13 @@ Registered hooks \u2014 ${scope}/${target} (${registered.length}):
8458
8922
  console.log(JSON.stringify(hooks));
8459
8923
  return;
8460
8924
  }
8925
+ const visible2 = hooks.slice(0, limit);
8461
8926
  console.log(chalk2.bold(`
8462
- ${category} (${hooks.length}):
8927
+ ${category} (${hooks.length}, showing ${visible2.length}):
8463
8928
  `));
8464
- for (const h of hooks) {
8465
- console.log(` ${chalk2.cyan(h.name)} ${chalk2.dim(`[${h.event}]`)} - ${h.description}`);
8466
- }
8929
+ for (const h of visible2)
8930
+ console.log(hookSummaryLine(h, { verbose: options.verbose }));
8931
+ printDisclosureHint(hooks.length - visible2.length, "hooks info <name>", { includeAll: true });
8467
8932
  return;
8468
8933
  }
8469
8934
  if (options.json) {
@@ -8474,19 +8939,15 @@ ${category} (${hooks.length}):
8474
8939
  console.log(JSON.stringify(result));
8475
8940
  return;
8476
8941
  }
8942
+ const visible = HOOKS.slice(0, limit);
8477
8943
  console.log(chalk2.bold(`
8478
- Available hooks (${HOOKS.length}):
8944
+ Available hooks (${HOOKS.length}, showing ${visible.length}):
8479
8945
  `));
8480
- for (const category of CATEGORIES) {
8481
- const hooks = getHooksByCategory(category);
8482
- console.log(chalk2.bold(`${category} (${hooks.length}):`));
8483
- for (const h of hooks) {
8484
- console.log(` ${chalk2.cyan(h.name)} ${chalk2.dim(`[${h.event}]`)} - ${h.description}`);
8485
- }
8486
- console.log();
8487
- }
8946
+ for (const h of visible)
8947
+ console.log(hookSummaryLine(h, { verbose: options.verbose }));
8948
+ printDisclosureHint(HOOKS.length - visible.length, "hooks info <name>", { includeAll: true });
8488
8949
  });
8489
- program2.command("search").argument("<query>", "Search term").option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
8950
+ program2.command("search").argument("<query>", "Search term").option("-n, --limit <n>", "Max rows to show in compact output", "10").option("--verbose", "Show descriptions for search results", false).option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
8490
8951
  const results = searchHooks(query);
8491
8952
  if (options.json) {
8492
8953
  console.log(JSON.stringify(results));
@@ -8496,13 +8957,14 @@ program2.command("search").argument("<query>", "Search term").option("-j, --json
8496
8957
  console.log(chalk2.dim(`No hooks found for "${query}"`));
8497
8958
  return;
8498
8959
  }
8960
+ const limit = parseLimit(options.limit, 10, 100);
8961
+ const visible = results.slice(0, limit);
8499
8962
  console.log(chalk2.bold(`
8500
- Found ${results.length} hook(s):
8963
+ Found ${results.length} hook(s), showing ${visible.length}:
8501
8964
  `));
8502
- for (const h of results) {
8503
- console.log(` ${chalk2.cyan(h.name)} ${chalk2.dim(`[${h.event}] [${h.category}]`)}`);
8504
- console.log(` ${h.description}`);
8505
- }
8965
+ for (const h of visible)
8966
+ console.log(hookSummaryLine(h, { verbose: options.verbose }));
8967
+ printDisclosureHint(results.length - visible.length, "hooks info <name>");
8506
8968
  });
8507
8969
  program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
8508
8970
  const scope = resolveScope(options);
@@ -8698,7 +9160,7 @@ Updating hooks...
8698
9160
  }
8699
9161
  }
8700
9162
  });
8701
- program2.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
9163
+ program2.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("--verbose", "Print full hook README content", false).option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
8702
9164
  if (hook) {
8703
9165
  const meta = getHook(hook);
8704
9166
  if (!meta) {
@@ -8733,13 +9195,23 @@ ${meta.displayName} v${meta.version}
8733
9195
  console.log(` hooks install ${meta.name} # global`);
8734
9196
  console.log(` hooks install ${meta.name} --project # project only`);
8735
9197
  console.log();
8736
- if (readme) {
9198
+ if (readme && options.verbose) {
8737
9199
  console.log(chalk2.bold(` README:
8738
9200
  `));
8739
9201
  for (const line of readme.split(`
8740
9202
  `)) {
8741
9203
  console.log(` ${line}`);
8742
9204
  }
9205
+ } else if (readme) {
9206
+ const preview = readmePreview(readme);
9207
+ if (preview) {
9208
+ console.log(chalk2.bold(` README Preview:
9209
+ `));
9210
+ console.log(` ${preview}
9211
+ `);
9212
+ }
9213
+ console.log(chalk2.dim(` README has ${readme.split(`
9214
+ `).length} lines. Use hooks docs ${meta.name} --verbose for the full README, or --json for machine-readable output.`));
8743
9215
  }
8744
9216
  return;
8745
9217
  }
@@ -8816,8 +9288,9 @@ ${meta.displayName} v${meta.version}
8816
9288
  console.log(chalk2.bold(`
8817
9289
  Hook-Specific Docs
8818
9290
  `));
8819
- console.log(` hooks docs <name> View README for a specific hook`);
8820
- console.log(` hooks docs --json Machine-readable documentation`);
9291
+ console.log(` hooks docs <name> Compact hook docs`);
9292
+ console.log(` hooks docs <name> --verbose Full hook README`);
9293
+ console.log(` hooks docs --json Machine-readable documentation`);
8821
9294
  console.log();
8822
9295
  });
8823
9296
  program2.command("upgrade").option("-c, --check", "Check for updates without installing", false).option("-j, --json", "Output as JSON", false).description("Update the @hasna/hooks package to the latest version").action(async (options) => {
@@ -8963,11 +9436,12 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
8963
9436
  `));
8964
9437
  for (const row of rows) {
8965
9438
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
8966
- const err = row.error ? chalk2.red(` ERR: ${row.error.slice(0, 60)}`) : "";
9439
+ const err = row.error ? chalk2.red(` ERR: ${truncateText2(row.error, 60)}`) : "";
8967
9440
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
8968
9441
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
8969
9442
  }
8970
- console.log();
9443
+ console.log(chalk2.dim(`
9444
+ Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
8971
9445
  });
8972
9446
  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) => {
8973
9447
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
@@ -8988,10 +9462,11 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
8988
9462
  `));
8989
9463
  for (const row of rows) {
8990
9464
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
8991
- const snippet = (row.tool_input || row.error || "").slice(0, 80);
9465
+ const snippet = truncateText2(row.tool_input || row.error || "", 80);
8992
9466
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
8993
9467
  }
8994
- console.log();
9468
+ console.log(chalk2.dim(`
9469
+ Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
8995
9470
  });
8996
9471
  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) => {
8997
9472
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
@@ -9011,11 +9486,12 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
9011
9486
  `));
9012
9487
  for (const row of rows) {
9013
9488
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
9014
- const err = row.error ? chalk2.red(` \u2717 ${row.error.slice(0, 60)}`) : "";
9489
+ const err = row.error ? chalk2.red(` \u2717 ${truncateText2(row.error, 60)}`) : "";
9015
9490
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
9016
9491
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
9017
9492
  }
9018
- console.log();
9493
+ console.log(chalk2.dim(`
9494
+ Compact rows shown. Use --json for full event records or -n <n> to change row count.`));
9019
9495
  });
9020
9496
  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) => {
9021
9497
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
@@ -9054,9 +9530,10 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
9054
9530
  `));
9055
9531
  for (const row of rows) {
9056
9532
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
9057
- console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(row.error.slice(0, 100))}`);
9533
+ console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(truncateText2(row.error, 100))}`);
9058
9534
  }
9059
- console.log();
9535
+ console.log(chalk2.dim(`
9536
+ Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
9060
9537
  });
9061
9538
  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) => {
9062
9539
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));