@hasna/hooks 0.3.6 → 0.3.8

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.5",
2092
+ version: "0.1.6",
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.5",
1013
+ version: "0.1.6",
1014
1014
  category: "Context Management",
1015
1015
  event: "SessionStart",
1016
1016
  events: ["SessionStart", "UserPromptSubmit", "SubagentStart"],
@@ -1,6 +1,8 @@
1
- # hook-knowledge-context
1
+ # knowledge-context
2
2
 
3
3
  Codewith lifecycle hook that injects deterministic Knowledge context packs.
4
+ This hook is distributed through the `@hasna/hooks` catalog only; do not publish
5
+ this directory as a standalone npm package.
4
6
 
5
7
  ## Installation
6
8
 
@@ -34,7 +36,9 @@ text from that pack and emits Codewith-native
34
36
  `hookSpecificOutput.additionalContext` with the same event name. Citation-only
35
37
  packs are expanded into progressive context lines with bounded previews and a
36
38
  single `knowledge get --id <item_id> --json` hint so agents can decide which
37
- full items are worth opening.
39
+ full items are worth opening. Items that mark themselves as
40
+ historical/reference-only or not suitable for auto-loading are filtered out by
41
+ default, because they are not useful ambient context for normal agent work.
38
42
 
39
43
  Example injected context:
40
44
 
@@ -55,7 +59,8 @@ bad stdin, or empty packs return `{"continue":true}` without context.
55
59
  export HOOKS_KNOWLEDGE_CONTEXT_DISABLE=1 # Kill switch
56
60
  export HOOKS_KNOWLEDGE_COMMAND=knowledge # CLI command/path
57
61
  export HOOKS_KNOWLEDGE_TIMEOUT_MS=5000 # Per-call timeout
58
- export HOOKS_KNOWLEDGE_MAX_ITEMS=3 # Context pack item budget
62
+ export HOOKS_KNOWLEDGE_MAX_ITEMS=3 # Rendered item budget
63
+ export HOOKS_KNOWLEDGE_CANDIDATE_ITEMS=12 # Internal candidate budget before filtering
59
64
  export HOOKS_KNOWLEDGE_MAX_TOKENS=1200 # Context pack token budget
60
65
  export HOOKS_KNOWLEDGE_REQUIRE_HIGH_SIGNAL=1
61
66
  export HOOKS_KNOWLEDGE_MIN_PROMPT_CHARS=6
