wolverine-ai 1.0.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 (79) hide show
  1. package/PLATFORM.md +442 -0
  2. package/README.md +475 -0
  3. package/SERVER_BEST_PRACTICES.md +62 -0
  4. package/TELEMETRY.md +108 -0
  5. package/bin/wolverine.js +95 -0
  6. package/examples/01-basic-typo.js +31 -0
  7. package/examples/02-multi-file/routes/users.js +15 -0
  8. package/examples/02-multi-file/server.js +25 -0
  9. package/examples/03-syntax-error.js +23 -0
  10. package/examples/04-secret-leak.js +14 -0
  11. package/examples/05-expired-key.js +27 -0
  12. package/examples/06-json-config/config.json +13 -0
  13. package/examples/06-json-config/server.js +28 -0
  14. package/examples/07-rate-limit-loop.js +11 -0
  15. package/examples/08-sandbox-escape.js +20 -0
  16. package/examples/buggy-server.js +39 -0
  17. package/examples/demos/01-basic-typo/index.js +20 -0
  18. package/examples/demos/01-basic-typo/routes/api.js +13 -0
  19. package/examples/demos/01-basic-typo/routes/health.js +4 -0
  20. package/examples/demos/02-multi-file/index.js +24 -0
  21. package/examples/demos/02-multi-file/routes/api.js +13 -0
  22. package/examples/demos/02-multi-file/routes/health.js +4 -0
  23. package/examples/demos/03-syntax-error/index.js +18 -0
  24. package/examples/demos/04-secret-leak/index.js +16 -0
  25. package/examples/demos/05-expired-key/index.js +21 -0
  26. package/examples/demos/06-json-config/config.json +9 -0
  27. package/examples/demos/06-json-config/index.js +20 -0
  28. package/examples/demos/07-null-crash/index.js +16 -0
  29. package/examples/run-demo.js +110 -0
  30. package/package.json +67 -0
  31. package/server/config/settings.json +62 -0
  32. package/server/index.js +33 -0
  33. package/server/routes/api.js +12 -0
  34. package/server/routes/health.js +16 -0
  35. package/server/routes/time.js +12 -0
  36. package/src/agent/agent-engine.js +727 -0
  37. package/src/agent/goal-loop.js +140 -0
  38. package/src/agent/research-agent.js +120 -0
  39. package/src/agent/sub-agents.js +176 -0
  40. package/src/backup/backup-manager.js +321 -0
  41. package/src/brain/brain.js +315 -0
  42. package/src/brain/embedder.js +131 -0
  43. package/src/brain/function-map.js +263 -0
  44. package/src/brain/vector-store.js +267 -0
  45. package/src/core/ai-client.js +387 -0
  46. package/src/core/cluster-manager.js +144 -0
  47. package/src/core/config.js +89 -0
  48. package/src/core/error-parser.js +87 -0
  49. package/src/core/health-monitor.js +129 -0
  50. package/src/core/models.js +132 -0
  51. package/src/core/patcher.js +55 -0
  52. package/src/core/runner.js +464 -0
  53. package/src/core/system-info.js +141 -0
  54. package/src/core/verifier.js +146 -0
  55. package/src/core/wolverine.js +290 -0
  56. package/src/dashboard/server.js +1332 -0
  57. package/src/index.js +94 -0
  58. package/src/logger/event-logger.js +237 -0
  59. package/src/logger/pricing.js +96 -0
  60. package/src/logger/repair-history.js +109 -0
  61. package/src/logger/token-tracker.js +277 -0
  62. package/src/mcp/mcp-client.js +224 -0
  63. package/src/mcp/mcp-registry.js +228 -0
  64. package/src/mcp/mcp-security.js +152 -0
  65. package/src/monitor/perf-monitor.js +300 -0
  66. package/src/monitor/process-monitor.js +231 -0
  67. package/src/monitor/route-prober.js +191 -0
  68. package/src/notifications/notifier.js +227 -0
  69. package/src/platform/heartbeat.js +93 -0
  70. package/src/platform/queue.js +53 -0
  71. package/src/platform/register.js +64 -0
  72. package/src/platform/telemetry.js +76 -0
  73. package/src/security/admin-auth.js +150 -0
  74. package/src/security/injection-detector.js +174 -0
  75. package/src/security/rate-limiter.js +152 -0
  76. package/src/security/sandbox.js +128 -0
  77. package/src/security/secret-redactor.js +217 -0
  78. package/src/skills/skill-registry.js +129 -0
  79. package/src/skills/sql.js +375 -0
