laminark 0.1.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.
Files changed (55) hide show
  1. package/README.md +147 -0
  2. package/package.json +65 -0
  3. package/plugin/.claude-plugin/plugin.json +13 -0
  4. package/plugin/.mcp.json +12 -0
  5. package/plugin/CLAUDE.md +10 -0
  6. package/plugin/commands/recall.md +55 -0
  7. package/plugin/commands/remember.md +34 -0
  8. package/plugin/commands/resume.md +45 -0
  9. package/plugin/commands/stash.md +34 -0
  10. package/plugin/commands/status.md +33 -0
  11. package/plugin/dist/analysis/worker.d.ts +1 -0
  12. package/plugin/dist/analysis/worker.js +233 -0
  13. package/plugin/dist/analysis/worker.js.map +1 -0
  14. package/plugin/dist/config-t8LZeB-u.mjs +90 -0
  15. package/plugin/dist/config-t8LZeB-u.mjs.map +1 -0
  16. package/plugin/dist/hooks/handler.d.ts +286 -0
  17. package/plugin/dist/hooks/handler.d.ts.map +1 -0
  18. package/plugin/dist/hooks/handler.js +2413 -0
  19. package/plugin/dist/hooks/handler.js.map +1 -0
  20. package/plugin/dist/index.d.ts +447 -0
  21. package/plugin/dist/index.d.ts.map +1 -0
  22. package/plugin/dist/index.js +7334 -0
  23. package/plugin/dist/index.js.map +1 -0
  24. package/plugin/dist/observations-CorAAc1A.d.mts +192 -0
  25. package/plugin/dist/observations-CorAAc1A.d.mts.map +1 -0
  26. package/plugin/dist/tool-registry-e710BvXq.mjs +3574 -0
  27. package/plugin/dist/tool-registry-e710BvXq.mjs.map +1 -0
  28. package/plugin/hooks/hooks.json +78 -0
  29. package/plugin/laminark.db +0 -0
  30. package/plugin/package.json +17 -0
  31. package/plugin/scripts/README.md +65 -0
  32. package/plugin/scripts/bump-version.sh +42 -0
  33. package/plugin/scripts/dev-sync.sh +58 -0
  34. package/plugin/scripts/ensure-deps.sh +15 -0
  35. package/plugin/scripts/install.sh +139 -0
  36. package/plugin/scripts/local-install.sh +138 -0
  37. package/plugin/scripts/uninstall.sh +133 -0
  38. package/plugin/scripts/update.sh +39 -0
  39. package/plugin/scripts/verify-install.sh +87 -0
  40. package/plugin/skills/status/SKILL.md +6 -0
  41. package/plugin/ui/activity.js +197 -0
  42. package/plugin/ui/app.js +1612 -0
  43. package/plugin/ui/graph.js +2560 -0
  44. package/plugin/ui/help/activity-feed.png +0 -0
  45. package/plugin/ui/help/analysis-panel.png +0 -0
  46. package/plugin/ui/help/graph-toolbar.png +0 -0
  47. package/plugin/ui/help/graph-view.png +0 -0
  48. package/plugin/ui/help/settings.png +0 -0
  49. package/plugin/ui/help/timeline.png +0 -0
  50. package/plugin/ui/help.js +932 -0
  51. package/plugin/ui/index.html +756 -0
  52. package/plugin/ui/settings.js +1414 -0
  53. package/plugin/ui/styles.css +3856 -0
  54. package/plugin/ui/timeline.js +652 -0
  55. package/plugin/ui/tools.js +826 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../../../src/analysis/engines/local-onnx.ts","../../../src/analysis/engines/keyword-only.ts","../../../src/analysis/embedder.ts","../../../src/analysis/worker.ts"],"sourcesContent":["/**\n * Local ONNX embedding engine using @huggingface/transformers.\n *\n * Loads BGE Small EN v1.5 (quantized q8) via dynamic import() for\n * zero startup cost (DQ-04). Model files are cached in ~/.laminark/models/.\n */\n\nimport { join } from 'node:path';\n\nimport { getConfigDir } from '../../shared/config.js';\nimport type { EmbeddingEngine } from '../embedder.js';\n\n// Pipeline type from @huggingface/transformers -- kept as `unknown` to avoid\n// hard dependency on the library's type definitions at import time.\ntype Pipeline = (\n text: string,\n options?: { pooling?: string; normalize?: boolean },\n) => Promise<{ data: ArrayLike<number> }>;\n\n/**\n * Embedding engine backed by BGE Small EN v1.5 running locally via ONNX Runtime.\n *\n * All public methods catch errors internally and return null/false.\n */\nexport class LocalOnnxEngine implements EmbeddingEngine {\n private pipe: Pipeline | null = null;\n private ready = false;\n\n /**\n * Lazily loads the model via dynamic import().\n *\n * - Uses `@huggingface/transformers` loaded at runtime (not bundled)\n * - Caches model files in ~/.laminark/models/\n * - Returns false on any error (missing runtime, download failure, etc.)\n */\n async initialize(): Promise<boolean> {\n try {\n const { pipeline, env } = await import('@huggingface/transformers');\n\n // Cache models in user config directory\n env.cacheDir = join(getConfigDir(), 'models');\n\n this.pipe = (await pipeline('feature-extraction', 'Xenova/bge-small-en-v1.5', {\n dtype: 'q8',\n })) as unknown as Pipeline;\n\n this.ready = true;\n return true;\n } catch {\n this.ready = false;\n return false;\n }\n }\n\n /**\n * Embeds a single text string into a 384-dimensional vector.\n *\n * Returns null if:\n * - Engine not initialized\n * - Input is empty/whitespace\n * - Pipeline throws\n */\n async embed(text: string): Promise<Float32Array | null> {\n if (!this.ready || !this.pipe) {\n return null;\n }\n\n if (!text || text.trim().length === 0) {\n return null;\n }\n\n try {\n const output = await this.pipe(text, { pooling: 'cls', normalize: true });\n return Float32Array.from(output.data);\n } catch {\n return null;\n }\n }\n\n /**\n * Embeds multiple texts, preserving order.\n *\n * Returns null for any text that was empty or failed.\n */\n async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {\n const results: (Float32Array | null)[] = [];\n\n for (const text of texts) {\n if (!text || text.trim().length === 0) {\n results.push(null);\n } else {\n results.push(await this.embed(text));\n }\n }\n\n return results;\n }\n\n /** BGE Small EN v1.5 produces 384-dimensional embeddings. */\n dimensions(): number {\n return 384;\n }\n\n /** Engine identifier. */\n name(): string {\n return 'bge-small-en-v1.5-q8';\n }\n\n /** Whether the model loaded successfully. */\n isReady(): boolean {\n return this.ready;\n }\n}\n","/**\n * Null fallback embedding engine for graceful degradation (DQ-03).\n *\n * Used when the ONNX runtime or model is unavailable.\n * All embedding methods return null -- search falls back to keyword-only (FTS5).\n */\n\nimport type { EmbeddingEngine } from '../embedder.js';\n\n/**\n * Embedding engine that produces no embeddings.\n *\n * Acts as a silent fallback so that the rest of the system can\n * operate in keyword-only mode without special-casing missing engines.\n */\nexport class KeywordOnlyEngine implements EmbeddingEngine {\n /** Always returns null -- no model available. */\n async embed(): Promise<Float32Array | null> {\n return null;\n }\n\n /** Returns array of nulls matching input length. */\n async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {\n return texts.map(() => null);\n }\n\n /** No dimensions -- no model. */\n dimensions(): number {\n return 0;\n }\n\n /** Engine identifier. */\n name(): string {\n return 'keyword-only';\n }\n\n /** Intentionally returns false -- this engine has no model. */\n async initialize(): Promise<boolean> {\n return false;\n }\n\n /** Always false -- no model loaded. */\n isReady(): boolean {\n return false;\n }\n}\n","/**\n * EmbeddingEngine interface and factory.\n *\n * Defines the pluggable abstraction for text embedding.\n * All consumers depend on this interface -- never on concrete engines.\n */\n\nimport { LocalOnnxEngine } from './engines/local-onnx.js';\nimport { KeywordOnlyEngine } from './engines/keyword-only.js';\n\n/**\n * Pluggable embedding engine abstraction.\n *\n * All methods that can fail return null/false -- engines NEVER throw.\n * This is critical for graceful degradation (DQ-03).\n */\nexport interface EmbeddingEngine {\n /** Embed a single text string. Returns null on failure or empty input. */\n embed(text: string): Promise<Float32Array | null>;\n\n /** Embed multiple texts. Returns null for any that failed or were empty. */\n embedBatch(texts: string[]): Promise<(Float32Array | null)[]>;\n\n /** Embedding dimensions (384 for BGE Small, 0 for keyword-only). */\n dimensions(): number;\n\n /** Engine identifier string. */\n name(): string;\n\n /** Lazy initialization. Returns true on success, false on failure. */\n initialize(): Promise<boolean>;\n\n /** Whether initialize() has been called and succeeded. */\n isReady(): boolean;\n}\n\n/**\n * Creates and initializes an embedding engine.\n *\n * Attempts LocalOnnxEngine first. If initialization fails (missing model,\n * ONNX runtime unavailable, etc.), falls back to KeywordOnlyEngine.\n *\n * Never throws -- always returns a valid engine.\n */\nexport async function createEmbeddingEngine(): Promise<EmbeddingEngine> {\n const onnxEngine = new LocalOnnxEngine();\n const success = await onnxEngine.initialize();\n\n if (success) {\n return onnxEngine;\n }\n\n return new KeywordOnlyEngine();\n}\n","/**\n * Worker thread entry point for off-main-thread embedding.\n *\n * Receives embed/embed_batch/shutdown messages from the main thread via\n * parentPort, runs the embedding engine, and responds with Float32Array\n * results using zero-copy transfer.\n *\n * Compiled as a separate tsdown entry point to dist/analysis/worker.js.\n */\n\nimport { parentPort } from 'node:worker_threads';\nimport { createEmbeddingEngine } from './embedder.js';\n\nif (!parentPort) {\n throw new Error('worker.ts must be run as a Worker thread');\n}\n\nconst port = parentPort;\n\ninterface EmbedMessage {\n type: 'embed';\n id: string;\n text: string;\n}\n\ninterface EmbedBatchMessage {\n type: 'embed_batch';\n id: string;\n texts: string[];\n}\n\ninterface ShutdownMessage {\n type: 'shutdown';\n}\n\ntype WorkerMessage = EmbedMessage | EmbedBatchMessage | ShutdownMessage;\n\nasync function init(): Promise<void> {\n let engineName = 'keyword-only';\n let dimensions = 0;\n\n try {\n const engine = await createEmbeddingEngine();\n engineName = engine.name();\n dimensions = engine.dimensions();\n\n port.postMessage({ type: 'ready', engineName, dimensions });\n\n port.on('message', async (msg: WorkerMessage) => {\n if (msg.type === 'embed') {\n try {\n const embedding = await engine.embed(msg.text);\n\n if (embedding === null) {\n port.postMessage({ type: 'embed_result', id: msg.id, embedding: null });\n } else {\n // Zero-copy transfer of the underlying ArrayBuffer\n const buf = embedding.buffer as ArrayBuffer;\n port.postMessage(\n { type: 'embed_result', id: msg.id, embedding },\n [buf],\n );\n }\n } catch {\n port.postMessage({ type: 'embed_result', id: msg.id, embedding: null });\n }\n } else if (msg.type === 'embed_batch') {\n try {\n const embeddings = await engine.embedBatch(msg.texts);\n\n // Collect non-null buffers for zero-copy transfer\n const transferList: ArrayBuffer[] = [];\n for (const emb of embeddings) {\n if (emb !== null) {\n transferList.push(emb.buffer as ArrayBuffer);\n }\n }\n\n port.postMessage(\n { type: 'embed_batch_result', id: msg.id, embeddings },\n transferList,\n );\n } catch {\n port.postMessage({\n type: 'embed_batch_result',\n id: msg.id,\n embeddings: msg.texts.map(() => null),\n });\n }\n } else if (msg.type === 'shutdown') {\n process.exit(0);\n }\n });\n } catch {\n // Engine creation failed -- still report ready with keyword-only fallback\n port.postMessage({ type: 'ready', engineName, dimensions });\n\n port.on('message', (msg: WorkerMessage) => {\n if (msg.type === 'embed') {\n port.postMessage({ type: 'embed_result', id: msg.id, embedding: null });\n } else if (msg.type === 'embed_batch') {\n port.postMessage({\n type: 'embed_batch_result',\n id: msg.id,\n embeddings: msg.texts.map(() => null),\n });\n } else if (msg.type === 'shutdown') {\n process.exit(0);\n }\n });\n }\n}\n\ninit();\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { mkdirSync, readFileSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { homedir } from "node:os";