@@ -16,6 +16,7 @@ const baseConfig: KnowledgeContextConfig = {
16
16
  command: "knowledge",
17
17
  timeoutMs: 5000,
18
18
  maxItems: 3,
19
+ candidateItems: 12,
19
20
  maxTokens: 1200,
20
21
  minPromptChars: 6,
21
22
  minSignalScore: 3,
@@ -28,6 +29,7 @@ describe("hook-knowledge-context", () => {
28
29
  test("defaults Knowledge CLI timeout to 5000ms and keeps env override", () => {
29
30
  expect(getConfig({}).timeoutMs).toBe(5000);
30
31
  expect(getConfig({}).maxItems).toBe(3);
32
+ expect(getConfig({}).candidateItems).toBe(12);
31
33
  expect(getConfig({}).minPromptChars).toBe(6);
32
34
  expect(getConfig({}).minSignalScore).toBe(3);
33
35
  expect(getConfig({}).requireHighSignal).toBe(true);
@@ -39,6 +41,7 @@ describe("hook-knowledge-context", () => {
39
41
  const config = getConfig({
40
42
  HOOKS_KNOWLEDGE_COMMAND: "/tmp/knowledge",
41
43
  HOOKS_KNOWLEDGE_MAX_ITEMS: "9",
44
+ HOOKS_KNOWLEDGE_CANDIDATE_ITEMS: "15",
42
45
  HOOKS_KNOWLEDGE_MAX_TOKENS: "2345",
43
46
  HOOKS_KNOWLEDGE_MIN_PROMPT_CHARS: "12",
44
47
  HOOKS_KNOWLEDGE_MIN_SIGNAL_SCORE: "4",
@@ -48,12 +51,13 @@ describe("hook-knowledge-context", () => {
48
51
 
49
52
  expect(config.command).toBe("/tmp/knowledge");
50
53
  expect(config.maxItems).toBe(9);
54
+ expect(config.candidateItems).toBe(15);
51
55
  expect(config.maxTokens).toBe(2345);
52
56
  expect(config.minPromptChars).toBe(12);
53
57
  expect(config.minSignalScore).toBe(4);
54
58
  expect(config.maxQueryChars).toBe(200);
55
59
  expect(config.maxOutputChars).toBe(9999);
56
- expect(buildKnowledgeArgs("q", config)).toContain("9");
60
+ expect(buildKnowledgeArgs("q", config)).toContain("15");
57
61
  expect(getConfig({ HOOKS_KNOWLEDGE_COMMAND: "bad command" }).command).toBe("knowledge");
58
62
  });
59
63
 
@@ -67,7 +71,7 @@ describe("hook-knowledge-context", () => {
67
71
  "--from",
68
72
  "search",
69
73
  "--max-items",
70
- "3",
74
+ "12",
71
75
  "--max-tokens",
72
76
  "1200",
73
77
  "--json",
@@ -303,4 +307,73 @@ describe("hook-knowledge-context", () => {
303
307
  expect(extractContextText(["- : legacy empty title preview"])).toBe("- legacy empty title preview");
304
308
  expect(extractContextText("- : legacy direct context")).toBe("- legacy direct context");
305
309
  });
310
+
311
+ test("filters historical reference-only archive items from injected context", () => {
312
+ const text = extractContextText(
313
+ {
314
+ citations: [
315
+ {
316
+ id: "cite_archived",
317
+ source_ref: "knowledge://item/k_archived",
318
+ quote_preview:
319
+ "Archived on 2026-06-25 before removing brain orchestration startup files from spark01. Historical/reference only; not an active startup instruction.",
320
+ },
321
+ {
322
+ id: "cite_active",
323
+ source_ref: "knowledge://item/k_active",
324
+ quote_preview: "Current npm publish convention: use scoped granular tokens through the secrets CLI.",
325
+ },
326
+ ],
327
+ },
328
+ { maxItems: 3 }
329
+ );
330
+
331
+ expect(text).toContain("item_id=k_active cite=cite_active");
332
+ expect(text).toContain("Current npm publish convention");
333
+ expect(text).not.toContain("k_archived");
334
+ expect(text).not.toContain("Archived on 2026-06-25");
335
+ });
336
+
337
+ test("fails open when all citation candidates are non-autoloadable archive items", () => {
338
+ expect(
339
+ extractContextText(
340
+ {
341
+ citations: [
342
+ {
343
+ id: "cite_archived",
344
+ source_ref: "knowledge://item/k_archived",
345
+ quote_preview:
346
+ "Archived on 2026-06-25 from local Claude startup context. These instructions are historical/reference only and should not be auto-loaded for normal agents.",
347
+ },
348
+ ],
349
+ },
350
+ { maxItems: 3 }
351
+ )
352
+ ).toBeNull();
353
+ });
354
+
355
+ test("caps emitted items after filtering extra search candidates", () => {
356
+ const text = extractContextText(
357
+ {
358
+ citations: [
359
+ {
360
+ id: "cite_archived",
361
+ source_ref: "knowledge://item/k_archived",
362
+ quote_preview:
363
+ "Archived on 2026-06-25 before removing brain orchestration startup files. Historical/reference only; not an active startup instruction.",
364
+ },
365
+ { id: "cite_one", source_ref: "knowledge://item/k_one", quote_preview: "First active item." },
366
+ { id: "cite_two", source_ref: "knowledge://item/k_two", quote_preview: "Second active item." },
367
+ { id: "cite_three", source_ref: "knowledge://item/k_three", quote_preview: "Third active item." },
368
+ ],
369
+ },
370
+ { maxItems: 2 }
371
+ );
372
+
373
+ expect(text?.match(/^- item_id=/gm) ?? []).toHaveLength(2);
374
+ expect(text).toContain("item_id=k_one");
375
+ expect(text).toContain("item_id=k_two");
376
+ expect(text).not.toContain("item_id=k_three");
377
+ expect(text).not.toContain("item_id=k_archived");
378
+ });
306
379
  });
@@ -39,6 +39,7 @@ export interface KnowledgeContextConfig {
39
39
  command: string;
40
40
  timeoutMs: number;
41
41
  maxItems: number;
42
+ candidateItems: number;
42
43
  maxTokens: number;
43
44
  minPromptChars: number;
44
45
  minSignalScore: number;
@@ -97,10 +98,17 @@ function commandFromEnv(raw: string | undefined): string {
97
98
  }
98
99
 
99
100
  export function getConfig(env: NodeJS.ProcessEnv = process.env): KnowledgeContextConfig {
101
+ const maxItems = boundedNumber(env.HOOKS_KNOWLEDGE_MAX_ITEMS, DEFAULT_MAX_ITEMS, 1, 20);
100
102
  return {
101
103
  command: commandFromEnv(env.HOOKS_KNOWLEDGE_COMMAND),
102
104
  timeoutMs: boundedNumber(env.HOOKS_KNOWLEDGE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, 100, 10_000),
103
- maxItems: boundedNumber(env.HOOKS_KNOWLEDGE_MAX_ITEMS, DEFAULT_MAX_ITEMS, 1, 20),
105
+ maxItems,
106
+ candidateItems: boundedNumber(
107
+ env.HOOKS_KNOWLEDGE_CANDIDATE_ITEMS,
108
+ Math.min(20, Math.max(maxItems, maxItems * 4)),
109
+ maxItems,
110
+ 20
111
+ ),
104
112
  maxTokens: boundedNumber(env.HOOKS_KNOWLEDGE_MAX_TOKENS, DEFAULT_MAX_TOKENS, 100, 8_000),
105
113
  minPromptChars: boundedNumber(env.HOOKS_KNOWLEDGE_MIN_PROMPT_CHARS, DEFAULT_MIN_PROMPT_CHARS, 0, 4_000),
106
114
  minSignalScore: boundedNumber(env.HOOKS_KNOWLEDGE_MIN_SIGNAL_SCORE, DEFAULT_MIN_SIGNAL_SCORE, 1, 10),
@@ -224,7 +232,7 @@ export function buildKnowledgeArgs(query: string, config: KnowledgeContextConfig
224
232
  "--from",
225
233
  "search",
226
234
  "--max-items",
227
- String(config.maxItems),
235
+ String(config.candidateItems),
228
236
  "--max-tokens",
229
237
  String(config.maxTokens),
230
238
  "--json",
@@ -319,14 +327,31 @@ function knowledgeItemId(source: string | null): string | null {
319
327
  return legacyPath?.[1] ?? null;
320
328
  }
321
329
 
322
- function formatPackItems(items: unknown[]): string | null {
330
+ interface ExtractContextOptions {
331
+ maxItems?: number;
332
+ includeHistorical?: boolean;
333
+ }
334
+
335
+ const NON_AUTOLOADABLE_HISTORY_RE =
336
+ /\b(historical\/reference only|historical reference only|should not be auto-?loaded|do not auto-?load|not an active startup instruction|brain orchestration startup files)\b/i;
337
+ const ARCHIVED_STARTUP_RE = /^Archived on \d{4}-\d{2}-\d{2}\b.*\b(startup context|startup files|CLAUDE\.md)\b/i;
338
+
339
+ function shouldSuppressKnowledgeItem(text: string, options: ExtractContextOptions): boolean {
340
+ if (options.includeHistorical) return false;
341
+ const compact = compactText(text);
342
+ return NON_AUTOLOADABLE_HISTORY_RE.test(compact) || ARCHIVED_STARTUP_RE.test(compact);
343
+ }
344
+
345
+ function formatPackItems(items: unknown[], options: ExtractContextOptions = {}): string | null {
323
346
  const lines: string[] = [];
324
347
  let hasKnowledgeItems = false;
348
+ const maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS;
325
349
 
326
350
  for (const [index, item] of items.entries()) {
351
+ if (lines.length >= maxItems) break;
327
352
  if (typeof item === "string" && item.trim()) {
328
353
  const text = cleanStringItem(item);
329
- if (text) lines.push(`- ${truncate(text, 900)}`);
354
+ if (text && !shouldSuppressKnowledgeItem(text, options)) lines.push(`- ${truncate(text, 900)}`);
330
355
  continue;
331
356
  }
332
357
  if (!item || typeof item !== "object") continue;
@@ -353,6 +378,8 @@ function formatPackItems(items: unknown[]): string | null {
353
378
  "description",
354
379
  ]);
355
380
  if (!body && !source) continue;
381
+ const itemText = [title, body, source].filter(Boolean).join(" ");
382
+ if (shouldSuppressKnowledgeItem(itemText, options)) continue;
356
383
 
357
384
  if (itemId) {
358
385
  hasKnowledgeItems = true;
@@ -380,12 +407,19 @@ function formatPackItems(items: unknown[]): string | null {
380
407
  ].join("\n");
381
408
  }
382
409
 
383
- export function extractContextText(pack: unknown): string | null {
410
+ function shouldIncludeHistoricalItems(input: HookInput): boolean {
411
+ const prompt = `${stringField(input, "prompt")} ${stringField(input, "user_prompt")}`;
412
+ return /\b(archive|archived|historical|history|reference-only|reference only|old startup|startup files|CLAUDE\.md)\b/i.test(prompt);
413
+ }
414
+
415
+ export function extractContextText(pack: unknown, options: ExtractContextOptions = {}): string | null {
384
416
  if (typeof pack === "string") {
385
- return cleanLegacyEmptyBullet(pack.trim()) || null;
417
+ const text = cleanLegacyEmptyBullet(pack.trim());
418
+ if (!text || shouldSuppressKnowledgeItem(text, options)) return null;
419
+ return text;
386
420
  }
387
421
  if (Array.isArray(pack)) {
388
- return formatPackItems(pack);
422
+ return formatPackItems(pack, options);
389
423
  }
390
424
  if (!pack || typeof pack !== "object") {
391
425
  return null;
@@ -393,16 +427,16 @@ export function extractContextText(pack: unknown): string | null {
393
427
 
394
428
  const obj = pack as Record<string, unknown>;
395
429
  const direct = firstString(obj, ["additionalContext", "context", "markdown", "content", "text", "summary"]);
396
- if (direct) return direct;
430
+ if (direct) return shouldSuppressKnowledgeItem(direct, options) ? null : direct;
397
431
 
398
432
  for (const key of ["pack", "contextPack", "context_pack", "data", "result"]) {
399
433
  const nested = obj[key];
400
- const text = extractContextText(nested);
434
+ const text = extractContextText(nested, options);
401
435
  if (text) return text;
402
436
  }
403
437
 
404
438
  const items = firstArray(obj, ["items", "results", "sources", "citations"]);
405
- return items ? formatPackItems(items) : null;
439
+ return items ? formatPackItems(items, options) : null;
406
440
  }
407
441
 
408
442
  export function formatAdditionalContext(
@@ -450,7 +484,10 @@ export async function buildHookOutput(
450
484
  }
451
485
 
452
486
  const parsed = JSON.parse(result.stdout);
453
- const context = extractContextText(parsed);
487
+ const context = extractContextText(parsed, {
488
+ maxItems: config.maxItems,
489
+ includeHistorical: shouldIncludeHistoricalItems(input),
490
+ });
454
491
  if (!context) {
455
492
  return { continue: true };
456
493
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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": {
@@ -1,52 +0,0 @@
1
- {
2
- "name": "@hasna/hook-knowledge-context",
3
- "version": "0.1.5",
4
- "description": "Codewith hook that injects deterministic Knowledge context packs into lifecycle events",
5
- "type": "module",
6
- "main": "./dist/hook.js",
7
- "exports": {
8
- ".": {
9
- "import": "./dist/hook.js",
10
- "types": "./dist/hook.d.ts"
11
- }
12
- },
13
- "files": [
14
- "dist/hook.js",
15
- "dist/hook.d.ts",
16
- "README.md"
17
- ],
18
- "scripts": {
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
- "prepublishOnly": "bun run build",
21
- "typecheck": "tsc --noEmit"
22
- },
23
- "keywords": [
24
- "codewith",
25
- "hook",
26
- "knowledge",
27
- "context",
28
- "deterministic",
29
- "session-start",
30
- "user-prompt-submit",
31
- "subagent-start"
32
- ],
33
- "author": "Hasna",
34
- "license": "Apache-2.0",
35
- "repository": {
36
- "type": "git",
37
- "url": "https://github.com/hasna/hooks.git"
38
- },
39
- "publishConfig": {
40
- "access": "public",
41
- "registry": "https://registry.npmjs.org/"
42
- },
43
- "engines": {
44
- "node": ">=18",
45
- "bun": ">=1.0"
46
- },
47
- "devDependencies": {
48
- "@types/bun": "^1.3.8",
49
- "@types/node": "^20",
50
- "typescript": "^5.0.0"
51
- }
52
- }
@@ -1,25 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "lib": ["ESNext"],
6
- "moduleResolution": "bundler",
7
- "allowImportingTsExtensions": true,
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
14
- "noEmit": true,
15
- "noUnusedLocals": true,
16
- "noUnusedParameters": true,
17
- "declaration": true,
18
- "declarationMap": true,
19
- "outDir": "./dist",
20
- "rootDir": "./src",
21
- "types": ["bun-types", "node"]
22
- },
23
- "include": ["src/**/*"],
24
- "exclude": ["node_modules", "dist"]
25
- }