obsidian-mcp-pro 1.5.3 → 1.7.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 +8 -0
- package/build/lib/errors.d.ts +22 -2
- package/build/lib/errors.d.ts.map +1 -1
- package/build/lib/errors.js +34 -4
- package/build/lib/errors.js.map +1 -1
- package/build/lib/link-rewriter.d.ts +85 -0
- package/build/lib/link-rewriter.d.ts.map +1 -0
- package/build/lib/link-rewriter.js +381 -0
- package/build/lib/link-rewriter.js.map +1 -0
- package/build/lib/markdown.d.ts +60 -0
- package/build/lib/markdown.d.ts.map +1 -1
- package/build/lib/markdown.js +232 -26
- package/build/lib/markdown.js.map +1 -1
- package/build/lib/vault.d.ts +48 -2
- package/build/lib/vault.d.ts.map +1 -1
- package/build/lib/vault.js +104 -33
- package/build/lib/vault.js.map +1 -1
- package/build/tools/write.d.ts.map +1 -1
- package/build/tools/write.js +61 -11
- package/build/tools/write.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -462,3 +462,11 @@ MIT
|
|
|
462
462
|
Contributions welcome! Please open an issue first to discuss what you'd like to change. Pull requests without a corresponding issue may be closed.
|
|
463
463
|
|
|
464
464
|
If you're adding or editing a tool, read [docs/TOOL_AUTHORING.md](./docs/TOOL_AUTHORING.md) first — it documents the description, schema, and annotation conventions that keep every tool at A-grade quality.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Acknowledgments
|
|
469
|
+
|
|
470
|
+
- Vault-wide link rewriting on `move_note` ([#3](https://github.com/rps321321/obsidian-mcp-pro/issues/3), [#4](https://github.com/rps321321/obsidian-mcp-pro/pull/4)) and the `sanitizeError` defense-in-depth hardening contributed by [@brentkearney](https://github.com/brentkearney).
|
|
471
|
+
|
|
472
|
+
For the full list of everyone who's contributed, see the [contributors page](https://github.com/rps321321/obsidian-mcp-pro/graphs/contributors).
|
package/build/lib/errors.d.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert an unknown thrown value into a message safe to return to an MCP
|
|
3
|
-
* client. Strips absolute paths, stack traces,
|
|
4
|
-
* codes to generic human-readable text
|
|
3
|
+
* client. Strips absolute paths, stack traces, collapses known errno
|
|
4
|
+
* codes to generic human-readable text, and escapes control characters so
|
|
5
|
+
* an attacker-controlled value (e.g. a filename with `\n` in it embedded
|
|
6
|
+
* in an error message) can't break out of its line and inject text into
|
|
7
|
+
* the LLM context.
|
|
5
8
|
*/
|
|
6
9
|
export declare function sanitizeError(err: unknown): string;
|
|
10
|
+
/**
|
|
11
|
+
* Escape ASCII control characters (newlines, carriage returns, tabs, NULs,
|
|
12
|
+
* etc.) so an attacker-controlled string interpolated into a multi-line tool
|
|
13
|
+
* response can't break out of its line. `\n` becomes the two literal
|
|
14
|
+
* characters `\` and `n`; other control bytes use `\xHH`. Printable input
|
|
15
|
+
* passes through unchanged.
|
|
16
|
+
*
|
|
17
|
+
* Exists as a separate export from `sanitizeError` because that function's
|
|
18
|
+
* path-stripping step would rewrite a path-shaped value to literal `<path>`
|
|
19
|
+
* — fine inside an error message that mentions a host path, but it would
|
|
20
|
+
* erase the value when the value itself is the path you want to display
|
|
21
|
+
* (e.g. `f.path` from `failedReferrers`). Both functions apply the same
|
|
22
|
+
* control-char escape, so passing `f.path` here and `f.error` to
|
|
23
|
+
* `sanitizeError` gives equivalent injection protection through different
|
|
24
|
+
* doors.
|
|
25
|
+
*/
|
|
26
|
+
export declare function escapeControlChars(s: string): string;
|
|
7
27
|
export declare function stripPaths(s: string): string;
|
|
8
28
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AA0BA
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AA0BA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAUlD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAOpD;AAUD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK5C"}
|
package/build/lib/errors.js
CHANGED
|
@@ -19,12 +19,15 @@ const FS_ERROR_MESSAGES = {
|
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
21
|
* Convert an unknown thrown value into a message safe to return to an MCP
|
|
22
|
-
* client. Strips absolute paths, stack traces,
|
|
23
|
-
* codes to generic human-readable text
|
|
22
|
+
* client. Strips absolute paths, stack traces, collapses known errno
|
|
23
|
+
* codes to generic human-readable text, and escapes control characters so
|
|
24
|
+
* an attacker-controlled value (e.g. a filename with `\n` in it embedded
|
|
25
|
+
* in an error message) can't break out of its line and inject text into
|
|
26
|
+
* the LLM context.
|
|
24
27
|
*/
|
|
25
28
|
export function sanitizeError(err) {
|
|
26
29
|
if (typeof err === "string")
|
|
27
|
-
return stripPaths(err);
|
|
30
|
+
return escapeControlChars(stripPaths(err));
|
|
28
31
|
if (!err || typeof err !== "object")
|
|
29
32
|
return "Unknown error";
|
|
30
33
|
const e = err;
|
|
@@ -32,7 +35,34 @@ export function sanitizeError(err) {
|
|
|
32
35
|
if (code && FS_ERROR_MESSAGES[code])
|
|
33
36
|
return FS_ERROR_MESSAGES[code];
|
|
34
37
|
const msg = typeof e.message === "string" ? e.message : String(err);
|
|
35
|
-
return stripPaths(msg);
|
|
38
|
+
return escapeControlChars(stripPaths(msg));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Escape ASCII control characters (newlines, carriage returns, tabs, NULs,
|
|
42
|
+
* etc.) so an attacker-controlled string interpolated into a multi-line tool
|
|
43
|
+
* response can't break out of its line. `\n` becomes the two literal
|
|
44
|
+
* characters `\` and `n`; other control bytes use `\xHH`. Printable input
|
|
45
|
+
* passes through unchanged.
|
|
46
|
+
*
|
|
47
|
+
* Exists as a separate export from `sanitizeError` because that function's
|
|
48
|
+
* path-stripping step would rewrite a path-shaped value to literal `<path>`
|
|
49
|
+
* — fine inside an error message that mentions a host path, but it would
|
|
50
|
+
* erase the value when the value itself is the path you want to display
|
|
51
|
+
* (e.g. `f.path` from `failedReferrers`). Both functions apply the same
|
|
52
|
+
* control-char escape, so passing `f.path` here and `f.error` to
|
|
53
|
+
* `sanitizeError` gives equivalent injection protection through different
|
|
54
|
+
* doors.
|
|
55
|
+
*/
|
|
56
|
+
export function escapeControlChars(s) {
|
|
57
|
+
return s.replace(/[\x00-\x1f\x7f]/g, (c) => {
|
|
58
|
+
if (c === "\n")
|
|
59
|
+
return "\\n";
|
|
60
|
+
if (c === "\r")
|
|
61
|
+
return "\\r";
|
|
62
|
+
if (c === "\t")
|
|
63
|
+
return "\\t";
|
|
64
|
+
return `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
65
|
+
});
|
|
36
66
|
}
|
|
37
67
|
// Replace anything that looks like an absolute path with `<path>`. Covers:
|
|
38
68
|
// - POSIX: starts with `/` followed by a non-space char
|
package/build/lib/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,yEAAyE;AACzE,uEAAuE;AAEvE,MAAM,iBAAiB,GAA2B;IAChD,MAAM,EAAE,6BAA6B;IACrC,MAAM,EAAE,mBAAmB;IAC3B,KAAK,EAAE,yBAAyB;IAChC,MAAM,EAAE,qBAAqB;IAC7B,MAAM,EAAE,qBAAqB;IAC7B,OAAO,EAAE,yBAAyB;IAClC,SAAS,EAAE,wBAAwB;IACnC,KAAK,EAAE,eAAe;IACtB,MAAM,EAAE,qBAAqB;IAC7B,YAAY,EAAE,kBAAkB;CACjC,CAAC;AAOF
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,yEAAyE;AACzE,uEAAuE;AAEvE,MAAM,iBAAiB,GAA2B;IAChD,MAAM,EAAE,6BAA6B;IACrC,MAAM,EAAE,mBAAmB;IAC3B,KAAK,EAAE,yBAAyB;IAChC,MAAM,EAAE,qBAAqB;IAC7B,MAAM,EAAE,qBAAqB;IAC7B,OAAO,EAAE,yBAAyB;IAClC,SAAS,EAAE,wBAAwB;IACnC,KAAK,EAAE,eAAe;IACtB,MAAM,EAAE,qBAAqB;IAC7B,YAAY,EAAE,kBAAkB;CACjC,CAAC;AAOF;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,kBAAkB,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACxE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,eAAe,CAAC;IAE5D,MAAM,CAAC,GAAG,GAAgB,CAAC;IAC3B,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7D,IAAI,IAAI,IAAI,iBAAiB,CAAC,IAAI,CAAC;QAAE,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAEpE,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACpE,OAAO,kBAAkB,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,CAAS;IAC1C,OAAO,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC,EAAE,EAAE;QACzC,IAAI,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC7B,OAAO,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,2EAA2E;AAC3E,0DAA0D;AAC1D,gCAAgC;AAChC,+CAA+C;AAC/C,EAAE;AACF,wEAAwE;AACxE,qEAAqE;AACrE,uCAAuC;AACvC,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,OAAO,CAAC;SACL,OAAO,CAAC,oBAAoB,EAAE,QAAQ,CAAC;SACvC,OAAO,CAAC,2BAA2B,EAAE,QAAQ,CAAC;SAC9C,OAAO,CAAC,mBAAmB,EAAE,UAAU,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** A single in-place edit on a referrer file, expressed as a byte-offset slice
|
|
2
|
+
* to replace. Edits within a file are listed in increasing-offset order.
|
|
3
|
+
*
|
|
4
|
+
* `expected` captures the exact bytes at `[start, end)` at plan time. The
|
|
5
|
+
* apply step compares against current contents so a parallel `write_note`
|
|
6
|
+
* that mutated the referrer between plan and apply (offsets shift but
|
|
7
|
+
* bounds still pass) doesn't silently splice the wrong bytes. */
|
|
8
|
+
interface SpanEdit {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
expected: string;
|
|
12
|
+
replacement: string;
|
|
13
|
+
}
|
|
14
|
+
/** A complete plan of edits across all referrer files. Canvas edits are
|
|
15
|
+
* represented separately because they're applied via JSON re-serialization
|
|
16
|
+
* rather than offset substitution. */
|
|
17
|
+
export interface RewritePlan {
|
|
18
|
+
/** Markdown file edits keyed by vault-relative path. Empty map = no work. */
|
|
19
|
+
notes: Map<string, SpanEdit[]>;
|
|
20
|
+
/** Canvas files that need their `nodes[].file` field updated. */
|
|
21
|
+
canvases: string[];
|
|
22
|
+
/** Old vault-relative path being moved (with `.md` extension). */
|
|
23
|
+
oldPath: string;
|
|
24
|
+
/** New vault-relative path being moved to (with `.md` extension). */
|
|
25
|
+
newPath: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ApplyResult {
|
|
28
|
+
/** Vault-relative paths whose contents were rewritten. */
|
|
29
|
+
updated: string[];
|
|
30
|
+
/** Per-file failures encountered during apply; rename has already committed
|
|
31
|
+
* by the time this runs, so partial failures are surfaced rather than
|
|
32
|
+
* rolled back. */
|
|
33
|
+
failed: Array<{
|
|
34
|
+
path: string;
|
|
35
|
+
error: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build an edit plan describing how to update every reference to `oldPath`
|
|
40
|
+
* across the vault so it points at `newPath` instead. Pure planning — does
|
|
41
|
+
* not write to disk. Caller is expected to invoke `applyRewrites` after the
|
|
42
|
+
* rename has committed.
|
|
43
|
+
*
|
|
44
|
+
* `preMoveNotes` is the list returned by `listNotes(vaultPath)` *before* the
|
|
45
|
+
* rename. Resolution is computed against this list so a wikilink that pointed
|
|
46
|
+
* at the moved file is correctly identified, even if a different note
|
|
47
|
+
* elsewhere happens to share its basename.
|
|
48
|
+
*/
|
|
49
|
+
export declare function planMoveRewrites(vaultPath: string, oldPath: string, newPath: string, preMoveNotes: string[]): Promise<RewritePlan>;
|
|
50
|
+
/**
|
|
51
|
+
* Build an edit plan describing how to strip every reference to `deletedPath`
|
|
52
|
+
* across the vault. Pure planning — does not write to disk. Caller is
|
|
53
|
+
* expected to invoke `applyRewrites` after the deletion has committed.
|
|
54
|
+
*
|
|
55
|
+
* Replacement rules:
|
|
56
|
+
*
|
|
57
|
+
* - Wikilinks: `[[file]]` becomes the deleted file's basename (or alias if
|
|
58
|
+
* present). `![[file]]` (embed) is removed entirely since an embed has
|
|
59
|
+
* no fallback text. Fragments (`#heading`, `#^block`) are dropped — the
|
|
60
|
+
* target is gone, so the fragment is meaningless.
|
|
61
|
+
* - Markdown links: `[text](file.md)` becomes the visible text. ``
|
|
62
|
+
* embed pointing at the deleted file is removed entirely.
|
|
63
|
+
* - Canvas: not handled here. Cleaning canvas refs after delete is a
|
|
64
|
+
* separate decision (remove node? leave dangling? convert to text node?)
|
|
65
|
+
* tracked separately. The returned plan has `canvases: []`.
|
|
66
|
+
*
|
|
67
|
+
* `preDeleteNotes` is the list returned by `listNotes(vaultPath)` *before*
|
|
68
|
+
* the delete. Resolution is computed against it so a wikilink that pointed
|
|
69
|
+
* at the deleted file is correctly identified, even if a different note
|
|
70
|
+
* elsewhere happens to share its basename.
|
|
71
|
+
*/
|
|
72
|
+
export declare function planDeleteRewrites(vaultPath: string, deletedPath: string, preDeleteNotes: string[]): Promise<RewritePlan>;
|
|
73
|
+
/**
|
|
74
|
+
* Apply a previously-built `RewritePlan` to the vault. Each file is
|
|
75
|
+
* serialized through the existing per-file lock so concurrent MCP writes
|
|
76
|
+
* don't lose updates. Markdown edits are applied back-to-front to keep
|
|
77
|
+
* earlier offsets valid.
|
|
78
|
+
*
|
|
79
|
+
* Failures are accumulated, not thrown — the rename has already committed
|
|
80
|
+
* by the time we get here, so callers need to know which referrers landed
|
|
81
|
+
* and which need a manual retry.
|
|
82
|
+
*/
|
|
83
|
+
export declare function applyRewrites(vaultPath: string, plan: RewritePlan): Promise<ApplyResult>;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=link-rewriter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-rewriter.d.ts","sourceRoot":"","sources":["../../src/lib/link-rewriter.ts"],"names":[],"mappings":"AAwBA;;;;;;kEAMkE;AAClE,UAAU,QAAQ;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;uCAEuC;AACvC,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,0DAA0D;IAC1D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;uBAEmB;IACnB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,WAAW,CAAC,CAiItB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EAAE,GACvB,OAAO,CAAC,WAAW,CAAC,CAwFtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,WAAW,CAAC,CA6EtB"}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { listCanvasFiles, readNote, readCanvasFile, resolveVaultPathSafe, withFileLock, atomicWriteFile, } from "./vault.js";
|
|
3
|
+
import { extractAliases, extractWikilinkSpans, extractMarkdownLinkSpans, formatWikilinkTarget, resolveWikilink, } from "./markdown.js";
|
|
4
|
+
import { mapConcurrent } from "./concurrency.js";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
const SCAN_CONCURRENCY = 8;
|
|
7
|
+
/**
|
|
8
|
+
* Build an edit plan describing how to update every reference to `oldPath`
|
|
9
|
+
* across the vault so it points at `newPath` instead. Pure planning — does
|
|
10
|
+
* not write to disk. Caller is expected to invoke `applyRewrites` after the
|
|
11
|
+
* rename has committed.
|
|
12
|
+
*
|
|
13
|
+
* `preMoveNotes` is the list returned by `listNotes(vaultPath)` *before* the
|
|
14
|
+
* rename. Resolution is computed against this list so a wikilink that pointed
|
|
15
|
+
* at the moved file is correctly identified, even if a different note
|
|
16
|
+
* elsewhere happens to share its basename.
|
|
17
|
+
*/
|
|
18
|
+
export async function planMoveRewrites(vaultPath, oldPath, newPath, preMoveNotes) {
|
|
19
|
+
// Build the post-move note set by substituting old → new. Used for picking
|
|
20
|
+
// the output form (basename vs path) that matches Obsidian's behavior.
|
|
21
|
+
const postMoveNotes = preMoveNotes.map((n) => (n === oldPath ? newPath : n));
|
|
22
|
+
// Build an alias map from the pre-move state. Mirrors the logic in
|
|
23
|
+
// tools/links.ts so resolution behavior is identical.
|
|
24
|
+
const aliasMap = new Map();
|
|
25
|
+
await mapConcurrent(preMoveNotes, SCAN_CONCURRENCY, async (notePath) => {
|
|
26
|
+
let content;
|
|
27
|
+
try {
|
|
28
|
+
content = await readNote(vaultPath, notePath);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
for (const alias of extractAliases(content)) {
|
|
34
|
+
const key = alias.toLowerCase();
|
|
35
|
+
if (key)
|
|
36
|
+
aliasMap.set(key, notePath);
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
});
|
|
40
|
+
const notesPlan = new Map();
|
|
41
|
+
// Markdown file pass.
|
|
42
|
+
await mapConcurrent(preMoveNotes, SCAN_CONCURRENCY, async (notePath) => {
|
|
43
|
+
let content;
|
|
44
|
+
try {
|
|
45
|
+
content = await readNote(vaultPath, notePath);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
log.warn("link-rewriter: read failed during plan", {
|
|
49
|
+
note: notePath,
|
|
50
|
+
err: err,
|
|
51
|
+
});
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const edits = [];
|
|
55
|
+
// Wikilinks.
|
|
56
|
+
for (const span of extractWikilinkSpans(content)) {
|
|
57
|
+
if (!span.target)
|
|
58
|
+
continue;
|
|
59
|
+
const resolved = resolveWikilink(span.target, notePath, preMoveNotes, { aliasMap });
|
|
60
|
+
if (resolved !== oldPath)
|
|
61
|
+
continue;
|
|
62
|
+
// Don't rewrite a self-reference inside the moved file itself — the
|
|
63
|
+
// file's path is changing too, so the link's resolution travels with
|
|
64
|
+
// the new file. Leaving it untouched is the simpler invariant.
|
|
65
|
+
if (notePath === oldPath)
|
|
66
|
+
continue;
|
|
67
|
+
const newTarget = formatWikilinkTarget(newPath, span.target, postMoveNotes);
|
|
68
|
+
const aliasPart = span.alias !== undefined ? `|${span.alias}` : "";
|
|
69
|
+
const replacement = `${span.isEmbed ? "!" : ""}[[${newTarget}${span.fragment}${aliasPart}]]`;
|
|
70
|
+
// Skip no-op rewrites: a bare `[[idea]]` that resolves to the moved
|
|
71
|
+
// note may already be in the right form when the basename stays
|
|
72
|
+
// unambiguous post-move. Reporting these as "updated" would be
|
|
73
|
+
// misleading and would touch mtimes for nothing.
|
|
74
|
+
const expected = content.slice(span.start, span.end);
|
|
75
|
+
if (replacement === expected)
|
|
76
|
+
continue;
|
|
77
|
+
edits.push({ start: span.start, end: span.end, expected, replacement });
|
|
78
|
+
}
|
|
79
|
+
// Markdown `[text](url)` links. Resolve URL paths the same way wikilinks
|
|
80
|
+
// resolve, so `[x](inbox/idea.md)` and `[x](inbox/idea)` both rewrite.
|
|
81
|
+
for (const span of extractMarkdownLinkSpans(content)) {
|
|
82
|
+
if (notePath === oldPath)
|
|
83
|
+
continue;
|
|
84
|
+
const url = span.urlPath;
|
|
85
|
+
// Skip absolute / external URLs — only intra-vault references rewrite.
|
|
86
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith("/"))
|
|
87
|
+
continue;
|
|
88
|
+
const decoded = safeDecode(url);
|
|
89
|
+
const resolved = resolveWikilink(decoded, notePath, preMoveNotes, { aliasMap });
|
|
90
|
+
if (resolved !== oldPath)
|
|
91
|
+
continue;
|
|
92
|
+
// Preserve the user's choice of with/without `.md` extension and their
|
|
93
|
+
// basename-vs-path style. `formatWikilinkTarget` handles the
|
|
94
|
+
// basename-collision check so a post-move ambiguous basename falls back
|
|
95
|
+
// to the full path automatically.
|
|
96
|
+
const hadExt = /\.md$/i.test(decoded);
|
|
97
|
+
const decodedBare = decoded.replace(/\.md$/i, "");
|
|
98
|
+
const newBare = formatWikilinkTarget(newPath, decodedBare, postMoveNotes);
|
|
99
|
+
const newUrl = encodeUrlPath(hadExt ? `${newBare}.md` : newBare);
|
|
100
|
+
const replacement = `${span.isEmbed ? "!" : ""}[${span.text}](${newUrl}${span.fragment}${span.title})`;
|
|
101
|
+
const expected = content.slice(span.start, span.end);
|
|
102
|
+
if (replacement === expected)
|
|
103
|
+
continue;
|
|
104
|
+
edits.push({ start: span.start, end: span.end, expected, replacement });
|
|
105
|
+
}
|
|
106
|
+
if (edits.length > 0) {
|
|
107
|
+
// Edits within a file may have been produced in interleaved order
|
|
108
|
+
// (wikilinks then markdown links). Sort ascending so the apply step can
|
|
109
|
+
// walk back-to-front cleanly.
|
|
110
|
+
edits.sort((a, b) => a.start - b.start);
|
|
111
|
+
notesPlan.set(notePath, edits);
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
});
|
|
115
|
+
// Canvas pass — `nodes[].file` is the structured equivalent of a wikilink,
|
|
116
|
+
// and Obsidian stores the vault-relative path verbatim. Match by exact
|
|
117
|
+
// (case-insensitive) path equality on the old path.
|
|
118
|
+
const canvasPaths = await listCanvasFiles(vaultPath);
|
|
119
|
+
const oldLower = oldPath.toLowerCase();
|
|
120
|
+
const canvasesToRewrite = [];
|
|
121
|
+
await mapConcurrent(canvasPaths, SCAN_CONCURRENCY, async (cp) => {
|
|
122
|
+
let data;
|
|
123
|
+
try {
|
|
124
|
+
data = await readCanvasFile(vaultPath, cp);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
log.warn("link-rewriter: canvas read failed during plan", {
|
|
128
|
+
note: cp,
|
|
129
|
+
err: err,
|
|
130
|
+
});
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
for (const node of data.nodes) {
|
|
134
|
+
if (typeof node.file === "string" && node.file.toLowerCase() === oldLower) {
|
|
135
|
+
canvasesToRewrite.push(cp);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
notes: notesPlan,
|
|
143
|
+
canvases: canvasesToRewrite,
|
|
144
|
+
oldPath,
|
|
145
|
+
newPath,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Build an edit plan describing how to strip every reference to `deletedPath`
|
|
150
|
+
* across the vault. Pure planning — does not write to disk. Caller is
|
|
151
|
+
* expected to invoke `applyRewrites` after the deletion has committed.
|
|
152
|
+
*
|
|
153
|
+
* Replacement rules:
|
|
154
|
+
*
|
|
155
|
+
* - Wikilinks: `[[file]]` becomes the deleted file's basename (or alias if
|
|
156
|
+
* present). `![[file]]` (embed) is removed entirely since an embed has
|
|
157
|
+
* no fallback text. Fragments (`#heading`, `#^block`) are dropped — the
|
|
158
|
+
* target is gone, so the fragment is meaningless.
|
|
159
|
+
* - Markdown links: `[text](file.md)` becomes the visible text. ``
|
|
160
|
+
* embed pointing at the deleted file is removed entirely.
|
|
161
|
+
* - Canvas: not handled here. Cleaning canvas refs after delete is a
|
|
162
|
+
* separate decision (remove node? leave dangling? convert to text node?)
|
|
163
|
+
* tracked separately. The returned plan has `canvases: []`.
|
|
164
|
+
*
|
|
165
|
+
* `preDeleteNotes` is the list returned by `listNotes(vaultPath)` *before*
|
|
166
|
+
* the delete. Resolution is computed against it so a wikilink that pointed
|
|
167
|
+
* at the deleted file is correctly identified, even if a different note
|
|
168
|
+
* elsewhere happens to share its basename.
|
|
169
|
+
*/
|
|
170
|
+
export async function planDeleteRewrites(vaultPath, deletedPath, preDeleteNotes) {
|
|
171
|
+
// Build alias map from the pre-delete state. Same logic as planMoveRewrites.
|
|
172
|
+
const aliasMap = new Map();
|
|
173
|
+
await mapConcurrent(preDeleteNotes, SCAN_CONCURRENCY, async (notePath) => {
|
|
174
|
+
let content;
|
|
175
|
+
try {
|
|
176
|
+
content = await readNote(vaultPath, notePath);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
for (const alias of extractAliases(content)) {
|
|
182
|
+
const key = alias.toLowerCase();
|
|
183
|
+
if (key)
|
|
184
|
+
aliasMap.set(key, notePath);
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
});
|
|
188
|
+
const notesPlan = new Map();
|
|
189
|
+
// Fallback display text for wikilinks without an alias: the basename of
|
|
190
|
+
// the deleted file, without extension or path. Matches what Obsidian would
|
|
191
|
+
// render for a now-broken bare wikilink.
|
|
192
|
+
const deletedBasename = deletedPath.replace(/\.md$/i, "").split("/").pop() ??
|
|
193
|
+
deletedPath.replace(/\.md$/i, "");
|
|
194
|
+
await mapConcurrent(preDeleteNotes, SCAN_CONCURRENCY, async (notePath) => {
|
|
195
|
+
// Skip the file being deleted itself — it's about to disappear, so
|
|
196
|
+
// editing its body is wasted work (and the FS write would race the
|
|
197
|
+
// unlink). The `removeReferences` path only edits *other* notes.
|
|
198
|
+
if (notePath === deletedPath)
|
|
199
|
+
return undefined;
|
|
200
|
+
let content;
|
|
201
|
+
try {
|
|
202
|
+
content = await readNote(vaultPath, notePath);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
log.warn("link-rewriter: read failed during delete plan", {
|
|
206
|
+
note: notePath,
|
|
207
|
+
err: err,
|
|
208
|
+
});
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
const edits = [];
|
|
212
|
+
// Wikilinks.
|
|
213
|
+
for (const span of extractWikilinkSpans(content)) {
|
|
214
|
+
if (!span.target)
|
|
215
|
+
continue;
|
|
216
|
+
const resolved = resolveWikilink(span.target, notePath, preDeleteNotes, { aliasMap });
|
|
217
|
+
if (resolved !== deletedPath)
|
|
218
|
+
continue;
|
|
219
|
+
// Embeds disappear entirely; plain wikilinks fall back to alias-or-basename.
|
|
220
|
+
const replacement = span.isEmbed
|
|
221
|
+
? ""
|
|
222
|
+
: (span.alias !== undefined ? span.alias : deletedBasename);
|
|
223
|
+
const expected = content.slice(span.start, span.end);
|
|
224
|
+
if (replacement === expected)
|
|
225
|
+
continue;
|
|
226
|
+
edits.push({ start: span.start, end: span.end, expected, replacement });
|
|
227
|
+
}
|
|
228
|
+
// Markdown `[text](url)` links.
|
|
229
|
+
for (const span of extractMarkdownLinkSpans(content)) {
|
|
230
|
+
const url = span.urlPath;
|
|
231
|
+
// Skip absolute / external URLs — only intra-vault refs strip.
|
|
232
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith("/"))
|
|
233
|
+
continue;
|
|
234
|
+
const decoded = safeDecode(url);
|
|
235
|
+
const resolved = resolveWikilink(decoded, notePath, preDeleteNotes, { aliasMap });
|
|
236
|
+
if (resolved !== deletedPath)
|
|
237
|
+
continue;
|
|
238
|
+
const replacement = span.isEmbed ? "" : span.text;
|
|
239
|
+
const expected = content.slice(span.start, span.end);
|
|
240
|
+
if (replacement === expected)
|
|
241
|
+
continue;
|
|
242
|
+
edits.push({ start: span.start, end: span.end, expected, replacement });
|
|
243
|
+
}
|
|
244
|
+
if (edits.length > 0) {
|
|
245
|
+
edits.sort((a, b) => a.start - b.start);
|
|
246
|
+
notesPlan.set(notePath, edits);
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
notes: notesPlan,
|
|
252
|
+
canvases: [],
|
|
253
|
+
oldPath: deletedPath,
|
|
254
|
+
newPath: "", // unused by applyRewrites when canvases is empty
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Apply a previously-built `RewritePlan` to the vault. Each file is
|
|
259
|
+
* serialized through the existing per-file lock so concurrent MCP writes
|
|
260
|
+
* don't lose updates. Markdown edits are applied back-to-front to keep
|
|
261
|
+
* earlier offsets valid.
|
|
262
|
+
*
|
|
263
|
+
* Failures are accumulated, not thrown — the rename has already committed
|
|
264
|
+
* by the time we get here, so callers need to know which referrers landed
|
|
265
|
+
* and which need a manual retry.
|
|
266
|
+
*/
|
|
267
|
+
export async function applyRewrites(vaultPath, plan) {
|
|
268
|
+
const updated = [];
|
|
269
|
+
const failed = [];
|
|
270
|
+
// Markdown pass.
|
|
271
|
+
await mapConcurrent(Array.from(plan.notes.keys()), SCAN_CONCURRENCY, async (notePath) => {
|
|
272
|
+
const edits = plan.notes.get(notePath);
|
|
273
|
+
if (!edits || edits.length === 0)
|
|
274
|
+
return undefined;
|
|
275
|
+
try {
|
|
276
|
+
const fullPath = await resolveVaultPathSafe(vaultPath, notePath);
|
|
277
|
+
let didWrite = false;
|
|
278
|
+
await withFileLock(fullPath, async () => {
|
|
279
|
+
// Re-read inside the lock so we apply edits to current content.
|
|
280
|
+
// applyEditsBackToFront verifies each edit's `expected` slice
|
|
281
|
+
// matches before splicing — a parallel `write_note` that landed
|
|
282
|
+
// between plan and apply will fail the comparison and we report
|
|
283
|
+
// the file rather than corrupting it.
|
|
284
|
+
const current = await fs.readFile(fullPath, "utf-8");
|
|
285
|
+
const next = applyEditsBackToFront(current, edits);
|
|
286
|
+
if (next === null) {
|
|
287
|
+
failed.push({
|
|
288
|
+
path: notePath,
|
|
289
|
+
error: "content changed during move; references not updated",
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (next !== current) {
|
|
294
|
+
await atomicWriteFile(fullPath, next);
|
|
295
|
+
didWrite = true;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
if (didWrite)
|
|
299
|
+
updated.push(notePath);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
failed.push({ path: notePath, error: err.message });
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
});
|
|
306
|
+
// Canvas pass.
|
|
307
|
+
for (const cp of plan.canvases) {
|
|
308
|
+
try {
|
|
309
|
+
const fullPath = await resolveVaultPathSafe(vaultPath, cp);
|
|
310
|
+
let didWrite = false;
|
|
311
|
+
await withFileLock(fullPath, async () => {
|
|
312
|
+
const raw = await fs.readFile(fullPath, "utf-8");
|
|
313
|
+
let parsed;
|
|
314
|
+
try {
|
|
315
|
+
parsed = JSON.parse(raw);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
throw new Error(`Invalid canvas file (malformed JSON): ${cp}`);
|
|
319
|
+
}
|
|
320
|
+
const obj = (parsed && typeof parsed === "object" ? parsed : {});
|
|
321
|
+
const nodes = Array.isArray(obj.nodes) ? obj.nodes : [];
|
|
322
|
+
let mutated = false;
|
|
323
|
+
const oldLower = plan.oldPath.toLowerCase();
|
|
324
|
+
for (const node of nodes) {
|
|
325
|
+
if (typeof node.file === "string" && node.file.toLowerCase() === oldLower) {
|
|
326
|
+
node.file = plan.newPath;
|
|
327
|
+
mutated = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (mutated) {
|
|
331
|
+
await atomicWriteFile(fullPath, JSON.stringify({ ...obj, nodes }, null, 2));
|
|
332
|
+
didWrite = true;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
if (didWrite)
|
|
336
|
+
updated.push(cp);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
failed.push({ path: cp, error: err.message });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return { updated, failed };
|
|
343
|
+
}
|
|
344
|
+
/** Apply pre-sorted edits back-to-front. Returns `null` if any edit's span
|
|
345
|
+
* is out of bounds or if the bytes at `[start, end)` don't match the edit's
|
|
346
|
+
* `expected` slice — which means the file was modified between plan and
|
|
347
|
+
* apply (offsets shifted, or the link itself was rewritten). Caller treats
|
|
348
|
+
* `null` as a failed referrer rather than corrupting the file. */
|
|
349
|
+
function applyEditsBackToFront(content, edits) {
|
|
350
|
+
// Walk from the back so earlier offsets stay valid as we splice.
|
|
351
|
+
let out = content;
|
|
352
|
+
for (let i = edits.length - 1; i >= 0; i--) {
|
|
353
|
+
const e = edits[i];
|
|
354
|
+
if (e.start < 0 || e.end > out.length || e.start > e.end) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
if (out.slice(e.start, e.end) !== e.expected) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
out = out.slice(0, e.start) + e.replacement + out.slice(e.end);
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
/** Tolerant `decodeURIComponent`: returns the input unchanged on malformed
|
|
365
|
+
* percent-escapes rather than throwing. Vault paths sometimes contain raw
|
|
366
|
+
* spaces or characters that aren't valid URI components. */
|
|
367
|
+
function safeDecode(s) {
|
|
368
|
+
try {
|
|
369
|
+
return decodeURIComponent(s);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return s;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** Encode a vault-relative path for use inside a markdown link URL. Keeps
|
|
376
|
+
* forward slashes literal (they're path separators) while escaping spaces
|
|
377
|
+
* and other characters that would break the `(...)` parser. */
|
|
378
|
+
function encodeUrlPath(p) {
|
|
379
|
+
return p.split("/").map(encodeURIComponent).join("/");
|
|
380
|
+
}
|
|
381
|
+
//# sourceMappingURL=link-rewriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-rewriter.js","sourceRoot":"","sources":["../../src/lib/link-rewriter.ts"],"names":[],"mappings":"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"}
|