pathnote 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/vault.js DELETED
@@ -1,109 +0,0 @@
1
- // Read and write markdown notes inside one folder. Every path that comes from the AI is
2
- // resolved against the vault root and refused if it points outside it or isn't a .md file.
3
- import fs from 'node:fs/promises';
4
- import path from 'node:path';
5
-
6
- const SKIP = new Set(['.git', '.obsidian', '.trash', 'node_modules']);
7
- const LINK = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]/g;
8
-
9
- export class Vault {
10
- constructor(root) {
11
- this.root = path.resolve(root);
12
- }
13
-
14
- // "Notes/Plan" or "Notes/Plan.md" -> absolute path, or throw if it escapes the vault
15
- resolve(rel) {
16
- let p = String(rel || '').trim().replace(/\\/g, '/');
17
- if (!p) throw new Error('Note path is empty.');
18
- if (!p.toLowerCase().endsWith('.md')) p += '.md';
19
- const abs = path.resolve(this.root, p);
20
- if (abs !== this.root && !abs.startsWith(this.root + path.sep)) throw new Error(`"${rel}" is outside the vault.`);
21
- if (abs.split(path.sep).some(part => SKIP.has(part))) throw new Error(`"${rel}" is in a folder Pathnote does not touch.`);
22
- return abs;
23
- }
24
-
25
- // Same check after following symlinks, so a link inside the vault can't lead outside it
26
- async real(rel) {
27
- const abs = this.resolve(rel);
28
- const root = await fs.realpath(this.root);
29
- let dir = abs;
30
- for (;;) {
31
- try { dir = await fs.realpath(dir); break; } catch (e) { if (e.code !== 'ENOENT') throw e; dir = path.dirname(dir); }
32
- }
33
- if (dir !== root && !dir.startsWith(root + path.sep)) throw new Error(`"${rel}" is outside the vault.`);
34
- return abs;
35
- }
36
-
37
- rel(abs) {
38
- return path.relative(this.root, abs).split(path.sep).join('/');
39
- }
40
-
41
- async list() {
42
- const out = [];
43
- const walk = async dir => {
44
- for (const e of await fs.readdir(dir, { withFileTypes: true })) {
45
- if (e.name.startsWith('.') || SKIP.has(e.name)) continue;
46
- const abs = path.join(dir, e.name);
47
- if (e.isDirectory()) await walk(abs);
48
- else if (e.name.toLowerCase().endsWith('.md')) out.push(this.rel(abs));
49
- }
50
- };
51
- await walk(this.root);
52
- return out.sort();
53
- }
54
-
55
- async read(rel) {
56
- return fs.readFile(await this.real(rel), 'utf8');
57
- }
58
-
59
- // Current text, or null when the note doesn't exist yet
60
- async readOrNull(rel) {
61
- try { return await this.read(rel); } catch (e) { if (e.code === 'ENOENT') return null; throw e; }
62
- }
63
-
64
- async write(rel, text) {
65
- const abs = await this.real(rel);
66
- await fs.mkdir(path.dirname(abs), { recursive: true });
67
- await fs.writeFile(abs, text, 'utf8');
68
- }
69
-
70
- async remove(rel) {
71
- await fs.rm(await this.real(rel), { force: true });
72
- }
73
-
74
- // Notes ranked by how often the query's words appear, title matches counted extra
75
- async search(query, limit = 10) {
76
- const words = String(query).toLowerCase().split(/\s+/).filter(Boolean);
77
- if (!words.length) return [];
78
- const hits = [];
79
- for (const rel of await this.list()) {
80
- const text = await this.read(rel);
81
- const lower = text.toLowerCase(), name = rel.toLowerCase();
82
- let score = 0;
83
- for (const w of words) {
84
- score += lower.split(w).length - 1;
85
- if (name.includes(w)) score += 5;
86
- }
87
- if (!score) continue;
88
- const at = Math.max(0, lower.indexOf(words.find(w => lower.includes(w)) ?? words[0]));
89
- const snippet = text.slice(Math.max(0, at - 60), at + 140).replace(/\s+/g, ' ').trim();
90
- hits.push({ path: rel, score, snippet });
91
- }
92
- return hits.sort((a, b) => b.score - a.score).slice(0, limit);
93
- }
94
-
95
- // Nodes and [[wikilink]] edges for the viewer. Links resolve by file name, like Obsidian.
96
- async graph() {
97
- const notes = await this.list();
98
- const byName = new Map(notes.map(n => [path.posix.basename(n, '.md').toLowerCase(), n]));
99
- const edges = [];
100
- for (const n of notes) {
101
- const text = await this.read(n);
102
- for (const m of text.matchAll(LINK)) {
103
- const target = byName.get(path.posix.basename(m[1].trim()).toLowerCase());
104
- if (target && target !== n) edges.push([n, target]);
105
- }
106
- }
107
- return { nodes: notes, edges };
108
- }
109
- }