@hasna/hooks 0.3.1 → 0.3.3

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.0",
2092
+ version: "0.1.2",
2093
2093
  category: "Context Management",
2094
2094
  event: "SessionStart",
2095
2095
  events: ["SessionStart", "UserPromptSubmit", "SubagentStart"],
@@ -4126,7 +4126,7 @@ function codewithMatcher(matcher) {
4126
4126
  function codewithTimeout(name) {
4127
4127
  switch (name) {
4128
4128
  case "knowledge-context":
4129
- return 2;
4129
+ return 6;
4130
4130
  case "session-start":
4131
4131
  return 8;
4132
4132
  case "pre-bash":
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.0",
1013
+ version: "0.1.2",
1014
1014
  category: "Context Management",
1015
1015
  event: "SessionStart",
1016
1016
  events: ["SessionStart", "UserPromptSubmit", "SubagentStart"],
@@ -1452,7 +1452,7 @@ function codewithMatcher(matcher) {
1452
1452
  function codewithTimeout(name) {
1453
1453
  switch (name) {
1454
1454
  case "knowledge-context":
1455
- return 2;
1455
+ return 6;
1456
1456
  case "session-start":
1457
1457
  return 8;
1458
1458
  case "pre-bash":
@@ -29,7 +29,10 @@ 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
30
  When Knowledge returns a nonempty context pack, the hook redacts credential-like
31
31
  text from that pack and emits Codewith-native
32
- `hookSpecificOutput.additionalContext` with the same event name.
32
+ `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.
33
36
 
34
37
  All failures are fail-open: missing CLI, timeout, nonzero exit, malformed JSON,
35
38
  bad stdin, or empty packs return `{"continue":true}` without context.
@@ -39,7 +42,7 @@ bad stdin, or empty packs return `{"continue":true}` without context.
39
42
  ```bash
40
43
  export HOOKS_KNOWLEDGE_CONTEXT_DISABLE=1 # Kill switch
41
44
  export HOOKS_KNOWLEDGE_COMMAND=knowledge # CLI command/path
42
- export HOOKS_KNOWLEDGE_TIMEOUT_MS=1500 # Per-call timeout
45
+ export HOOKS_KNOWLEDGE_TIMEOUT_MS=5000 # Per-call timeout
43
46
  export HOOKS_KNOWLEDGE_MAX_ITEMS=6 # Context pack item budget
44
47
  export HOOKS_KNOWLEDGE_MAX_TOKENS=1200 # Context pack token budget
45
48
  export HOOKS_KNOWLEDGE_MAX_QUERY_CHARS=1200 # Redacted query bound
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hook-knowledge-context",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Codewith hook that injects deterministic Knowledge context packs into lifecycle events",
5
5
  "type": "module",
6
6
  "main": "./dist/hook.js",
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  const baseConfig: KnowledgeContextConfig = {
15
15
  command: "knowledge",
16
- timeoutMs: 1500,
16
+ timeoutMs: 5000,
17
17
  maxItems: 6,
18
18
  maxTokens: 1200,
19
19
  maxQueryChars: 1200,
@@ -21,6 +21,11 @@ const baseConfig: KnowledgeContextConfig = {
21
21
  };
22
22
 
23
23
  describe("hook-knowledge-context", () => {
24
+ test("defaults Knowledge CLI timeout to 5000ms and keeps env override", () => {
25
+ expect(getConfig({}).timeoutMs).toBe(5000);
26
+ expect(getConfig({ HOOKS_KNOWLEDGE_TIMEOUT_MS: "2345" }).timeoutMs).toBe(2345);
27
+ });
28
+
24
29
  test("builds deterministic knowledge context pack args without semantic, web, or generation flags", () => {
25
30
  const args = buildKnowledgeArgs("repo hook context", baseConfig);
26
31
 
@@ -165,4 +170,21 @@ describe("hook-knowledge-context", () => {
165
170
  expect(extractContextText({ pack: { context: "direct context" } })).toBe("direct context");
166
171
  expect(extractContextText([{ title: "One", text: "Body", uri: "knowledge://item/one" }])).toContain("Body");
167
172
  });
173
+
174
+ test("formats citation previews as useful progressive blurbs with read commands", () => {
175
+ const text = extractContextText({
176
+ citations: [
177
+ {
178
+ id: "cite_123",
179
+ source_ref: "knowledge://item/k_example",
180
+ quote_preview: "A short preview that tells the agent why this Knowledge item might matter.",
181
+ },
182
+ ],
183
+ });
184
+
185
+ expect(text).toContain("Knowledge item k_example");
186
+ expect(text).toContain("cite_123");
187
+ expect(text).toContain("A short preview");
188
+ expect(text).toContain("read: knowledge get --id k_example --json");
189
+ });
168
190
  });
@@ -55,7 +55,7 @@ export type KnowledgeExecutor = (
55
55
  config: KnowledgeContextConfig
56
56
  ) => Promise<KnowledgeExecResult>;
57
57
 
58
- const DEFAULT_TIMEOUT_MS = 1500;
58
+ const DEFAULT_TIMEOUT_MS = 5000;
59
59
  const DEFAULT_MAX_ITEMS = 6;
60
60
  const DEFAULT_MAX_TOKENS = 1200;
61
61
  const DEFAULT_MAX_QUERY_CHARS = 1200;
@@ -227,6 +227,18 @@ function firstString(obj: Record<string, unknown>, keys: string[]): string | nul
227
227
  return null;
228
228
  }
229
229
 
230
+ function firstSourceString(obj: Record<string, unknown>): string | null {
231
+ const direct = firstString(obj, ["source_ref", "source_uri", "source", "uri", "ref", "url", "citation"]);
232
+ if (direct) return direct;
233
+
234
+ const source = obj.source;
235
+ if (source && typeof source === "object") {
236
+ return firstString(source as Record<string, unknown>, ["uri", "ref", "url", "id"]);
237
+ }
238
+
239
+ return null;
240
+ }
241
+
230
242
  function firstArray(obj: Record<string, unknown>, keys: string[]): unknown[] | null {
231
243
  for (const key of keys) {
232
244
  const value = obj[key];
@@ -241,6 +253,15 @@ export function truncate(text: string, max: number): string {
241
253
  return `${text.slice(0, max - 3)}...`;
242
254
  }
243
255
 
256
+ function compactText(text: string): string {
257
+ return text.replace(/\s+/g, " ").trim();
258
+ }
259
+
260
+ function knowledgeItemId(source: string | null): string | null {
261
+ const match = source?.match(/^knowledge:\/\/item\/([^/?#\s]+)$/);
262
+ return match?.[1] ?? null;
263
+ }
264
+
244
265
  function formatPackItems(items: unknown[]): string | null {
245
266
  const lines: string[] = [];
246
267
 
@@ -252,12 +273,35 @@ function formatPackItems(items: unknown[]): string | null {
252
273
  if (!item || typeof item !== "object") continue;
253
274
 
254
275
  const obj = item as Record<string, unknown>;
255
- const title = firstString(obj, ["title", "name", "id", "ref", "uri"]) || `item ${index + 1}`;
256
- const body = firstString(obj, ["summary", "text", "content", "snippet", "preview", "body"]);
257
- const source = firstString(obj, ["source", "uri", "ref", "url", "citation"]);
276
+ const citationId = firstString(obj, ["id", "citation_id", "citationId"]);
277
+ const source = firstSourceString(obj);
278
+ const itemId = knowledgeItemId(source);
279
+ const title =
280
+ firstString(obj, ["title", "name"]) ||
281
+ (itemId ? `Knowledge item ${itemId}` : citationId || `item ${index + 1}`);
282
+ const body = firstString(obj, [
283
+ "summary",
284
+ "text",
285
+ "content",
286
+ "snippet",
287
+ "preview",
288
+ "quote_preview",
289
+ "quotePreview",
290
+ "excerpt",
291
+ "body",
292
+ "description",
293
+ ]);
258
294
  if (!body && !source) continue;
259
- const suffix = source ? ` (source: ${truncate(source, 180)})` : "";
260
- lines.push(`- ${truncate(title, 160)}${suffix}${body ? `: ${truncate(body, 900)}` : ""}`);
295
+
296
+ const suffixParts = [
297
+ citationId && citationId !== title ? citationId : null,
298
+ source ? `source: ${truncate(source, 180)}` : null,
299
+ ].filter(Boolean);
300
+ const suffix = suffixParts.length > 0 ? ` (${suffixParts.join("; ")})` : "";
301
+ lines.push(`- ${truncate(title, 160)}${suffix}${body ? `: ${truncate(compactText(body), 520)}` : ""}`);
302
+ if (itemId) {
303
+ lines.push(` read: knowledge get --id ${itemId} --json`);
304
+ }
261
305
  }
262
306
 
263
307
  return lines.length > 0 ? lines.join("\n") : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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": {