@hasna/hooks 0.3.4 → 0.3.6

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
@@ -2089,7 +2089,7 @@ var init_registry = __esm(() => {
2089
2089
  name: "knowledge-context",
2090
2090
  displayName: "Knowledge Context",
2091
2091
  description: "Injects deterministic Knowledge context packs into Codewith SessionStart, UserPromptSubmit, and SubagentStart",
2092
- version: "0.1.3",
2092
+ version: "0.1.5",
2093
2093
  category: "Context Management",
2094
2094
  event: "SessionStart",
2095
2095
  events: ["SessionStart", "UserPromptSubmit", "SubagentStart"],
package/dist/index.js CHANGED
@@ -1010,7 +1010,7 @@ var HOOKS = [
1010
1010
  name: "knowledge-context",
1011
1011
  displayName: "Knowledge Context",
1012
1012
  description: "Injects deterministic Knowledge context packs into Codewith SessionStart, UserPromptSubmit, and SubagentStart",
1013
- version: "0.1.3",
1013
+ version: "0.1.5",
1014
1014
  category: "Context Management",
1015
1015
  event: "SessionStart",
1016
1016
  events: ["SessionStart", "UserPromptSubmit", "SubagentStart"],
@@ -27,12 +27,24 @@ knowledge context pack <query> --from search --max-items <n> --max-tokens <n> --
27
27
 
28
28
  The hook intentionally does not pass `--semantic`, does not call web search,
29
29
  does not call ask/build/generate flows, and does not crawl raw stores directly.
30
+ For `UserPromptSubmit`, the hook first applies a deterministic high-signal gate
31
+ so short acknowledgements and casual follow-ups do not inject Knowledge context.
30
32
  When Knowledge returns a nonempty context pack, the hook redacts credential-like
31
33
  text from that pack and emits Codewith-native
32
34
  `hookSpecificOutput.additionalContext` with the same event name. Citation-only
33
- packs are expanded into progressive context lines with a bounded preview and a
34
- `knowledge get --id ... --json` read hint so agents can decide which full items
35
- are worth opening.
35
+ packs are expanded into progressive context lines with bounded previews and a
36
+ single `knowledge get --id <item_id> --json` hint so agents can decide which
37
+ full items are worth opening.
38
+
39
+ Example injected context:
40
+
41
+ ```text
42
+ [hook-knowledge-context] Knowledge matches (UserPromptSubmit; top 3, deterministic search):
43
+
44
+ If a match looks relevant, read it with: knowledge get --id <item_id> --json
45
+
46
+ - item_id=k_example cite=cite_123: Bounded preview text...
47
+ ```
36
48
 
37
49
  All failures are fail-open: missing CLI, timeout, nonzero exit, malformed JSON,
38
50
  bad stdin, or empty packs return `{"continue":true}` without context.
@@ -45,10 +57,17 @@ export HOOKS_KNOWLEDGE_COMMAND=knowledge # CLI command/path
45
57
  export HOOKS_KNOWLEDGE_TIMEOUT_MS=5000 # Per-call timeout
46
58
  export HOOKS_KNOWLEDGE_MAX_ITEMS=3 # Context pack item budget
47
59
  export HOOKS_KNOWLEDGE_MAX_TOKENS=1200 # Context pack token budget
60
+ export HOOKS_KNOWLEDGE_REQUIRE_HIGH_SIGNAL=1
61
+ export HOOKS_KNOWLEDGE_MIN_PROMPT_CHARS=6
62
+ export HOOKS_KNOWLEDGE_MIN_SIGNAL_SCORE=3
48
63
  export HOOKS_KNOWLEDGE_MAX_QUERY_CHARS=1200 # Redacted query bound
49
64
  export HOOKS_KNOWLEDGE_MAX_OUTPUT_CHARS=8000
50
65
  ```
51
66
 
67
+ All numeric env overrides are bounded. `HOOKS_KNOWLEDGE_COMMAND` accepts only a
68
+ single command/path token; unsafe values with whitespace fall back to
69
+ `knowledge`.
70
+
52
71
  ## Events
53
72
 
54
73
  - `SessionStart`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hook-knowledge-context",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Codewith hook that injects deterministic Knowledge context packs into lifecycle events",
5
5
  "type": "module",
6
6
  "main": "./dist/hook.js",
@@ -16,7 +16,7 @@
16
16
  "README.md"
17
17
  ],
18
18
  "scripts": {
19
- "build": "rm -rf dist && bun build ./src/hook.ts --outdir ./dist --target node && tsc -p tsconfig.json --emitDeclarationOnly --declaration --declarationMap false --noEmit false",
19
+ "build": "rm -rf dist && bun build ./src/hook.ts --outdir ./dist --target bun && tsc -p tsconfig.json --emitDeclarationOnly --declaration --declarationMap false --noEmit false",
20
20
  "prepublishOnly": "bun run build",
21
21
  "typecheck": "tsc --noEmit"
22
22
  },
@@ -5,6 +5,7 @@ import {
5
5
  buildQuery,
6
6
  extractContextText,
7
7
  getConfig,
8
+ promptSignalScore,
8
9
  redactSecrets,
9
10
  sanitizeQuery,
10
11
  type KnowledgeContextConfig,
@@ -16,15 +17,44 @@ const baseConfig: KnowledgeContextConfig = {
16
17
  timeoutMs: 5000,
17
18
  maxItems: 3,
18
19
  maxTokens: 1200,
20
+ minPromptChars: 6,
21
+ minSignalScore: 3,
19
22
  maxQueryChars: 1200,
20
23
  maxOutputChars: 8000,
24
+ requireHighSignal: true,
21
25
  };
22
26
 
23
27
  describe("hook-knowledge-context", () => {
24
28
  test("defaults Knowledge CLI timeout to 5000ms and keeps env override", () => {
25
29
  expect(getConfig({}).timeoutMs).toBe(5000);
26
30
  expect(getConfig({}).maxItems).toBe(3);
31
+ expect(getConfig({}).minPromptChars).toBe(6);
32
+ expect(getConfig({}).minSignalScore).toBe(3);
33
+ expect(getConfig({}).requireHighSignal).toBe(true);
27
34
  expect(getConfig({ HOOKS_KNOWLEDGE_TIMEOUT_MS: "2345" }).timeoutMs).toBe(2345);
35
+ expect(getConfig({ HOOKS_KNOWLEDGE_REQUIRE_HIGH_SIGNAL: "0" }).requireHighSignal).toBe(false);
36
+ });
37
+
38
+ test("applies bounded env overrides and rejects unsafe command override", () => {
39
+ const config = getConfig({
40
+ HOOKS_KNOWLEDGE_COMMAND: "/tmp/knowledge",
41
+ HOOKS_KNOWLEDGE_MAX_ITEMS: "9",
42
+ HOOKS_KNOWLEDGE_MAX_TOKENS: "2345",
43
+ HOOKS_KNOWLEDGE_MIN_PROMPT_CHARS: "12",
44
+ HOOKS_KNOWLEDGE_MIN_SIGNAL_SCORE: "4",
45
+ HOOKS_KNOWLEDGE_MAX_QUERY_CHARS: "200",
46
+ HOOKS_KNOWLEDGE_MAX_OUTPUT_CHARS: "9999",
47
+ });
48
+
49
+ expect(config.command).toBe("/tmp/knowledge");
50
+ expect(config.maxItems).toBe(9);
51
+ expect(config.maxTokens).toBe(2345);
52
+ expect(config.minPromptChars).toBe(12);
53
+ expect(config.minSignalScore).toBe(4);
54
+ expect(config.maxQueryChars).toBe(200);
55
+ expect(config.maxOutputChars).toBe(9999);
56
+ expect(buildKnowledgeArgs("q", config)).toContain("9");
57
+ expect(getConfig({ HOOKS_KNOWLEDGE_COMMAND: "bad command" }).command).toBe("knowledge");
28
58
  });
29
59
 
30
60
  test("builds deterministic knowledge context pack args without semantic, web, or generation flags", () => {
@@ -67,7 +97,7 @@ describe("hook-knowledge-context", () => {
67
97
 
68
98
  expect(output.continue).toBe(true);
69
99
  expect(output.hookSpecificOutput?.hookEventName).toBe("SessionStart");
70
- expect(output.hookSpecificOutput?.additionalContext).toContain("knowledge context pack --from search");
100
+ expect(output.hookSpecificOutput?.additionalContext).toContain("Knowledge matches");
71
101
  expect(output.hookSpecificOutput?.additionalContext).toContain("Use hooks.json");
72
102
  });
73
103
 
@@ -87,7 +117,7 @@ describe("hook-knowledge-context", () => {
87
117
  {
88
118
  hook_event_name: "UserPromptSubmit",
89
119
  cwd: "/repo",
90
- prompt: "implement this with token=demo",
120
+ prompt: "implement the knowledge hook context output with token=demo",
91
121
  },
92
122
  executor
93
123
  );
@@ -151,6 +181,46 @@ describe("hook-knowledge-context", () => {
151
181
  ).toEqual({ continue: true });
152
182
  });
153
183
 
184
+ test("fails open for low-signal UserPromptSubmit prompts before calling Knowledge", async () => {
185
+ let executorCalls = 0;
186
+ const executor: KnowledgeExecutor = async () => {
187
+ executorCalls += 1;
188
+ throw new Error("executor should not run for low-signal prompts");
189
+ };
190
+
191
+ for (const prompt of ["", "ok", "thanks", "?", "continue", "tell me again"]) {
192
+ expect(
193
+ await buildHookOutput({ hook_event_name: "UserPromptSubmit", cwd: "/repo", prompt }, executor)
194
+ ).toEqual({ continue: true });
195
+ }
196
+ expect(executorCalls).toBe(0);
197
+ for (const prompt of [
198
+ "fix authz",
199
+ "release",
200
+ "publish npm",
201
+ "check src/hook.ts",
202
+ "@hasna/hooks",
203
+ "merge PR 12",
204
+ "review staged diff",
205
+ "rerun tests",
206
+ ]) {
207
+ expect(promptSignalScore(prompt, "/home/hasna/workspace/hasna/opensource/open-hooks")).toBeGreaterThanOrEqual(3);
208
+ }
209
+ });
210
+
211
+ test("can opt out of high-signal gating for UserPromptSubmit", async () => {
212
+ const output = await buildHookOutput(
213
+ { hook_event_name: "UserPromptSubmit", cwd: "/repo", prompt: "ok" },
214
+ async () => ({
215
+ ok: true,
216
+ stdout: JSON.stringify({ context: "Forced low-signal context." }),
217
+ }),
218
+ { HOOKS_KNOWLEDGE_REQUIRE_HIGH_SIGNAL: "0" }
219
+ );
220
+
221
+ expect(output.hookSpecificOutput?.additionalContext).toContain("Forced low-signal context");
222
+ });
223
+
154
224
  test("sanitizes and bounds queries", () => {
155
225
  expect(redactSecrets("password=hunter2 token=demo")).toBe(
156
226
  "password=<redacted> token=<redacted>"
@@ -172,21 +242,65 @@ describe("hook-knowledge-context", () => {
172
242
  expect(extractContextText([{ title: "One", text: "Body", uri: "knowledge://item/one" }])).toContain("Body");
173
243
  });
174
244
 
175
- test("formats citation previews as useful progressive blurbs with compact open commands", () => {
245
+ test("formats citation-only packs as item_id/cite bullets with one read hint", () => {
176
246
  const text = extractContextText({
177
247
  citations: [
178
248
  {
179
- id: "cite_123",
180
- source_ref: "knowledge://item/k_example",
181
- quote_preview: "A short preview that tells the agent why this Knowledge item might matter.",
249
+ id: "cite_1",
250
+ source_ref: "knowledge://item/k_one",
251
+ quote_preview: "First preview that tells the agent why this Knowledge item might matter.",
252
+ },
253
+ {
254
+ id: "cite_2",
255
+ source_ref: "knowledge://item/k_two",
256
+ quote_preview: "Second preview that tells the agent why this Knowledge item might matter.",
182
257
  },
183
258
  ],
184
259
  });
185
260
 
186
- expect(text).toContain("- k_example (cite_123): A short preview");
187
- expect(text).toContain("cite_123");
188
- expect(text).toContain("A short preview");
189
- expect(text).toContain("open: knowledge get --id k_example --json");
190
- expect(text).not.toContain("source: knowledge://item/k_example");
261
+ expect(text).toContain("If a match looks relevant, read it with: knowledge get --id <item_id> --json");
262
+ expect(text?.match(/knowledge get --id/g) ?? []).toHaveLength(1);
263
+ expect(text).toContain("- item_id=k_one cite=cite_1: First preview");
264
+ expect(text).toContain("- item_id=k_two cite=cite_2: Second preview");
265
+ expect(text).not.toContain("open: knowledge get --id k_one --json");
266
+ expect(text).not.toContain("(cite_1)");
267
+ expect(text).not.toContain("source: knowledge://item/k_one");
268
+ });
269
+
270
+ test("extracts item_id from legacy Knowledge source paths", () => {
271
+ const text = extractContextText({
272
+ citations: [
273
+ {
274
+ id: "cite_abc",
275
+ source_ref: "open-files://source/legacy-json/path/k_mqyrz7gt_olq82e",
276
+ quote_preview: "Legacy file source preview.",
277
+ },
278
+ ],
279
+ });
280
+
281
+ expect(text).toContain("item_id=k_mqyrz7gt_olq82e");
282
+ expect(text).toContain("cite=cite_abc");
283
+ expect(text).not.toContain("source=open-files://source/legacy-json/path/k_mqyrz7gt_olq82e");
284
+ });
285
+
286
+ test("keeps source fallback for non-Knowledge citations without item ids", () => {
287
+ const text = extractContextText({
288
+ citations: [
289
+ {
290
+ id: "cite_url",
291
+ source_ref: "https://example.test/doc",
292
+ quote_preview: "External preview.",
293
+ },
294
+ ],
295
+ });
296
+
297
+ expect(text).toContain("- cite=cite_url source=https://example.test/doc: External preview.");
298
+ expect(text).not.toContain("item_id=");
299
+ expect(text).not.toContain("knowledge get --id");
300
+ });
301
+
302
+ test("cleans legacy empty-title bullet artifacts in item text", () => {
303
+ expect(extractContextText(["- : legacy empty title preview"])).toBe("- legacy empty title preview");
304
+ expect(extractContextText("- : legacy direct context")).toBe("- legacy direct context");
191
305
  });
192
306
  });
@@ -40,8 +40,11 @@ export interface KnowledgeContextConfig {
40
40
  timeoutMs: number;
41
41
  maxItems: number;
42
42
  maxTokens: number;
43
+ minPromptChars: number;
44
+ minSignalScore: number;
43
45
  maxQueryChars: number;
44
46
  maxOutputChars: number;
47
+ requireHighSignal: boolean;
45
48
  }
46
49
 
47
50
  export interface KnowledgeExecResult {
@@ -58,6 +61,8 @@ export type KnowledgeExecutor = (
58
61
  const DEFAULT_TIMEOUT_MS = 5000;
59
62
  const DEFAULT_MAX_ITEMS = 3;
60
63
  const DEFAULT_MAX_TOKENS = 1200;
64
+ const DEFAULT_MIN_PROMPT_CHARS = 6;
65
+ const DEFAULT_MIN_SIGNAL_SCORE = 3;
61
66
  const DEFAULT_MAX_QUERY_CHARS = 1200;
62
67
  const DEFAULT_MAX_OUTPUT_CHARS = 8000;
63
68
  const SUPPORTED_EVENTS = new Set<KnowledgeContextEvent>([
@@ -97,8 +102,11 @@ export function getConfig(env: NodeJS.ProcessEnv = process.env): KnowledgeContex
97
102
  timeoutMs: boundedNumber(env.HOOKS_KNOWLEDGE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, 100, 10_000),
98
103
  maxItems: boundedNumber(env.HOOKS_KNOWLEDGE_MAX_ITEMS, DEFAULT_MAX_ITEMS, 1, 20),
99
104
  maxTokens: boundedNumber(env.HOOKS_KNOWLEDGE_MAX_TOKENS, DEFAULT_MAX_TOKENS, 100, 8_000),
105
+ minPromptChars: boundedNumber(env.HOOKS_KNOWLEDGE_MIN_PROMPT_CHARS, DEFAULT_MIN_PROMPT_CHARS, 0, 4_000),
106
+ minSignalScore: boundedNumber(env.HOOKS_KNOWLEDGE_MIN_SIGNAL_SCORE, DEFAULT_MIN_SIGNAL_SCORE, 1, 10),
100
107
  maxQueryChars: boundedNumber(env.HOOKS_KNOWLEDGE_MAX_QUERY_CHARS, DEFAULT_MAX_QUERY_CHARS, 80, 4_000),
101
108
  maxOutputChars: boundedNumber(env.HOOKS_KNOWLEDGE_MAX_OUTPUT_CHARS, DEFAULT_MAX_OUTPUT_CHARS, 500, 20_000),
109
+ requireHighSignal: env.HOOKS_KNOWLEDGE_REQUIRE_HIGH_SIGNAL !== "0",
102
110
  };
103
111
  }
104
112
 
@@ -135,6 +143,52 @@ export function sanitizeQuery(text: string, maxChars: number): string {
135
143
  return redacted.length > maxChars ? redacted.slice(0, maxChars).trim() : redacted;
136
144
  }
137
145
 
146
+ function cleanLegacyEmptyBullet(text: string): string {
147
+ return text.replace(/^(\s*[-*])\s*:\s*/gm, "$1 ");
148
+ }
149
+
150
+ function cleanStringItem(text: string): string {
151
+ return cleanLegacyEmptyBullet(text).replace(/^[-*]\s*/, "").trim();
152
+ }
153
+
154
+ const DOMAIN_SIGNAL_RE =
155
+ /\b(auth|authz|rls|tenant|deploy|release|publish|rollback|merge|pr|staged|diff|security|secret|credential|prod|incident|migration|billing|stripe|oauth|webhook|connector|loop|workflow|agent|subagent|knowledge|hook|hooks|codewith|worktree|repo|repository|project|task|todo|todos|memento|mementos|conversation|conversations|context|config|configuration|npm|bun|package|cli|install|global|station|spark|machine|file|path|markdown|docs?|ci|tests?|build|typecheck|verify|debug|error|failing|failure|bug|regression)\b/i;
156
+ const ACTION_SIGNAL_RE =
157
+ /\b(add|build|change|check|configure|create|debug|fix|implement|improve|inspect|install|merge|patch|publish|refactor|release|rerun|review|rollback|test|update|verify)\b/i;
158
+ const STRUCTURAL_SIGNAL_RE =
159
+ /(`[^`]+`|(?:^|\s)(?:\.{1,2}\/)?[\w.-]+\/[\w./-]+|\/[\w./-]+|@hasna\/[a-z0-9-]+|\b(?:open|platform|iapp)-[a-z0-9-]+\b|[\w.-]+\.(ts|tsx|js|jsx|json|toml|md|yml|yaml|rs|py|go|sh|sql))/i;
160
+ const PROJECT_CWD_SIGNAL_RE = /\/(?:open|platform|iapp)-[a-z0-9-]+(?:\/|$)|\/hasna\/(?:opensource|infra|community)\//i;
161
+
162
+ export function promptSignalScore(prompt: string, cwd = ""): number {
163
+ const text = compactText(prompt);
164
+ if (!text) return 0;
165
+
166
+ let score = 0;
167
+ if (text.length >= 80) score += 3;
168
+ else if (text.length >= 40) score += 1;
169
+
170
+ const wordCount = text.split(/\s+/).filter(Boolean).length;
171
+ if (wordCount >= 12) score += 3;
172
+ if (DOMAIN_SIGNAL_RE.test(text)) score += 3;
173
+ if (ACTION_SIGNAL_RE.test(text)) score += 2;
174
+ if (STRUCTURAL_SIGNAL_RE.test(text)) score += 3;
175
+ if (cwd && PROJECT_CWD_SIGNAL_RE.test(cwd) && text.length >= 20) score += 2;
176
+ if (/\b(error|exception|failed|failure|regression|stack trace|traceback)\b/i.test(text)) score += 2;
177
+
178
+ return score;
179
+ }
180
+
181
+ export function shouldSearchKnowledge(input: HookInput, config: KnowledgeContextConfig): boolean {
182
+ if (!config.requireHighSignal) return true;
183
+ if (input.hook_event_name !== "UserPromptSubmit") return true;
184
+
185
+ const prompt = stringField(input, "prompt") || stringField(input, "user_prompt");
186
+ const score = promptSignalScore(prompt, stringField(input, "cwd"));
187
+ const promptChars = compactText(prompt).length;
188
+
189
+ return promptChars >= config.minPromptChars && score >= config.minSignalScore;
190
+ }
191
+
138
192
  export function buildQuery(input: HookInput, config: KnowledgeContextConfig): string | null {
139
193
  const cwd = stringField(input, "cwd");
140
194
  const event = stringField(input, "hook_event_name");
@@ -258,16 +312,21 @@ function compactText(text: string): string {
258
312
  }
259
313
 
260
314
  function knowledgeItemId(source: string | null): string | null {
261
- const match = source?.match(/^knowledge:\/\/item\/([^/?#\s]+)$/);
262
- return match?.[1] ?? null;
315
+ const direct = source?.match(/^knowledge:\/\/item\/([^/?#\s]+)$/);
316
+ if (direct) return direct[1];
317
+
318
+ const legacyPath = source?.match(/(?:^|\/)(k_[A-Za-z0-9]+_[A-Za-z0-9]+)(?:[?#].*)?$/);
319
+ return legacyPath?.[1] ?? null;
263
320
  }
264
321
 
265
322
  function formatPackItems(items: unknown[]): string | null {
266
323
  const lines: string[] = [];
324
+ let hasKnowledgeItems = false;
267
325
 
268
326
  for (const [index, item] of items.entries()) {
269
327
  if (typeof item === "string" && item.trim()) {
270
- lines.push(`- ${truncate(item.trim(), 900)}`);
328
+ const text = cleanStringItem(item);
329
+ if (text) lines.push(`- ${truncate(text, 900)}`);
271
330
  continue;
272
331
  }
273
332
  if (!item || typeof item !== "object") continue;
@@ -295,23 +354,35 @@ function formatPackItems(items: unknown[]): string | null {
295
354
  ]);
296
355
  if (!body && !source) continue;
297
356
 
298
- const suffixParts = [
299
- citationId && citationId !== title ? citationId : null,
300
- source && !itemId ? `source: ${truncate(source, 180)}` : null,
301
- ].filter(Boolean);
302
- const suffix = suffixParts.length > 0 ? ` (${suffixParts.join("; ")})` : "";
303
- lines.push(`- ${truncate(title, 160)}${suffix}${body ? `: ${truncate(compactText(body), 520)}` : ""}`);
304
357
  if (itemId) {
305
- lines.push(` open: knowledge get --id ${itemId} --json`);
358
+ hasKnowledgeItems = true;
359
+ const labelParts = [`item_id=${itemId}`];
360
+ if (citationId) labelParts.push(`cite=${citationId}`);
361
+ const preview = body ? compactText(body) : title !== itemId ? title : "";
362
+ lines.push(`- ${labelParts.join(" ")}${preview ? `: ${truncate(preview, 520)}` : ""}`);
363
+ continue;
306
364
  }
365
+
366
+ const suffixParts = [
367
+ citationId ? `cite=${citationId}` : null,
368
+ source ? `source=${truncate(source, 180)}` : null,
369
+ ].filter(Boolean);
370
+ const label = suffixParts.length > 0 ? suffixParts.join(" ") : truncate(title, 160);
371
+ lines.push(`- ${label}${body ? `: ${truncate(compactText(body), 520)}` : ""}`);
307
372
  }
308
373
 
309
- return lines.length > 0 ? lines.join("\n") : null;
374
+ if (lines.length === 0) return null;
375
+ if (!hasKnowledgeItems) return lines.join("\n");
376
+ return [
377
+ "If a match looks relevant, read it with: knowledge get --id <item_id> --json",
378
+ "",
379
+ ...lines,
380
+ ].join("\n");
310
381
  }
311
382
 
312
383
  export function extractContextText(pack: unknown): string | null {
313
384
  if (typeof pack === "string") {
314
- return pack.trim() || null;
385
+ return cleanLegacyEmptyBullet(pack.trim()) || null;
315
386
  }
316
387
  if (Array.isArray(pack)) {
317
388
  return formatPackItems(pack);
@@ -339,7 +410,7 @@ export function formatAdditionalContext(
339
410
  context: string,
340
411
  config: KnowledgeContextConfig
341
412
  ): string {
342
- const header = `[hook-knowledge-context] Deterministic Knowledge context (${event}; knowledge context pack --from search):`;
413
+ const header = `[hook-knowledge-context] Knowledge matches (${event}; top ${config.maxItems}, deterministic search):`;
343
414
  const safeContext = redactSecrets(context.trim());
344
415
  return `${header}\n\n${truncate(safeContext, Math.max(0, config.maxOutputChars - header.length - 2))}`;
345
416
  }
@@ -363,6 +434,10 @@ export async function buildHookOutput(
363
434
  }
364
435
 
365
436
  const config = getConfig(env);
437
+ if (!shouldSearchKnowledge(input, config)) {
438
+ return { continue: true };
439
+ }
440
+
366
441
  const query = buildQuery(input, config);
367
442
  if (!query) {
368
443
  return { continue: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -73,6 +73,7 @@
73
73
  "bin/",
74
74
  "dist/",
75
75
  "hooks/",
76
+ "!hooks/*/dist/",
76
77
  "README.md",
77
78
  "LICENSE"
78
79
  ],