@abhishekmcp/notes 0.1.0 → 0.2.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.
@@ -0,0 +1,17 @@
1
+ /** Index cache format version — bump to force a full rebuild on upgrade. */
2
+ export declare const INDEX_VERSION = 1;
3
+ /** Cache file name kept inside the notes dir (excluded from notes). */
4
+ export declare const INDEX_FILENAME = ".notes-index.json";
5
+ /** Refuse to read/index any single note larger than this (DoS / context guard). */
6
+ export declare const MAX_FILE_BYTES: number;
7
+ /**
8
+ * Resolve the notes directory from NOTES_DIR, defaulting to ~/notes.
9
+ * A leading "~" is expanded to the home directory.
10
+ */
11
+ export declare function getNotesDir(): string;
12
+ /** Absolute path to the persisted index cache. */
13
+ export declare function getIndexPath(): string;
14
+ /** When true, all mutating tools are disabled (safe for sharing a vault read-only). */
15
+ export declare function isReadOnly(): boolean;
16
+ /** When true, skip the on-disk index cache and rebuild in memory each start. */
17
+ export declare function cacheDisabled(): boolean;
package/dist/config.js ADDED
@@ -0,0 +1,32 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ /** Index cache format version — bump to force a full rebuild on upgrade. */
4
+ export const INDEX_VERSION = 1;
5
+ /** Cache file name kept inside the notes dir (excluded from notes). */
6
+ export const INDEX_FILENAME = ".notes-index.json";
7
+ /** Refuse to read/index any single note larger than this (DoS / context guard). */
8
+ export const MAX_FILE_BYTES = 5 * 1024 * 1024; // 5 MB
9
+ /**
10
+ * Resolve the notes directory from NOTES_DIR, defaulting to ~/notes.
11
+ * A leading "~" is expanded to the home directory.
12
+ */
13
+ export function getNotesDir() {
14
+ const raw = process.env.NOTES_DIR ?? path.join(homedir(), "notes");
15
+ const expanded = raw.startsWith("~")
16
+ ? path.join(homedir(), raw.slice(1))
17
+ : raw;
18
+ return path.resolve(expanded);
19
+ }
20
+ /** Absolute path to the persisted index cache. */
21
+ export function getIndexPath() {
22
+ return path.join(getNotesDir(), INDEX_FILENAME);
23
+ }
24
+ /** When true, all mutating tools are disabled (safe for sharing a vault read-only). */
25
+ export function isReadOnly() {
26
+ return process.env.NOTES_READONLY === "1";
27
+ }
28
+ /** When true, skip the on-disk index cache and rebuild in memory each start. */
29
+ export function cacheDisabled() {
30
+ return process.env.NOTES_NO_CACHE === "1";
31
+ }
32
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,4EAA4E;AAC5E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC;AAE/B,uEAAuE;AACvE,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAElD,mFAAmF;AACnF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO;AAEtD;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;QAClC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACpC,CAAC,CAAC,GAAG,CAAC;IACR,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,YAAY;IAC1B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,CAAC;AAClD,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,UAAU;IACxB,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,GAAG,CAAC;AAC5C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,GAAG,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Validate a user-supplied note name. Rejects control characters, absolute
3
+ * paths, and absurdly long names. Returns the name with a trailing ".md".
4
+ */
5
+ export declare function validateName(name: string): string;
6
+ /**
7
+ * Resolve a note name to a safe absolute path inside the notes dir, checking
8
+ * both lexical traversal (`../`) and symlink escapes. Async because the symlink
9
+ * check touches the filesystem.
10
+ */
11
+ export declare function resolveSafe(name: string): Promise<string>;
12
+ /** Read a note's raw text, guarding against oversized files. */
13
+ export declare function readRaw(absPath: string): Promise<string>;
14
+ /** Atomically write a file: write to a temp sibling, then rename into place. */
15
+ export declare function atomicWrite(absPath: string, content: string): Promise<void>;
16
+ export interface NoteFile {
17
+ name: string;
18
+ fullPath: string;
19
+ size: number;
20
+ mtimeMs: number;
21
+ }
22
+ /** Recursively list markdown note files, skipping dotfiles and the index cache. */
23
+ export declare function listNoteFiles(): Promise<NoteFile[]>;
package/dist/fsutil.js ADDED
@@ -0,0 +1,113 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { getNotesDir, INDEX_FILENAME, MAX_FILE_BYTES } from "./config.js";
4
+ /** Matches ASCII control characters (0x00–0x1f and DEL 0x7f). */
5
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
6
+ /**
7
+ * Validate a user-supplied note name. Rejects control characters, absolute
8
+ * paths, and absurdly long names. Returns the name with a trailing ".md".
9
+ */
10
+ export function validateName(name) {
11
+ if (typeof name !== "string" || name.length === 0) {
12
+ throw new Error("Note name must be a non-empty string.");
13
+ }
14
+ if (name.length > 512) {
15
+ throw new Error("Note name is too long (max 512 chars).");
16
+ }
17
+ if (CONTROL_CHARS.test(name)) {
18
+ throw new Error("Note name contains control characters.");
19
+ }
20
+ if (path.isAbsolute(name)) {
21
+ throw new Error("Note name must be relative, not an absolute path.");
22
+ }
23
+ return name.endsWith(".md") ? name : `${name}.md`;
24
+ }
25
+ /** Lexical containment check: does the resolved path stay inside the notes dir? */
26
+ function lexicallyInside(dir, resolved) {
27
+ const rel = path.relative(dir, resolved);
28
+ return rel === "" ? false : !rel.startsWith("..") && !path.isAbsolute(rel);
29
+ }
30
+ /**
31
+ * Verify, following symlinks, that `absPath` resolves inside the notes dir.
32
+ * Walks up to the nearest existing ancestor (so it works for files that don't
33
+ * exist yet), realpaths it, then re-appends the non-existing suffix lexically.
34
+ * Defeats symlinks placed *inside* the notes dir that point outside it.
35
+ */
36
+ async function realpathInside(absPath) {
37
+ const root = await fs.realpath(getNotesDir());
38
+ let cur = absPath;
39
+ // Walk up until we hit a path that exists on disk.
40
+ for (;;) {
41
+ try {
42
+ const realCur = await fs.realpath(cur);
43
+ const suffix = path.relative(cur, absPath); // "" when cur === absPath
44
+ const finalReal = suffix ? path.join(realCur, suffix) : realCur;
45
+ const rel = path.relative(root, finalReal);
46
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
47
+ }
48
+ catch (err) {
49
+ if (err.code !== "ENOENT")
50
+ throw err;
51
+ const parent = path.dirname(cur);
52
+ if (parent === cur)
53
+ return false; // reached filesystem root
54
+ cur = parent;
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * Resolve a note name to a safe absolute path inside the notes dir, checking
60
+ * both lexical traversal (`../`) and symlink escapes. Async because the symlink
61
+ * check touches the filesystem.
62
+ */
63
+ export async function resolveSafe(name) {
64
+ const dir = getNotesDir();
65
+ const resolved = path.resolve(dir, validateName(name));
66
+ if (!lexicallyInside(dir, resolved)) {
67
+ throw new Error(`Refusing to access "${name}": path escapes the notes directory.`);
68
+ }
69
+ if (!(await realpathInside(resolved))) {
70
+ throw new Error(`Refusing to access "${name}": resolves (via symlink) outside the notes directory.`);
71
+ }
72
+ return resolved;
73
+ }
74
+ /** Read a note's raw text, guarding against oversized files. */
75
+ export async function readRaw(absPath) {
76
+ const stat = await fs.stat(absPath);
77
+ if (stat.size > MAX_FILE_BYTES) {
78
+ throw new Error(`Note is too large to read (${stat.size} bytes > ${MAX_FILE_BYTES} limit).`);
79
+ }
80
+ return fs.readFile(absPath, "utf8");
81
+ }
82
+ /** Atomically write a file: write to a temp sibling, then rename into place. */
83
+ export async function atomicWrite(absPath, content) {
84
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
85
+ const tmp = `${absPath}.${process.pid}.tmp`;
86
+ await fs.writeFile(tmp, content, "utf8");
87
+ await fs.rename(tmp, absPath);
88
+ }
89
+ /** Recursively list markdown note files, skipping dotfiles and the index cache. */
90
+ export async function listNoteFiles() {
91
+ const dir = getNotesDir();
92
+ await fs.mkdir(dir, { recursive: true });
93
+ const out = [];
94
+ async function walk(current) {
95
+ const entries = await fs.readdir(current, { withFileTypes: true });
96
+ for (const entry of entries) {
97
+ if (entry.name.startsWith(".") || entry.name === INDEX_FILENAME)
98
+ continue; // skip dotfiles/cache
99
+ const full = path.join(current, entry.name);
100
+ if (entry.isDirectory()) {
101
+ await walk(full);
102
+ }
103
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
104
+ const stat = await fs.stat(full);
105
+ const rel = path.relative(dir, full).replace(/\.md$/, "").split(path.sep).join("/");
106
+ out.push({ name: rel, fullPath: full, size: stat.size, mtimeMs: stat.mtimeMs });
107
+ }
108
+ }
109
+ }
110
+ await walk(dir);
111
+ return out;
112
+ }
113
+ //# sourceMappingURL=fsutil.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fsutil.js","sourceRoot":"","sources":["../src/fsutil.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ export interface NodeRef {
2
+ name: string;
3
+ title: string;
4
+ }
5
+ /** Notes that link to `name` via [[wiki-link]]. */
6
+ export declare function getBacklinks(name: string): NodeRef[];
7
+ export interface Neighbor extends NodeRef {
8
+ distance: number;
9
+ }
10
+ /** BFS over the undirected link graph up to `depth`, capped at `limit` nodes. */
11
+ export declare function getNeighbors(name: string, depth?: number, limit?: number): Neighbor[];
12
+ /** Shortest wiki-link chain between two notes (BFS, undirected). Null if none. */
13
+ export declare function findPath(a: string, b: string): NodeRef[] | null;
14
+ export interface RelatedNote extends NodeRef {
15
+ score: number;
16
+ sharedLinks: number;
17
+ sharedTags: number;
18
+ }
19
+ /** Rank other notes by shared out-links + shared tags with `name`. */
20
+ export declare function relatedNotes(name: string, limit?: number): RelatedNote[];
21
+ export interface GraphOverview {
22
+ notes: number;
23
+ links: number;
24
+ tags: number;
25
+ brokenLinks: number;
26
+ orphans: NodeRef[];
27
+ hubs: Array<NodeRef & {
28
+ degree: number;
29
+ }>;
30
+ }
31
+ /** Aggregate graph health: counts, top hubs, orphans. */
32
+ export declare function graphOverview(hubLimit?: number, orphanLimit?: number): GraphOverview;
33
+ export interface BrokenLink {
34
+ from: string;
35
+ target: string;
36
+ }
37
+ /** Wiki-links that point at notes which don't exist. */
38
+ export declare function brokenLinks(): BrokenLink[];
package/dist/graph.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Knowledge-graph queries over the wiki-link + tag structure captured in the
3
+ * store's per-note metadata. Everything here is read-only and returns compact
4
+ * node references (name + title), never full note bodies, to stay token-cheap.
5
+ */
6
+ import { normalizeLinkTarget } from "./parse.js";
7
+ import { getAllMeta, getMeta, noteExists } from "./store.js";
8
+ const ref = (name, meta) => ({ name, title: meta?.title ?? name });
9
+ /** out: name -> existing targets it links to; in: name -> notes linking to it. */
10
+ function adjacency() {
11
+ const out = new Map();
12
+ const inc = new Map();
13
+ const meta = getAllMeta();
14
+ for (const name of meta.keys()) {
15
+ out.set(name, new Set());
16
+ inc.set(name, new Set());
17
+ }
18
+ for (const [name, m] of meta) {
19
+ for (const link of m.outLinks) {
20
+ const target = normalizeLinkTarget(link);
21
+ if (!meta.has(target) || target === name)
22
+ continue; // skip broken + self
23
+ out.get(name).add(target);
24
+ inc.get(target).add(name);
25
+ }
26
+ }
27
+ return { out, inc };
28
+ }
29
+ /** Notes that link to `name` via [[wiki-link]]. */
30
+ export function getBacklinks(name) {
31
+ const target = normalizeLinkTarget(name);
32
+ const out = [];
33
+ for (const [n, m] of getAllMeta()) {
34
+ if (n === target)
35
+ continue;
36
+ if (m.outLinks.some((l) => normalizeLinkTarget(l) === target))
37
+ out.push(ref(n, m));
38
+ }
39
+ return out.sort((a, b) => a.name.localeCompare(b.name));
40
+ }
41
+ /** BFS over the undirected link graph up to `depth`, capped at `limit` nodes. */
42
+ export function getNeighbors(name, depth = 1, limit = 50) {
43
+ const start = normalizeLinkTarget(name);
44
+ if (!noteExists(start))
45
+ return [];
46
+ const { out, inc } = adjacency();
47
+ const seen = new Set([start]);
48
+ const result = [];
49
+ let frontier = [start];
50
+ for (let d = 1; d <= depth && frontier.length; d++) {
51
+ const next = [];
52
+ for (const node of frontier) {
53
+ const around = new Set([...(out.get(node) ?? []), ...(inc.get(node) ?? [])]);
54
+ for (const nb of around) {
55
+ if (seen.has(nb))
56
+ continue;
57
+ seen.add(nb);
58
+ result.push({ ...ref(nb, getMeta(nb)), distance: d });
59
+ next.push(nb);
60
+ if (result.length >= limit)
61
+ return result;
62
+ }
63
+ }
64
+ frontier = next;
65
+ }
66
+ return result;
67
+ }
68
+ /** Shortest wiki-link chain between two notes (BFS, undirected). Null if none. */
69
+ export function findPath(a, b) {
70
+ const from = normalizeLinkTarget(a);
71
+ const to = normalizeLinkTarget(b);
72
+ if (!noteExists(from) || !noteExists(to))
73
+ return null;
74
+ if (from === to)
75
+ return [ref(from, getMeta(from))];
76
+ const { out, inc } = adjacency();
77
+ const prev = new Map();
78
+ const seen = new Set([from]);
79
+ const queue = [from];
80
+ while (queue.length) {
81
+ const node = queue.shift();
82
+ const around = new Set([...(out.get(node) ?? []), ...(inc.get(node) ?? [])]);
83
+ for (const nb of around) {
84
+ if (seen.has(nb))
85
+ continue;
86
+ seen.add(nb);
87
+ prev.set(nb, node);
88
+ if (nb === to) {
89
+ const chain = [to];
90
+ let cur = to;
91
+ while (cur !== from) {
92
+ cur = prev.get(cur);
93
+ chain.unshift(cur);
94
+ }
95
+ return chain.map((n) => ref(n, getMeta(n)));
96
+ }
97
+ queue.push(nb);
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ /** Rank other notes by shared out-links + shared tags with `name`. */
103
+ export function relatedNotes(name, limit = 10) {
104
+ const target = normalizeLinkTarget(name);
105
+ const self = getMeta(target);
106
+ if (!self)
107
+ return [];
108
+ const myLinks = new Set(self.outLinks.map(normalizeLinkTarget));
109
+ const myTags = new Set(self.tags.map((t) => t.toLowerCase()));
110
+ const scored = [];
111
+ for (const [n, m] of getAllMeta()) {
112
+ if (n === target)
113
+ continue;
114
+ const sharedLinks = m.outLinks.filter((l) => myLinks.has(normalizeLinkTarget(l))).length;
115
+ const sharedTags = m.tags.filter((t) => myTags.has(t.toLowerCase())).length;
116
+ const score = sharedLinks + sharedTags;
117
+ if (score > 0)
118
+ scored.push({ ...ref(n, m), score, sharedLinks, sharedTags });
119
+ }
120
+ return scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
121
+ }
122
+ /** Aggregate graph health: counts, top hubs, orphans. */
123
+ export function graphOverview(hubLimit = 10, orphanLimit = 25) {
124
+ const { out, inc } = adjacency();
125
+ const meta = getAllMeta();
126
+ let links = 0;
127
+ const tags = new Set();
128
+ const hubs = [];
129
+ const orphans = [];
130
+ for (const [name, m] of meta) {
131
+ for (const t of m.tags)
132
+ tags.add(t.toLowerCase());
133
+ const degree = (out.get(name)?.size ?? 0) + (inc.get(name)?.size ?? 0);
134
+ links += out.get(name)?.size ?? 0;
135
+ if (degree === 0)
136
+ orphans.push(ref(name, m));
137
+ hubs.push({ ...ref(name, m), degree });
138
+ }
139
+ return {
140
+ notes: meta.size,
141
+ links,
142
+ tags: tags.size,
143
+ brokenLinks: brokenLinks().length,
144
+ orphans: orphans.slice(0, orphanLimit),
145
+ hubs: hubs.filter((h) => h.degree > 0).sort((a, b) => b.degree - a.degree).slice(0, hubLimit),
146
+ };
147
+ }
148
+ /** Wiki-links that point at notes which don't exist. */
149
+ export function brokenLinks() {
150
+ const out = [];
151
+ for (const [name, m] of getAllMeta()) {
152
+ for (const link of m.outLinks) {
153
+ const target = normalizeLinkTarget(link);
154
+ if (target && !noteExists(target))
155
+ out.push({ from: name, target });
156
+ }
157
+ }
158
+ return out.sort((a, b) => a.from.localeCompare(b.from) || a.target.localeCompare(b.target));
159
+ }
160
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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"}