@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/README.md +22 -0
- package/bin/index.js +628 -151
- package/hooks/codewith-native-common.ts +155 -3
- package/hooks/hook-fleet-blockers-gate/README.md +23 -11
- package/hooks/hook-fleet-blockers-gate/package.json +2 -2
- package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +211 -15
- package/hooks/hook-fleet-blockers-gate/src/hook.ts +210 -49
- package/package.json +1 -1
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
|
-
}
|
|
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
|
-
|
|
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(
|
|
5357
|
-
|
|
5358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
5378
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5518
|
-
scope: z.enum(["global", "project"]).default("global").describe("Scope to check")
|
|
5519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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().
|
|
5822
|
-
since: z.string().optional().describe("ISO timestamp or duration string (e.g. '1h', '30m', '7d') to filter from")
|
|
5823
|
-
|
|
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(
|
|
5964
|
+
params.push(maxRows);
|
|
5853
5965
|
const rows = db.query(sql).all(...params);
|
|
5854
|
-
return {
|
|
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
|
-
|
|
5857
|
-
n: z.number().default(20).describe("Number of most recent events to return")
|
|
5858
|
-
|
|
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
|
|
5862
|
-
|
|
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
|
-
|
|
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().
|
|
5867
|
-
|
|
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,
|
|
5880
|
-
return {
|
|
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
|
-
|
|
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
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5975
|
-
|
|
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
|
|
6448
|
+
async appendEventOnce(event, options = {}) {
|
|
6273
6449
|
await this.init();
|
|
6274
|
-
|
|
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
|
|
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.
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
6580
|
-
return { event, deliveries, deduped: false };
|
|
6581
|
-
}
|
|
6582
|
-
async listEvents() {
|
|
6583
|
-
|
|
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
|
|
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
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
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
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
|
8899
|
+
for (const name of visible2) {
|
|
8440
8900
|
const meta = getHook(name);
|
|
8441
|
-
|
|
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
|
|
8465
|
-
console.log(
|
|
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
|
|
8481
|
-
|
|
8482
|
-
|
|
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
|
|
8503
|
-
console.log(
|
|
8504
|
-
|
|
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>
|
|
8820
|
-
console.log(` hooks docs --
|
|
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
|
|
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 || ""
|
|
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
|
|
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
|
|
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));
|