harper-knowledge 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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/config.yaml +17 -0
  4. package/dist/core/embeddings.d.ts +29 -0
  5. package/dist/core/embeddings.js +199 -0
  6. package/dist/core/entries.d.ts +85 -0
  7. package/dist/core/entries.js +235 -0
  8. package/dist/core/history.d.ts +30 -0
  9. package/dist/core/history.js +119 -0
  10. package/dist/core/search.d.ts +23 -0
  11. package/dist/core/search.js +306 -0
  12. package/dist/core/tags.d.ts +32 -0
  13. package/dist/core/tags.js +76 -0
  14. package/dist/core/triage.d.ts +55 -0
  15. package/dist/core/triage.js +126 -0
  16. package/dist/http-utils.d.ts +37 -0
  17. package/dist/http-utils.js +132 -0
  18. package/dist/index.d.ts +21 -0
  19. package/dist/index.js +76 -0
  20. package/dist/mcp/server.d.ts +24 -0
  21. package/dist/mcp/server.js +124 -0
  22. package/dist/mcp/tools.d.ts +13 -0
  23. package/dist/mcp/tools.js +497 -0
  24. package/dist/oauth/authorize.d.ts +27 -0
  25. package/dist/oauth/authorize.js +438 -0
  26. package/dist/oauth/github.d.ts +28 -0
  27. package/dist/oauth/github.js +62 -0
  28. package/dist/oauth/keys.d.ts +33 -0
  29. package/dist/oauth/keys.js +100 -0
  30. package/dist/oauth/metadata.d.ts +21 -0
  31. package/dist/oauth/metadata.js +55 -0
  32. package/dist/oauth/middleware.d.ts +22 -0
  33. package/dist/oauth/middleware.js +64 -0
  34. package/dist/oauth/register.d.ts +14 -0
  35. package/dist/oauth/register.js +83 -0
  36. package/dist/oauth/token.d.ts +15 -0
  37. package/dist/oauth/token.js +178 -0
  38. package/dist/oauth/validate.d.ts +30 -0
  39. package/dist/oauth/validate.js +52 -0
  40. package/dist/resources/HistoryResource.d.ts +38 -0
  41. package/dist/resources/HistoryResource.js +38 -0
  42. package/dist/resources/KnowledgeEntryResource.d.ts +64 -0
  43. package/dist/resources/KnowledgeEntryResource.js +157 -0
  44. package/dist/resources/QueryLogResource.d.ts +20 -0
  45. package/dist/resources/QueryLogResource.js +57 -0
  46. package/dist/resources/ServiceKeyResource.d.ts +51 -0
  47. package/dist/resources/ServiceKeyResource.js +132 -0
  48. package/dist/resources/TagResource.d.ts +25 -0
  49. package/dist/resources/TagResource.js +32 -0
  50. package/dist/resources/TriageResource.d.ts +51 -0
  51. package/dist/resources/TriageResource.js +107 -0
  52. package/dist/types.d.ts +317 -0
  53. package/dist/types.js +7 -0
  54. package/dist/webhooks/datadog.d.ts +26 -0
  55. package/dist/webhooks/datadog.js +120 -0
  56. package/dist/webhooks/github.d.ts +24 -0
  57. package/dist/webhooks/github.js +167 -0
  58. package/dist/webhooks/middleware.d.ts +14 -0
  59. package/dist/webhooks/middleware.js +161 -0
  60. package/dist/webhooks/types.d.ts +17 -0
  61. package/dist/webhooks/types.js +4 -0
  62. package/package.json +72 -0
  63. package/schema/knowledge.graphql +134 -0
  64. package/web/index.html +735 -0
  65. package/web/js/app.js +461 -0
  66. package/web/js/detail.js +223 -0
  67. package/web/js/editor.js +303 -0
  68. package/web/js/search.js +238 -0
  69. package/web/js/triage.js +305 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nathan Heskew
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # harper-knowledge
2
+
3
+ Knowledge base for [Harper](https://harper.fast/), built on Harper, with MCP server integration.
4
+
5
+ A Harper sub-component plugin that provides searchable, scoped knowledge entries with vector embeddings for semantic search. Exposes a REST API, MCP endpoint, and web UI.
6
+
7
+ ## Consumers
8
+
9
+ - **Support team** — finding solutions, patterns, gotchas, customer edge cases
10
+ - **DX lab "Harper expert"** — backing knowledge for the AI expert role in Gas Town labs
11
+ - **Claude Code / IDE assistants** — Harper context via MCP without per-project CLAUDE.md files
12
+ - **Any MCP client** — Cursor, VS Code + Copilot, JetBrains, ChatGPT, Gemini, etc.
13
+
14
+ ## Quick Start
15
+
16
+ ### Prerequisites
17
+
18
+ - [Harper](https://harper.fast/) >= 4.7.0
19
+ - Node.js >= 22
20
+
21
+ ### Install
22
+
23
+ ```bash
24
+ npm install harper-knowledge
25
+ ```
26
+
27
+ ### Configure
28
+
29
+ Add to your parent application's `config.yaml`:
30
+
31
+ ```yaml
32
+ "harper-knowledge":
33
+ package: "harper-knowledge"
34
+ embeddingModel: nomic-embed-text # default
35
+ ```
36
+
37
+ ### Download the Embedding Model
38
+
39
+ The plugin uses [nomic-embed-text](https://huggingface.co/nomic-ai) for vector embeddings, run locally via [node-llama-cpp](https://github.com/withcatai/node-llama-cpp). The model is downloaded to `~/hdb/models/` on first startup, but you can pre-download it:
40
+
41
+ ```bash
42
+ # Download the default model
43
+ npm run model:download
44
+
45
+ # Download and verify with a test embedding
46
+ npm run model:test
47
+ ```
48
+
49
+ ### Run
50
+
51
+ ```bash
52
+ harperdb dev .
53
+ ```
54
+
55
+ ## Embedding Models
56
+
57
+ Two [Nomic](https://huggingface.co/nomic-ai) embedding models are supported, both run entirely on CPU with no cloud dependency via [node-llama-cpp](https://github.com/withcatai/node-llama-cpp). On first plugin startup (or `npm run model:download`), the configured model is downloaded from Hugging Face to `~/hdb/models/`. A file lock prevents multiple Harper worker threads from downloading simultaneously.
58
+
59
+ ### nomic-embed-text v1.5 (default)
60
+
61
+ | | |
62
+ | ---------------- | ------------------------------------------------------------------------------------------------- |
63
+ | **Model** | [nomic-ai/nomic-embed-text-v1.5-GGUF](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF) |
64
+ | **Config key** | `nomic-embed-text` |
65
+ | **Parameters** | 137M |
66
+ | **Dimensions** | 768 |
67
+ | **Quantization** | Q4_K_M (~135 MB) |
68
+ | **Context** | 8192 tokens |
69
+ | **License** | Apache 2.0 |
70
+
71
+ ### nomic-embed-text v2 MoE
72
+
73
+ | | |
74
+ | ---------------- | ----------------------------------------------------------------------------------------------------- |
75
+ | **Model** | [nomic-ai/nomic-embed-text-v2-moe-GGUF](https://huggingface.co/nomic-ai/nomic-embed-text-v2-moe-GGUF) |
76
+ | **Config key** | `nomic-embed-text-v2-moe` |
77
+ | **Parameters** | 475M (Mixture of Experts) |
78
+ | **Dimensions** | 768 |
79
+ | **Quantization** | Q4_K_M |
80
+ | **Context** | 8192 tokens |
81
+ | **License** | Apache 2.0 |
82
+
83
+ The v2 MoE model is larger but produces higher-quality embeddings, especially for longer and more nuanced content.
84
+
85
+ ### Switching models
86
+
87
+ ```yaml
88
+ "harper-knowledge":
89
+ package: "harper-knowledge"
90
+ embeddingModel: nomic-embed-text # v1.5 (default)
91
+ # embeddingModel: nomic-embed-text-v2-moe # v2 MoE
92
+ ```
93
+
94
+ ## Architecture
95
+
96
+ ```
97
+ harper-knowledge
98
+ ├── src/
99
+ │ ├── index.ts ← plugin entry: handleApplication()
100
+ │ ├── core/ ← shared logic
101
+ │ │ ├── embeddings.ts ← model download, init, vector generation
102
+ │ │ ├── entries.ts ← CRUD + relationship management
103
+ │ │ ├── history.ts ← edit history audit log
104
+ │ │ ├── search.ts ← keyword / semantic / hybrid search
105
+ │ │ ├── tags.ts ← tag registry with counts
106
+ │ │ └── triage.ts ← webhook intake queue
107
+ │ ├── resources/ ← REST Resource classes
108
+ │ │ ├── KnowledgeEntryResource.ts
109
+ │ │ ├── TriageResource.ts
110
+ │ │ ├── TagResource.ts
111
+ │ │ ├── QueryLogResource.ts
112
+ │ │ ├── ServiceKeyResource.ts
113
+ │ │ └── HistoryResource.ts
114
+ │ ├── mcp/ ← MCP server (Streamable HTTP)
115
+ │ │ ├── server.ts
116
+ │ │ └── tools.ts
117
+ │ ├── oauth/ ← OAuth 2.1 authorization server
118
+ │ │ ├── authorize.ts
119
+ │ │ ├── keys.ts
120
+ │ │ ├── metadata.ts
121
+ │ │ ├── middleware.ts
122
+ │ │ ├── register.ts
123
+ │ │ ├── token.ts
124
+ │ │ └── validate.ts
125
+ │ ├── webhooks/ ← webhook intake (GitHub, Datadog)
126
+ │ │ ├── middleware.ts
127
+ │ │ ├── github.ts
128
+ │ │ └── datadog.ts
129
+ │ └── types.ts
130
+ ├── schema/
131
+ │ └── knowledge.graphql ← table definitions (database: "kb")
132
+ ├── web/ ← static web UI
133
+ ├── scripts/
134
+ │ └── download-model.js ← standalone model download/test
135
+ ├── config.yaml
136
+ ├── package.json
137
+ └── test/
138
+ ```
139
+
140
+ Both REST and MCP run in the Harper process, both call the same core functions with zero overhead.
141
+
142
+ ## REST API
143
+
144
+ | Endpoint | Method | Auth | Description |
145
+ | ----------------------- | --------------- | ---------- | ------------------------- |
146
+ | `/Knowledge/<id>` | GET | Public | Get entry by ID |
147
+ | `/Knowledge/?query=...` | GET | Public | Search entries |
148
+ | `/Knowledge/` | POST | Required | Create entry |
149
+ | `/Knowledge/<id>` | PUT | Required | Update entry |
150
+ | `/Knowledge/<id>` | DELETE | Team | Deprecate entry |
151
+ | `/KnowledgeTag/` | GET | Public | List all tags |
152
+ | `/Triage/` | GET | Team | List pending triage items |
153
+ | `/Triage/` | POST | Service/AI | Submit triage item |
154
+ | `/Triage/<id>` | PUT | Team | Process triage item |
155
+ | `/QueryLog/` | GET | Team | Search analytics |
156
+ | `/ServiceKey/` | GET/POST/DELETE | Team | API key management |
157
+ | `/History/<entryId>` | GET | Public | Edit history for an entry |
158
+
159
+ ### Search Parameters
160
+
161
+ ```
162
+ GET /Knowledge/?query=MQTT+auth&tags=mqtt,config&limit=10&mode=keyword&context={"harper":"5.0","storageEngine":"lmdb"}
163
+ ```
164
+
165
+ - `query` — search text (required)
166
+ - `tags` — comma-separated tag filter
167
+ - `limit` — max results (default 10)
168
+ - `mode` — `keyword`, `semantic`, or `hybrid` (default)
169
+ - `context` — JSON applicability context for result boosting
170
+
171
+ ## MCP Endpoint
172
+
173
+ Connect any MCP-compatible client to `/mcp`:
174
+
175
+ ```json
176
+ {
177
+ "mcpServers": {
178
+ "harper-kb": {
179
+ "url": "https://kb.harper.fast:9926/mcp"
180
+ }
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### Tools
186
+
187
+ | Tool | Description |
188
+ | --------------------- | ----------------------------------------------------------------- |
189
+ | `knowledge_search` | Search with keyword/semantic/hybrid modes + applicability context |
190
+ | `knowledge_add` | Add a new entry (auto-tagged `ai-generated`) |
191
+ | `knowledge_get` | Get entry by ID with full relationship chain |
192
+ | `knowledge_update` | Update an entry with edit history tracking |
193
+ | `knowledge_related` | Find related entries (explicit + semantic similarity) |
194
+ | `knowledge_list_tags` | List all tags with counts |
195
+ | `knowledge_triage` | Submit to triage queue for review |
196
+ | `knowledge_history` | Get edit history for an entry (who changed what, when, why) |
197
+
198
+ ## Schema
199
+
200
+ Tables in the `kb` database:
201
+
202
+ - **KnowledgeEntry** — core entries with HNSW vector index, `@relationship` directives for supersession/siblings/related, `@createdTime`/`@updatedTime`
203
+ - **KnowledgeEntryEdit** — append-only edit history audit log
204
+ - **TriageItem** — webhook intake queue (7-day TTL)
205
+ - **KnowledgeTag** — tag name as primary key with entry counts
206
+ - **QueryLog** — search analytics (30-day TTL)
207
+ - **ServiceKey** — API keys with scrypt-hashed secrets
208
+ - **OAuthClient** — dynamic client registrations (RFC 7591)
209
+ - **OAuthCode** — authorization codes (5-minute TTL)
210
+ - **OAuthRefreshToken** — refresh tokens (30-day TTL)
211
+ - **OAuthSigningKey** — RSA key pair for JWT signing
212
+
213
+ ### Applicability Scoping
214
+
215
+ Entries carry an `appliesTo` scope:
216
+
217
+ ```json
218
+ {
219
+ "harper": ">=4.0 <5.0",
220
+ "storageEngine": "lmdb",
221
+ "node": ">=22",
222
+ "platform": "linux"
223
+ }
224
+ ```
225
+
226
+ Search results are boosted or demoted (never hidden) based on the caller's context.
227
+
228
+ ### Entry Relationships
229
+
230
+ - **Supersedes** — "This replaces that for newer versions"
231
+ - **Siblings** — "Same topic, different config" (e.g., LMDB vs RocksDB behavior)
232
+ - **Related** — loose "see also" association
233
+
234
+ ## Auth Model
235
+
236
+ | Role | Read | Write | Review | Manage |
237
+ | ----------------- | ---- | ---------------------------- | ------ | ------ |
238
+ | `team` | Yes | Yes | Yes | Yes |
239
+ | `ai_agent` | Yes | Yes (flagged `ai-generated`) | No | No |
240
+ | `service_account` | Yes | Triage queue only | No | No |
241
+
242
+ MCP uses OAuth 2.1 with PKCE for authentication. MCP clients discover auth requirements via `/.well-known/oauth-protected-resource`, register dynamically, and authenticate through a browser-based login flow (GitHub OAuth primary, Harper credentials fallback). The web UI uses GitHub OAuth via `@harperfast/oauth` with Harper credentials as fallback.
243
+
244
+ ## Development
245
+
246
+ ```bash
247
+ # Build
248
+ npm run build
249
+
250
+ # Run tests (202 tests)
251
+ npm test
252
+
253
+ # Test with coverage
254
+ npm run test:coverage
255
+
256
+ # Download embedding model
257
+ npm run model:download
258
+
259
+ # Download + verify embedding model
260
+ npm run model:test
261
+
262
+ # Watch mode
263
+ npm run dev
264
+ ```
265
+
266
+ ### Testing
267
+
268
+ Tests use Node.js built-in test runner (`node:test`) with mock Harper globals (in-memory tables). Tests run against compiled output in `dist/`.
269
+
270
+ ```bash
271
+ npm test
272
+ ```
273
+
274
+ ## License
275
+
276
+ MIT
package/config.yaml ADDED
@@ -0,0 +1,17 @@
1
+ # Knowledge Base Plugin Configuration
2
+ # This file defines the plugin entry point and default settings
3
+ # All settings can be overridden in your application's config.yaml
4
+
5
+ # Plugin entry point (required by Harper)
6
+ pluginModule: "dist/index.js"
7
+
8
+ # GraphQL schema for knowledge base tables
9
+ graphqlSchema:
10
+ files: "schema/knowledge.graphql"
11
+
12
+ # Static web UI files
13
+ static:
14
+ files: "web/**"
15
+
16
+ # Default settings (optional - these are used if not specified in app config)
17
+ embeddingModel: "nomic-embed-text"
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Embedding Model Management
3
+ *
4
+ * Downloads and initializes the nomic-embed-text model for generating
5
+ * vector embeddings. Uses file-based locking to prevent concurrent
6
+ * downloads across worker threads.
7
+ */
8
+ /**
9
+ * Initialize the embedding model.
10
+ * Downloads the model to ~/hdb/models/ if not present.
11
+ * Uses file-based locking so only one thread downloads.
12
+ *
13
+ * @param config - Plugin configuration with embeddingModel name
14
+ */
15
+ export declare function initEmbeddingModel(config: {
16
+ embeddingModel: string;
17
+ }): Promise<void>;
18
+ /**
19
+ * Generate an embedding vector for the given text.
20
+ *
21
+ * @param text - Text to generate embedding for
22
+ * @returns Embedding vector as array of numbers
23
+ * @throws Error if the model has not been initialized
24
+ */
25
+ export declare function generateEmbedding(text: string): Promise<number[]>;
26
+ /**
27
+ * Clean up embedding model resources.
28
+ */
29
+ export declare function dispose(): Promise<void>;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Embedding Model Management
3
+ *
4
+ * Downloads and initializes the nomic-embed-text model for generating
5
+ * vector embeddings. Uses file-based locking to prevent concurrent
6
+ * downloads across worker threads.
7
+ */
8
+ import { writeFile, readFile, unlink, mkdir } from "node:fs/promises";
9
+ import { existsSync } from "node:fs";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ // Module-level state for the loaded model and context
13
+ let llama = null;
14
+ let embeddingModel = null;
15
+ let embeddingContext = null;
16
+ // Model configuration: Hugging Face model identifiers for node-llama-cpp
17
+ const MODEL_CONFIGS = {
18
+ "nomic-embed-text": {
19
+ repo: "nomic-ai/nomic-embed-text-v1.5-GGUF",
20
+ file: "nomic-embed-text-v1.5.Q4_K_M.gguf",
21
+ },
22
+ "nomic-embed-text-v2-moe": {
23
+ repo: "nomic-ai/nomic-embed-text-v2-moe-GGUF",
24
+ file: "nomic-embed-text-v2-moe.Q4_K_M.gguf",
25
+ },
26
+ };
27
+ /**
28
+ * Get the directory where models are stored.
29
+ */
30
+ function getModelsDir() {
31
+ return path.join(os.homedir(), "hdb", "models");
32
+ }
33
+ /**
34
+ * Get the lock file path for download synchronization.
35
+ */
36
+ function getLockFilePath(modelName) {
37
+ return path.join(getModelsDir(), `${modelName}.lock`);
38
+ }
39
+ /**
40
+ * Get the model URI for node-llama-cpp downloads.
41
+ */
42
+ function getModelUri(modelName) {
43
+ const config = MODEL_CONFIGS[modelName];
44
+ if (!config) {
45
+ throw new Error(`Unknown embedding model: ${modelName}. Supported: ${Object.keys(MODEL_CONFIGS).join(", ")}`);
46
+ }
47
+ return `hf:${config.repo}/${config.file}`;
48
+ }
49
+ /**
50
+ * Acquire a file-based lock for model download.
51
+ * Returns true if lock was acquired, false if another thread holds it.
52
+ */
53
+ async function acquireDownloadLock(modelName) {
54
+ const lockPath = getLockFilePath(modelName);
55
+ try {
56
+ if (existsSync(lockPath)) {
57
+ // Check if lock is stale (older than 10 minutes)
58
+ const lockContent = await readFile(lockPath, "utf-8");
59
+ const lockTime = parseInt(lockContent, 10);
60
+ if (!isNaN(lockTime) && Date.now() - lockTime < 10 * 60 * 1000) {
61
+ return false; // Lock is held and not stale
62
+ }
63
+ // Stale lock — remove and re-acquire
64
+ }
65
+ await writeFile(lockPath, String(Date.now()), { flag: "wx" }).catch(async () => {
66
+ // wx flag failed (file exists), try overwriting stale lock
67
+ await writeFile(lockPath, String(Date.now()));
68
+ });
69
+ return true;
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
75
+ /**
76
+ * Release the download lock.
77
+ */
78
+ async function releaseDownloadLock(modelName) {
79
+ const lockPath = getLockFilePath(modelName);
80
+ try {
81
+ await unlink(lockPath);
82
+ }
83
+ catch {
84
+ // Lock file already removed — safe to ignore
85
+ }
86
+ }
87
+ /**
88
+ * Wait for another thread's download to complete.
89
+ * Polls the lock file until it disappears or the model file appears.
90
+ */
91
+ async function waitForDownload(modelName, modelPath) {
92
+ const lockPath = getLockFilePath(modelName);
93
+ const maxWait = 10 * 60 * 1000; // 10 minutes
94
+ const pollInterval = 2000; // 2 seconds
95
+ const start = Date.now();
96
+ while (Date.now() - start < maxWait) {
97
+ if (existsSync(modelPath) && !existsSync(lockPath)) {
98
+ return; // Download completed
99
+ }
100
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
101
+ }
102
+ throw new Error(`Timed out waiting for model download: ${modelName}`);
103
+ }
104
+ /**
105
+ * Download the embedding model if not already present.
106
+ * Uses file-based locking so only one worker thread downloads.
107
+ */
108
+ async function downloadModelIfNeeded(modelName) {
109
+ const modelUri = getModelUri(modelName);
110
+ const modelsDir = getModelsDir();
111
+ // Ensure models directory exists
112
+ await mkdir(modelsDir, { recursive: true });
113
+ // Use node-llama-cpp to resolve the actual file path and download if needed.
114
+ // node-llama-cpp prefixes filenames (e.g., hf_nomic-ai_<file>.gguf),
115
+ // so we use its entrypointFilePath rather than guessing the name.
116
+ const { createModelDownloader } = (await import("node-llama-cpp"));
117
+ const downloader = await createModelDownloader({
118
+ modelUri,
119
+ dirPath: modelsDir,
120
+ skipExisting: true,
121
+ });
122
+ const modelPath = downloader.entrypointFilePath;
123
+ // Already downloaded
124
+ if (existsSync(modelPath)) {
125
+ return modelPath;
126
+ }
127
+ // Try to acquire download lock
128
+ const acquired = await acquireDownloadLock(modelName);
129
+ if (!acquired) {
130
+ // Another thread is downloading — wait for it
131
+ logger?.info?.(`Another thread is downloading ${modelName}, waiting...`);
132
+ await waitForDownload(modelName, modelPath);
133
+ return modelPath;
134
+ }
135
+ try {
136
+ logger?.info?.(`Downloading embedding model: ${modelName} from ${modelUri}`);
137
+ const resultPath = await downloader.download();
138
+ logger?.info?.(`Model ${modelName} downloaded successfully to ${resultPath}`);
139
+ return resultPath;
140
+ }
141
+ finally {
142
+ await releaseDownloadLock(modelName);
143
+ }
144
+ }
145
+ /**
146
+ * Initialize the embedding model.
147
+ * Downloads the model to ~/hdb/models/ if not present.
148
+ * Uses file-based locking so only one thread downloads.
149
+ *
150
+ * @param config - Plugin configuration with embeddingModel name
151
+ */
152
+ export async function initEmbeddingModel(config) {
153
+ const modelName = config.embeddingModel || "nomic-embed-text";
154
+ if (embeddingModel) {
155
+ logger?.debug?.("Embedding model already initialized");
156
+ return;
157
+ }
158
+ const modelPath = await downloadModelIfNeeded(modelName);
159
+ // Load the model with node-llama-cpp
160
+ const { getLlama } = (await import("node-llama-cpp"));
161
+ llama = await getLlama({ progressLogs: false });
162
+ embeddingModel = await llama.loadModel({ modelPath });
163
+ embeddingContext = await embeddingModel.createEmbeddingContext({
164
+ contextSize: "auto",
165
+ });
166
+ logger?.info?.(`Embedding model ${modelName} loaded successfully`);
167
+ }
168
+ /**
169
+ * Generate an embedding vector for the given text.
170
+ *
171
+ * @param text - Text to generate embedding for
172
+ * @returns Embedding vector as array of numbers
173
+ * @throws Error if the model has not been initialized
174
+ */
175
+ export async function generateEmbedding(text) {
176
+ if (!embeddingContext) {
177
+ throw new Error("Embedding model not initialized. Call initEmbeddingModel() first.");
178
+ }
179
+ const result = await embeddingContext.getEmbeddingFor(text);
180
+ return Array.from(result.vector);
181
+ }
182
+ /**
183
+ * Clean up embedding model resources.
184
+ */
185
+ export async function dispose() {
186
+ if (embeddingContext && !embeddingContext.disposed) {
187
+ await embeddingContext.dispose();
188
+ }
189
+ embeddingContext = null;
190
+ if (embeddingModel) {
191
+ await embeddingModel.dispose();
192
+ }
193
+ embeddingModel = null;
194
+ if (llama) {
195
+ await llama.dispose();
196
+ }
197
+ llama = null;
198
+ logger?.info?.("Embedding model disposed");
199
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Knowledge Entry Management
3
+ *
4
+ * CRUD operations for knowledge base entries. Handles embedding generation,
5
+ * tag synchronization, and relationship management.
6
+ */
7
+ import type { KnowledgeEntry, KnowledgeEntryInput, KnowledgeEntryUpdate } from "../types.ts";
8
+ /**
9
+ * Strip embedding vectors from an entry to keep responses compact.
10
+ * Embeddings are large float arrays not useful in API responses.
11
+ */
12
+ export declare function stripEmbedding<T extends {
13
+ embedding?: number[];
14
+ }>(entry: T): Omit<T, "embedding">;
15
+ /**
16
+ * Create a new knowledge entry.
17
+ *
18
+ * Generates an embedding from title + content, synchronizes tags,
19
+ * and stores the entry. A UUID is generated if no id is provided.
20
+ *
21
+ * @param data - Entry data to create
22
+ * @returns The created knowledge entry
23
+ */
24
+ export declare function createEntry(data: KnowledgeEntryInput): Promise<KnowledgeEntry>;
25
+ /**
26
+ * Get a knowledge entry by ID.
27
+ *
28
+ * @param id - Entry ID
29
+ * @returns The entry, or null if not found
30
+ */
31
+ export declare function getEntry(id: string): Promise<KnowledgeEntry | null>;
32
+ /**
33
+ * Update an existing knowledge entry.
34
+ *
35
+ * Merges the update data with the existing entry. If title or content changed,
36
+ * regenerates the embedding. Synchronizes tag counts if tags changed.
37
+ * Optionally logs the edit to the history table.
38
+ *
39
+ * @param id - ID of the entry to update
40
+ * @param data - Fields to update
41
+ * @param options - Optional edit tracking metadata
42
+ * @returns The updated entry
43
+ * @throws Error if the entry does not exist
44
+ */
45
+ export declare function updateEntry(id: string, data: KnowledgeEntryUpdate, options?: {
46
+ editedBy?: string;
47
+ editSummary?: string;
48
+ }): Promise<KnowledgeEntry>;
49
+ /**
50
+ * Mark an entry as deprecated.
51
+ *
52
+ * @param id - ID of the entry to deprecate
53
+ * @throws Error if the entry does not exist
54
+ */
55
+ export declare function deprecateEntry(id: string): Promise<void>;
56
+ /**
57
+ * Link a new entry as superseding an old entry.
58
+ *
59
+ * Sets newEntry.supersedesId = oldId and oldEntry.supersededById = newId.
60
+ *
61
+ * @param newId - ID of the new (superseding) entry
62
+ * @param oldId - ID of the old (superseded) entry
63
+ * @throws Error if either entry does not exist
64
+ */
65
+ export declare function linkSupersedes(newId: string, oldId: string): Promise<void>;
66
+ /**
67
+ * Link multiple entries as siblings.
68
+ *
69
+ * For each entry, adds all other entry IDs to its siblingIds array (deduplicated).
70
+ *
71
+ * @param ids - IDs of entries to link as siblings
72
+ * @throws Error if any entry does not exist
73
+ */
74
+ export declare function linkSiblings(ids: string[]): Promise<void>;
75
+ /**
76
+ * Link two entries as related.
77
+ *
78
+ * Adds relatedId to the entry's relatedIds array (deduplicated).
79
+ * This is a one-directional link; call twice for bidirectional.
80
+ *
81
+ * @param id - ID of the entry to add a related link to
82
+ * @param relatedId - ID of the related entry
83
+ * @throws Error if the entry does not exist
84
+ */
85
+ export declare function linkRelated(id: string, relatedId: string): Promise<void>;