@gettymade/roux 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.
- package/README.md +106 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +2736 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +339 -0
- package/dist/index.js +1425 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// node_modules/string-similarity/src/index.js
|
|
28
|
+
var require_src = __commonJS({
|
|
29
|
+
"node_modules/string-similarity/src/index.js"(exports, module) {
|
|
30
|
+
"use strict";
|
|
31
|
+
module.exports = {
|
|
32
|
+
compareTwoStrings,
|
|
33
|
+
findBestMatch
|
|
34
|
+
};
|
|
35
|
+
function compareTwoStrings(first, second) {
|
|
36
|
+
first = first.replace(/\s+/g, "");
|
|
37
|
+
second = second.replace(/\s+/g, "");
|
|
38
|
+
if (first === second) return 1;
|
|
39
|
+
if (first.length < 2 || second.length < 2) return 0;
|
|
40
|
+
let firstBigrams = /* @__PURE__ */ new Map();
|
|
41
|
+
for (let i = 0; i < first.length - 1; i++) {
|
|
42
|
+
const bigram = first.substring(i, i + 2);
|
|
43
|
+
const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1;
|
|
44
|
+
firstBigrams.set(bigram, count);
|
|
45
|
+
}
|
|
46
|
+
;
|
|
47
|
+
let intersectionSize = 0;
|
|
48
|
+
for (let i = 0; i < second.length - 1; i++) {
|
|
49
|
+
const bigram = second.substring(i, i + 2);
|
|
50
|
+
const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0;
|
|
51
|
+
if (count > 0) {
|
|
52
|
+
firstBigrams.set(bigram, count - 1);
|
|
53
|
+
intersectionSize++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return 2 * intersectionSize / (first.length + second.length - 2);
|
|
57
|
+
}
|
|
58
|
+
function findBestMatch(mainString, targetStrings) {
|
|
59
|
+
if (!areArgsValid(mainString, targetStrings)) throw new Error("Bad arguments: First argument should be a string, second should be an array of strings");
|
|
60
|
+
const ratings = [];
|
|
61
|
+
let bestMatchIndex = 0;
|
|
62
|
+
for (let i = 0; i < targetStrings.length; i++) {
|
|
63
|
+
const currentTargetString = targetStrings[i];
|
|
64
|
+
const currentRating = compareTwoStrings(mainString, currentTargetString);
|
|
65
|
+
ratings.push({ target: currentTargetString, rating: currentRating });
|
|
66
|
+
if (currentRating > ratings[bestMatchIndex].rating) {
|
|
67
|
+
bestMatchIndex = i;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const bestMatch = ratings[bestMatchIndex];
|
|
71
|
+
return { ratings, bestMatch, bestMatchIndex };
|
|
72
|
+
}
|
|
73
|
+
function areArgsValid(mainString, targetStrings) {
|
|
74
|
+
if (typeof mainString !== "string") return false;
|
|
75
|
+
if (!Array.isArray(targetStrings)) return false;
|
|
76
|
+
if (!targetStrings.length) return false;
|
|
77
|
+
if (targetStrings.find(function(s) {
|
|
78
|
+
return typeof s !== "string";
|
|
79
|
+
})) return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// src/types/node.ts
|
|
86
|
+
function isNode(value) {
|
|
87
|
+
if (typeof value !== "object" || value === null) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const obj = value;
|
|
91
|
+
return typeof obj["id"] === "string" && typeof obj["title"] === "string" && typeof obj["content"] === "string" && Array.isArray(obj["tags"]) && obj["tags"].every((t) => typeof t === "string") && Array.isArray(obj["outgoingLinks"]) && obj["outgoingLinks"].every((l) => typeof l === "string") && typeof obj["properties"] === "object" && obj["properties"] !== null;
|
|
92
|
+
}
|
|
93
|
+
function isSourceRef(value) {
|
|
94
|
+
if (typeof value !== "object" || value === null) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const obj = value;
|
|
98
|
+
const validTypes = ["file", "api", "manual"];
|
|
99
|
+
return typeof obj["type"] === "string" && validTypes.includes(obj["type"]) && (obj["path"] === void 0 || typeof obj["path"] === "string") && (obj["lastModified"] === void 0 || obj["lastModified"] instanceof Date);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/types/provider.ts
|
|
103
|
+
function isVectorProvider(value) {
|
|
104
|
+
if (value === null || typeof value !== "object") {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const obj = value;
|
|
108
|
+
return typeof obj.store === "function" && typeof obj.search === "function" && typeof obj.delete === "function" && typeof obj.getModel === "function" && typeof obj.hasEmbedding === "function";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/types/config.ts
|
|
112
|
+
var DEFAULT_CONFIG = {
|
|
113
|
+
source: {
|
|
114
|
+
path: ".",
|
|
115
|
+
include: ["*.md"],
|
|
116
|
+
exclude: []
|
|
117
|
+
},
|
|
118
|
+
cache: {
|
|
119
|
+
path: ".roux/"
|
|
120
|
+
},
|
|
121
|
+
system: {
|
|
122
|
+
onModelChange: "lazy"
|
|
123
|
+
},
|
|
124
|
+
providers: {
|
|
125
|
+
store: {
|
|
126
|
+
type: "docstore"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/providers/docstore/index.ts
|
|
132
|
+
import { readFile, writeFile, stat, readdir, mkdir, rm } from "fs/promises";
|
|
133
|
+
import { join as join3, relative, dirname, resolve } from "path";
|
|
134
|
+
import { watch } from "chokidar";
|
|
135
|
+
|
|
136
|
+
// src/providers/docstore/cache.ts
|
|
137
|
+
var import_string_similarity = __toESM(require_src(), 1);
|
|
138
|
+
import Database from "better-sqlite3";
|
|
139
|
+
import { join } from "path";
|
|
140
|
+
import { mkdirSync } from "fs";
|
|
141
|
+
var Cache = class {
|
|
142
|
+
db;
|
|
143
|
+
constructor(cacheDir) {
|
|
144
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
145
|
+
const dbPath = join(cacheDir, "cache.db");
|
|
146
|
+
this.db = new Database(dbPath);
|
|
147
|
+
this.db.pragma("journal_mode = WAL");
|
|
148
|
+
this.initSchema();
|
|
149
|
+
}
|
|
150
|
+
initSchema() {
|
|
151
|
+
this.db.exec(`
|
|
152
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
153
|
+
id TEXT PRIMARY KEY,
|
|
154
|
+
title TEXT,
|
|
155
|
+
content TEXT,
|
|
156
|
+
tags TEXT,
|
|
157
|
+
outgoing_links TEXT,
|
|
158
|
+
properties TEXT,
|
|
159
|
+
source_type TEXT,
|
|
160
|
+
source_path TEXT,
|
|
161
|
+
source_modified INTEGER
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
165
|
+
node_id TEXT PRIMARY KEY,
|
|
166
|
+
model TEXT,
|
|
167
|
+
vector BLOB,
|
|
168
|
+
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
CREATE TABLE IF NOT EXISTS centrality (
|
|
172
|
+
node_id TEXT PRIMARY KEY,
|
|
173
|
+
pagerank REAL,
|
|
174
|
+
in_degree INTEGER,
|
|
175
|
+
out_degree INTEGER,
|
|
176
|
+
computed_at INTEGER,
|
|
177
|
+
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_source_path ON nodes(source_path);
|
|
181
|
+
`);
|
|
182
|
+
this.db.pragma("foreign_keys = ON");
|
|
183
|
+
}
|
|
184
|
+
getTableNames() {
|
|
185
|
+
const rows = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
186
|
+
return rows.map((r) => r.name);
|
|
187
|
+
}
|
|
188
|
+
upsertNode(node, sourceType, sourcePath, sourceModified) {
|
|
189
|
+
const stmt = this.db.prepare(`
|
|
190
|
+
INSERT INTO nodes (id, title, content, tags, outgoing_links, properties, source_type, source_path, source_modified)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
192
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
193
|
+
title = excluded.title,
|
|
194
|
+
content = excluded.content,
|
|
195
|
+
tags = excluded.tags,
|
|
196
|
+
outgoing_links = excluded.outgoing_links,
|
|
197
|
+
properties = excluded.properties,
|
|
198
|
+
source_type = excluded.source_type,
|
|
199
|
+
source_path = excluded.source_path,
|
|
200
|
+
source_modified = excluded.source_modified
|
|
201
|
+
`);
|
|
202
|
+
stmt.run(
|
|
203
|
+
node.id,
|
|
204
|
+
node.title,
|
|
205
|
+
node.content,
|
|
206
|
+
JSON.stringify(node.tags),
|
|
207
|
+
JSON.stringify(node.outgoingLinks),
|
|
208
|
+
JSON.stringify(node.properties),
|
|
209
|
+
sourceType,
|
|
210
|
+
sourcePath,
|
|
211
|
+
sourceModified
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
getNode(id) {
|
|
215
|
+
const row = this.db.prepare("SELECT * FROM nodes WHERE id = ?").get(id);
|
|
216
|
+
if (!row) return null;
|
|
217
|
+
return this.rowToNode(row);
|
|
218
|
+
}
|
|
219
|
+
getNodes(ids) {
|
|
220
|
+
if (ids.length === 0) return [];
|
|
221
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
222
|
+
const rows = this.db.prepare(`SELECT * FROM nodes WHERE id IN (${placeholders})`).all(...ids);
|
|
223
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
224
|
+
for (const row of rows) {
|
|
225
|
+
nodeMap.set(row.id, this.rowToNode(row));
|
|
226
|
+
}
|
|
227
|
+
const result = [];
|
|
228
|
+
for (const id of ids) {
|
|
229
|
+
const node = nodeMap.get(id);
|
|
230
|
+
if (node) result.push(node);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
deleteNode(id) {
|
|
235
|
+
this.db.prepare("DELETE FROM nodes WHERE id = ?").run(id);
|
|
236
|
+
}
|
|
237
|
+
getAllNodes() {
|
|
238
|
+
const rows = this.db.prepare("SELECT * FROM nodes").all();
|
|
239
|
+
return rows.map((row) => this.rowToNode(row));
|
|
240
|
+
}
|
|
241
|
+
searchByTags(tags, mode) {
|
|
242
|
+
if (tags.length === 0) return [];
|
|
243
|
+
const allNodes = this.getAllNodes();
|
|
244
|
+
const lowerTags = tags.map((t) => t.toLowerCase());
|
|
245
|
+
return allNodes.filter((node) => {
|
|
246
|
+
const nodeTags = node.tags.map((t) => t.toLowerCase());
|
|
247
|
+
if (mode === "any") {
|
|
248
|
+
return lowerTags.some((t) => nodeTags.includes(t));
|
|
249
|
+
} else {
|
|
250
|
+
return lowerTags.every((t) => nodeTags.includes(t));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
getModifiedTime(sourcePath) {
|
|
255
|
+
const row = this.db.prepare("SELECT source_modified FROM nodes WHERE source_path = ?").get(sourcePath);
|
|
256
|
+
return row?.source_modified ?? null;
|
|
257
|
+
}
|
|
258
|
+
getNodeByPath(sourcePath) {
|
|
259
|
+
const row = this.db.prepare("SELECT * FROM nodes WHERE source_path = ?").get(sourcePath);
|
|
260
|
+
if (!row) return null;
|
|
261
|
+
return this.rowToNode(row);
|
|
262
|
+
}
|
|
263
|
+
getAllTrackedPaths() {
|
|
264
|
+
const rows = this.db.prepare("SELECT source_path FROM nodes").all();
|
|
265
|
+
return new Set(rows.map((r) => r.source_path));
|
|
266
|
+
}
|
|
267
|
+
resolveTitles(ids) {
|
|
268
|
+
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
|
269
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
270
|
+
const rows = this.db.prepare(`SELECT id, title FROM nodes WHERE id IN (${placeholders})`).all(...ids);
|
|
271
|
+
const result = /* @__PURE__ */ new Map();
|
|
272
|
+
for (const row of rows) {
|
|
273
|
+
result.set(row.id, row.title);
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
nodesExist(ids) {
|
|
278
|
+
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
|
279
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
280
|
+
const rows = this.db.prepare(`SELECT id FROM nodes WHERE id IN (${placeholders})`).all(...ids);
|
|
281
|
+
const existingIds = new Set(rows.map((r) => r.id));
|
|
282
|
+
const result = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const id of ids) {
|
|
284
|
+
result.set(id, existingIds.has(id));
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
listNodes(filter, options) {
|
|
289
|
+
const limit = Math.min(options?.limit ?? 100, 1e3);
|
|
290
|
+
const offset = options?.offset ?? 0;
|
|
291
|
+
const conditions = [];
|
|
292
|
+
const params = [];
|
|
293
|
+
if (filter.tag) {
|
|
294
|
+
conditions.push("EXISTS (SELECT 1 FROM json_each(tags) WHERE LOWER(json_each.value) = LOWER(?))");
|
|
295
|
+
params.push(filter.tag);
|
|
296
|
+
}
|
|
297
|
+
if (filter.path) {
|
|
298
|
+
conditions.push("id LIKE ? || '%'");
|
|
299
|
+
params.push(filter.path);
|
|
300
|
+
}
|
|
301
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
302
|
+
const countQuery = `SELECT COUNT(*) as count FROM nodes ${whereClause}`;
|
|
303
|
+
const countRow = this.db.prepare(countQuery).get(...params);
|
|
304
|
+
const total = countRow.count;
|
|
305
|
+
const query = `SELECT id, title FROM nodes ${whereClause} LIMIT ? OFFSET ?`;
|
|
306
|
+
const rows = this.db.prepare(query).all(...params, limit, offset);
|
|
307
|
+
const nodes = rows.map((row) => ({ id: row.id, title: row.title }));
|
|
308
|
+
return { nodes, total };
|
|
309
|
+
}
|
|
310
|
+
resolveNodes(names, options) {
|
|
311
|
+
if (names.length === 0) return [];
|
|
312
|
+
const strategy = options?.strategy ?? "fuzzy";
|
|
313
|
+
const threshold = options?.threshold ?? 0.7;
|
|
314
|
+
const filter = {};
|
|
315
|
+
if (options?.tag) filter.tag = options.tag;
|
|
316
|
+
if (options?.path) filter.path = options.path;
|
|
317
|
+
const { nodes: candidates } = this.listNodes(filter, { limit: 1e3 });
|
|
318
|
+
if (candidates.length === 0) {
|
|
319
|
+
return names.map((query) => ({ query, match: null, score: 0 }));
|
|
320
|
+
}
|
|
321
|
+
const candidateTitles = candidates.map((c) => c.title.toLowerCase());
|
|
322
|
+
const titleToId = /* @__PURE__ */ new Map();
|
|
323
|
+
for (const c of candidates) {
|
|
324
|
+
titleToId.set(c.title.toLowerCase(), c.id);
|
|
325
|
+
}
|
|
326
|
+
return names.map((query) => {
|
|
327
|
+
const queryLower = query.toLowerCase();
|
|
328
|
+
if (strategy === "exact") {
|
|
329
|
+
const matchedId = titleToId.get(queryLower);
|
|
330
|
+
if (matchedId) {
|
|
331
|
+
return { query, match: matchedId, score: 1 };
|
|
332
|
+
}
|
|
333
|
+
return { query, match: null, score: 0 };
|
|
334
|
+
}
|
|
335
|
+
if (strategy === "fuzzy") {
|
|
336
|
+
const result = import_string_similarity.default.findBestMatch(queryLower, candidateTitles);
|
|
337
|
+
const bestMatch = result.bestMatch;
|
|
338
|
+
if (bestMatch.rating >= threshold) {
|
|
339
|
+
const matchedId = titleToId.get(bestMatch.target);
|
|
340
|
+
return { query, match: matchedId, score: bestMatch.rating };
|
|
341
|
+
}
|
|
342
|
+
return { query, match: null, score: 0 };
|
|
343
|
+
}
|
|
344
|
+
return { query, match: null, score: 0 };
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
updateOutgoingLinks(nodeId, links) {
|
|
348
|
+
this.db.prepare("UPDATE nodes SET outgoing_links = ? WHERE id = ?").run(JSON.stringify(links), nodeId);
|
|
349
|
+
}
|
|
350
|
+
storeEmbedding(nodeId, vector, model) {
|
|
351
|
+
const buffer = Buffer.from(new Float32Array(vector).buffer);
|
|
352
|
+
this.db.prepare(
|
|
353
|
+
`
|
|
354
|
+
INSERT INTO embeddings (node_id, model, vector)
|
|
355
|
+
VALUES (?, ?, ?)
|
|
356
|
+
ON CONFLICT(node_id) DO UPDATE SET
|
|
357
|
+
model = excluded.model,
|
|
358
|
+
vector = excluded.vector
|
|
359
|
+
`
|
|
360
|
+
).run(nodeId, model, buffer);
|
|
361
|
+
}
|
|
362
|
+
getEmbedding(nodeId) {
|
|
363
|
+
const row = this.db.prepare("SELECT model, vector FROM embeddings WHERE node_id = ?").get(nodeId);
|
|
364
|
+
if (!row) return null;
|
|
365
|
+
const float32 = new Float32Array(
|
|
366
|
+
row.vector.buffer,
|
|
367
|
+
row.vector.byteOffset,
|
|
368
|
+
row.vector.length / 4
|
|
369
|
+
);
|
|
370
|
+
return {
|
|
371
|
+
model: row.model,
|
|
372
|
+
vector: Array.from(float32)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
storeCentrality(nodeId, pagerank, inDegree, outDegree, computedAt) {
|
|
376
|
+
this.db.prepare(
|
|
377
|
+
`
|
|
378
|
+
INSERT INTO centrality (node_id, pagerank, in_degree, out_degree, computed_at)
|
|
379
|
+
VALUES (?, ?, ?, ?, ?)
|
|
380
|
+
ON CONFLICT(node_id) DO UPDATE SET
|
|
381
|
+
pagerank = excluded.pagerank,
|
|
382
|
+
in_degree = excluded.in_degree,
|
|
383
|
+
out_degree = excluded.out_degree,
|
|
384
|
+
computed_at = excluded.computed_at
|
|
385
|
+
`
|
|
386
|
+
).run(nodeId, pagerank, inDegree, outDegree, computedAt);
|
|
387
|
+
}
|
|
388
|
+
getCentrality(nodeId) {
|
|
389
|
+
const row = this.db.prepare("SELECT * FROM centrality WHERE node_id = ?").get(nodeId);
|
|
390
|
+
if (!row) return null;
|
|
391
|
+
return {
|
|
392
|
+
pagerank: row.pagerank,
|
|
393
|
+
inDegree: row.in_degree,
|
|
394
|
+
outDegree: row.out_degree,
|
|
395
|
+
computedAt: row.computed_at
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
getStats() {
|
|
399
|
+
const nodeCount = this.db.prepare("SELECT COUNT(*) as count FROM nodes").get();
|
|
400
|
+
const embeddingCount = this.db.prepare("SELECT COUNT(*) as count FROM embeddings").get();
|
|
401
|
+
const edgeSum = this.db.prepare("SELECT SUM(in_degree) as total FROM centrality").get();
|
|
402
|
+
return {
|
|
403
|
+
nodeCount: nodeCount.count,
|
|
404
|
+
embeddingCount: embeddingCount.count,
|
|
405
|
+
edgeCount: edgeSum.total ?? 0
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
clear() {
|
|
409
|
+
this.db.exec("DELETE FROM centrality");
|
|
410
|
+
this.db.exec("DELETE FROM embeddings");
|
|
411
|
+
this.db.exec("DELETE FROM nodes");
|
|
412
|
+
}
|
|
413
|
+
close() {
|
|
414
|
+
this.db.close();
|
|
415
|
+
}
|
|
416
|
+
rowToNode(row) {
|
|
417
|
+
const sourceRef = {
|
|
418
|
+
type: row.source_type,
|
|
419
|
+
path: row.source_path,
|
|
420
|
+
lastModified: new Date(row.source_modified)
|
|
421
|
+
};
|
|
422
|
+
return {
|
|
423
|
+
id: row.id,
|
|
424
|
+
title: row.title,
|
|
425
|
+
content: row.content,
|
|
426
|
+
tags: JSON.parse(row.tags),
|
|
427
|
+
outgoingLinks: JSON.parse(row.outgoing_links),
|
|
428
|
+
properties: JSON.parse(row.properties),
|
|
429
|
+
sourceRef
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// src/providers/vector/sqlite.ts
|
|
435
|
+
import Database2 from "better-sqlite3";
|
|
436
|
+
import { join as join2 } from "path";
|
|
437
|
+
var SqliteVectorProvider = class {
|
|
438
|
+
db;
|
|
439
|
+
ownsDb;
|
|
440
|
+
constructor(pathOrDb) {
|
|
441
|
+
if (typeof pathOrDb === "string") {
|
|
442
|
+
this.db = new Database2(join2(pathOrDb, "vectors.db"));
|
|
443
|
+
this.ownsDb = true;
|
|
444
|
+
} else {
|
|
445
|
+
this.db = pathOrDb;
|
|
446
|
+
this.ownsDb = false;
|
|
447
|
+
}
|
|
448
|
+
this.init();
|
|
449
|
+
}
|
|
450
|
+
init() {
|
|
451
|
+
this.db.exec(`
|
|
452
|
+
CREATE TABLE IF NOT EXISTS vectors (
|
|
453
|
+
id TEXT PRIMARY KEY,
|
|
454
|
+
model TEXT NOT NULL,
|
|
455
|
+
vector BLOB NOT NULL
|
|
456
|
+
)
|
|
457
|
+
`);
|
|
458
|
+
}
|
|
459
|
+
async store(id, vector, model) {
|
|
460
|
+
if (vector.length === 0) {
|
|
461
|
+
throw new Error("Cannot store empty vector");
|
|
462
|
+
}
|
|
463
|
+
for (const v of vector) {
|
|
464
|
+
if (!Number.isFinite(v)) {
|
|
465
|
+
throw new Error(`Invalid vector value: ${v}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const existing = this.db.prepare("SELECT LENGTH(vector) / 4 as dim FROM vectors WHERE id != ? LIMIT 1").get(id);
|
|
469
|
+
if (existing && existing.dim !== vector.length) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`Dimension mismatch: cannot store ${vector.length}-dim vector, existing vectors have ${existing.dim} dimensions`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const blob = Buffer.from(new Float32Array(vector).buffer);
|
|
475
|
+
this.db.prepare(
|
|
476
|
+
`INSERT OR REPLACE INTO vectors (id, model, vector) VALUES (?, ?, ?)`
|
|
477
|
+
).run(id, model, blob);
|
|
478
|
+
}
|
|
479
|
+
async search(vector, limit) {
|
|
480
|
+
if (vector.length === 0) {
|
|
481
|
+
throw new Error("Cannot search with empty vector");
|
|
482
|
+
}
|
|
483
|
+
for (const v of vector) {
|
|
484
|
+
if (!Number.isFinite(v)) {
|
|
485
|
+
throw new Error(`Invalid vector value: ${v}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (limit <= 0) {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
const rows = this.db.prepare("SELECT id, vector FROM vectors").all();
|
|
492
|
+
if (rows.length === 0) {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
const firstStoredDim = rows[0].vector.byteLength / 4;
|
|
496
|
+
if (vector.length !== firstStoredDim) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`Dimension mismatch: query has ${vector.length} dimensions, stored vectors have ${firstStoredDim}`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const queryVec = new Float32Array(vector);
|
|
502
|
+
const results = [];
|
|
503
|
+
for (const row of rows) {
|
|
504
|
+
const storedVec = new Float32Array(
|
|
505
|
+
row.vector.buffer,
|
|
506
|
+
row.vector.byteOffset,
|
|
507
|
+
row.vector.byteLength / 4
|
|
508
|
+
);
|
|
509
|
+
const distance = cosineDistance(queryVec, storedVec);
|
|
510
|
+
results.push({ id: row.id, distance });
|
|
511
|
+
}
|
|
512
|
+
results.sort((a, b) => a.distance - b.distance);
|
|
513
|
+
return results.slice(0, limit);
|
|
514
|
+
}
|
|
515
|
+
async delete(id) {
|
|
516
|
+
this.db.prepare("DELETE FROM vectors WHERE id = ?").run(id);
|
|
517
|
+
}
|
|
518
|
+
async getModel(id) {
|
|
519
|
+
const row = this.db.prepare("SELECT model FROM vectors WHERE id = ?").get(id);
|
|
520
|
+
return row?.model ?? null;
|
|
521
|
+
}
|
|
522
|
+
hasEmbedding(id) {
|
|
523
|
+
const row = this.db.prepare("SELECT 1 FROM vectors WHERE id = ?").get(id);
|
|
524
|
+
return row !== void 0;
|
|
525
|
+
}
|
|
526
|
+
/** For testing: get table names */
|
|
527
|
+
getTableNames() {
|
|
528
|
+
const rows = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
529
|
+
return rows.map((r) => r.name);
|
|
530
|
+
}
|
|
531
|
+
/** For testing: get vector blob size */
|
|
532
|
+
getVectorBlobSize(id) {
|
|
533
|
+
const row = this.db.prepare("SELECT LENGTH(vector) as size FROM vectors WHERE id = ?").get(id);
|
|
534
|
+
return row?.size ?? null;
|
|
535
|
+
}
|
|
536
|
+
/** Get total number of stored embeddings */
|
|
537
|
+
getEmbeddingCount() {
|
|
538
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM vectors").get();
|
|
539
|
+
return row.count;
|
|
540
|
+
}
|
|
541
|
+
close() {
|
|
542
|
+
if (this.ownsDb) {
|
|
543
|
+
this.db.close();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
function cosineDistance(a, b) {
|
|
548
|
+
let dotProduct = 0;
|
|
549
|
+
let magnitudeA = 0;
|
|
550
|
+
let magnitudeB = 0;
|
|
551
|
+
for (let i = 0; i < a.length; i++) {
|
|
552
|
+
dotProduct += a[i] * b[i];
|
|
553
|
+
magnitudeA += a[i] * a[i];
|
|
554
|
+
magnitudeB += b[i] * b[i];
|
|
555
|
+
}
|
|
556
|
+
magnitudeA = Math.sqrt(magnitudeA);
|
|
557
|
+
magnitudeB = Math.sqrt(magnitudeB);
|
|
558
|
+
if (magnitudeA === 0 || magnitudeB === 0) {
|
|
559
|
+
return 1;
|
|
560
|
+
}
|
|
561
|
+
const similarity = dotProduct / (magnitudeA * magnitudeB);
|
|
562
|
+
return 1 - similarity;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/providers/docstore/parser.ts
|
|
566
|
+
import matter from "gray-matter";
|
|
567
|
+
function parseMarkdown(raw) {
|
|
568
|
+
let parsed;
|
|
569
|
+
try {
|
|
570
|
+
parsed = matter(raw);
|
|
571
|
+
} catch {
|
|
572
|
+
return {
|
|
573
|
+
title: void 0,
|
|
574
|
+
tags: [],
|
|
575
|
+
properties: {},
|
|
576
|
+
content: raw
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const data = parsed.data;
|
|
580
|
+
const title = typeof data["title"] === "string" ? data["title"] : void 0;
|
|
581
|
+
let tags = [];
|
|
582
|
+
if (Array.isArray(data["tags"])) {
|
|
583
|
+
tags = data["tags"].filter((t) => typeof t === "string");
|
|
584
|
+
}
|
|
585
|
+
const properties = {};
|
|
586
|
+
for (const [key, value] of Object.entries(data)) {
|
|
587
|
+
if (key !== "title" && key !== "tags") {
|
|
588
|
+
properties[key] = value;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
title,
|
|
593
|
+
tags,
|
|
594
|
+
properties,
|
|
595
|
+
content: parsed.content.trim()
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function extractWikiLinks(content) {
|
|
599
|
+
const withoutCodeBlocks = content.replace(/```[\s\S]*?```/g, "");
|
|
600
|
+
const withoutInlineCode = withoutCodeBlocks.replace(/`[^`]+`/g, "");
|
|
601
|
+
const linkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
602
|
+
const seen = /* @__PURE__ */ new Set();
|
|
603
|
+
const links = [];
|
|
604
|
+
let match;
|
|
605
|
+
while ((match = linkRegex.exec(withoutInlineCode)) !== null) {
|
|
606
|
+
const target = match[1]?.trim();
|
|
607
|
+
if (target && !seen.has(target)) {
|
|
608
|
+
seen.add(target);
|
|
609
|
+
links.push(target);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return links;
|
|
613
|
+
}
|
|
614
|
+
function normalizeId(path) {
|
|
615
|
+
return path.toLowerCase().replace(/\\/g, "/");
|
|
616
|
+
}
|
|
617
|
+
function titleFromPath(path) {
|
|
618
|
+
const parts = path.split(/[/\\]/);
|
|
619
|
+
const filename = parts.at(-1);
|
|
620
|
+
const withoutExt = filename.replace(/\.[^.]+$/, "");
|
|
621
|
+
const spaced = withoutExt.replace(/[-_]+/g, " ").toLowerCase();
|
|
622
|
+
return spaced.split(" ").filter((w) => w.length > 0).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
623
|
+
}
|
|
624
|
+
function serializeToMarkdown(parsed) {
|
|
625
|
+
const hasFrontmatter = parsed.title !== void 0 || parsed.tags.length > 0 || Object.keys(parsed.properties).length > 0;
|
|
626
|
+
if (!hasFrontmatter) {
|
|
627
|
+
return parsed.content;
|
|
628
|
+
}
|
|
629
|
+
const frontmatter = {};
|
|
630
|
+
if (parsed.title !== void 0) {
|
|
631
|
+
frontmatter["title"] = parsed.title;
|
|
632
|
+
}
|
|
633
|
+
if (parsed.tags.length > 0) {
|
|
634
|
+
frontmatter["tags"] = parsed.tags;
|
|
635
|
+
}
|
|
636
|
+
for (const [key, value] of Object.entries(parsed.properties)) {
|
|
637
|
+
frontmatter[key] = value;
|
|
638
|
+
}
|
|
639
|
+
return matter.stringify(parsed.content, frontmatter);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/graph/builder.ts
|
|
643
|
+
import { DirectedGraph } from "graphology";
|
|
644
|
+
function buildGraph(nodes) {
|
|
645
|
+
const graph = new DirectedGraph();
|
|
646
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
647
|
+
for (const node of nodes) {
|
|
648
|
+
graph.addNode(node.id);
|
|
649
|
+
nodeIds.add(node.id);
|
|
650
|
+
}
|
|
651
|
+
for (const node of nodes) {
|
|
652
|
+
const seen = /* @__PURE__ */ new Set();
|
|
653
|
+
for (const target of node.outgoingLinks) {
|
|
654
|
+
if (!nodeIds.has(target) || seen.has(target)) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
seen.add(target);
|
|
658
|
+
graph.addDirectedEdge(node.id, target);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return graph;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/graph/operations.ts
|
|
665
|
+
import { bidirectional } from "graphology-shortest-path";
|
|
666
|
+
function getNeighborIds(graph, id, options) {
|
|
667
|
+
if (!graph.hasNode(id)) {
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
let neighbors;
|
|
671
|
+
switch (options.direction) {
|
|
672
|
+
case "in":
|
|
673
|
+
neighbors = graph.inNeighbors(id);
|
|
674
|
+
break;
|
|
675
|
+
case "out":
|
|
676
|
+
neighbors = graph.outNeighbors(id);
|
|
677
|
+
break;
|
|
678
|
+
case "both":
|
|
679
|
+
neighbors = graph.neighbors(id);
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
if (options.limit !== void 0) {
|
|
683
|
+
if (options.limit <= 0) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
if (options.limit < neighbors.length) {
|
|
687
|
+
return neighbors.slice(0, options.limit);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return neighbors;
|
|
691
|
+
}
|
|
692
|
+
function findPath(graph, source, target) {
|
|
693
|
+
if (!graph.hasNode(source) || !graph.hasNode(target)) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
if (source === target) {
|
|
697
|
+
return [source];
|
|
698
|
+
}
|
|
699
|
+
const path = bidirectional(graph, source, target);
|
|
700
|
+
return path;
|
|
701
|
+
}
|
|
702
|
+
function getHubs(graph, metric, limit) {
|
|
703
|
+
if (limit <= 0) {
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
const scores = [];
|
|
707
|
+
graph.forEachNode((id) => {
|
|
708
|
+
let score;
|
|
709
|
+
switch (metric) {
|
|
710
|
+
case "in_degree":
|
|
711
|
+
score = graph.inDegree(id);
|
|
712
|
+
break;
|
|
713
|
+
case "out_degree":
|
|
714
|
+
score = graph.outDegree(id);
|
|
715
|
+
break;
|
|
716
|
+
case "pagerank":
|
|
717
|
+
score = graph.inDegree(id);
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
scores.push([id, score]);
|
|
721
|
+
});
|
|
722
|
+
scores.sort((a, b) => b[1] - a[1]);
|
|
723
|
+
return scores.slice(0, limit);
|
|
724
|
+
}
|
|
725
|
+
function computeCentrality(graph) {
|
|
726
|
+
const result = /* @__PURE__ */ new Map();
|
|
727
|
+
graph.forEachNode((id) => {
|
|
728
|
+
result.set(id, {
|
|
729
|
+
inDegree: graph.inDegree(id),
|
|
730
|
+
outDegree: graph.outDegree(id)
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/providers/docstore/index.ts
|
|
737
|
+
var DocStore = class _DocStore {
|
|
738
|
+
cache;
|
|
739
|
+
sourceRoot;
|
|
740
|
+
graph = null;
|
|
741
|
+
vectorProvider;
|
|
742
|
+
ownsVectorProvider;
|
|
743
|
+
watcher = null;
|
|
744
|
+
debounceTimer = null;
|
|
745
|
+
pendingChanges = /* @__PURE__ */ new Map();
|
|
746
|
+
onChangeCallback;
|
|
747
|
+
constructor(sourceRoot, cacheDir, vectorProvider) {
|
|
748
|
+
this.sourceRoot = sourceRoot;
|
|
749
|
+
this.cache = new Cache(cacheDir);
|
|
750
|
+
this.ownsVectorProvider = !vectorProvider;
|
|
751
|
+
this.vectorProvider = vectorProvider ?? new SqliteVectorProvider(cacheDir);
|
|
752
|
+
}
|
|
753
|
+
async sync() {
|
|
754
|
+
const currentPaths = await this.collectMarkdownFiles(this.sourceRoot);
|
|
755
|
+
const trackedPaths = this.cache.getAllTrackedPaths();
|
|
756
|
+
for (const filePath of currentPaths) {
|
|
757
|
+
try {
|
|
758
|
+
const mtime = await this.getFileMtime(filePath);
|
|
759
|
+
const cachedMtime = this.cache.getModifiedTime(filePath);
|
|
760
|
+
if (cachedMtime === null || mtime > cachedMtime) {
|
|
761
|
+
const node = await this.fileToNode(filePath);
|
|
762
|
+
this.cache.upsertNode(node, "file", filePath, mtime);
|
|
763
|
+
}
|
|
764
|
+
} catch (err) {
|
|
765
|
+
if (err.code === "ENOENT") {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
throw err;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const currentSet = new Set(currentPaths);
|
|
772
|
+
for (const tracked of trackedPaths) {
|
|
773
|
+
if (!currentSet.has(tracked)) {
|
|
774
|
+
const node = this.cache.getNodeByPath(tracked);
|
|
775
|
+
if (node) {
|
|
776
|
+
this.cache.deleteNode(node.id);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const filenameIndex = this.buildFilenameIndex();
|
|
781
|
+
this.resolveOutgoingLinks(filenameIndex);
|
|
782
|
+
this.rebuildGraph();
|
|
783
|
+
}
|
|
784
|
+
async createNode(node) {
|
|
785
|
+
const normalizedId = normalizeId(node.id);
|
|
786
|
+
this.validatePathWithinSource(normalizedId);
|
|
787
|
+
const existing = this.cache.getNode(normalizedId);
|
|
788
|
+
if (existing) {
|
|
789
|
+
throw new Error(`Node already exists: ${normalizedId}`);
|
|
790
|
+
}
|
|
791
|
+
const filePath = join3(this.sourceRoot, normalizedId);
|
|
792
|
+
const dir = dirname(filePath);
|
|
793
|
+
await mkdir(dir, { recursive: true });
|
|
794
|
+
const parsed = {
|
|
795
|
+
title: node.title,
|
|
796
|
+
tags: node.tags,
|
|
797
|
+
properties: node.properties,
|
|
798
|
+
content: node.content
|
|
799
|
+
};
|
|
800
|
+
const markdown = serializeToMarkdown(parsed);
|
|
801
|
+
await writeFile(filePath, markdown, "utf-8");
|
|
802
|
+
const mtime = await this.getFileMtime(filePath);
|
|
803
|
+
const normalizedNode = { ...node, id: normalizedId };
|
|
804
|
+
this.cache.upsertNode(normalizedNode, "file", filePath, mtime);
|
|
805
|
+
this.rebuildGraph();
|
|
806
|
+
}
|
|
807
|
+
async updateNode(id, updates) {
|
|
808
|
+
const normalizedId = normalizeId(id);
|
|
809
|
+
const existing = this.cache.getNode(normalizedId);
|
|
810
|
+
if (!existing) {
|
|
811
|
+
throw new Error(`Node not found: ${id}`);
|
|
812
|
+
}
|
|
813
|
+
let outgoingLinks = updates.outgoingLinks;
|
|
814
|
+
if (updates.content !== void 0 && outgoingLinks === void 0) {
|
|
815
|
+
const rawLinks = extractWikiLinks(updates.content);
|
|
816
|
+
outgoingLinks = rawLinks.map((link) => this.normalizeWikiLink(link));
|
|
817
|
+
}
|
|
818
|
+
const updated = {
|
|
819
|
+
...existing,
|
|
820
|
+
...updates,
|
|
821
|
+
outgoingLinks: outgoingLinks ?? existing.outgoingLinks,
|
|
822
|
+
id: existing.id
|
|
823
|
+
// ID cannot be changed
|
|
824
|
+
};
|
|
825
|
+
const filePath = join3(this.sourceRoot, existing.id);
|
|
826
|
+
const parsed = {
|
|
827
|
+
title: updated.title,
|
|
828
|
+
tags: updated.tags,
|
|
829
|
+
properties: updated.properties,
|
|
830
|
+
content: updated.content
|
|
831
|
+
};
|
|
832
|
+
const markdown = serializeToMarkdown(parsed);
|
|
833
|
+
await writeFile(filePath, markdown, "utf-8");
|
|
834
|
+
const mtime = await this.getFileMtime(filePath);
|
|
835
|
+
this.cache.upsertNode(updated, "file", filePath, mtime);
|
|
836
|
+
if (outgoingLinks !== void 0 || updates.outgoingLinks !== void 0) {
|
|
837
|
+
this.rebuildGraph();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async deleteNode(id) {
|
|
841
|
+
const normalizedId = normalizeId(id);
|
|
842
|
+
const existing = this.cache.getNode(normalizedId);
|
|
843
|
+
if (!existing) {
|
|
844
|
+
throw new Error(`Node not found: ${id}`);
|
|
845
|
+
}
|
|
846
|
+
const filePath = join3(this.sourceRoot, existing.id);
|
|
847
|
+
await rm(filePath);
|
|
848
|
+
this.cache.deleteNode(existing.id);
|
|
849
|
+
await this.vectorProvider.delete(existing.id);
|
|
850
|
+
this.rebuildGraph();
|
|
851
|
+
}
|
|
852
|
+
async getNode(id) {
|
|
853
|
+
const normalizedId = normalizeId(id);
|
|
854
|
+
return this.cache.getNode(normalizedId);
|
|
855
|
+
}
|
|
856
|
+
async getNodes(ids) {
|
|
857
|
+
const normalizedIds = ids.map(normalizeId);
|
|
858
|
+
return this.cache.getNodes(normalizedIds);
|
|
859
|
+
}
|
|
860
|
+
async getAllNodeIds() {
|
|
861
|
+
const nodes = this.cache.getAllNodes();
|
|
862
|
+
return nodes.map((n) => n.id);
|
|
863
|
+
}
|
|
864
|
+
async searchByTags(tags, mode) {
|
|
865
|
+
return this.cache.searchByTags(tags, mode);
|
|
866
|
+
}
|
|
867
|
+
async getRandomNode(tags) {
|
|
868
|
+
let candidates;
|
|
869
|
+
if (tags && tags.length > 0) {
|
|
870
|
+
candidates = await this.searchByTags(tags, "any");
|
|
871
|
+
} else {
|
|
872
|
+
candidates = this.cache.getAllNodes();
|
|
873
|
+
}
|
|
874
|
+
if (candidates.length === 0) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
const randomIndex = Math.floor(Math.random() * candidates.length);
|
|
878
|
+
return candidates[randomIndex];
|
|
879
|
+
}
|
|
880
|
+
async resolveTitles(ids) {
|
|
881
|
+
return this.cache.resolveTitles(ids);
|
|
882
|
+
}
|
|
883
|
+
async listNodes(filter, options) {
|
|
884
|
+
return this.cache.listNodes(filter, options);
|
|
885
|
+
}
|
|
886
|
+
async resolveNodes(names, options) {
|
|
887
|
+
const strategy = options?.strategy ?? "fuzzy";
|
|
888
|
+
if (strategy === "exact" || strategy === "fuzzy") {
|
|
889
|
+
return this.cache.resolveNodes(names, options);
|
|
890
|
+
}
|
|
891
|
+
return names.map((query) => ({ query, match: null, score: 0 }));
|
|
892
|
+
}
|
|
893
|
+
async nodesExist(ids) {
|
|
894
|
+
const normalizedIds = ids.map(normalizeId);
|
|
895
|
+
return this.cache.nodesExist(normalizedIds);
|
|
896
|
+
}
|
|
897
|
+
async getNeighbors(id, options) {
|
|
898
|
+
this.ensureGraph();
|
|
899
|
+
const neighborIds = getNeighborIds(this.graph, id, options);
|
|
900
|
+
return this.cache.getNodes(neighborIds);
|
|
901
|
+
}
|
|
902
|
+
async findPath(source, target) {
|
|
903
|
+
this.ensureGraph();
|
|
904
|
+
return findPath(this.graph, source, target);
|
|
905
|
+
}
|
|
906
|
+
async getHubs(metric, limit) {
|
|
907
|
+
this.ensureGraph();
|
|
908
|
+
return getHubs(this.graph, metric, limit);
|
|
909
|
+
}
|
|
910
|
+
async storeEmbedding(id, vector, model) {
|
|
911
|
+
return this.vectorProvider.store(id, vector, model);
|
|
912
|
+
}
|
|
913
|
+
async searchByVector(vector, limit) {
|
|
914
|
+
return this.vectorProvider.search(vector, limit);
|
|
915
|
+
}
|
|
916
|
+
hasEmbedding(id) {
|
|
917
|
+
return this.vectorProvider.hasEmbedding(id);
|
|
918
|
+
}
|
|
919
|
+
close() {
|
|
920
|
+
this.stopWatching();
|
|
921
|
+
this.cache.close();
|
|
922
|
+
if (this.ownsVectorProvider && "close" in this.vectorProvider) {
|
|
923
|
+
this.vectorProvider.close();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
startWatching(onChange) {
|
|
927
|
+
if (this.watcher) {
|
|
928
|
+
throw new Error("Already watching. Call stopWatching() first.");
|
|
929
|
+
}
|
|
930
|
+
this.onChangeCallback = onChange;
|
|
931
|
+
return new Promise((resolve2, reject) => {
|
|
932
|
+
this.watcher = watch(this.sourceRoot, {
|
|
933
|
+
ignoreInitial: true,
|
|
934
|
+
ignored: [..._DocStore.EXCLUDED_DIRS].map((dir) => `**/${dir}/**`),
|
|
935
|
+
awaitWriteFinish: {
|
|
936
|
+
stabilityThreshold: 100
|
|
937
|
+
},
|
|
938
|
+
followSymlinks: false
|
|
939
|
+
});
|
|
940
|
+
this.watcher.on("ready", () => resolve2()).on("add", (path) => this.queueChange(path, "add")).on("change", (path) => this.queueChange(path, "change")).on("unlink", (path) => this.queueChange(path, "unlink")).on("error", (err) => {
|
|
941
|
+
if (err.code === "EMFILE") {
|
|
942
|
+
console.error(
|
|
943
|
+
"File watcher hit file descriptor limit. Try: ulimit -n 65536 or reduce watched files."
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
reject(err);
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
stopWatching() {
|
|
951
|
+
if (this.debounceTimer) {
|
|
952
|
+
clearTimeout(this.debounceTimer);
|
|
953
|
+
this.debounceTimer = null;
|
|
954
|
+
}
|
|
955
|
+
this.pendingChanges.clear();
|
|
956
|
+
if (this.watcher) {
|
|
957
|
+
this.watcher.close();
|
|
958
|
+
this.watcher = null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
isWatching() {
|
|
962
|
+
return this.watcher !== null;
|
|
963
|
+
}
|
|
964
|
+
queueChange(filePath, event) {
|
|
965
|
+
const relativePath = relative(this.sourceRoot, filePath);
|
|
966
|
+
const id = normalizeId(relativePath);
|
|
967
|
+
if (!filePath.endsWith(".md")) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const pathParts = relativePath.split("/");
|
|
971
|
+
for (const part of pathParts) {
|
|
972
|
+
if (_DocStore.EXCLUDED_DIRS.has(part)) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const existing = this.pendingChanges.get(id);
|
|
977
|
+
if (existing) {
|
|
978
|
+
if (existing === "add" && event === "change") {
|
|
979
|
+
return;
|
|
980
|
+
} else if (existing === "add" && event === "unlink") {
|
|
981
|
+
this.pendingChanges.delete(id);
|
|
982
|
+
} else if (existing === "change" && event === "unlink") {
|
|
983
|
+
this.pendingChanges.set(id, "unlink");
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
this.pendingChanges.set(id, event);
|
|
987
|
+
}
|
|
988
|
+
if (this.debounceTimer) {
|
|
989
|
+
clearTimeout(this.debounceTimer);
|
|
990
|
+
}
|
|
991
|
+
this.debounceTimer = setTimeout(() => {
|
|
992
|
+
this.processQueue();
|
|
993
|
+
}, 1e3);
|
|
994
|
+
}
|
|
995
|
+
async processQueue() {
|
|
996
|
+
const changes = new Map(this.pendingChanges);
|
|
997
|
+
this.pendingChanges.clear();
|
|
998
|
+
this.debounceTimer = null;
|
|
999
|
+
const processedIds = [];
|
|
1000
|
+
for (const [id, event] of changes) {
|
|
1001
|
+
try {
|
|
1002
|
+
if (event === "unlink") {
|
|
1003
|
+
const existing = this.cache.getNode(id);
|
|
1004
|
+
if (existing) {
|
|
1005
|
+
this.cache.deleteNode(id);
|
|
1006
|
+
await this.vectorProvider.delete(id);
|
|
1007
|
+
processedIds.push(id);
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
const filePath = join3(this.sourceRoot, id);
|
|
1011
|
+
const node = await this.fileToNode(filePath);
|
|
1012
|
+
const mtime = await this.getFileMtime(filePath);
|
|
1013
|
+
this.cache.upsertNode(node, "file", filePath, mtime);
|
|
1014
|
+
processedIds.push(id);
|
|
1015
|
+
}
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
console.warn(`Failed to process file change for ${id}:`, err);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (processedIds.length > 0) {
|
|
1021
|
+
const filenameIndex = this.buildFilenameIndex();
|
|
1022
|
+
this.resolveOutgoingLinks(filenameIndex);
|
|
1023
|
+
this.rebuildGraph();
|
|
1024
|
+
}
|
|
1025
|
+
if (this.onChangeCallback && processedIds.length > 0) {
|
|
1026
|
+
this.onChangeCallback(processedIds);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
buildFilenameIndex() {
|
|
1030
|
+
const index = /* @__PURE__ */ new Map();
|
|
1031
|
+
for (const node of this.cache.getAllNodes()) {
|
|
1032
|
+
const basename = node.id.split("/").pop();
|
|
1033
|
+
const existing = index.get(basename) ?? [];
|
|
1034
|
+
existing.push(node.id);
|
|
1035
|
+
index.set(basename, existing);
|
|
1036
|
+
}
|
|
1037
|
+
for (const paths of index.values()) {
|
|
1038
|
+
paths.sort();
|
|
1039
|
+
}
|
|
1040
|
+
return index;
|
|
1041
|
+
}
|
|
1042
|
+
resolveOutgoingLinks(filenameIndex) {
|
|
1043
|
+
const validNodeIds = /* @__PURE__ */ new Set();
|
|
1044
|
+
for (const paths of filenameIndex.values()) {
|
|
1045
|
+
for (const path of paths) {
|
|
1046
|
+
validNodeIds.add(path);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
for (const node of this.cache.getAllNodes()) {
|
|
1050
|
+
const resolved = node.outgoingLinks.map((link) => {
|
|
1051
|
+
if (validNodeIds.has(link)) {
|
|
1052
|
+
return link;
|
|
1053
|
+
}
|
|
1054
|
+
if (link.includes("/")) {
|
|
1055
|
+
return link;
|
|
1056
|
+
}
|
|
1057
|
+
const matches = filenameIndex.get(link);
|
|
1058
|
+
if (matches && matches.length > 0) {
|
|
1059
|
+
return matches[0];
|
|
1060
|
+
}
|
|
1061
|
+
return link;
|
|
1062
|
+
});
|
|
1063
|
+
if (resolved.some((r, i) => r !== node.outgoingLinks[i])) {
|
|
1064
|
+
this.cache.updateOutgoingLinks(node.id, resolved);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
ensureGraph() {
|
|
1069
|
+
if (!this.graph) {
|
|
1070
|
+
this.rebuildGraph();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
rebuildGraph() {
|
|
1074
|
+
const nodes = this.cache.getAllNodes();
|
|
1075
|
+
this.graph = buildGraph(nodes);
|
|
1076
|
+
const centrality = computeCentrality(this.graph);
|
|
1077
|
+
const now = Date.now();
|
|
1078
|
+
for (const [id, metrics] of centrality) {
|
|
1079
|
+
this.cache.storeCentrality(id, 0, metrics.inDegree, metrics.outDegree, now);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
static EXCLUDED_DIRS = /* @__PURE__ */ new Set([".roux", "node_modules", ".git", ".obsidian"]);
|
|
1083
|
+
async collectMarkdownFiles(dir) {
|
|
1084
|
+
const results = [];
|
|
1085
|
+
let entries;
|
|
1086
|
+
try {
|
|
1087
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1088
|
+
} catch {
|
|
1089
|
+
return results;
|
|
1090
|
+
}
|
|
1091
|
+
for (const entry of entries) {
|
|
1092
|
+
const fullPath = join3(dir, entry.name);
|
|
1093
|
+
if (entry.isDirectory()) {
|
|
1094
|
+
if (_DocStore.EXCLUDED_DIRS.has(entry.name)) {
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
const nested = await this.collectMarkdownFiles(fullPath);
|
|
1098
|
+
results.push(...nested);
|
|
1099
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1100
|
+
results.push(fullPath);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return results;
|
|
1104
|
+
}
|
|
1105
|
+
async getFileMtime(filePath) {
|
|
1106
|
+
const stats = await stat(filePath);
|
|
1107
|
+
return stats.mtimeMs;
|
|
1108
|
+
}
|
|
1109
|
+
async fileToNode(filePath) {
|
|
1110
|
+
const raw = await readFile(filePath, "utf-8");
|
|
1111
|
+
const parsed = parseMarkdown(raw);
|
|
1112
|
+
const relativePath = relative(this.sourceRoot, filePath);
|
|
1113
|
+
const id = normalizeId(relativePath);
|
|
1114
|
+
const title = parsed.title ?? titleFromPath(id);
|
|
1115
|
+
const rawLinks = extractWikiLinks(parsed.content);
|
|
1116
|
+
const outgoingLinks = rawLinks.map((link) => this.normalizeWikiLink(link));
|
|
1117
|
+
return {
|
|
1118
|
+
id,
|
|
1119
|
+
title,
|
|
1120
|
+
content: parsed.content,
|
|
1121
|
+
tags: parsed.tags,
|
|
1122
|
+
outgoingLinks,
|
|
1123
|
+
properties: parsed.properties,
|
|
1124
|
+
sourceRef: {
|
|
1125
|
+
type: "file",
|
|
1126
|
+
path: filePath,
|
|
1127
|
+
lastModified: new Date(await this.getFileMtime(filePath))
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Normalize a wiki-link target to an ID.
|
|
1133
|
+
* - If it has a file extension, normalize as-is
|
|
1134
|
+
* - If no extension, add .md
|
|
1135
|
+
* - Lowercase, forward slashes
|
|
1136
|
+
*/
|
|
1137
|
+
normalizeWikiLink(target) {
|
|
1138
|
+
let normalized = target.toLowerCase().replace(/\\/g, "/");
|
|
1139
|
+
if (!this.hasFileExtension(normalized)) {
|
|
1140
|
+
normalized += ".md";
|
|
1141
|
+
}
|
|
1142
|
+
return normalized;
|
|
1143
|
+
}
|
|
1144
|
+
hasFileExtension(path) {
|
|
1145
|
+
const match = path.match(/\.([a-z0-9]{1,4})$/i);
|
|
1146
|
+
if (!match?.[1]) return false;
|
|
1147
|
+
return /[a-z]/i.test(match[1]);
|
|
1148
|
+
}
|
|
1149
|
+
validatePathWithinSource(id) {
|
|
1150
|
+
const resolvedPath = resolve(this.sourceRoot, id);
|
|
1151
|
+
const resolvedRoot = resolve(this.sourceRoot);
|
|
1152
|
+
if (!resolvedPath.startsWith(resolvedRoot + "/")) {
|
|
1153
|
+
throw new Error(`Path traversal detected: ${id} resolves outside source root`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// src/providers/embedding/transformers.ts
|
|
1159
|
+
import { pipeline } from "@xenova/transformers";
|
|
1160
|
+
var DEFAULT_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
1161
|
+
var DEFAULT_DIMENSIONS = 384;
|
|
1162
|
+
var TransformersEmbeddingProvider = class {
|
|
1163
|
+
model;
|
|
1164
|
+
dims;
|
|
1165
|
+
pipe = null;
|
|
1166
|
+
constructor(model = DEFAULT_MODEL, dimensions = DEFAULT_DIMENSIONS) {
|
|
1167
|
+
this.model = model;
|
|
1168
|
+
this.dims = dimensions;
|
|
1169
|
+
}
|
|
1170
|
+
async getPipeline() {
|
|
1171
|
+
if (!this.pipe) {
|
|
1172
|
+
this.pipe = await pipeline("feature-extraction", this.model);
|
|
1173
|
+
}
|
|
1174
|
+
return this.pipe;
|
|
1175
|
+
}
|
|
1176
|
+
async embed(text) {
|
|
1177
|
+
const pipe = await this.getPipeline();
|
|
1178
|
+
const output = await pipe(text, { pooling: "mean", normalize: true });
|
|
1179
|
+
return Array.from(output.data);
|
|
1180
|
+
}
|
|
1181
|
+
async embedBatch(texts) {
|
|
1182
|
+
if (texts.length === 0) {
|
|
1183
|
+
return [];
|
|
1184
|
+
}
|
|
1185
|
+
return Promise.all(texts.map((t) => this.embed(t)));
|
|
1186
|
+
}
|
|
1187
|
+
dimensions() {
|
|
1188
|
+
return this.dims;
|
|
1189
|
+
}
|
|
1190
|
+
modelId() {
|
|
1191
|
+
return this.model;
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
// src/core/graphcore.ts
|
|
1196
|
+
var GraphCoreImpl = class _GraphCoreImpl {
|
|
1197
|
+
store = null;
|
|
1198
|
+
embedding = null;
|
|
1199
|
+
registerStore(provider) {
|
|
1200
|
+
if (!provider) {
|
|
1201
|
+
throw new Error("Store provider is required");
|
|
1202
|
+
}
|
|
1203
|
+
this.store = provider;
|
|
1204
|
+
}
|
|
1205
|
+
registerEmbedding(provider) {
|
|
1206
|
+
if (!provider) {
|
|
1207
|
+
throw new Error("Embedding provider is required");
|
|
1208
|
+
}
|
|
1209
|
+
this.embedding = provider;
|
|
1210
|
+
}
|
|
1211
|
+
requireStore() {
|
|
1212
|
+
if (!this.store) {
|
|
1213
|
+
throw new Error("StoreProvider not registered");
|
|
1214
|
+
}
|
|
1215
|
+
return this.store;
|
|
1216
|
+
}
|
|
1217
|
+
requireEmbedding() {
|
|
1218
|
+
if (!this.embedding) {
|
|
1219
|
+
throw new Error("EmbeddingProvider not registered");
|
|
1220
|
+
}
|
|
1221
|
+
return this.embedding;
|
|
1222
|
+
}
|
|
1223
|
+
async search(query, options) {
|
|
1224
|
+
const store = this.requireStore();
|
|
1225
|
+
const embedding = this.requireEmbedding();
|
|
1226
|
+
const limit = options?.limit ?? 10;
|
|
1227
|
+
const vector = await embedding.embed(query);
|
|
1228
|
+
const results = await store.searchByVector(vector, limit);
|
|
1229
|
+
const ids = results.map((r) => r.id);
|
|
1230
|
+
return store.getNodes(ids);
|
|
1231
|
+
}
|
|
1232
|
+
async getNode(id, depth) {
|
|
1233
|
+
const store = this.requireStore();
|
|
1234
|
+
const node = await store.getNode(id);
|
|
1235
|
+
if (!node) {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
if (!depth || depth === 0) {
|
|
1239
|
+
return node;
|
|
1240
|
+
}
|
|
1241
|
+
const [incomingNeighbors, outgoingNeighbors] = await Promise.all([
|
|
1242
|
+
store.getNeighbors(id, { direction: "in" }),
|
|
1243
|
+
store.getNeighbors(id, { direction: "out" })
|
|
1244
|
+
]);
|
|
1245
|
+
const neighborMap = /* @__PURE__ */ new Map();
|
|
1246
|
+
for (const n of [...incomingNeighbors, ...outgoingNeighbors]) {
|
|
1247
|
+
neighborMap.set(n.id, n);
|
|
1248
|
+
}
|
|
1249
|
+
const result = {
|
|
1250
|
+
...node,
|
|
1251
|
+
neighbors: Array.from(neighborMap.values()),
|
|
1252
|
+
incomingCount: incomingNeighbors.length,
|
|
1253
|
+
outgoingCount: outgoingNeighbors.length
|
|
1254
|
+
};
|
|
1255
|
+
return result;
|
|
1256
|
+
}
|
|
1257
|
+
async createNode(partial) {
|
|
1258
|
+
const store = this.requireStore();
|
|
1259
|
+
if (!partial.id || partial.id.trim() === "") {
|
|
1260
|
+
throw new Error("Node id is required and cannot be empty");
|
|
1261
|
+
}
|
|
1262
|
+
if (!partial.title) {
|
|
1263
|
+
throw new Error("Node title is required");
|
|
1264
|
+
}
|
|
1265
|
+
const node = {
|
|
1266
|
+
id: partial.id,
|
|
1267
|
+
title: partial.title,
|
|
1268
|
+
content: partial.content ?? "",
|
|
1269
|
+
tags: partial.tags ?? [],
|
|
1270
|
+
outgoingLinks: partial.outgoingLinks ?? [],
|
|
1271
|
+
properties: partial.properties ?? {},
|
|
1272
|
+
...partial.sourceRef && { sourceRef: partial.sourceRef }
|
|
1273
|
+
};
|
|
1274
|
+
await store.createNode(node);
|
|
1275
|
+
return await store.getNode(node.id) ?? node;
|
|
1276
|
+
}
|
|
1277
|
+
async updateNode(id, updates) {
|
|
1278
|
+
const store = this.requireStore();
|
|
1279
|
+
await store.updateNode(id, updates);
|
|
1280
|
+
const updated = await store.getNode(id);
|
|
1281
|
+
if (!updated) {
|
|
1282
|
+
throw new Error(`Node not found after update: ${id}`);
|
|
1283
|
+
}
|
|
1284
|
+
return updated;
|
|
1285
|
+
}
|
|
1286
|
+
async deleteNode(id) {
|
|
1287
|
+
const store = this.requireStore();
|
|
1288
|
+
try {
|
|
1289
|
+
await store.deleteNode(id);
|
|
1290
|
+
return true;
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
if (err instanceof Error && /not found/i.test(err.message)) {
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
throw err;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
async getNeighbors(id, options) {
|
|
1299
|
+
const store = this.requireStore();
|
|
1300
|
+
return store.getNeighbors(id, options);
|
|
1301
|
+
}
|
|
1302
|
+
async findPath(source, target) {
|
|
1303
|
+
const store = this.requireStore();
|
|
1304
|
+
return store.findPath(source, target);
|
|
1305
|
+
}
|
|
1306
|
+
async getHubs(metric, limit) {
|
|
1307
|
+
const store = this.requireStore();
|
|
1308
|
+
return store.getHubs(metric, limit);
|
|
1309
|
+
}
|
|
1310
|
+
async searchByTags(tags, mode, limit) {
|
|
1311
|
+
const store = this.requireStore();
|
|
1312
|
+
const results = await store.searchByTags(tags, mode);
|
|
1313
|
+
if (limit !== void 0) {
|
|
1314
|
+
return results.slice(0, limit);
|
|
1315
|
+
}
|
|
1316
|
+
return results;
|
|
1317
|
+
}
|
|
1318
|
+
async getRandomNode(tags) {
|
|
1319
|
+
const store = this.requireStore();
|
|
1320
|
+
return store.getRandomNode(tags);
|
|
1321
|
+
}
|
|
1322
|
+
async listNodes(filter, options) {
|
|
1323
|
+
return this.requireStore().listNodes(filter, options);
|
|
1324
|
+
}
|
|
1325
|
+
async resolveNodes(names, options) {
|
|
1326
|
+
const store = this.requireStore();
|
|
1327
|
+
const strategy = options?.strategy ?? "fuzzy";
|
|
1328
|
+
if (strategy === "semantic") {
|
|
1329
|
+
if (!this.embedding) {
|
|
1330
|
+
throw new Error("Semantic resolution requires EmbeddingProvider");
|
|
1331
|
+
}
|
|
1332
|
+
const filter = {};
|
|
1333
|
+
if (options?.tag) filter.tag = options.tag;
|
|
1334
|
+
if (options?.path) filter.path = options.path;
|
|
1335
|
+
const { nodes: candidates } = await store.listNodes(filter, { limit: 1e3 });
|
|
1336
|
+
if (candidates.length === 0 || names.length === 0) {
|
|
1337
|
+
return names.map((query) => ({ query, match: null, score: 0 }));
|
|
1338
|
+
}
|
|
1339
|
+
const threshold = options?.threshold ?? 0.7;
|
|
1340
|
+
const queryVectors = await this.embedding.embedBatch(names);
|
|
1341
|
+
const candidateTitles = candidates.map((c) => c.title);
|
|
1342
|
+
const candidateVectors = await this.embedding.embedBatch(candidateTitles);
|
|
1343
|
+
if (queryVectors.length > 0 && candidateVectors.length > 0) {
|
|
1344
|
+
const queryDim = queryVectors[0].length;
|
|
1345
|
+
const candidateDim = candidateVectors[0].length;
|
|
1346
|
+
if (queryDim !== candidateDim) {
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`Embedding dimension mismatch: query=${queryDim}, candidate=${candidateDim}`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return names.map((query, qIdx) => {
|
|
1353
|
+
const queryVector = queryVectors[qIdx];
|
|
1354
|
+
let bestScore = 0;
|
|
1355
|
+
let bestMatch = null;
|
|
1356
|
+
for (let cIdx = 0; cIdx < candidates.length; cIdx++) {
|
|
1357
|
+
const similarity = this.cosineSimilarity(queryVector, candidateVectors[cIdx]);
|
|
1358
|
+
if (similarity > bestScore) {
|
|
1359
|
+
bestScore = similarity;
|
|
1360
|
+
bestMatch = candidates[cIdx].id;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (bestScore >= threshold) {
|
|
1364
|
+
return { query, match: bestMatch, score: bestScore };
|
|
1365
|
+
}
|
|
1366
|
+
return { query, match: null, score: 0 };
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
return store.resolveNodes(names, options);
|
|
1370
|
+
}
|
|
1371
|
+
cosineSimilarity(a, b) {
|
|
1372
|
+
let dotProduct = 0;
|
|
1373
|
+
let normA = 0;
|
|
1374
|
+
let normB = 0;
|
|
1375
|
+
for (let i = 0; i < a.length; i++) {
|
|
1376
|
+
dotProduct += a[i] * b[i];
|
|
1377
|
+
normA += a[i] * a[i];
|
|
1378
|
+
normB += b[i] * b[i];
|
|
1379
|
+
}
|
|
1380
|
+
if (normA === 0 || normB === 0) return 0;
|
|
1381
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
1382
|
+
}
|
|
1383
|
+
static fromConfig(config) {
|
|
1384
|
+
if (!config.providers?.store) {
|
|
1385
|
+
throw new Error("StoreProvider configuration is required");
|
|
1386
|
+
}
|
|
1387
|
+
const core = new _GraphCoreImpl();
|
|
1388
|
+
if (config.providers.store.type === "docstore") {
|
|
1389
|
+
const sourcePath = config.source?.path ?? ".";
|
|
1390
|
+
const cachePath = config.cache?.path ?? ".roux";
|
|
1391
|
+
const store = new DocStore(sourcePath, cachePath);
|
|
1392
|
+
core.registerStore(store);
|
|
1393
|
+
} else {
|
|
1394
|
+
throw new Error(
|
|
1395
|
+
`Unsupported store provider type: ${config.providers.store.type}. Supported: docstore`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
const embeddingConfig = config.providers.embedding;
|
|
1399
|
+
if (!embeddingConfig || embeddingConfig.type === "local") {
|
|
1400
|
+
const model = embeddingConfig?.model;
|
|
1401
|
+
const embedding = new TransformersEmbeddingProvider(model);
|
|
1402
|
+
core.registerEmbedding(embedding);
|
|
1403
|
+
} else {
|
|
1404
|
+
throw new Error(
|
|
1405
|
+
`Unsupported embedding provider type: ${embeddingConfig.type}. Supported: local`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
return core;
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// src/index.ts
|
|
1413
|
+
var VERSION = "0.1.0";
|
|
1414
|
+
export {
|
|
1415
|
+
DEFAULT_CONFIG,
|
|
1416
|
+
DocStore,
|
|
1417
|
+
GraphCoreImpl,
|
|
1418
|
+
SqliteVectorProvider,
|
|
1419
|
+
TransformersEmbeddingProvider,
|
|
1420
|
+
VERSION,
|
|
1421
|
+
isNode,
|
|
1422
|
+
isSourceRef,
|
|
1423
|
+
isVectorProvider
|
|
1424
|
+
};
|
|
1425
|
+
//# sourceMappingURL=index.js.map
|