5
+
6
+ //#region src/shared/config.ts
7
+ /**
8
+ * Cached debug-enabled flag.
9
+ * Resolved once per process -- debug mode does not change at runtime.
10
+ */
11
+ let _debugCached = null;
12
+ /**
13
+ * Returns whether debug logging is enabled for this process.
14
+ *
15
+ * Resolution order:
16
+ * 1. `LAMINARK_DEBUG` env var -- `"1"` or `"true"` enables debug mode
17
+ * 2. `~/.laminark/config.json` -- `{ "debug": true }` enables debug mode
18
+ * 3. Default: disabled
19
+ *
20
+ * The result is cached after the first call.
21
+ */
22
+ function isDebugEnabled() {
23
+ if (_debugCached !== null) return _debugCached;
24
+ const envVal = process.env.LAMINARK_DEBUG;
25
+ if (envVal === "1" || envVal === "true") {
26
+ _debugCached = true;
27
+ return true;
28
+ }
29
+ try {
30
+ const raw = readFileSync(join(getConfigDir(), "config.json"), "utf-8");
31
+ if (JSON.parse(raw).debug === true) {
32
+ _debugCached = true;
33
+ return true;
34
+ }
35
+ } catch {}
36
+ _debugCached = false;
37
+ return false;
38
+ }
39
+ /**
40
+ * Default busy timeout in milliseconds.
41
+ * Must be >= 5000ms to prevent SQLITE_BUSY under concurrent load.
42
+ * Source: SQLite docs + better-sqlite3 performance recommendations.
43
+ */
44
+ const DEFAULT_BUSY_TIMEOUT = 5e3;
45
+ /**
46
+ * Returns the Laminark data directory.
47
+ * Default: ~/.claude/plugins/cache/laminark/data/
48
+ * Creates the directory recursively if it does not exist.
49
+ *
50
+ * Supports LAMINARK_DATA_DIR env var override for testing --
51
+ * redirects all data storage to a custom directory without
52
+ * affecting the real plugin data.
53
+ */
54
+ function getConfigDir() {
55
+ const dir = process.env.LAMINARK_DATA_DIR || join(homedir(), ".claude", "plugins", "cache", "laminark", "data");
56
+ mkdirSync(dir, { recursive: true });
57
+ return dir;
58
+ }
59
+ /**
60
+ * Returns the path to the single Laminark database file.
61
+ * Single database at ~/.claude/plugins/cache/laminark/data/data.db for ALL projects.
62
+ */
63
+ function getDbPath() {
64
+ return join(getConfigDir(), "data.db");
65
+ }
66
+ /**
67
+ * Creates a deterministic SHA-256 hash of a project directory path.
68
+ * Uses realpathSync to canonicalize (resolves symlinks) to prevent
69
+ * multiple hashes for the same directory via different paths.
70
+ *
71
+ * @param projectDir - The project directory path to hash
72
+ * @returns First 16 hex characters of the SHA-256 hash
73
+ */
74
+ function getProjectHash(projectDir) {
75
+ const canonical = realpathSync(resolve(projectDir));
76
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
77
+ }
78
+ /**
79
+ * Returns the default database configuration.
80
+ */
81
+ function getDatabaseConfig() {
82
+ return {
83
+ dbPath: getDbPath(),
84
+ busyTimeout: DEFAULT_BUSY_TIMEOUT
85
+ };
86
+ }
87
+
88
+ //#endregion
89
+ export { isDebugEnabled as a, getProjectHash as i, getDatabaseConfig as n, getDbPath as r, getConfigDir as t };
90
+ //# sourceMappingURL=config-t8LZeB-u.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-t8LZeB-u.mjs","names":[],"sources":["../../src/shared/config.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { mkdirSync, readFileSync, realpathSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\n\nimport type { DatabaseConfig } from './types.js';\n\n/**\n * Cached debug-enabled flag.\n * Resolved once per process -- debug mode does not change at runtime.\n */\nlet _debugCached: boolean | null = null;\n\n/**\n * Returns whether debug logging is enabled for this process.\n *\n * Resolution order:\n * 1. `LAMINARK_DEBUG` env var -- `\"1\"` or `\"true\"` enables debug mode\n * 2. `~/.laminark/config.json` -- `{ \"debug\": true }` enables debug mode\n * 3. Default: disabled\n *\n * The result is cached after the first call.\n */\nexport function isDebugEnabled(): boolean {\n if (_debugCached !== null) {\n return _debugCached;\n }\n\n // Check environment variable first\n const envVal = process.env.LAMINARK_DEBUG;\n if (envVal === '1' || envVal === 'true') {\n _debugCached = true;\n return true;\n }\n\n // Check config.json\n try {\n const configPath = join(getConfigDir(), 'config.json');\n const raw = readFileSync(configPath, 'utf-8');\n const config = JSON.parse(raw) as Record<string, unknown>;\n if (config.debug === true) {\n _debugCached = true;\n return true;\n }\n } catch {\n // Config file doesn't exist or is invalid -- that's fine\n }\n\n _debugCached = false;\n return false;\n}\n\n/**\n * Default busy timeout in milliseconds.\n * Must be >= 5000ms to prevent SQLITE_BUSY under concurrent load.\n * Source: SQLite docs + better-sqlite3 performance recommendations.\n */\nexport const DEFAULT_BUSY_TIMEOUT = 5000;\n\n/**\n * Returns the Laminark data directory.\n * Default: ~/.claude/plugins/cache/laminark/data/\n * Creates the directory recursively if it does not exist.\n *\n * Supports LAMINARK_DATA_DIR env var override for testing --\n * redirects all data storage to a custom directory without\n * affecting the real plugin data.\n */\nexport function getConfigDir(): string {\n const dir = process.env.LAMINARK_DATA_DIR || join(homedir(), '.claude', 'plugins', 'cache', 'laminark', 'data');\n mkdirSync(dir, { recursive: true });\n return dir;\n}\n\n/**\n * Returns the path to the single Laminark database file.\n * Single database at ~/.claude/plugins/cache/laminark/data/data.db for ALL projects.\n */\nexport function getDbPath(): string {\n return join(getConfigDir(), 'data.db');\n}\n\n/**\n * Creates a deterministic SHA-256 hash of a project directory path.\n * Uses realpathSync to canonicalize (resolves symlinks) to prevent\n * multiple hashes for the same directory via different paths.\n *\n * @param projectDir - The project directory path to hash\n * @returns First 16 hex characters of the SHA-256 hash\n */\nexport function getProjectHash(projectDir: string): string {\n const canonical = realpathSync(resolve(projectDir));\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\n}\n\n/**\n * Returns the default database configuration.\n */\nexport function getDatabaseConfig(): DatabaseConfig {\n return {\n dbPath: getDbPath(),\n busyTimeout: DEFAULT_BUSY_TIMEOUT,\n };\n}\n"],"mappings":";;;;;;;;;;AAWA,IAAI,eAA+B;;;;;;;;;;;AAYnC,SAAgB,iBAA0B;AACxC,KAAI,iBAAiB,KACnB,QAAO;CAIT,MAAM,SAAS,QAAQ,IAAI;AAC3B,KAAI,WAAW,OAAO,WAAW,QAAQ;AACvC,iBAAe;AACf,SAAO;;AAIT,KAAI;EAEF,MAAM,MAAM,aADO,KAAK,cAAc,EAAE,cAAc,EACjB,QAAQ;AAE7C,MADe,KAAK,MAAM,IAAI,CACnB,UAAU,MAAM;AACzB,kBAAe;AACf,UAAO;;SAEH;AAIR,gBAAe;AACf,QAAO;;;;;;;AAQT,MAAa,uBAAuB;;;;;;;;;;AAWpC,SAAgB,eAAuB;CACrC,MAAM,MAAM,QAAQ,IAAI,qBAAqB,KAAK,SAAS,EAAE,WAAW,WAAW,SAAS,YAAY,OAAO;AAC/G,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AACnC,QAAO;;;;;;AAOT,SAAgB,YAAoB;AAClC,QAAO,KAAK,cAAc,EAAE,UAAU;;;;;;;;;;AAWxC,SAAgB,eAAe,YAA4B;CACzD,MAAM,YAAY,aAAa,QAAQ,WAAW,CAAC;AACnD,QAAO,WAAW,SAAS,CAAC,OAAO,UAAU,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,GAAG;;;;;AAM1E,SAAgB,oBAAoC;AAClD,QAAO;EACL,QAAQ,WAAW;EACnB,aAAa;EACd"}
@@ -0,0 +1,286 @@
1
+ import { t as ObservationRepository } from "../observations-CorAAc1A.mjs";
2
+ import * as better_sqlite30 from "better-sqlite3";
3
+ import Database from "better-sqlite3";
4
+
5
+ //#region src/storage/research-buffer.d.ts
6
+ /**
7
+ * Lightweight buffer for exploration tool events (Read, Glob, Grep).
8
+ *
9
+ * Instead of creating full observations for these low-signal tools,
10
+ * they are stored in a temporary buffer. When a Write/Edit observation
11
+ * is created, the recent buffer entries are attached as research context,
12
+ * creating provenance links between exploration and changes.
13
+ *
14
+ * Buffer entries are flushed after 30 minutes.
15
+ */
16
+ declare class ResearchBufferRepository {
17
+ private readonly db;
18
+ private readonly projectHash;
19
+ private readonly stmtInsert;
20
+ private readonly stmtGetRecent;
21
+ private readonly stmtFlush;
22
+ constructor(db: Database.Database, projectHash: string);
23
+ /**
24
+ * Records a research tool event in the buffer.
25
+ */
26
+ add(entry: {
27
+ sessionId: string | null;
28
+ toolName: string;
29
+ target: string;
30
+ }): void;
31
+ /**
32
+ * Returns recent buffer entries for a session within a time window.
33
+ */
34
+ getRecent(sessionId: string, windowMinutes?: number): Array<{
35
+ toolName: string;
36
+ target: string;
37
+ createdAt: string;
38
+ }>;
39
+ /**
40
+ * Deletes buffer entries older than the specified number of minutes.
41
+ */
42
+ flush(olderThanMinutes?: number): number;
43
+ }
44
+ //#endregion
45
+ //#region src/shared/tool-types.d.ts
46
+ /**
47
+ * Tool type classification based on how the tool is provided.
48
+ */
49
+ type ToolType = 'mcp_server' | 'mcp_tool' | 'slash_command' | 'skill' | 'plugin' | 'builtin' | 'unknown';
50
+ /**
51
+ * Scope origin of a tool -- where it was discovered from.
52
+ */
53
+ type ToolScope = 'global' | 'project' | 'plugin';
54
+ /**
55
+ * A tool discovered during config scanning (SessionStart).
56
+ * Used as input to ToolRegistryRepository.upsert().
57
+ */
58
+ interface DiscoveredTool {
59
+ name: string;
60
+ toolType: ToolType;
61
+ scope: ToolScope;
62
+ source: string;
63
+ projectHash: string | null;
64
+ description: string | null;
65
+ serverName: string | null;
66
+ triggerHints: string | null;
67
+ }
68
+ /**
69
+ * Raw database row from the tool_registry table (snake_case).
70
+ */
71
+ interface ToolRegistryRow {
72
+ id: number;
73
+ name: string;
74
+ tool_type: string;
75
+ scope: string;
76
+ source: string;
77
+ project_hash: string | null;
78
+ description: string | null;
79
+ server_name: string | null;
80
+ trigger_hints: string | null;
81
+ usage_count: number;
82
+ last_used_at: string | null;
83
+ discovered_at: string;
84
+ updated_at: string;
85
+ status: string;
86
+ }
87
+ /**
88
+ * Aggregated usage stats for temporal queries.
89
+ */
90
+ interface ToolUsageStats {
91
+ tool_name: string;
92
+ usage_count: number;
93
+ last_used: string;
94
+ }
95
+ /**
96
+ * A search result from hybrid tool search (FTS5 + vector via RRF).
97
+ */
98
+ interface ToolSearchResult {
99
+ tool: ToolRegistryRow;
100
+ score: number;
101
+ matchType: 'fts' | 'vector' | 'hybrid';
102
+ }
103
+ //#endregion
104
+ //#region src/storage/tool-registry.d.ts
105
+ /**
106
+ * Repository for tool registry CRUD operations.
107
+ *
108
+ * Unlike ObservationRepository, this is NOT scoped to a single project --
109
+ * the tool registry spans all scopes (global, project, plugin) and is
110
+ * queried cross-project for tool discovery and routing.
111
+ *
112
+ * All SQL statements are prepared once in the constructor and reused for
113
+ * every call (better-sqlite3 performance best practice).
114
+ */
115
+ declare class ToolRegistryRepository {
116
+ private readonly db;
117
+ private readonly stmtUpsert;
118
+ private readonly stmtRecordUsage;
119
+ private readonly stmtGetByScope;
120
+ private readonly stmtGetByName;
121
+ private readonly stmtGetAll;
122
+ private readonly stmtCount;
123
+ private readonly stmtGetAvailableForSession;
124
+ private readonly stmtInsertEvent;
125
+ private readonly stmtGetUsageForTool;
126
+ private readonly stmtGetUsageForSession;
127
+ private readonly stmtGetUsageSince;
128
+ private readonly stmtGetRecentUsage;
129
+ private readonly stmtMarkStale;
130
+ private readonly stmtMarkDemoted;
131
+ private readonly stmtMarkActive;
132
+ private readonly stmtGetConfigSourced;
133
+ private readonly stmtGetRecentEventsForTool;
134
+ constructor(db: Database.Database);
135
+ /**
136
+ * Inserts or updates a discovered tool in the registry.
137
+ * On conflict (same name + project_hash), updates description and source.
138
+ */
139
+ upsert(tool: DiscoveredTool): void;
140
+ /**
141
+ * Increments usage_count and updates last_used_at for a tool.
142
+ * Called from organic PostToolUse discovery to track usage.
143
+ */
144
+ recordUsage(name: string, projectHash: string | null): void;
145
+ /**
146
+ * Records usage for an existing tool, or creates it if not yet in the registry.
147
+ * This is the entry point for organic discovery -- an upsert-and-increment-if-exists pattern.
148
+ *
149
+ * First tries recordUsage. If the tool is not in the registry (changes === 0),
150
+ * calls upsert with the full tool info, which initializes it with usage_count = 0.
151
+ */
152
+ recordOrCreate(name: string, defaults: Omit<DiscoveredTool, 'name'>, sessionId?: string | null, success?: boolean): void;
153
+ /**
154
+ * Returns global tools plus project-specific tools for the given project.
155
+ */
156
+ getForProject(projectHash: string): ToolRegistryRow[];
157
+ /**
158
+ * Returns tools available in the resolved scope for a given project.
159
+ * Implements SCOP-01/SCOP-02/SCOP-03 scope resolution rules.
160
+ */
161
+ getAvailableForSession(projectHash: string): ToolRegistryRow[];
162
+ /**
163
+ * Returns the top-usage entry for a given tool name.
164
+ */
165
+ getByName(name: string): ToolRegistryRow | null;
166
+ /**
167
+ * Returns all tools in the registry (for debugging/admin).
168
+ */
169
+ getAll(): ToolRegistryRow[];
170
+ /**
171
+ * Returns total number of tools in the registry.
172
+ */
173
+ count(): number;
174
+ /**
175
+ * Returns usage stats for a specific tool within a time window.
176
+ * @param timeModifier - SQLite datetime modifier, e.g., '-7 days', '-30 days'
177
+ */
178
+ getUsageForTool(toolName: string, projectHash: string, timeModifier?: string): ToolUsageStats | null;
179
+ /**
180
+ * Returns per-tool usage stats for a specific session.
181
+ */
182
+ getUsageForSession(sessionId: string): ToolUsageStats[];
183
+ /**
184
+ * Returns per-tool usage stats since a time offset for a project.
185
+ * @param timeModifier - SQLite datetime modifier, e.g., '-7 days', '-30 days'
186
+ */
187
+ getUsageSince(projectHash: string, timeModifier?: string): ToolUsageStats[];
188
+ /**
189
+ * Returns per-tool usage stats from the last N events for a project.
190
+ * Event-count-based window instead of time-based — immune to usage gaps.
191
+ * @param limit - Number of recent events to consider (default 200)
192
+ */
193
+ getRecentUsage(projectHash: string, limit?: number): ToolUsageStats[];
194
+ /**
195
+ * Marks a tool as stale (no longer in config but still in registry).
196
+ * Idempotent -- no-op if already stale.
197
+ */
198
+ markStale(name: string, projectHash: string | null): void;
199
+ /**
200
+ * Marks a tool as demoted (high failure rate detected).
201
+ */
202
+ markDemoted(name: string, projectHash: string | null): void;
203
+ /**
204
+ * Marks a tool as active (restored from stale/demoted).
205
+ * Idempotent -- no-op if already active.
206
+ */
207
+ markActive(name: string, projectHash: string | null): void;
208
+ /**
209
+ * Returns all config-sourced active tools for a given project (or global).
210
+ * Used by staleness detection to compare against current config state.
211
+ */
212
+ getConfigSourcedTools(projectHash: string): ToolRegistryRow[];
213
+ /**
214
+ * Returns recent success/failure events for a specific tool.
215
+ * Used by failure-driven demotion to check failure rate.
216
+ * @param limit - Number of recent events to check (default 5)
217
+ */
218
+ getRecentEventsForTool(toolName: string, projectHash: string, limit?: number): Array<{
219
+ success: number;
220
+ }>;
221
+ /**
222
+ * Sanitizes a user query for safe FTS5 MATCH usage.
223
+ * Removes FTS5 operators and special characters to prevent syntax errors.
224
+ * Returns null if the query is empty after sanitization.
225
+ */
226
+ private sanitizeQuery;
227
+ /**
228
+ * FTS5 keyword search on tool_registry_fts (name + description).
229
+ * Returns ranked results using BM25 with name weighted 2x over description.
230
+ */
231
+ searchByKeyword(query: string, options?: {
232
+ scope?: string;
233
+ limit?: number;
234
+ }): ToolSearchResult[];
235
+ /**
236
+ * Vector similarity search on tool_registry_embeddings using vec0 KNN.
237
+ * Returns tool IDs and distances sorted by cosine similarity.
238
+ */
239
+ searchByVector(queryEmbedding: Float32Array, options?: {
240
+ scope?: string;
241
+ limit?: number;
242
+ }): Array<{
243
+ tool_id: number;
244
+ distance: number;
245
+ }>;
246
+ /**
247
+ * Hybrid search combining FTS5 keyword and vec0 vector results via
248
+ * reciprocal rank fusion (RRF). Falls back to FTS5-only when vector
249
+ * search is unavailable (no worker, no sqlite-vec, no embeddings).
250
+ */
251
+ searchTools(query: string, options?: {
252
+ scope?: string;
253
+ limit?: number;
254
+ worker?: {
255
+ isReady(): boolean;
256
+ embed(text: string): Promise<Float32Array | null>;
257
+ } | null;
258
+ hasVectorSupport?: boolean;
259
+ }): Promise<ToolSearchResult[]>;
260
+ /**
261
+ * Stores an embedding vector for a tool in tool_registry_embeddings.
262
+ * Used by the background embedding loop to index tool descriptions.
263
+ */
264
+ storeEmbedding(toolId: number, embedding: Float32Array): void;
265
+ /**
266
+ * Returns tools that have descriptions but no embedding yet.
267
+ * Used by the background embedding loop to find work.
268
+ */
269
+ findUnembeddedTools(limit?: number): Array<{
270
+ id: number;
271
+ name: string;
272
+ description: string;
273
+ }>;
274
+ }
275
+ //#endregion
276
+ //#region src/hooks/handler.d.ts
277
+ /**
278
+ * Processes a PostToolUse or PostToolUseFailure event through the full
279
+ * filter pipeline: route research tools -> extract -> privacy -> admission -> store.
280
+ *
281
+ * Exported for unit testing of the pipeline logic.
282
+ */
283
+ declare function processPostToolUseFiltered(input: Record<string, unknown>, obsRepo: ObservationRepository, researchBuffer?: ResearchBufferRepository, toolRegistry?: ToolRegistryRepository, projectHash?: string, db?: better_sqlite30.Database): void;
284
+ //#endregion
285
+ export { processPostToolUseFiltered };
286
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","names":[],"sources":["../../../src/storage/research-buffer.ts","../../../src/shared/tool-types.ts","../../../src/storage/tool-registry.ts","../../../src/hooks/handler.ts"],"mappings":"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"}