@hasna/hooks 0.3.6 → 0.3.7
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 +1 -1
- package/dist/index.js +1 -1
- package/hooks/hook-knowledge-context/README.md +5 -2
- package/hooks/hook-knowledge-context/package.json +1 -1
- package/hooks/hook-knowledge-context/src/hook.test.ts +75 -2
- package/hooks/hook-knowledge-context/src/hook.ts +48 -11
- package/package.json +1 -1
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.
|
|
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.
|
|
1013
|
+
version: "0.1.6",
|
|
1014
1014
|
category: "Context Management",
|
|
1015
1015
|
event: "SessionStart",
|
|
1016
1016
|
events: ["SessionStart", "UserPromptSubmit", "SubagentStart"],
|
|
@@ -34,7 +34,9 @@ text from that pack and emits Codewith-native
|
|
|
34
34
|
`hookSpecificOutput.additionalContext` with the same event name. Citation-only
|
|
35
35
|
packs are expanded into progressive context lines with bounded previews and a
|
|
36
36
|
single `knowledge get --id <item_id> --json` hint so agents can decide which
|
|
37
|
-
full items are worth opening.
|
|
37
|
+
full items are worth opening. Items that mark themselves as
|
|
38
|
+
historical/reference-only or not suitable for auto-loading are filtered out by
|
|
39
|
+
default, because they are not useful ambient context for normal agent work.
|
|
38
40
|
|
|
39
41
|
Example injected context:
|
|
40
42
|
|
|
@@ -55,7 +57,8 @@ bad stdin, or empty packs return `{"continue":true}` without context.
|
|
|
55
57
|
export HOOKS_KNOWLEDGE_CONTEXT_DISABLE=1 # Kill switch
|
|
56
58
|
export HOOKS_KNOWLEDGE_COMMAND=knowledge # CLI command/path
|
|
57
59
|
export HOOKS_KNOWLEDGE_TIMEOUT_MS=5000 # Per-call timeout
|
|
58
|
-
export HOOKS_KNOWLEDGE_MAX_ITEMS=3 #
|
|
60
|
+
export HOOKS_KNOWLEDGE_MAX_ITEMS=3 # Rendered item budget
|
|
61
|
+
export HOOKS_KNOWLEDGE_CANDIDATE_ITEMS=12 # Internal candidate budget before filtering
|
|
59
62
|
export HOOKS_KNOWLEDGE_MAX_TOKENS=1200 # Context pack token budget
|
|
60
63
|
export HOOKS_KNOWLEDGE_REQUIRE_HIGH_SIGNAL=1
|
|
61
64
|
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("
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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