@hasna/hooks 0.3.11 → 0.4.1

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
@@ -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
  }
@@ -8208,6 +8378,39 @@ function resolveTarget(options) {
8208
8378
  return "all";
8209
8379
  return "claude";
8210
8380
  }
8381
+ function parseLimit(value, fallback, max) {
8382
+ const parsed = value ? parseInt(value, 10) : fallback;
8383
+ if (!Number.isFinite(parsed) || parsed <= 0)
8384
+ return fallback;
8385
+ return Math.min(parsed, max);
8386
+ }
8387
+ function truncateText2(value, max = 96) {
8388
+ const text = (value ?? "").replace(/\s+/g, " ").trim();
8389
+ if (text.length <= max)
8390
+ return text;
8391
+ return `${text.slice(0, Math.max(0, max - 3))}...`;
8392
+ }
8393
+ function readmePreview(readme, max = 280) {
8394
+ 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));
8395
+ const preview = blocks[0] ?? readme.split(`
8396
+ `).map((line) => line.trim()).find((line) => line && !line.startsWith("#"));
8397
+ return preview ? truncateText2(preview, max) : undefined;
8398
+ }
8399
+ function hookSummaryLine(hook, options = {}) {
8400
+ const matcher = hook.matcher ? ` ${hook.matcher}` : "";
8401
+ const description = options.verbose ? ` - ${truncateText2(hook.description, 110)}` : "";
8402
+ return ` ${chalk2.cyan(hook.name.padEnd(17))} ${chalk2.dim(`[${hook.event}${matcher}]`)} ${chalk2.dim(hook.category)}${description}`;
8403
+ }
8404
+ function printDisclosureHint(hidden, detailCommand, options = {}) {
8405
+ const rowControls = options.includeAll ? "--limit, --all, --verbose" : "--limit, --verbose";
8406
+ if (hidden > 0) {
8407
+ console.log(chalk2.dim(`
8408
+ Showing a compact subset. ${hidden} more hidden; use ${rowControls}, or ${detailCommand}.`));
8409
+ } else {
8410
+ console.log(chalk2.dim(`
8411
+ Use --verbose or ${detailCommand} for details.`));
8412
+ }
8413
+ }
8211
8414
  function editDistance(a, b) {
8212
8415
  const m = a.length, n = b.length;
8213
8416
  const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
@@ -8417,8 +8620,9 @@ Installing hooks (${scope}, ${target})...
8417
8620
  console.log(chalk2.dim(`
8418
8621
  Registered in ${settingsFile}`));
8419
8622
  });
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) => {
8623
+ 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
8624
  const scope = resolveScope(options);
8625
+ const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
8422
8626
  if (options.registered || options.installed) {
8423
8627
  const target = options.target === "gemini" ? "gemini" : options.target === "codewith" ? "codewith" : "claude";
8424
8628
  const registered = getRegisteredHooksForTarget(scope, target);
@@ -8433,13 +8637,18 @@ program2.command("list").alias("ls").option("-c, --category <category>", "Filter
8433
8637
  console.log(chalk2.dim(`No hooks registered (${scope}, ${target})`));
8434
8638
  return;
8435
8639
  }
8640
+ const visible2 = registered.slice(0, limit);
8436
8641
  console.log(chalk2.bold(`
8437
- Registered hooks \u2014 ${scope}/${target} (${registered.length}):
8642
+ Registered hooks \u2014 ${scope}/${target} (${registered.length}, showing ${visible2.length}):
8438
8643
  `));
8439
- for (const name of registered) {
8644
+ for (const name of visible2) {
8440
8645
  const meta = getHook(name);
8441
- console.log(` ${chalk2.cyan(name)} ${chalk2.dim(`[${meta?.event || "unknown"}]`)} - ${meta?.description || ""}`);
8646
+ if (meta)
8647
+ console.log(hookSummaryLine(meta, { verbose: options.verbose }));
8648
+ else
8649
+ console.log(` ${chalk2.cyan(name)} ${chalk2.dim("[unknown]")}`);
8442
8650
  }
8651
+ printDisclosureHint(registered.length - visible2.length, "hooks info <name>", { includeAll: true });
8443
8652
  return;
8444
8653
  }
8445
8654
  if (options.category) {
@@ -8458,12 +8667,13 @@ Registered hooks \u2014 ${scope}/${target} (${registered.length}):
8458
8667
  console.log(JSON.stringify(hooks));
8459
8668
  return;
8460
8669
  }
8670
+ const visible2 = hooks.slice(0, limit);
8461
8671
  console.log(chalk2.bold(`
8462
- ${category} (${hooks.length}):
8672
+ ${category} (${hooks.length}, showing ${visible2.length}):
8463
8673
  `));
8464
- for (const h of hooks) {
8465
- console.log(` ${chalk2.cyan(h.name)} ${chalk2.dim(`[${h.event}]`)} - ${h.description}`);
8466
- }
8674
+ for (const h of visible2)
8675
+ console.log(hookSummaryLine(h, { verbose: options.verbose }));
8676
+ printDisclosureHint(hooks.length - visible2.length, "hooks info <name>", { includeAll: true });
8467
8677
  return;
8468
8678
  }
8469
8679
  if (options.json) {
@@ -8474,19 +8684,15 @@ ${category} (${hooks.length}):
8474
8684
  console.log(JSON.stringify(result));
8475
8685
  return;
8476
8686
  }
8687
+ const visible = HOOKS.slice(0, limit);
8477
8688
  console.log(chalk2.bold(`
8478
- Available hooks (${HOOKS.length}):
8689
+ Available hooks (${HOOKS.length}, showing ${visible.length}):
8479
8690
  `));
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
- }
8691
+ for (const h of visible)
8692
+ console.log(hookSummaryLine(h, { verbose: options.verbose }));
8693
+ printDisclosureHint(HOOKS.length - visible.length, "hooks info <name>", { includeAll: true });
8488
8694
  });
