@theglitchking/semantic-pages 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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/chunk-KF45H64M.js +622 -0
- package/dist/chunk-KF45H64M.js.map +1 -0
- package/dist/chunk-TDC45FQJ.js +114 -0
- package/dist/chunk-TDC45FQJ.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +177 -0
- package/dist/core/index.js +25 -0
- package/dist/core/index.js.map +1 -0
- package/dist/indexer-HSCSXWIO.js +7 -0
- package/dist/indexer-HSCSXWIO.js.map +1 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +4392 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
// src/core/embedder.ts
|
|
2
|
+
import { pipeline } from "@huggingface/transformers";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
var DEFAULT_MODEL = "nomic-ai/nomic-embed-text-v1.5";
|
|
8
|
+
var CACHE_DIR = join(homedir(), ".semantic-pages", "models");
|
|
9
|
+
var Embedder = class {
|
|
10
|
+
model;
|
|
11
|
+
extractor = null;
|
|
12
|
+
dimensions = 0;
|
|
13
|
+
constructor(model = DEFAULT_MODEL) {
|
|
14
|
+
this.model = model;
|
|
15
|
+
}
|
|
16
|
+
async init() {
|
|
17
|
+
if (this.extractor) return;
|
|
18
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
19
|
+
this.extractor = await pipeline("feature-extraction", this.model, {
|
|
20
|
+
cache_dir: CACHE_DIR,
|
|
21
|
+
dtype: "fp32"
|
|
22
|
+
});
|
|
23
|
+
const test = await this.embed("test");
|
|
24
|
+
this.dimensions = test.length;
|
|
25
|
+
}
|
|
26
|
+
async embed(text) {
|
|
27
|
+
if (!this.extractor) throw new Error("Embedder not initialized. Call init() first.");
|
|
28
|
+
const output = await this.extractor(text, { pooling: "mean", normalize: true });
|
|
29
|
+
return new Float32Array(output.data);
|
|
30
|
+
}
|
|
31
|
+
async embedBatch(texts) {
|
|
32
|
+
return Promise.all(texts.map((t) => this.embed(t)));
|
|
33
|
+
}
|
|
34
|
+
getDimensions() {
|
|
35
|
+
return this.dimensions;
|
|
36
|
+
}
|
|
37
|
+
getModel() {
|
|
38
|
+
return this.model;
|
|
39
|
+
}
|
|
40
|
+
async saveEmbeddings(embeddings, indexPath) {
|
|
41
|
+
const entries = [];
|
|
42
|
+
for (const [key, vec] of embeddings) {
|
|
43
|
+
entries.push({ key, data: Array.from(vec) });
|
|
44
|
+
}
|
|
45
|
+
await writeFile(join(indexPath, "embeddings.json"), JSON.stringify(entries));
|
|
46
|
+
}
|
|
47
|
+
async loadEmbeddings(indexPath) {
|
|
48
|
+
const filePath = join(indexPath, "embeddings.json");
|
|
49
|
+
if (!existsSync(filePath)) return /* @__PURE__ */ new Map();
|
|
50
|
+
const raw = await readFile(filePath, "utf-8");
|
|
51
|
+
const entries = JSON.parse(raw);
|
|
52
|
+
const map = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
map.set(entry.key, new Float32Array(entry.data));
|
|
55
|
+
}
|
|
56
|
+
return map;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/core/graph.ts
|
|
61
|
+
import Graph from "graphology";
|
|
62
|
+
import { bfsFromNode } from "graphology-traversal";
|
|
63
|
+
import { bidirectional } from "graphology-shortest-path";
|
|
64
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
65
|
+
import { join as join2 } from "path";
|
|
66
|
+
import { existsSync as existsSync2 } from "fs";
|
|
67
|
+
var GraphBuilder = class {
|
|
68
|
+
graph;
|
|
69
|
+
constructor() {
|
|
70
|
+
this.graph = new Graph({ type: "directed", multi: false });
|
|
71
|
+
}
|
|
72
|
+
buildFromDocuments(documents) {
|
|
73
|
+
this.graph.clear();
|
|
74
|
+
for (const doc of documents) {
|
|
75
|
+
this.graph.addNode(doc.path, {
|
|
76
|
+
title: doc.title,
|
|
77
|
+
tags: doc.tags
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const pathLookup = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const doc of documents) {
|
|
82
|
+
const nameNoExt = doc.path.replace(/\.md$/, "");
|
|
83
|
+
const basename = nameNoExt.split("/").pop();
|
|
84
|
+
pathLookup.set(basename.toLowerCase(), doc.path);
|
|
85
|
+
pathLookup.set(nameNoExt.toLowerCase(), doc.path);
|
|
86
|
+
}
|
|
87
|
+
for (const doc of documents) {
|
|
88
|
+
for (const link of doc.wikilinks) {
|
|
89
|
+
const target = pathLookup.get(link.toLowerCase());
|
|
90
|
+
if (target && target !== doc.path && !this.graph.hasEdge(doc.path, target)) {
|
|
91
|
+
this.graph.addEdge(doc.path, target, { type: "wikilink", weight: 1 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const tagToNotes = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const doc of documents) {
|
|
97
|
+
for (const tag of doc.tags) {
|
|
98
|
+
const notes = tagToNotes.get(tag) || [];
|
|
99
|
+
notes.push(doc.path);
|
|
100
|
+
tagToNotes.set(tag, notes);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const [, notes] of tagToNotes) {
|
|
104
|
+
for (let i = 0; i < notes.length; i++) {
|
|
105
|
+
for (let j = i + 1; j < notes.length; j++) {
|
|
106
|
+
if (!this.graph.hasEdge(notes[i], notes[j])) {
|
|
107
|
+
this.graph.addEdge(notes[i], notes[j], { type: "tag", weight: 0.5 });
|
|
108
|
+
}
|
|
109
|
+
if (!this.graph.hasEdge(notes[j], notes[i])) {
|
|
110
|
+
this.graph.addEdge(notes[j], notes[i], { type: "tag", weight: 0.5 });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
backlinks(notePath) {
|
|
117
|
+
if (!this.graph.hasNode(notePath)) return [];
|
|
118
|
+
return this.graph.inNeighbors(notePath).map((n) => this.nodeToGraphNode(n));
|
|
119
|
+
}
|
|
120
|
+
forwardlinks(notePath) {
|
|
121
|
+
if (!this.graph.hasNode(notePath)) return [];
|
|
122
|
+
return this.graph.outNeighbors(notePath).map((n) => this.nodeToGraphNode(n));
|
|
123
|
+
}
|
|
124
|
+
findPath(from, to) {
|
|
125
|
+
if (!this.graph.hasNode(from) || !this.graph.hasNode(to)) return null;
|
|
126
|
+
const path = bidirectional(this.graph, from, to);
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
searchGraph(concept, maxDepth = 2) {
|
|
130
|
+
const startNodes = this.graph.nodes().filter((n) => {
|
|
131
|
+
const attrs = this.graph.getNodeAttributes(n);
|
|
132
|
+
return n.toLowerCase().includes(concept.toLowerCase()) || attrs.title?.toLowerCase().includes(concept.toLowerCase()) || attrs.tags?.some((t) => t.toLowerCase().includes(concept.toLowerCase()));
|
|
133
|
+
});
|
|
134
|
+
const visited = /* @__PURE__ */ new Set();
|
|
135
|
+
for (const start of startNodes) {
|
|
136
|
+
let depth = 0;
|
|
137
|
+
bfsFromNode(this.graph, start, (node) => {
|
|
138
|
+
visited.add(node);
|
|
139
|
+
depth++;
|
|
140
|
+
return depth > maxDepth;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return [...visited].map((n) => this.nodeToGraphNode(n));
|
|
144
|
+
}
|
|
145
|
+
statistics() {
|
|
146
|
+
const nodes = this.graph.order;
|
|
147
|
+
const edges = this.graph.size;
|
|
148
|
+
const orphans = this.graph.nodes().filter(
|
|
149
|
+
(n) => this.graph.degree(n) === 0
|
|
150
|
+
);
|
|
151
|
+
const connections = this.graph.nodes().map((n) => ({
|
|
152
|
+
path: n,
|
|
153
|
+
connections: this.graph.degree(n)
|
|
154
|
+
}));
|
|
155
|
+
connections.sort((a, b) => b.connections - a.connections);
|
|
156
|
+
const maxPossibleEdges = nodes * (nodes - 1);
|
|
157
|
+
const density = maxPossibleEdges > 0 ? edges / maxPossibleEdges : 0;
|
|
158
|
+
return {
|
|
159
|
+
totalNodes: nodes,
|
|
160
|
+
totalEdges: edges,
|
|
161
|
+
orphanCount: orphans.length,
|
|
162
|
+
mostConnected: connections.slice(0, 10),
|
|
163
|
+
density
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
nodeToGraphNode(nodePath) {
|
|
167
|
+
const attrs = this.graph.getNodeAttributes(nodePath);
|
|
168
|
+
return {
|
|
169
|
+
path: nodePath,
|
|
170
|
+
title: attrs.title || nodePath,
|
|
171
|
+
tags: attrs.tags || [],
|
|
172
|
+
linkCount: this.graph.outDegree(nodePath),
|
|
173
|
+
backlinkCount: this.graph.inDegree(nodePath)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async save(indexPath) {
|
|
177
|
+
const data = this.graph.export();
|
|
178
|
+
await writeFile2(join2(indexPath, "graph.json"), JSON.stringify(data));
|
|
179
|
+
}
|
|
180
|
+
async load(indexPath) {
|
|
181
|
+
const filePath = join2(indexPath, "graph.json");
|
|
182
|
+
if (!existsSync2(filePath)) return false;
|
|
183
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
184
|
+
const data = JSON.parse(raw);
|
|
185
|
+
this.graph.import(data);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
getGraph() {
|
|
189
|
+
return this.graph;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/core/vector.ts
|
|
194
|
+
import hnswlib from "hnswlib-node";
|
|
195
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
196
|
+
import { join as join3 } from "path";
|
|
197
|
+
import { existsSync as existsSync3 } from "fs";
|
|
198
|
+
var { HierarchicalNSW } = hnswlib;
|
|
199
|
+
var VectorIndex = class {
|
|
200
|
+
index = null;
|
|
201
|
+
dimensions;
|
|
202
|
+
chunkMeta = [];
|
|
203
|
+
constructor(dimensions) {
|
|
204
|
+
this.dimensions = dimensions;
|
|
205
|
+
}
|
|
206
|
+
build(embeddings, meta) {
|
|
207
|
+
if (embeddings.length === 0) {
|
|
208
|
+
this.index = null;
|
|
209
|
+
this.chunkMeta = [];
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.index = new HierarchicalNSW("cosine", this.dimensions);
|
|
213
|
+
this.index.initIndex(embeddings.length);
|
|
214
|
+
for (let i = 0; i < embeddings.length; i++) {
|
|
215
|
+
this.index.addPoint(Array.from(embeddings[i]), i);
|
|
216
|
+
}
|
|
217
|
+
this.chunkMeta = meta;
|
|
218
|
+
}
|
|
219
|
+
search(queryEmbedding, k = 10) {
|
|
220
|
+
if (!this.index || this.chunkMeta.length === 0) return [];
|
|
221
|
+
const numResults = Math.min(k, this.chunkMeta.length);
|
|
222
|
+
const result = this.index.searchKnn(Array.from(queryEmbedding), numResults);
|
|
223
|
+
const seen = /* @__PURE__ */ new Set();
|
|
224
|
+
const results = [];
|
|
225
|
+
for (let i = 0; i < result.neighbors.length; i++) {
|
|
226
|
+
const idx = result.neighbors[i];
|
|
227
|
+
const meta = this.chunkMeta[idx];
|
|
228
|
+
if (!meta || seen.has(meta.docPath)) continue;
|
|
229
|
+
seen.add(meta.docPath);
|
|
230
|
+
results.push({
|
|
231
|
+
path: meta.docPath,
|
|
232
|
+
title: meta.docPath,
|
|
233
|
+
score: 1 - result.distances[i],
|
|
234
|
+
snippet: meta.text.slice(0, 200),
|
|
235
|
+
matchedChunk: meta.text
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return results;
|
|
239
|
+
}
|
|
240
|
+
async save(indexPath) {
|
|
241
|
+
if (!this.index) return;
|
|
242
|
+
this.index.writeIndexSync(join3(indexPath, "hnsw.bin"));
|
|
243
|
+
await writeFile3(
|
|
244
|
+
join3(indexPath, "hnsw-meta.json"),
|
|
245
|
+
JSON.stringify(this.chunkMeta)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
async load(indexPath) {
|
|
249
|
+
const hnswPath = join3(indexPath, "hnsw.bin");
|
|
250
|
+
const metaPath = join3(indexPath, "hnsw-meta.json");
|
|
251
|
+
if (!existsSync3(hnswPath) || !existsSync3(metaPath)) return false;
|
|
252
|
+
const raw = await readFile3(metaPath, "utf-8");
|
|
253
|
+
this.chunkMeta = JSON.parse(raw);
|
|
254
|
+
this.index = new HierarchicalNSW("cosine", this.dimensions);
|
|
255
|
+
this.index.initIndex(this.chunkMeta.length);
|
|
256
|
+
this.index.readIndexSync(hnswPath);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
getChunkMeta() {
|
|
260
|
+
return this.chunkMeta;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/core/search-text.ts
|
|
265
|
+
import { minimatch } from "minimatch";
|
|
266
|
+
var TextSearch = class {
|
|
267
|
+
documents = [];
|
|
268
|
+
setDocuments(documents) {
|
|
269
|
+
this.documents = documents;
|
|
270
|
+
}
|
|
271
|
+
search(options) {
|
|
272
|
+
const { pattern, regex, caseSensitive, pathGlob, tagFilter, limit = 20 } = options;
|
|
273
|
+
let matcher;
|
|
274
|
+
if (regex) {
|
|
275
|
+
const flags = caseSensitive ? "g" : "gi";
|
|
276
|
+
const re = new RegExp(pattern, flags);
|
|
277
|
+
matcher = (text) => {
|
|
278
|
+
re.lastIndex = 0;
|
|
279
|
+
const m = re.exec(text);
|
|
280
|
+
return { matched: !!m, index: m?.index ?? -1 };
|
|
281
|
+
};
|
|
282
|
+
} else {
|
|
283
|
+
const needle = caseSensitive ? pattern : pattern.toLowerCase();
|
|
284
|
+
matcher = (text) => {
|
|
285
|
+
const haystack = caseSensitive ? text : text.toLowerCase();
|
|
286
|
+
const idx = haystack.indexOf(needle);
|
|
287
|
+
return { matched: idx >= 0, index: idx };
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const results = [];
|
|
291
|
+
for (const doc of this.documents) {
|
|
292
|
+
if (pathGlob && !minimatch(doc.path, pathGlob)) continue;
|
|
293
|
+
if (tagFilter?.length) {
|
|
294
|
+
const hasTag = tagFilter.some((t) => doc.tags.includes(t));
|
|
295
|
+
if (!hasTag) continue;
|
|
296
|
+
}
|
|
297
|
+
const { matched, index } = matcher(doc.content);
|
|
298
|
+
if (!matched) continue;
|
|
299
|
+
const snippetStart = Math.max(0, index - 80);
|
|
300
|
+
const snippetEnd = Math.min(doc.content.length, index + 120);
|
|
301
|
+
const snippet = doc.content.slice(snippetStart, snippetEnd).trim();
|
|
302
|
+
results.push({
|
|
303
|
+
path: doc.path,
|
|
304
|
+
title: doc.title,
|
|
305
|
+
score: 1,
|
|
306
|
+
snippet: (snippetStart > 0 ? "..." : "") + snippet + (snippetEnd < doc.content.length ? "..." : "")
|
|
307
|
+
});
|
|
308
|
+
if (results.length >= limit) break;
|
|
309
|
+
}
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// src/core/crud.ts
|
|
315
|
+
import { readFile as readFile4, writeFile as writeFile4, unlink, rename, mkdir as mkdir2 } from "fs/promises";
|
|
316
|
+
import { dirname, join as join4 } from "path";
|
|
317
|
+
import { existsSync as existsSync4 } from "fs";
|
|
318
|
+
import { glob } from "glob";
|
|
319
|
+
import matter from "gray-matter";
|
|
320
|
+
var NoteCrud = class {
|
|
321
|
+
notesPath;
|
|
322
|
+
constructor(notesPath) {
|
|
323
|
+
this.notesPath = notesPath;
|
|
324
|
+
}
|
|
325
|
+
async create(relativePath, content, frontmatter) {
|
|
326
|
+
const absPath = join4(this.notesPath, relativePath);
|
|
327
|
+
if (existsSync4(absPath)) {
|
|
328
|
+
throw new Error(`Note already exists: ${relativePath}`);
|
|
329
|
+
}
|
|
330
|
+
await mkdir2(dirname(absPath), { recursive: true });
|
|
331
|
+
let fileContent;
|
|
332
|
+
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
333
|
+
fileContent = matter.stringify(content, frontmatter);
|
|
334
|
+
} else {
|
|
335
|
+
fileContent = content;
|
|
336
|
+
}
|
|
337
|
+
await writeFile4(absPath, fileContent, "utf-8");
|
|
338
|
+
return relativePath;
|
|
339
|
+
}
|
|
340
|
+
async read(relativePath) {
|
|
341
|
+
const absPath = join4(this.notesPath, relativePath);
|
|
342
|
+
return readFile4(absPath, "utf-8");
|
|
343
|
+
}
|
|
344
|
+
async readMultiple(paths) {
|
|
345
|
+
const results = /* @__PURE__ */ new Map();
|
|
346
|
+
await Promise.all(
|
|
347
|
+
paths.map(async (p) => {
|
|
348
|
+
try {
|
|
349
|
+
const content = await this.read(p);
|
|
350
|
+
results.set(p, content);
|
|
351
|
+
} catch {
|
|
352
|
+
results.set(p, `[Error: could not read ${p}]`);
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
);
|
|
356
|
+
return results;
|
|
357
|
+
}
|
|
358
|
+
async update(relativePath, content, options) {
|
|
359
|
+
const absPath = join4(this.notesPath, relativePath);
|
|
360
|
+
if (!existsSync4(absPath)) {
|
|
361
|
+
throw new Error(`Note does not exist: ${relativePath}`);
|
|
362
|
+
}
|
|
363
|
+
const existing = await readFile4(absPath, "utf-8");
|
|
364
|
+
let updated;
|
|
365
|
+
switch (options.mode) {
|
|
366
|
+
case "overwrite":
|
|
367
|
+
updated = content;
|
|
368
|
+
break;
|
|
369
|
+
case "append":
|
|
370
|
+
updated = existing + "\n" + content;
|
|
371
|
+
break;
|
|
372
|
+
case "prepend": {
|
|
373
|
+
const { data, content: body } = matter(existing);
|
|
374
|
+
const newBody = content + "\n" + body;
|
|
375
|
+
updated = Object.keys(data).length > 0 ? matter.stringify(newBody, data) : newBody;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "patch-by-heading": {
|
|
379
|
+
if (!options.heading) throw new Error("patch-by-heading requires a heading");
|
|
380
|
+
updated = this.patchByHeading(existing, options.heading, content);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
await writeFile4(absPath, updated, "utf-8");
|
|
385
|
+
}
|
|
386
|
+
async delete(relativePath) {
|
|
387
|
+
const absPath = join4(this.notesPath, relativePath);
|
|
388
|
+
if (!existsSync4(absPath)) {
|
|
389
|
+
throw new Error(`Note does not exist: ${relativePath}`);
|
|
390
|
+
}
|
|
391
|
+
await unlink(absPath);
|
|
392
|
+
}
|
|
393
|
+
async move(fromPath, toPath) {
|
|
394
|
+
const absFrom = join4(this.notesPath, fromPath);
|
|
395
|
+
const absTo = join4(this.notesPath, toPath);
|
|
396
|
+
if (!existsSync4(absFrom)) {
|
|
397
|
+
throw new Error(`Note does not exist: ${fromPath}`);
|
|
398
|
+
}
|
|
399
|
+
if (existsSync4(absTo)) {
|
|
400
|
+
throw new Error(`Destination already exists: ${toPath}`);
|
|
401
|
+
}
|
|
402
|
+
await mkdir2(dirname(absTo), { recursive: true });
|
|
403
|
+
await rename(absFrom, absTo);
|
|
404
|
+
await this.updateWikilinksAfterMove(fromPath, toPath);
|
|
405
|
+
}
|
|
406
|
+
async updateWikilinksAfterMove(oldPath, newPath) {
|
|
407
|
+
const oldName = oldPath.replace(/\.md$/, "").split("/").pop();
|
|
408
|
+
const newName = newPath.replace(/\.md$/, "").split("/").pop();
|
|
409
|
+
if (oldName === newName) return;
|
|
410
|
+
const files = await glob("**/*.md", { cwd: this.notesPath });
|
|
411
|
+
for (const file of files) {
|
|
412
|
+
const absPath = join4(this.notesPath, file);
|
|
413
|
+
const content = await readFile4(absPath, "utf-8");
|
|
414
|
+
const pattern = new RegExp(`\\[\\[${escapeRegex(oldName)}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
415
|
+
if (!pattern.test(content)) continue;
|
|
416
|
+
const updated = content.replace(pattern, `[[${newName}$1]]`);
|
|
417
|
+
await writeFile4(absPath, updated, "utf-8");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
patchByHeading(content, heading, newContent) {
|
|
421
|
+
const lines = content.split("\n");
|
|
422
|
+
const headingPattern = new RegExp(`^#{1,6}\\s+${escapeRegex(heading)}\\s*$`, "i");
|
|
423
|
+
let headingIndex = -1;
|
|
424
|
+
let headingLevel = 0;
|
|
425
|
+
for (let i = 0; i < lines.length; i++) {
|
|
426
|
+
if (headingPattern.test(lines[i])) {
|
|
427
|
+
headingIndex = i;
|
|
428
|
+
const match = lines[i].match(/^(#{1,6})\s+/);
|
|
429
|
+
headingLevel = match ? match[1].length : 1;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (headingIndex === -1) {
|
|
434
|
+
throw new Error(`Heading not found: ${heading}`);
|
|
435
|
+
}
|
|
436
|
+
let endIndex = lines.length;
|
|
437
|
+
for (let i = headingIndex + 1; i < lines.length; i++) {
|
|
438
|
+
const match = lines[i].match(/^(#{1,6})\s+/);
|
|
439
|
+
if (match && match[1].length <= headingLevel) {
|
|
440
|
+
endIndex = i;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const before = lines.slice(0, headingIndex + 1);
|
|
445
|
+
const after = lines.slice(endIndex);
|
|
446
|
+
return [...before, "", newContent, "", ...after].join("\n");
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
function escapeRegex(str) {
|
|
450
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/core/frontmatter.ts
|
|
454
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
455
|
+
import { join as join5 } from "path";
|
|
456
|
+
import { glob as glob2 } from "glob";
|
|
457
|
+
import matter2 from "gray-matter";
|
|
458
|
+
var FrontmatterManager = class {
|
|
459
|
+
notesPath;
|
|
460
|
+
constructor(notesPath) {
|
|
461
|
+
this.notesPath = notesPath;
|
|
462
|
+
}
|
|
463
|
+
async get(relativePath) {
|
|
464
|
+
const absPath = join5(this.notesPath, relativePath);
|
|
465
|
+
const raw = await readFile5(absPath, "utf-8");
|
|
466
|
+
const { data } = matter2(raw);
|
|
467
|
+
return data;
|
|
468
|
+
}
|
|
469
|
+
async update(relativePath, fields) {
|
|
470
|
+
const absPath = join5(this.notesPath, relativePath);
|
|
471
|
+
const raw = await readFile5(absPath, "utf-8");
|
|
472
|
+
const { data, content } = matter2(raw);
|
|
473
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
474
|
+
if (value === null || value === void 0) {
|
|
475
|
+
delete data[key];
|
|
476
|
+
} else {
|
|
477
|
+
data[key] = value;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const updated = matter2.stringify(content, data);
|
|
481
|
+
await writeFile5(absPath, updated, "utf-8");
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var TagManager = class {
|
|
485
|
+
notesPath;
|
|
486
|
+
constructor(notesPath) {
|
|
487
|
+
this.notesPath = notesPath;
|
|
488
|
+
}
|
|
489
|
+
async list(relativePath) {
|
|
490
|
+
const absPath = join5(this.notesPath, relativePath);
|
|
491
|
+
const raw = await readFile5(absPath, "utf-8");
|
|
492
|
+
const { data, content } = matter2(raw);
|
|
493
|
+
const fmTags = Array.isArray(data.tags) ? data.tags : [];
|
|
494
|
+
const inlineTags = [...content.matchAll(/(?:^|\s)#([a-zA-Z][\w-/]*)/g)].map(
|
|
495
|
+
(m) => m[1]
|
|
496
|
+
);
|
|
497
|
+
return [.../* @__PURE__ */ new Set([...fmTags, ...inlineTags])];
|
|
498
|
+
}
|
|
499
|
+
async add(relativePath, tags) {
|
|
500
|
+
const absPath = join5(this.notesPath, relativePath);
|
|
501
|
+
const raw = await readFile5(absPath, "utf-8");
|
|
502
|
+
const { data, content } = matter2(raw);
|
|
503
|
+
const existing = Array.isArray(data.tags) ? data.tags : [];
|
|
504
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...tags])];
|
|
505
|
+
data.tags = merged;
|
|
506
|
+
const updated = matter2.stringify(content, data);
|
|
507
|
+
await writeFile5(absPath, updated, "utf-8");
|
|
508
|
+
}
|
|
509
|
+
async remove(relativePath, tags) {
|
|
510
|
+
const absPath = join5(this.notesPath, relativePath);
|
|
511
|
+
const raw = await readFile5(absPath, "utf-8");
|
|
512
|
+
const { data, content } = matter2(raw);
|
|
513
|
+
if (Array.isArray(data.tags)) {
|
|
514
|
+
data.tags = data.tags.filter((t) => !tags.includes(t));
|
|
515
|
+
}
|
|
516
|
+
let updatedContent = content;
|
|
517
|
+
for (const tag of tags) {
|
|
518
|
+
const pattern = new RegExp(`(^|\\s)#${escapeRegex2(tag)}(?=\\s|$)`, "g");
|
|
519
|
+
updatedContent = updatedContent.replace(pattern, "$1");
|
|
520
|
+
}
|
|
521
|
+
const updated = matter2.stringify(updatedContent, data);
|
|
522
|
+
await writeFile5(absPath, updated, "utf-8");
|
|
523
|
+
}
|
|
524
|
+
async renameVaultWide(oldTag, newTag) {
|
|
525
|
+
const files = await glob2("**/*.md", { cwd: this.notesPath });
|
|
526
|
+
let count = 0;
|
|
527
|
+
for (const file of files) {
|
|
528
|
+
const absPath = join5(this.notesPath, file);
|
|
529
|
+
const raw = await readFile5(absPath, "utf-8");
|
|
530
|
+
const { data, content } = matter2(raw);
|
|
531
|
+
let changed = false;
|
|
532
|
+
if (Array.isArray(data.tags)) {
|
|
533
|
+
const idx = data.tags.indexOf(oldTag);
|
|
534
|
+
if (idx >= 0) {
|
|
535
|
+
data.tags[idx] = newTag;
|
|
536
|
+
changed = true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const pattern = new RegExp(`(^|\\s)#${escapeRegex2(oldTag)}(?=\\s|$)`, "g");
|
|
540
|
+
const updatedContent = content.replace(pattern, `$1#${newTag}`);
|
|
541
|
+
if (updatedContent !== content) changed = true;
|
|
542
|
+
if (changed) {
|
|
543
|
+
const updated = matter2.stringify(updatedContent, data);
|
|
544
|
+
await writeFile5(absPath, updated, "utf-8");
|
|
545
|
+
count++;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return count;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
function escapeRegex2(str) {
|
|
552
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/core/watcher.ts
|
|
556
|
+
import { watch } from "chokidar";
|
|
557
|
+
import { EventEmitter } from "events";
|
|
558
|
+
var Watcher = class extends EventEmitter {
|
|
559
|
+
notesPath;
|
|
560
|
+
fsWatcher = null;
|
|
561
|
+
debounceMs;
|
|
562
|
+
pendingChanges = /* @__PURE__ */ new Set();
|
|
563
|
+
debounceTimer = null;
|
|
564
|
+
readyPromise = null;
|
|
565
|
+
usePolling;
|
|
566
|
+
constructor(notesPath, debounceMs = 500, usePolling = false) {
|
|
567
|
+
super();
|
|
568
|
+
this.notesPath = notesPath;
|
|
569
|
+
this.debounceMs = debounceMs;
|
|
570
|
+
this.usePolling = usePolling;
|
|
571
|
+
}
|
|
572
|
+
start() {
|
|
573
|
+
if (this.fsWatcher) return;
|
|
574
|
+
this.fsWatcher = watch("**/*.md", {
|
|
575
|
+
cwd: this.notesPath,
|
|
576
|
+
ignoreInitial: true,
|
|
577
|
+
ignored: [
|
|
578
|
+
"**/node_modules/**",
|
|
579
|
+
"**/.semantic-pages-index/**",
|
|
580
|
+
"**/.git/**"
|
|
581
|
+
],
|
|
582
|
+
...this.usePolling ? { usePolling: true, interval: 100 } : {}
|
|
583
|
+
});
|
|
584
|
+
this.readyPromise = new Promise((resolve) => {
|
|
585
|
+
this.fsWatcher.on("ready", resolve);
|
|
586
|
+
});
|
|
587
|
+
this.fsWatcher.on("add", (path) => this.enqueue(path));
|
|
588
|
+
this.fsWatcher.on("change", (path) => this.enqueue(path));
|
|
589
|
+
this.fsWatcher.on("unlink", (path) => this.enqueue(path));
|
|
590
|
+
this.fsWatcher.on("error", (err) => this.emit("error", err));
|
|
591
|
+
}
|
|
592
|
+
async ready() {
|
|
593
|
+
if (this.readyPromise) await this.readyPromise;
|
|
594
|
+
}
|
|
595
|
+
stop() {
|
|
596
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
597
|
+
this.fsWatcher?.close();
|
|
598
|
+
this.fsWatcher = null;
|
|
599
|
+
this.pendingChanges.clear();
|
|
600
|
+
}
|
|
601
|
+
enqueue(path) {
|
|
602
|
+
this.pendingChanges.add(path);
|
|
603
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
604
|
+
this.debounceTimer = setTimeout(() => {
|
|
605
|
+
const paths = [...this.pendingChanges];
|
|
606
|
+
this.pendingChanges.clear();
|
|
607
|
+
this.emit("changed", paths);
|
|
608
|
+
}, this.debounceMs);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
export {
|
|
613
|
+
Embedder,
|
|
614
|
+
GraphBuilder,
|
|
615
|
+
VectorIndex,
|
|
616
|
+
TextSearch,
|
|
617
|
+
NoteCrud,
|
|
618
|
+
FrontmatterManager,
|
|
619
|
+
TagManager,
|
|
620
|
+
Watcher
|
|
621
|
+
};
|
|
622
|
+
//# sourceMappingURL=chunk-KF45H64M.js.map
|