@@ -0,0 +1,131 @@
1
+ const { getClient, aiCall } = require("../core/ai-client");
2
+ const { getModel } = require("../core/models");
3
+
4
+ /**
5
+ * Embedder — converts text to vector embeddings using TEXT_EMBEDDING_MODEL.
6
+ *
7
+ * Optimized for speed:
8
+ * - Batch embedding support (multiple texts in one API call)
9
+ * - LRU cache for repeated queries
10
+ * - Dimensionality matches text-embedding-3-small (1536) by default
11
+ */
12
+
13
+ // Simple LRU cache for embedding results
14
+ const CACHE_SIZE = 500;
15
+ const _cache = new Map();
16
+
17
+ function _cacheGet(key) {
18
+ if (_cache.has(key)) {
19
+ const val = _cache.get(key);
20
+ // Move to end (most recently used)
21
+ _cache.delete(key);
22
+ _cache.set(key, val);
23
+ return val;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ function _cacheSet(key, val) {
29
+ if (_cache.size >= CACHE_SIZE) {
30
+ // Evict oldest (first entry)
31
+ const oldest = _cache.keys().next().value;
32
+ _cache.delete(oldest);
33
+ }
34
+ _cache.set(key, val);
35
+ }
36
+
37
+ /**
38
+ * Embed a single text string. Returns number[].
39
+ */
40
+ async function embed(text) {
41
+ const cached = _cacheGet(text);
42
+ if (cached) return cached;
43
+
44
+ const openai = getClient();
45
+ const model = getModel("embedding");
46
+
47
+ const response = await openai.embeddings.create({
48
+ model,
49
+ input: text,
50
+ });
51
+
52
+ const embedding = response.data[0].embedding;
53
+ _cacheSet(text, embedding);
54
+ return embedding;
55
+ }
56
+
57
+ /**
58
+ * Embed multiple texts in a single API call. Returns number[][].
59
+ * Much faster than calling embed() in a loop.
60
+ */
61
+ async function embedBatch(texts) {
62
+ if (texts.length === 0) return [];
63
+
64
+ // Check cache for all
65
+ const results = new Array(texts.length);
66
+ const uncached = [];
67
+ const uncachedIndices = [];
68
+
69
+ for (let i = 0; i < texts.length; i++) {
70
+ const cached = _cacheGet(texts[i]);
71
+ if (cached) {
72
+ results[i] = cached;
73
+ } else {
74
+ uncached.push(texts[i]);
75
+ uncachedIndices.push(i);
76
+ }
77
+ }
78
+
79
+ if (uncached.length === 0) return results;
80
+
81
+ const openai = getClient();
82
+ const model = getModel("embedding");
83
+
84
+ const response = await openai.embeddings.create({
85
+ model,
86
+ input: uncached,
87
+ });
88
+
89
+ // Sort by index to maintain order
90
+ const sorted = response.data.sort((a, b) => a.index - b.index);
91
+
92
+ for (let i = 0; i < sorted.length; i++) {
93
+ const embedding = sorted[i].embedding;
94
+ const originalIdx = uncachedIndices[i];
95
+ results[originalIdx] = embedding;
96
+ _cacheSet(uncached[i], embedding);
97
+ }
98
+
99
+ return results;
100
+ }
101
+
102
+ /**
103
+ * Compact text using UTILITY_MODEL before embedding.
104
+ * Reduces token count while preserving semantic meaning.
105
+ * This makes embeddings more focused and search more accurate.
106
+ */
107
+ async function compact(text) {
108
+ // Skip compaction for short texts
109
+ if (text.length < 200) return text;
110
+
111
+ const result = await aiCall({
112
+ model: getModel("utility"),
113
+ systemPrompt: "Compress the following text into a dense, semantically rich summary. Keep all technical terms, function names, file paths, and error messages. Remove filler words. Output ONLY the compressed text, nothing else.",
114
+ userPrompt: text,
115
+ maxTokens: 256,
116
+ category: "brain",
117
+ });
118
+
119
+ return result.content || text;
120
+ }
121
+
122
+ /**
123
+ * Full pipeline: compact → embed. Returns { compacted, embedding }.
124
+ */
125
+ async function compactAndEmbed(text) {
126
+ const compacted = await compact(text);
127
+ const embedding = await embed(compacted);
128
+ return { compacted, embedding };
129
+ }
130
+
131
+ module.exports = { embed, embedBatch, compact, compactAndEmbed };
@@ -0,0 +1,263 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Live Function Map — scans the server project on startup to build
6
+ * a complete map of routes, exports, functions, and modules.
7
+ *
8
+ * This gives the agent instant context about what the server IS and
9
+ * what it's capable of, without reading every file at heal time.
10
+ *
11
+ * Scans for:
12
+ * - HTTP route handlers (express, http, fastify, koa patterns)
13
+ * - Module exports (module.exports, exports.X)
14
+ * - Function declarations and arrow functions
15
+ * - Class definitions
16
+ * - Package.json dependencies
17
+ * - Config files (.env, .json, .yaml)
18
+ */
19
+
20
+ const SKIP_DIRS = new Set(["node_modules", ".wolverine", ".git", "dist", "build", "coverage", "src", "bin", "tests"]);
21
+ const CODE_EXTENSIONS = new Set([".js", ".ts", ".mjs", ".cjs", ".jsx", ".tsx"]);
22
+ const CONFIG_EXTENSIONS = new Set([".json", ".yaml", ".yml", ".toml", ".env"]);
23
+
24
+ /**
25
+ * Scan the project and return a structured function map.
26
+ */
27
+ function scanProject(projectRoot) {
28
+ const root = path.resolve(projectRoot);
29
+ const map = {
30
+ routes: [],
31
+ exports: [],
32
+ functions: [],
33
+ classes: [],
34
+ files: [],
35
+ configs: [],
36
+ dependencies: {},
37
+ summary: "",
38
+ };
39
+
40
+ // Scan package.json for dependencies
41
+ const pkgPath = path.join(root, "package.json");
42
+ if (fs.existsSync(pkgPath)) {
43
+ try {
44
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
45
+ map.dependencies = {
46
+ ...(pkg.dependencies || {}),
47
+ ...(pkg.devDependencies || {}),
48
+ };
49
+ } catch { /* skip */ }
50
+ }
51
+
52
+ // Recursive scan
53
+ _scanDir(root, root, map);
54
+
55
+ // Build summary
56
+ map.summary = _buildSummary(map);
57
+
58
+ return map;
59
+ }
60
+
61
+ function _scanDir(dir, root, map) {
62
+ let entries;
63
+ try {
64
+ entries = fs.readdirSync(dir, { withFileTypes: true });
65
+ } catch { return; }
66
+
67
+ for (const entry of entries) {
68
+ if (SKIP_DIRS.has(entry.name)) continue;
69
+ if (entry.name.startsWith(".") && entry.name !== ".env") continue;
70
+
71
+ const fullPath = path.join(dir, entry.name);
72
+ const relPath = path.relative(root, fullPath).replace(/\\/g, "/");
73
+
74
+ if (entry.isDirectory()) {
75
+ _scanDir(fullPath, root, map);
76
+ continue;
77
+ }
78
+
79
+ const ext = path.extname(entry.name);
80
+
81
+ if (CONFIG_EXTENSIONS.has(ext) || entry.name.startsWith(".env")) {
82
+ map.configs.push(relPath);
83
+ map.files.push({ path: relPath, type: "config" });
84
+ continue;
85
+ }
86
+
87
+ if (!CODE_EXTENSIONS.has(ext)) continue;
88
+
89
+ map.files.push({ path: relPath, type: "code" });
90
+
91
+ // Parse the file for patterns
92
+ let content;
93
+ try {
94
+ content = fs.readFileSync(fullPath, "utf-8");
95
+ } catch { continue; }
96
+
97
+ _extractRoutes(content, relPath, map);
98
+ _extractExports(content, relPath, map);
99
+ _extractFunctions(content, relPath, map);
100
+ _extractClasses(content, relPath, map);
101
+ }
102
+ }
103
+
104
+ function _extractRoutes(content, filePath, map) {
105
+ // Express-style routes: app.get('/path', ...), router.post('/path', ...)
106
+ const expressPattern = /(?:app|router)\.(get|post|put|delete|patch|use|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
107
+ let match;
108
+ while ((match = expressPattern.exec(content)) !== null) {
109
+ map.routes.push({ method: match[1].toUpperCase(), path: match[2], file: filePath });
110
+ }
111
+
112
+ // http.createServer with URL checks: req.url === '/path'
113
+ const httpPattern = /req\.url\s*===?\s*['"`]([^'"`]+)['"`]/gi;
114
+ while ((match = httpPattern.exec(content)) !== null) {
115
+ // Try to find method from nearby req.method check
116
+ const nearby = content.slice(Math.max(0, match.index - 200), match.index + 200);
117
+ const methodMatch = nearby.match(/req\.method\s*===?\s*['"`]([^'"`]+)['"`]/i);
118
+ map.routes.push({
119
+ method: methodMatch ? methodMatch[1].toUpperCase() : "*",
120
+ path: match[1],
121
+ file: filePath,
122
+ });
123
+ }
124
+
125
+ // Fastify-style: fastify.get('/path', ...)
126
+ const fastifyPattern = /fastify\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
127
+ while ((match = fastifyPattern.exec(content)) !== null) {
128
+ map.routes.push({ method: match[1].toUpperCase(), path: match[2], file: filePath });
129
+ }
130
+ }
131
+
132
+ function _extractExports(content, filePath, map) {
133
+ // module.exports = { ... } or module.exports = functionName
134
+ const moduleExport = content.match(/module\.exports\s*=\s*(?:\{([^}]+)\}|(\w+))/);
135
+ if (moduleExport) {
136
+ if (moduleExport[1]) {
137
+ // Object exports: { foo, bar, baz }
138
+ const names = moduleExport[1].split(",").map(s => s.trim().split(":")[0].trim()).filter(Boolean);
139
+ for (const name of names) {
140
+ map.exports.push({ name, file: filePath });
141
+ }
142
+ } else if (moduleExport[2]) {
143
+ map.exports.push({ name: moduleExport[2], file: filePath });
144
+ }
145
+ }
146
+
147
+ // exports.name = ...
148
+ const namedExports = content.matchAll(/exports\.(\w+)\s*=/g);
149
+ for (const m of namedExports) {
150
+ map.exports.push({ name: m[1], file: filePath });
151
+ }
152
+ }
153
+
154
+ function _extractFunctions(content, filePath, map) {
155
+ // function declarations
156
+ const funcDecls = content.matchAll(/(?:async\s+)?function\s+(\w+)\s*\(/g);
157
+ for (const m of funcDecls) {
158
+ map.functions.push({ name: m[0].includes("async") ? `async ${m[1]}` : m[1], file: filePath });
159
+ }
160
+
161
+ // Named arrow functions: const foo = (...) => or const foo = async (...) =>
162
+ const arrowFuncs = content.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/g);
163
+ for (const m of arrowFuncs) {
164
+ map.functions.push({ name: m[1], file: filePath });
165
+ }
166
+ }
167
+
168
+ function _extractClasses(content, filePath, map) {
169
+ const classDecls = content.matchAll(/class\s+(\w+)(?:\s+extends\s+(\w+))?\s*\{/g);
170
+ for (const m of classDecls) {
171
+ map.classes.push({ name: m[1], extends: m[2] || null, file: filePath });
172
+ }
173
+ }
174
+
175
+ function _buildSummary(map) {
176
+ const lines = [];
177
+ lines.push(`Project: ${map.files.length} files (${map.files.filter(f => f.type === "code").length} code, ${map.configs.length} config)`);
178
+ lines.push(`Dependencies: ${Object.keys(map.dependencies).length}`);
179
+
180
+ if (map.routes.length > 0) {
181
+ lines.push(`Routes (${map.routes.length}):`);
182
+ for (const r of map.routes.slice(0, 20)) {
183
+ lines.push(` ${r.method.padEnd(7)} ${r.path} → ${r.file}`);
184
+ }
185
+ if (map.routes.length > 20) lines.push(` ... and ${map.routes.length - 20} more`);
186
+ }
187
+
188
+ if (map.classes.length > 0) {
189
+ lines.push(`Classes: ${map.classes.map(c => c.name).join(", ")}`);
190
+ }
191
+
192
+ if (map.exports.length > 0) {
193
+ lines.push(`Exports (${map.exports.length}): ${map.exports.slice(0, 15).map(e => e.name).join(", ")}${map.exports.length > 15 ? "..." : ""}`);
194
+ }
195
+
196
+ if (map.functions.length > 0) {
197
+ lines.push(`Functions (${map.functions.length}): ${map.functions.slice(0, 15).map(f => f.name).join(", ")}${map.functions.length > 15 ? "..." : ""}`);
198
+ }
199
+
200
+ return lines.join("\n");
201
+ }
202
+
203
+ /**
204
+ * Convert the function map to embeddable text chunks.
205
+ * Each chunk is focused on one aspect for precise vector search.
206
+ */
207
+ function mapToChunks(map) {
208
+ const chunks = [];
209
+
210
+ // Routes chunk
211
+ if (map.routes.length > 0) {
212
+ chunks.push({
213
+ namespace: "functions",
214
+ text: `HTTP Routes:\n${map.routes.map(r => `${r.method} ${r.path} (${r.file})`).join("\n")}`,
215
+ metadata: { type: "routes", count: map.routes.length },
216
+ });
217
+ }
218
+
219
+ // Per-file function chunks
220
+ const byFile = {};
221
+ for (const fn of map.functions) {
222
+ if (!byFile[fn.file]) byFile[fn.file] = [];
223
+ byFile[fn.file].push(fn.name);
224
+ }
225
+ for (const [file, fns] of Object.entries(byFile)) {
226
+ chunks.push({
227
+ namespace: "functions",
228
+ text: `File ${file} functions: ${fns.join(", ")}`,
229
+ metadata: { type: "file-functions", file },
230
+ });
231
+ }
232
+
233
+ // Dependencies chunk
234
+ if (Object.keys(map.dependencies).length > 0) {
235
+ chunks.push({
236
+ namespace: "functions",
237
+ text: `Dependencies: ${Object.entries(map.dependencies).map(([k, v]) => `${k}@${v}`).join(", ")}`,
238
+ metadata: { type: "dependencies" },
239
+ });
240
+ }
241
+
242
+ // Classes chunk
243
+ if (map.classes.length > 0) {
244
+ chunks.push({
245
+ namespace: "functions",
246
+ text: `Classes: ${map.classes.map(c => `${c.name}${c.extends ? ` extends ${c.extends}` : ""} (${c.file})`).join(", ")}`,
247
+ metadata: { type: "classes" },
248
+ });
249
+ }
250
+
251
+ // Config files chunk
252
+ if (map.configs.length > 0) {
253
+ chunks.push({
254
+ namespace: "functions",
255
+ text: `Config files: ${map.configs.join(", ")}`,
256
+ metadata: { type: "configs" },
257
+ });
258
+ }
259
+
260
+ return chunks;
261
+ }
262
+
263
+ module.exports = { scanProject, mapToChunks };
@@ -0,0 +1,267 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * In-memory vector store with file persistence.
6
+ *
7
+ * Design priorities:
8
+ * 1. SPEED — everything in RAM, cosine similarity is just dot products
9
+ * 2. Persistence — saved to .wolverine/brain/vectors.bin for restart survival
10
+ * 3. No dependencies — pure JS, no external vector DB needed
11
+ *
12
+ * Storage: each entry is { id, namespace, text, metadata, embedding: Float32Array }
13
+ * Namespaces partition the store: "docs", "errors", "fixes", "functions", "learnings"
14
+ */
15
+
16
+ const BRAIN_DIR = ".wolverine/brain";
17
+ const STORE_FILE = "vectors.json";
18
+
19
+ class VectorStore {
20
+ constructor(projectRoot) {
21
+ this.projectRoot = path.resolve(projectRoot);
22
+ this.brainDir = path.join(this.projectRoot, BRAIN_DIR);
23
+ this.storePath = path.join(this.brainDir, STORE_FILE);
24
+
25
+ // In-memory entries: Map<id, Entry>
26
+ this._entries = new Map();
27
+ // Namespace index for fast filtered search: Map<namespace, Set<id>>
28
+ this._nsIndex = new Map();
29
+ // Auto-increment ID
30
+ this._nextId = 1;
31
+
32
+ this._ensureDir();
33
+ this._load();
34
+ }
35
+
36
+ /**
37
+ * Add an entry to the store. Returns the entry ID.
38
+ *
39
+ * @param {string} namespace - Category: "docs", "errors", "fixes", "functions", "learnings"
40
+ * @param {string} text - The compacted text (what gets searched against)
41
+ * @param {number[]} embedding - Float array from the embedding model
42
+ * @param {object} metadata - Arbitrary metadata (timestamps, file paths, etc.)
43
+ */
44
+ add(namespace, text, embedding, metadata = {}) {
45
+ const id = `${namespace}-${(this._nextId++).toString(36)}`;
46
+ const entry = {
47
+ id,
48
+ namespace,
49
+ text,
50
+ metadata: { ...metadata, createdAt: Date.now() },
51
+ embedding: new Float32Array(embedding),
52
+ };
53
+
54
+ this._entries.set(id, entry);
55
+
56
+ if (!this._nsIndex.has(namespace)) {
57
+ this._nsIndex.set(namespace, new Set());
58
+ }
59
+ this._nsIndex.get(namespace).add(id);
60
+
61
+ return id;
62
+ }
63
+
64
+ /**
65
+ * Semantic search — find the top-k most similar entries.
66
+ *
67
+ * @param {number[]} queryEmbedding - Embedding of the search query
68
+ * @param {object} options
69
+ * @param {number} options.topK - Max results (default: 5)
70
+ * @param {string} options.namespace - Filter to a specific namespace
71
+ * @param {number} options.minScore - Minimum similarity score (default: 0.3)
72
+ * @returns {Array<{ id, namespace, text, metadata, score }>}
73
+ */
74
+ search(queryEmbedding, { topK = 5, namespace, minScore = 0.3 } = {}) {
75
+ const queryVec = new Float32Array(queryEmbedding);
76
+ const results = [];
77
+
78
+ // Determine which entries to search
79
+ let entryIds;
80
+ if (namespace && this._nsIndex.has(namespace)) {
81
+ entryIds = this._nsIndex.get(namespace);
82
+ } else if (namespace) {
83
+ return []; // namespace doesn't exist
84
+ } else {
85
+ entryIds = this._entries.keys();
86
+ }
87
+
88
+ for (const id of entryIds) {
89
+ const entry = this._entries.get(id);
90
+ if (!entry) continue;
91
+
92
+ const score = cosineSimilarity(queryVec, entry.embedding);
93
+ if (score >= minScore) {
94
+ results.push({
95
+ id: entry.id,
96
+ namespace: entry.namespace,
97
+ text: entry.text,
98
+ metadata: entry.metadata,
99
+ score,
100
+ });
101
+ }
102
+ }
103
+
104
+ // Sort by score descending, take topK
105
+ results.sort((a, b) => b.score - a.score);
106
+ return results.slice(0, topK);
107
+ }
108
+
109
+ /**
110
+ * Fast keyword search — no embedding API call, instant.
111
+ * Tokenizes query and scores entries by keyword overlap.
112
+ * Use as first-pass before expensive semantic search.
113
+ */
114
+ keywordSearch(query, { topK = 5, namespace, minTokens = 2 } = {}) {
115
+ const tokens = query.toLowerCase()
116
+ .replace(/[^a-z0-9\s]/g, " ")
117
+ .split(/\s+/)
118
+ .filter(t => t.length > 2);
119
+
120
+ if (tokens.length === 0) return [];
121
+
122
+ const results = [];
123
+ let entryIds;
124
+ if (namespace && this._nsIndex.has(namespace)) {
125
+ entryIds = this._nsIndex.get(namespace);
126
+ } else {
127
+ entryIds = this._entries.keys();
128
+ }
129
+
130
+ for (const id of entryIds) {
131
+ const entry = this._entries.get(id);
132
+ if (!entry) continue;
133
+
134
+ const textLower = entry.text.toLowerCase();
135
+ let score = 0;
136
+ for (const token of tokens) {
137
+ if (textLower.includes(token)) score++;
138
+ }
139
+
140
+ if (score >= minTokens) {
141
+ results.push({
142
+ id: entry.id,
143
+ namespace: entry.namespace,
144
+ text: entry.text,
145
+ metadata: entry.metadata,
146
+ score: score / tokens.length, // normalize 0-1
147
+ });
148
+ }
149
+ }
150
+
151
+ results.sort((a, b) => b.score - a.score);
152
+ return results.slice(0, topK);
153
+ }
154
+
155
+ /**
156
+ * Get all entries in a namespace.
157
+ */
158
+ getNamespace(namespace) {
159
+ const ids = this._nsIndex.get(namespace);
160
+ if (!ids) return [];
161
+ return Array.from(ids).map(id => {
162
+ const e = this._entries.get(id);
163
+ return { id: e.id, namespace: e.namespace, text: e.text, metadata: e.metadata };
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Delete an entry by ID.
169
+ */
170
+ delete(id) {
171
+ const entry = this._entries.get(id);
172
+ if (!entry) return false;
173
+ this._entries.delete(id);
174
+ const nsSet = this._nsIndex.get(entry.namespace);
175
+ if (nsSet) nsSet.delete(id);
176
+ return true;
177
+ }
178
+
179
+ /**
180
+ * Get store stats.
181
+ */
182
+ getStats() {
183
+ const nsCounts = {};
184
+ for (const [ns, ids] of this._nsIndex) {
185
+ nsCounts[ns] = ids.size;
186
+ }
187
+ return { totalEntries: this._entries.size, namespaces: nsCounts };
188
+ }
189
+
190
+ /**
191
+ * Persist to disk. Call periodically or after batch operations.
192
+ */
193
+ save() {
194
+ const data = {
195
+ version: 1,
196
+ nextId: this._nextId,
197
+ entries: [],
198
+ };
199
+
200
+ for (const entry of this._entries.values()) {
201
+ data.entries.push({
202
+ id: entry.id,
203
+ namespace: entry.namespace,
204
+ text: entry.text,
205
+ metadata: entry.metadata,
206
+ embedding: Array.from(entry.embedding),
207
+ });
208
+ }
209
+
210
+ // Atomic write: write to temp file, then rename (prevents corruption on kill)
211
+ const tmpPath = this.storePath + ".tmp";
212
+ fs.writeFileSync(tmpPath, JSON.stringify(data), "utf-8");
213
+ fs.renameSync(tmpPath, this.storePath);
214
+ }
215
+
216
+ // -- Private --
217
+
218
+ _ensureDir() {
219
+ fs.mkdirSync(this.brainDir, { recursive: true });
220
+ }
221
+
222
+ _load() {
223
+ if (!fs.existsSync(this.storePath)) return;
224
+
225
+ try {
226
+ const data = JSON.parse(fs.readFileSync(this.storePath, "utf-8"));
227
+ this._nextId = data.nextId || 1;
228
+
229
+ for (const entry of data.entries) {
230
+ const stored = {
231
+ id: entry.id,
232
+ namespace: entry.namespace,
233
+ text: entry.text,
234
+ metadata: entry.metadata,
235
+ embedding: new Float32Array(entry.embedding),
236
+ };
237
+ this._entries.set(stored.id, stored);
238
+
239
+ if (!this._nsIndex.has(stored.namespace)) {
240
+ this._nsIndex.set(stored.namespace, new Set());
241
+ }
242
+ this._nsIndex.get(stored.namespace).add(stored.id);
243
+ }
244
+ } catch {
245
+ // Corrupt store — start fresh
246
+ this._entries.clear();
247
+ this._nsIndex.clear();
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Cosine similarity between two Float32Arrays.
254
+ * Returns value between -1 and 1 (higher = more similar).
255
+ */
256
+ function cosineSimilarity(a, b) {
257
+ let dot = 0, normA = 0, normB = 0;
258
+ for (let i = 0; i < a.length; i++) {
259
+ dot += a[i] * b[i];
260
+ normA += a[i] * a[i];
261
+ normB += b[i] * b[i];
262
+ }
263
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
264
+ return denom === 0 ? 0 : dot / denom;
265
+ }
266
+
267
+ module.exports = { VectorStore, cosineSimilarity };