@lincoln504/pi-research 0.1.13 → 1.0.1

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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -138
  3. package/dist/cli.mjs +23375 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +130 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +211 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +122 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +66 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1195 -0
  30. package/src/config.ts +1032 -155
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +258 -11
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -0,0 +1,126 @@
1
+ import * as fs from 'node:fs';
2
+ import { KnowledgeStore } from './store.ts';
3
+ import { WriterQueue } from './writer-queue.ts';
4
+ import { Chunker } from './chunker.ts';
5
+ import { getConfig, validateConfig, getDbDir, type Config } from '../config.ts';
6
+ import { logger } from '../logger.ts';
7
+ import { getModelChunkConfig } from './model-config.ts';
8
+ import type { MigrationStrategy } from './migration.ts';
9
+ import type {
10
+ IEmbedder,
11
+ IKnowledgeStore
12
+ } from '../core/interfaces/knowledge-interfaces.ts';
13
+
14
+ export { SUPPORTED_MODELS, getModelEmbedderConfig, getModelChunkConfig } from './model-config.ts';
15
+
16
+ /** Migration strategy for model changes (read from config) */
17
+ function getMigrationStrategy(config: Config): MigrationStrategy | undefined {
18
+ const strategy = config.MIGRATION_STRATEGY;
19
+ if (!strategy) return undefined;
20
+
21
+ const validStrategies: MigrationStrategy[] = ['drop', 're-embed', 'backup'];
22
+ if (validStrategies.includes(strategy as MigrationStrategy)) {
23
+ return strategy as MigrationStrategy;
24
+ }
25
+
26
+ logger.warn(`[knowledge] Invalid migration strategy '${strategy}'. Valid options: drop, re-embed, backup. Falling back to default (backup).`);
27
+ return undefined;
28
+ }
29
+
30
+ /**
31
+ * Result of knowledge store initialization
32
+ */
33
+ export interface KnowledgeStoreComponents {
34
+ embedder: IEmbedder;
35
+ store: IKnowledgeStore;
36
+ writerQueue: WriterQueue;
37
+ }
38
+
39
+ const MAX_INIT_RETRIES = 5;
40
+ const BASE_RETRY_DELAY_MS = 1000;
41
+ const MAX_RETRY_DELAY_MS = 30000;
42
+
43
+ /**
44
+ * Initialize knowledge store components.
45
+ */
46
+ export async function createKnowledgeStoreComponents(
47
+ embedderFactory: () => Promise<IEmbedder>,
48
+ reconnectFactory?: () => Promise<IEmbedder>,
49
+ withLock?: <T>(fn: () => Promise<T>) => Promise<T>,
50
+ config: Config = getConfig(),
51
+ workspace: string = process.cwd()
52
+ ): Promise<KnowledgeStoreComponents | null> {
53
+ if (config.KNOWLEDGE_STORE_MODE === 'none') {
54
+ logger.debug('[knowledge] Knowledge store is disabled in configuration');
55
+ return null;
56
+ }
57
+ validateConfig(config);
58
+
59
+ for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
60
+ let store: KnowledgeStore | undefined;
61
+ try {
62
+ logger.info(`[knowledge] Creating Knowledge Store components (attempt ${attempt}/${MAX_INIT_RETRIES})...`);
63
+
64
+ const embedder = await embedderFactory();
65
+ const migrationStrategy = getMigrationStrategy(config);
66
+
67
+ store = new KnowledgeStore({
68
+ dbDir: getDbDir(config, workspace),
69
+ embedder,
70
+ modelName: config.EMBEDDING_MODEL,
71
+ migrationStrategy: migrationStrategy,
72
+ reconnectFactory,
73
+ withLock,
74
+ workspace,
75
+ knowledgeMode: config.KNOWLEDGE_STORE_MODE,
76
+ ttlDays: config.KNOWLEDGE_STORE_CACHE_TTL_DAYS,
77
+ maxServeAgeDays: config.KNOWLEDGE_STORE_MAX_SERVE_AGE_DAYS,
78
+ });
79
+
80
+ const chunkCfg = getModelChunkConfig(config.EMBEDDING_MODEL);
81
+ const chunkOverlap = Math.round(chunkCfg.chunkSize * chunkCfg.overlapPct);
82
+ const chunker = new Chunker({ targetSize: chunkCfg.chunkSize, overlap: chunkOverlap });
83
+ const writerQueue = new WriterQueue({ store, chunker });
84
+
85
+ await store.initialize();
86
+
87
+ return { embedder, store, writerQueue };
88
+ } catch (err) {
89
+ // Close any partially-opened store before retrying/giving up: store.initialize()
90
+ // can throw AFTER lancedb.connect() set its db handle, and discarding the object
91
+ // without close() leaks the LanceDB connection/file handles (up to MAX_INIT_RETRIES
92
+ // times per failing startup).
93
+ if (store) {
94
+ try { await store.close(); } catch { /* best-effort cleanup */ }
95
+ }
96
+ const givingUp = attempt === MAX_INIT_RETRIES;
97
+ if (givingUp) {
98
+ throw err;
99
+ }
100
+ const backoffDelay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt - 1), MAX_RETRY_DELAY_MS);
101
+ const totalDelay = backoffDelay + Math.random() * 500;
102
+ logger.warn(`[knowledge] Attempt ${attempt}/${MAX_INIT_RETRIES} failed. Retrying in ${(totalDelay / 1000).toFixed(1)}s...`, err);
103
+ await new Promise(resolve => setTimeout(resolve, totalDelay));
104
+ }
105
+ }
106
+
107
+ throw new Error('Failed to create knowledge store components');
108
+ }
109
+
110
+ /**
111
+ * Force delete the entire knowledge store directory.
112
+ * Used for system-level clearing or recovery from corruption.
113
+ */
114
+ export async function forceDeleteKnowledgeStore(config?: Config, workspace?: string): Promise<void> {
115
+ const dbDir = getDbDir(config, workspace);
116
+ if (fs.existsSync(dbDir)) {
117
+ try {
118
+ fs.rmSync(dbDir, { recursive: true, force: true });
119
+ fs.mkdirSync(dbDir, { recursive: true });
120
+ logger.info(`[knowledge] Knowledge store at ${dbDir} forcefully deleted and recreated.`);
121
+ } catch (err) {
122
+ logger.error(`[knowledge] Failed to forcefully delete directory ${dbDir}:`, err);
123
+ throw err;
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Lazy loader for the @lancedb/lancedb native module.
3
+ *
4
+ * The knowledge store is only reached behind a dynamic import() (see
5
+ * infrastructure/service-initialization.ts, which loads the store service via
6
+ * `await import(...)` inside a lazy service factory). That source-level lazy
7
+ * boundary is, however, defeated by the CLI build: esbuild bundles src/cli.ts
8
+ * into a single ESM file with `packages: 'external'` and no code-splitting, and
9
+ * in that mode it HOISTS every *static* `import` — including transitive ones —
10
+ * to the top of the bundle. A top-of-module `import * as lancedb from
11
+ * '@lancedb/lancedb'` therefore becomes an eager top-level import in dist/cli.mjs,
12
+ * evaluated before main() runs. lancedb's entry eagerly loads its native binding,
13
+ * so on any platform where that binding is absent (Intel macOS — no darwin-x64
14
+ * binding is published; musl/Alpine; or an npm optional-dependency install miss,
15
+ * npm/cli#4828) the whole bundle fails to evaluate and even `pi-research --help`
16
+ * / `status` exit 1.
17
+ *
18
+ * Loading lancedb through a runtime import() instead keeps esbuild from hoisting
19
+ * it (a dynamic import stays a deferred call), so the native binding is required
20
+ * only on first actual store access — exactly where
21
+ * KnowledgeStoreService.initialize() already catches a missing native stack
22
+ * (isNativeStackUnavailableError) and degrades the store to DISABLED. Help,
23
+ * version, status, and browser-only research then work with no binding present.
24
+ *
25
+ * Modules that need lancedb *types* should keep `import type * as lancedb from
26
+ * '@lancedb/lancedb'` (type imports are erased and never hoisted); only runtime
27
+ * value access (connect, Index, rerankers, …) must go through getLancedb().
28
+ */
29
+
30
+ let _lancedb: typeof import('@lancedb/lancedb') | undefined;
31
+
32
+ /** Resolve the lancedb module, memoizing after the first load. */
33
+ export async function getLancedb(): Promise<typeof import('@lancedb/lancedb')> {
34
+ return (_lancedb ??= await import('@lancedb/lancedb'));
35
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Simplified migration system for Knowledge Store.
3
+ *
4
+ * For a local research tool, we only need 3 strategies:
5
+ * - 'drop': Recreate table (fast, simple, data loss)
6
+ * - 're-embed': Preserve data by re-embedding with new model
7
+ * - 'backup': Rename old data and start fresh (safe, no data loss)
8
+ */
9
+
10
+ export type MigrationStrategy = 'drop' | 're-embed' | 'backup';
11
+
12
+ export interface MigrationResult {
13
+ strategy: MigrationStrategy;
14
+ success: boolean;
15
+ documentsProcessed: number;
16
+ error?: string;
17
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Embedding Model Configuration
3
+ *
4
+ * Pure lookup table for supported embedding models.
5
+ * Extracted to a standalone module so both knowledge/index.ts and
6
+ * infrastructure/embedding/embedding-factory.ts can import it without
7
+ * creating a circular dependency between those two modules.
8
+ */
9
+
10
+ interface ModelConfig {
11
+ pooling: 'mean' | 'cls' | 'last_token';
12
+ queryPrefix?: string;
13
+ chunkSize: number;
14
+ overlapPct: number;
15
+ maxTokens?: number;
16
+ batchSize?: number;
17
+ charsPerToken?: number;
18
+ documentPrefix?: string;
19
+ useCache?: boolean;
20
+ multilingual: boolean;
21
+ }
22
+
23
+ const MODEL_CONFIG: Record<string, ModelConfig> = {
24
+ 'Xenova/multilingual-e5-small': {
25
+ pooling: 'mean',
26
+ chunkSize: 1500,
27
+ overlapPct: 0.15,
28
+ queryPrefix: 'query: ',
29
+ documentPrefix: 'passage: ',
30
+ charsPerToken: 3.5,
31
+ multilingual: true,
32
+ },
33
+ 'Xenova/multilingual-e5-base': {
34
+ pooling: 'mean',
35
+ chunkSize: 1500,
36
+ overlapPct: 0.15,
37
+ queryPrefix: 'query: ',
38
+ documentPrefix: 'passage: ',
39
+ charsPerToken: 3.5,
40
+ multilingual: true,
41
+ },
42
+ 'Xenova/bge-m3': {
43
+ pooling: 'cls',
44
+ chunkSize: 1500,
45
+ overlapPct: 0.15,
46
+ charsPerToken: 3.5,
47
+ multilingual: true,
48
+ },
49
+ 'onnx-community/embeddinggemma-300m-ONNX': {
50
+ pooling: 'mean',
51
+ chunkSize: 1600,
52
+ overlapPct: 0.15,
53
+ queryPrefix: 'Instruct: Given a web search query, retrieve relevant passages that answer the query.\nQuery: ',
54
+ charsPerToken: 3.5,
55
+ multilingual: true,
56
+ },
57
+ 'onnx-community/Qwen3-Embedding-0.6B-ONNX': {
58
+ pooling: 'last_token',
59
+ queryPrefix: 'Instruct: Given a web search query, retrieve relevant passages.\nQuery: ',
60
+ chunkSize: 1200,
61
+ overlapPct: 0.15,
62
+ maxTokens: 512,
63
+ batchSize: 2,
64
+ charsPerToken: 2.5,
65
+ useCache: false,
66
+ multilingual: true,
67
+ },
68
+ 'Xenova/all-MiniLM-L6-v2': {
69
+ pooling: 'mean',
70
+ chunkSize: 800,
71
+ overlapPct: 0.15,
72
+ maxTokens: 256,
73
+ multilingual: false,
74
+ },
75
+ 'Xenova/bge-small-en-v1.5': {
76
+ pooling: 'cls',
77
+ chunkSize: 1500,
78
+ overlapPct: 0.15,
79
+ multilingual: false,
80
+ },
81
+ 'Xenova/all-mpnet-base-v2': {
82
+ pooling: 'mean',
83
+ chunkSize: 1200,
84
+ overlapPct: 0.15,
85
+ maxTokens: 384,
86
+ multilingual: false,
87
+ },
88
+ 'onnx-community/granite-embedding-small-english-r2-ONNX': {
89
+ pooling: 'cls',
90
+ chunkSize: 1800,
91
+ overlapPct: 0.15,
92
+ maxTokens: 512,
93
+ batchSize: 8,
94
+ multilingual: false,
95
+ },
96
+ };
97
+
98
+ export const SUPPORTED_MODELS: ReadonlyArray<{ id: string; multilingual: boolean }> =
99
+ Object.entries(MODEL_CONFIG).map(([id, cfg]) => ({ id, multilingual: cfg.multilingual }));
100
+
101
+ const DEFAULT_CHUNK_SIZE = 1200;
102
+ const DEFAULT_OVERLAP_PCT = 0.15;
103
+
104
+ export function getModelEmbedderConfig(modelId: string): {
105
+ pooling: 'mean' | 'cls' | 'last_token';
106
+ queryPrefix?: string;
107
+ documentPrefix?: string;
108
+ maxTokens?: number;
109
+ batchSize?: number;
110
+ charsPerToken?: number;
111
+ useCache?: boolean;
112
+ } {
113
+ const cfg = MODEL_CONFIG[modelId];
114
+ return cfg
115
+ ? {
116
+ pooling: cfg.pooling,
117
+ queryPrefix: cfg.queryPrefix,
118
+ documentPrefix: cfg.documentPrefix,
119
+ maxTokens: cfg.maxTokens,
120
+ batchSize: cfg.batchSize,
121
+ charsPerToken: cfg.charsPerToken,
122
+ useCache: cfg.useCache,
123
+ }
124
+ : { pooling: 'mean' };
125
+ }
126
+
127
+ export function getModelChunkConfig(modelId: string): { chunkSize: number; overlapPct: number } {
128
+ const cfg = MODEL_CONFIG[modelId];
129
+ return cfg
130
+ ? { chunkSize: cfg.chunkSize, overlapPct: cfg.overlapPct }
131
+ : { chunkSize: DEFAULT_CHUNK_SIZE, overlapPct: DEFAULT_OVERLAP_PCT };
132
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ONNX / transformers.js environment setup.
3
+ *
4
+ * `@huggingface/transformers` (which in turn loads the native onnxruntime-node
5
+ * binding) is pulled in LAZILY via ./transformers-loader.ts, never as a static
6
+ * import — otherwise esbuild would hoist it to the top of the CLI bundle and crash
7
+ * startup on any platform lacking the binding (see transformers-loader.ts). Both
8
+ * accessors below are async so the native ML stack loads only when the embedder is
9
+ * actually constructed — i.e. when the knowledge store is enabled and used.
10
+ */
11
+
12
+ import { HFEnv } from '../core/interfaces/knowledge-interfaces.ts';
13
+ import { logger } from '../logger.ts';
14
+ import { getModelCacheDir } from './embedder-utils.ts';
15
+ import { getTransformers } from './transformers-loader.ts';
16
+
17
+ /**
18
+ * Get the HuggingFace env object (loads transformers lazily on first call).
19
+ */
20
+ export async function getHFEnv() {
21
+ return (await getTransformers()).env;
22
+ }
23
+
24
+ /**
25
+ * Initialize the ONNX environment
26
+ */
27
+ let onnxInitialized = false;
28
+ export async function initializeONNXEnv(): Promise<void> {
29
+ if (onnxInitialized) return;
30
+
31
+ const hfEnv = (await getTransformers()).env;
32
+ hfEnv.cacheDir = getModelCacheDir();
33
+
34
+ try {
35
+ const envObj = hfEnv as unknown as HFEnv;
36
+ if (envObj.backends?.onnx) {
37
+ envObj.backends.onnx.logLevel = 'error';
38
+ }
39
+ // Fallback for older transformers.js versions if any
40
+ if ((envObj as any).onnx) {
41
+ (envObj as any).onnx.logLevel = 'error';
42
+ }
43
+ } catch (e) {
44
+ logger.debug('[embedder] Failed to set ONNX logLevel:', e);
45
+ }
46
+
47
+ onnxInitialized = true;
48
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Knowledge Store Operations
3
+ *
4
+ * Core database operations for the KnowledgeStore.
5
+ */
6
+
7
+ // Type-only import: this module references lancedb solely in type positions
8
+ // (lancedb.Table / lancedb.rerankers.*). A type import is erased at build time,
9
+ // so esbuild never hoists a native-loading `import` into the CLI bundle. See
10
+ // ./lancedb-loader.ts for why that matters.
11
+ import type * as lancedb from '@lancedb/lancedb';
12
+ import { logger } from '../logger.ts';
13
+ import { metrics } from '../utils/metrics.ts';
14
+ import { StoreDocument } from '../core/interfaces/knowledge-interfaces.ts';
15
+
16
+ /**
17
+ * Add documents to the store
18
+ */
19
+ export async function addDocumentsToStore(
20
+ table: lancedb.Table,
21
+ docs: StoreDocument[],
22
+ embedder: { embedMany(texts: string[]): Promise<(Float32Array | number[])[]>; getDimension?(): number | null },
23
+ isClosing: () => boolean,
24
+ workspace: string,
25
+ isGlobal: boolean
26
+ ): Promise<void> {
27
+ if (docs.length === 0) return;
28
+ if (isClosing()) {
29
+ logger.warn('[store] Ignoring addDocuments during close');
30
+ metrics.increment('knowledge_store_add_documents_total', 1, { status: 'ignored_closing' });
31
+ return;
32
+ }
33
+
34
+ const startTime = Date.now();
35
+
36
+ try {
37
+ const vectors = await embedder.embedMany(docs.map(d => d.text));
38
+
39
+ // Guard against a broken embedder silently writing corrupt vectors: a
40
+ // wrong-count, empty, or ragged result would otherwise land in the
41
+ // FixedSizeList column and surface only as an opaque Arrow error or, worse,
42
+ // as silently degraded search. Fail loudly here instead.
43
+ if (vectors.length !== docs.length) {
44
+ throw new Error(
45
+ `[store] embedder returned ${vectors.length} vectors for ${docs.length} documents`,
46
+ );
47
+ }
48
+ // Expected width: the embedder's declared dimension, falling back to the
49
+ // first vector's width. Every vector must match it exactly — a uniformly
50
+ // wrong width is just as corrupting as a ragged one.
51
+ const declaredDim = embedder.getDimension?.();
52
+ const dim = declaredDim && declaredDim > 0 ? declaredDim : (vectors[0]?.length ?? 0);
53
+ if (dim === 0) {
54
+ throw new Error('[store] embedder returned empty/zero-width vectors');
55
+ }
56
+ for (let i = 0; i < vectors.length; i++) {
57
+ const v = vectors[i];
58
+ if (!v || v.length !== dim) {
59
+ throw new Error(
60
+ `[store] embedder returned a wrong-width vector at index ${i} (got ${v?.length ?? 0}, expected ${dim})`,
61
+ );
62
+ }
63
+ // A correct-width vector can still be poisoned: a WebGPU device-lost
64
+ // mid-inference, or a truncated server response coerced through
65
+ // Float32Array, yields NaN/Inf entries that pass every length check but
66
+ // silently corrupt the ANN index (NaN distances break ordering). Scan
67
+ // here — this is the last gate before the vector is persisted.
68
+ for (let j = 0; j < v.length; j++) {
69
+ if (!Number.isFinite(v[j]!)) {
70
+ throw new Error(
71
+ `[store] embedder returned a non-finite value (${v[j]}) at vector ${i}, position ${j}`,
72
+ );
73
+ }
74
+ }
75
+ }
76
+
77
+ const data = docs.map((doc, i) => ({
78
+ vector: vectors[i]!,
79
+ url: doc.url,
80
+ text: doc.text,
81
+ content: doc.content ?? null,
82
+ metadata: JSON.stringify(doc.metadata),
83
+ workspace,
84
+ is_global: isGlobal,
85
+ ingestion_type: doc.ingestion_type || (doc.metadata['ingestionType'] as string) || 'unknown',
86
+ timestamp: BigInt(doc.timestamp),
87
+ }));
88
+
89
+ await table.add(data);
90
+ const duration = Date.now() - startTime;
91
+ metrics.observe('knowledge_store_add_documents_duration_ms', duration);
92
+ metrics.increment('knowledge_store_add_documents_total', 1, { status: 'success' });
93
+ metrics.increment('knowledge_store_chunks_added_total', docs.length);
94
+ logger.log(`[store] Added ${docs.length} chunk(s) for ${docs[0]?.url} [workspace=${workspace}, global=${isGlobal}]`);
95
+ } catch (err) {
96
+ const duration = Date.now() - startTime;
97
+ metrics.observe('knowledge_store_add_documents_duration_ms', duration, { status: 'error' });
98
+ metrics.increment('knowledge_store_add_documents_total', 1, { status: 'error' });
99
+ logger.error('[store] Failed to add documents:', err);
100
+ throw err;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Search for documents in the store using hybrid search.
106
+ *
107
+ * Semantic search (vector similarity) targets the 'text' field (synthesis descriptions)
108
+ * since vectors are computed from description chunks. Keyword search (BM25/FTS) targets
109
+ * BOTH 'text' and 'content' fields — 'text' for description keywords and 'content'
110
+ * for full article body text. Results are fused via Reciprocal Rank Fusion (RRF).
111
+ */
112
+ export async function searchStore(
113
+ table: lancedb.Table,
114
+ embedder: { embed(query: string): Promise<Float32Array | number[]> },
115
+ query: string,
116
+ getReranker: () => Promise<lancedb.rerankers.RRFReranker>,
117
+ limit: number,
118
+ scopeFilter?: string
119
+ ): Promise<StoreDocument[]> {
120
+ const startTime = Date.now();
121
+
122
+ const rowCount = scopeFilter ? await table.countRows(scopeFilter) : await table.countRows();
123
+ if (rowCount === 0) {
124
+ metrics.increment('knowledge_store_search_total', 1, { status: 'empty' });
125
+ return [];
126
+ }
127
+
128
+ const vector = await embedder.embed(query);
129
+
130
+ let filter = "ingestion_type = 'synthesis-description'";
131
+ if (scopeFilter) {
132
+ filter = `(${filter}) AND (${scopeFilter})`;
133
+ }
134
+
135
+ const results = await table
136
+ .query()
137
+ .nearestTo(vector)
138
+ .where(filter)
139
+ .fullTextSearch(query, { columns: ['text', 'content'] })
140
+ .rerank(await getReranker())
141
+ .limit(limit)
142
+ .toArray();
143
+
144
+ const filteredResults = results
145
+ .map(r => {
146
+ let metadata: Record<string, unknown>;
147
+ try {
148
+ metadata = JSON.parse(r.metadata as string) as Record<string, unknown>;
149
+ } catch {
150
+ logger.debug('[store] Corrupted metadata in searchStore, skipping row');
151
+ return null;
152
+ }
153
+ return {
154
+ url: r.url as string,
155
+ text: r.text as string,
156
+ content: (r.content as string | null) ?? undefined,
157
+ metadata,
158
+ timestamp: Number(r.timestamp),
159
+ };
160
+ })
161
+ .filter((doc): doc is NonNullable<typeof doc> => doc !== null);
162
+
163
+ const duration = Date.now() - startTime;
164
+ metrics.observe('knowledge_store_search_duration_ms', duration);
165
+ metrics.increment('knowledge_store_search_total', 1, { status: 'success' });
166
+ metrics.increment('knowledge_store_search_results_total', filteredResults.length);
167
+
168
+ return filteredResults;
169
+ }
170
+
171
+ /**
172
+ * Find documents by URL
173
+ */
174
+ export async function findDocumentsByUrl(
175
+ table: lancedb.Table,
176
+ url: string,
177
+ scopeFilter?: string
178
+ ): Promise<StoreDocument[]> {
179
+ const startTime = Date.now();
180
+
181
+ // Escape single quotes in URL to prevent SQL injection
182
+ const escapedUrl = url.replace(/'/g, "''");
183
+
184
+ let filter = `url = '${escapedUrl}'`;
185
+ if (scopeFilter) {
186
+ filter = `(${filter}) AND (${scopeFilter})`;
187
+ }
188
+
189
+ const results = await table
190
+ .query()
191
+ .where(filter)
192
+ .limit(1000)
193
+ .toArray();
194
+
195
+ const duration = Date.now() - startTime;
196
+ metrics.observe('knowledge_store_query_duration_ms', duration, { operation: 'find_by_url' });
197
+ metrics.increment('knowledge_store_query_total', 1, { operation: 'find_by_url' });
198
+
199
+ if (results.length === 1000) {
200
+ logger.warn(`[store] findByUrl hit 1000-chunk cap for ${url} - some chunks may be missing`);
201
+ metrics.increment('knowledge_store_query_cap_hits_total', 1);
202
+ }
203
+
204
+ return results.map(r => {
205
+ let metadata: Record<string, unknown> = {};
206
+ try {
207
+ metadata = JSON.parse(r.metadata as string);
208
+ } catch {
209
+ logger.debug('[store] Corrupted metadata in findDocumentsByUrl, using empty object');
210
+ }
211
+ return {
212
+ url: r.url as string,
213
+ text: r.text as string,
214
+ content: (r.content as string | null) ?? undefined,
215
+ metadata,
216
+ timestamp: Number(r.timestamp),
217
+ };
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Find URLs relevant to a query using hybrid search.
223
+ *
224
+ * Semantic search (vector) targets the 'text' field (descriptions).
225
+ * Keyword search (BM25/FTS) targets both 'text' and 'content' fields.
226
+ * Results are fused via RRF reranking.
227
+ */
228
+ export async function findRelevantUrls(
229
+ table: lancedb.Table,
230
+ embedder: { embed(query: string): Promise<Float32Array | number[]> },
231
+ query: string,
232
+ getReranker: () => Promise<lancedb.rerankers.RRFReranker>,
233
+ limit: number,
234
+ scopeFilter?: string
235
+ ): Promise<{ url: string; description: string; provenance?: string }[]> {
236
+ const startTime = Date.now();
237
+ const rowCount = scopeFilter ? await table.countRows(scopeFilter) : await table.countRows();
238
+ if (rowCount === 0) {
239
+ metrics.increment('knowledge_store_find_urls_total', 1, { status: 'empty' });
240
+ return [];
241
+ }
242
+
243
+ const vector = await embedder.embed(query);
244
+
245
+ let filter = "ingestion_type = 'synthesis-description'";
246
+ if (scopeFilter) {
247
+ filter = `(${filter}) AND (${scopeFilter})`;
248
+ }
249
+
250
+ const results = await table
251
+ .query()
252
+ .nearestTo(vector)
253
+ .where(filter)
254
+ .fullTextSearch(query, { columns: ['text', 'content'] })
255
+ .rerank(await getReranker())
256
+ .limit(limit)
257
+ .toArray();
258
+
259
+ const entries: { url: string; description: string; provenance?: string }[] = [];
260
+ const seen = new Set<string>();
261
+ for (const r of results) {
262
+ const url = r.url as string;
263
+ if (seen.has(url)) continue;
264
+ seen.add(url);
265
+
266
+ let description = '';
267
+ let provenance = 'description-unverified';
268
+ try {
269
+ const meta = JSON.parse(r.metadata as string);
270
+ description = meta.description as string ?? '';
271
+ // Surface provenance metadata so consumers can prefer verified entries
272
+ if (meta.provenance) provenance = meta.provenance;
273
+ else if (meta.hasContent === true) provenance = 'scraped-verified';
274
+ } catch { logger.debug('[store] Corrupted metadata in findRelevantUrls, skipping row'); }
275
+
276
+ if (!description) description = (r.text as string ?? '').substring(0, 300);
277
+
278
+ // Add store scope to provenance
279
+ const scope = r.is_global === true ? 'Global Store' : `Local Project (${r.workspace || 'unknown'})`;
280
+ const finalProvenance = `${provenance} [${scope}]`;
281
+
282
+ entries.push({ url, description, provenance: finalProvenance });
283
+ }
284
+
285
+ const duration = Date.now() - startTime;
286
+ metrics.observe('knowledge_store_find_urls_duration_ms', duration);
287
+ metrics.increment('knowledge_store_find_urls_total', 1, { status: 'success' });
288
+ metrics.increment('knowledge_store_urls_found_total', entries.length);
289
+
290
+ return entries;
291
+ }