8489
- program2.command("search").argument("<query>", "Search term").option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
8695
+ 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
8696
  const results = searchHooks(query);
8491
8697
  if (options.json) {
8492
8698
  console.log(JSON.stringify(results));
@@ -8496,13 +8702,14 @@ program2.command("search").argument("<query>", "Search term").option("-j, --json
8496
8702
  console.log(chalk2.dim(`No hooks found for "${query}"`));
8497
8703
  return;
8498
8704
  }
8705
+ const limit = parseLimit(options.limit, 10, 100);
8706
+ const visible = results.slice(0, limit);
8499
8707
  console.log(chalk2.bold(`
8500
- Found ${results.length} hook(s):
8708
+ Found ${results.length} hook(s), showing ${visible.length}:
8501
8709
  `));
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
- }
8710
+ for (const h of visible)
8711
+ console.log(hookSummaryLine(h, { verbose: options.verbose }));
8712
+ printDisclosureHint(results.length - visible.length, "hooks info <name>");
8506
8713
  });
8507
8714
  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
8715
  const scope = resolveScope(options);
@@ -8698,7 +8905,7 @@ Updating hooks...
8698
8905
  }
8699
8906
  }
8700
8907
  });
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) => {
8908
+ 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
8909
  if (hook) {
8703
8910
  const meta = getHook(hook);
8704
8911
  if (!meta) {
@@ -8733,13 +8940,23 @@ ${meta.displayName} v${meta.version}
8733
8940
  console.log(` hooks install ${meta.name} # global`);
8734
8941
  console.log(` hooks install ${meta.name} --project # project only`);
8735
8942
  console.log();
8736
- if (readme) {
8943
+ if (readme && options.verbose) {
8737
8944
  console.log(chalk2.bold(` README:
8738
8945
  `));
8739
8946
  for (const line of readme.split(`
8740
8947
  `)) {
8741
8948
  console.log(` ${line}`);
8742
8949
  }
8950
+ } else if (readme) {
8951
+ const preview = readmePreview(readme);
8952
+ if (preview) {
8953
+ console.log(chalk2.bold(` README Preview:
8954
+ `));
8955
+ console.log(` ${preview}
8956
+ `);
8957
+ }
8958
+ console.log(chalk2.dim(` README has ${readme.split(`
8959
+ `).length} lines. Use hooks docs ${meta.name} --verbose for the full README, or --json for machine-readable output.`));
8743
8960
  }
8744
8961
  return;
8745
8962
  }
@@ -8816,8 +9033,9 @@ ${meta.displayName} v${meta.version}
8816
9033
  console.log(chalk2.bold(`
8817
9034
  Hook-Specific Docs
8818
9035
  `));
8819
- console.log(` hooks docs <name> View README for a specific hook`);
8820
- console.log(` hooks docs --json Machine-readable documentation`);
9036
+ console.log(` hooks docs <name> Compact hook docs`);
9037
+ console.log(` hooks docs <name> --verbose Full hook README`);
9038
+ console.log(` hooks docs --json Machine-readable documentation`);
8821
9039
  console.log();
8822
9040
  });
8823
9041
  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 +9181,12 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
8963
9181
  `));
8964
9182
  for (const row of rows) {
8965
9183
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
8966
- const err = row.error ? chalk2.red(` ERR: ${row.error.slice(0, 60)}`) : "";
9184
+ const err = row.error ? chalk2.red(` ERR: ${truncateText2(row.error, 60)}`) : "";
8967
9185
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
8968
9186
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
8969
9187
  }
8970
- console.log();
9188
+ console.log(chalk2.dim(`
9189
+ Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
8971
9190
  });
8972
9191
  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
9192
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
@@ -8988,10 +9207,11 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
8988
9207
  `));
8989
9208
  for (const row of rows) {
8990
9209
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
8991
- const snippet = (row.tool_input || row.error || "").slice(0, 80);
9210
+ const snippet = truncateText2(row.tool_input || row.error || "", 80);
8992
9211
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.dim(snippet)}`);
8993
9212
  }
8994
- console.log();
9213
+ console.log(chalk2.dim(`
9214
+ Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
8995
9215
  });
8996
9216
  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
9217
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
@@ -9011,11 +9231,12 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
9011
9231
  `));
9012
9232
  for (const row of rows) {
9013
9233
  const ts = row.timestamp.slice(0, 19).replace("T", " ");
9014
- const err = row.error ? chalk2.red(` \u2717 ${row.error.slice(0, 60)}`) : "";
9234
+ const err = row.error ? chalk2.red(` \u2717 ${truncateText2(row.error, 60)}`) : "";
9015
9235
  const tool = row.tool_name ? chalk2.dim(` [${row.tool_name}]`) : "";
9016
9236
  console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))}${tool}${err}`);
9017
9237
  }
9018
- console.log();
9238
+ console.log(chalk2.dim(`
9239
+ Compact rows shown. Use --json for full event records or -n <n> to change row count.`));
9019
9240
  });
9020
9241
  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
9242
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
@@ -9054,9 +9275,10 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
9054
9275
  `));
9055
9276
  for (const row of rows) {
9056
9277
  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))}`);
9278
+ console.log(` ${chalk2.dim(ts)} ${chalk2.cyan(row.hook_name.padEnd(14))} ${chalk2.red(truncateText2(row.error, 100))}`);
9058
9279
  }
9059
- console.log();
9280
+ console.log(chalk2.dim(`
9281
+ Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
9060
9282
  });
9061
9283
  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
9284
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));