@openparachute/vault 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/CHANGELOG.md +87 -0
- package/CLAUDE.md +2 -2
- package/README.md +289 -44
- package/core/src/core.test.ts +802 -346
- package/core/src/expand.ts +140 -0
- package/core/src/hooks.test.ts +27 -27
- package/core/src/hooks.ts +1 -1
- package/core/src/mcp.ts +102 -39
- package/core/src/notes.ts +82 -4
- package/core/src/obsidian.test.ts +11 -11
- package/core/src/paths.test.ts +46 -46
- package/core/src/schema.ts +18 -2
- package/core/src/store.ts +51 -51
- package/core/src/types.ts +29 -29
- package/core/src/wikilinks.test.ts +61 -61
- package/docs/HTTP_API.md +4 -2
- package/package.json +1 -1
- package/src/auth.test.ts +319 -0
- package/src/backup-launchd.test.ts +90 -0
- package/src/backup-launchd.ts +169 -0
- package/src/backup.test.ts +715 -0
- package/src/backup.ts +699 -0
- package/src/cli.ts +923 -31
- package/src/config.test.ts +173 -0
- package/src/config.ts +345 -15
- package/src/daemon.ts +136 -0
- package/src/doctor.test.ts +356 -0
- package/src/health.test.ts +201 -0
- package/src/health.ts +115 -0
- package/src/launchd.test.ts +91 -0
- package/src/launchd.ts +37 -40
- package/src/mcp-http.ts +1 -1
- package/src/mcp-tools.ts +7 -9
- package/src/oauth.test.ts +289 -8
- package/src/oauth.ts +66 -13
- package/src/published.test.ts +21 -21
- package/src/routes.ts +152 -70
- package/src/routing.test.ts +478 -0
- package/src/routing.ts +413 -0
- package/src/server.ts +7 -278
- package/src/systemd.test.ts +15 -0
- package/src/systemd.ts +18 -11
- package/src/triggers.test.ts +7 -7
- package/src/triggers.ts +6 -6
- package/src/vault-store.ts +20 -3
- package/src/vault.test.ts +356 -262
- package/.claude/settings.local.json +0 -31
- package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +0 -2
- package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +0 -1
- package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +0 -2
- package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +0 -2
- package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +0 -1
- package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +0 -1
- package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +0 -211
- package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +0 -59
- package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +0 -232
- package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +0 -182
- package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +0 -91
- package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +0 -70
- package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +0 -59
- package/religions-abrahamic-filter.png +0 -0
- package/religions-buddhism-v2.png +0 -0
- package/religions-buddhism.png +0 -0
- package/religions-final.png +0 -0
- package/religions-v1.png +0 -0
- package/religions-v2.png +0 -0
- package/religions-zen.png +0 -0
- package/web/README.md +0 -73
- package/web/bun.lock +0 -827
- package/web/eslint.config.js +0 -23
- package/web/index.html +0 -15
- package/web/package.json +0 -36
- package/web/public/favicon.svg +0 -1
- package/web/public/icons.svg +0 -24
- package/web/src/App.tsx +0 -149
- package/web/src/Graph.tsx +0 -200
- package/web/src/NoteView.tsx +0 -155
- package/web/src/Sidebar.tsx +0 -186
- package/web/src/api.ts +0 -21
- package/web/src/index.css +0 -50
- package/web/src/main.tsx +0 -10
- package/web/src/types.ts +0 -37
- package/web/src/utils.ts +0 -107
- package/web/tsconfig.app.json +0 -25
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -24
- package/web/vite.config.ts +0 -15
package/src/published.test.ts
CHANGED
|
@@ -57,18 +57,18 @@ function makeStore(notes: Record<string, { content: string; tags?: string[]; met
|
|
|
57
57
|
} as any;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
describe("handleViewNote", () => {
|
|
61
|
-
it("returns 404 for non-existent note", () => {
|
|
60
|
+
describe("handleViewNote", async () => {
|
|
61
|
+
it("returns 404 for non-existent note", async () => {
|
|
62
62
|
const store = makeStore({});
|
|
63
|
-
const resp = handleViewNote(store, "missing");
|
|
63
|
+
const resp = await handleViewNote(store, "missing");
|
|
64
64
|
expect(resp.status).toBe(404);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
it("returns 404 for note without publish tag (unauthenticated)", () => {
|
|
67
|
+
it("returns 404 for note without publish tag (unauthenticated)", async () => {
|
|
68
68
|
const store = makeStore({
|
|
69
69
|
"n1": { content: "hello", tags: ["other"] },
|
|
70
70
|
});
|
|
71
|
-
const resp = handleViewNote(store, "n1");
|
|
71
|
+
const resp = await handleViewNote(store, "n1");
|
|
72
72
|
expect(resp.status).toBe(404);
|
|
73
73
|
});
|
|
74
74
|
|
|
@@ -76,7 +76,7 @@ describe("handleViewNote", () => {
|
|
|
76
76
|
const store = makeStore({
|
|
77
77
|
"n1": { content: "# Hello\n\nWorld", tags: ["publish"], path: "Blog/My Post.md" },
|
|
78
78
|
});
|
|
79
|
-
const resp = handleViewNote(store, "n1");
|
|
79
|
+
const resp = await handleViewNote(store, "n1");
|
|
80
80
|
expect(resp.status).toBe(200);
|
|
81
81
|
expect(resp.headers.get("Content-Type")).toBe("text/html; charset=utf-8");
|
|
82
82
|
|
|
@@ -90,7 +90,7 @@ describe("handleViewNote", () => {
|
|
|
90
90
|
const store = makeStore({
|
|
91
91
|
"n2": { content: "Content here", metadata: { published: true } },
|
|
92
92
|
});
|
|
93
|
-
const resp = handleViewNote(store, "n2");
|
|
93
|
+
const resp = await handleViewNote(store, "n2");
|
|
94
94
|
expect(resp.status).toBe(200);
|
|
95
95
|
const html = await resp.text();
|
|
96
96
|
expect(html).toContain("<p>Content here</p>");
|
|
@@ -103,7 +103,7 @@ describe("handleViewNote", () => {
|
|
|
103
103
|
tags: ["publish"],
|
|
104
104
|
},
|
|
105
105
|
});
|
|
106
|
-
const resp = handleViewNote(store, "n3");
|
|
106
|
+
const resp = await handleViewNote(store, "n3");
|
|
107
107
|
const html = await resp.text();
|
|
108
108
|
expect(html).toContain("<strong>bold</strong>");
|
|
109
109
|
expect(html).toContain("<em>italic</em>");
|
|
@@ -117,7 +117,7 @@ describe("handleViewNote", () => {
|
|
|
117
117
|
const store = makeStore({
|
|
118
118
|
"n4": { content: "<script>alert('xss')</script>", tags: ["publish"] },
|
|
119
119
|
});
|
|
120
|
-
const resp = handleViewNote(store, "n4");
|
|
120
|
+
const resp = await handleViewNote(store, "n4");
|
|
121
121
|
const html = await resp.text();
|
|
122
122
|
expect(html).not.toContain("<script>");
|
|
123
123
|
expect(html).toContain("<script>");
|
|
@@ -127,7 +127,7 @@ describe("handleViewNote", () => {
|
|
|
127
127
|
const store = makeStore({
|
|
128
128
|
"n6": { content: "[click me](javascript:alert(1))", tags: ["publish"] },
|
|
129
129
|
});
|
|
130
|
-
const resp = handleViewNote(store, "n6");
|
|
130
|
+
const resp = await handleViewNote(store, "n6");
|
|
131
131
|
const html = await resp.text();
|
|
132
132
|
expect(html).not.toContain("javascript:");
|
|
133
133
|
expect(html).toContain("click me");
|
|
@@ -138,7 +138,7 @@ describe("handleViewNote", () => {
|
|
|
138
138
|
const store = makeStore({
|
|
139
139
|
"n7": { content: "[click](data:text/html;base64,PHNjcmlwdD4=)", tags: ["publish"] },
|
|
140
140
|
});
|
|
141
|
-
const resp = handleViewNote(store, "n7");
|
|
141
|
+
const resp = await handleViewNote(store, "n7");
|
|
142
142
|
const html = await resp.text();
|
|
143
143
|
expect(html).not.toContain("data:");
|
|
144
144
|
expect(html).toContain("click");
|
|
@@ -148,7 +148,7 @@ describe("handleViewNote", () => {
|
|
|
148
148
|
const store = makeStore({
|
|
149
149
|
"n8": { content: "[site](https://example.com) and [mail](mailto:a@b.com)", tags: ["publish"] },
|
|
150
150
|
});
|
|
151
|
-
const resp = handleViewNote(store, "n8");
|
|
151
|
+
const resp = await handleViewNote(store, "n8");
|
|
152
152
|
const html = await resp.text();
|
|
153
153
|
expect(html).toContain('href="https://example.com"');
|
|
154
154
|
expect(html).toContain('href="mailto:a@b.com"');
|
|
@@ -158,17 +158,17 @@ describe("handleViewNote", () => {
|
|
|
158
158
|
const store = makeStore({
|
|
159
159
|
"n5": { content: "test", tags: ["publish"] },
|
|
160
160
|
});
|
|
161
|
-
const resp = handleViewNote(store, "n5");
|
|
161
|
+
const resp = await handleViewNote(store, "n5");
|
|
162
162
|
const html = await resp.text();
|
|
163
163
|
expect(html).toContain("prefers-color-scheme: dark");
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
// CSP header
|
|
167
|
-
it("includes Content-Security-Policy header", () => {
|
|
167
|
+
it("includes Content-Security-Policy header", async () => {
|
|
168
168
|
const store = makeStore({
|
|
169
169
|
"n1": { content: "test", tags: ["publish"] },
|
|
170
170
|
});
|
|
171
|
-
const resp = handleViewNote(store, "n1");
|
|
171
|
+
const resp = await handleViewNote(store, "n1");
|
|
172
172
|
const csp = resp.headers.get("Content-Security-Policy");
|
|
173
173
|
expect(csp).toContain("script-src 'none'");
|
|
174
174
|
expect(csp).toContain("default-src 'self'");
|
|
@@ -179,17 +179,17 @@ describe("handleViewNote", () => {
|
|
|
179
179
|
const store = makeStore({
|
|
180
180
|
"private": { content: "secret stuff", tags: ["internal"] },
|
|
181
181
|
});
|
|
182
|
-
const resp = handleViewNote(store, "private", { authenticated: true });
|
|
182
|
+
const resp = await handleViewNote(store, "private", { authenticated: true });
|
|
183
183
|
expect(resp.status).toBe(200);
|
|
184
184
|
const html = await resp.text();
|
|
185
185
|
expect(html).toContain("<p>secret stuff</p>");
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
it("returns 404 for unpublished note when not authenticated", () => {
|
|
188
|
+
it("returns 404 for unpublished note when not authenticated", async () => {
|
|
189
189
|
const store = makeStore({
|
|
190
190
|
"private": { content: "secret stuff", tags: ["internal"] },
|
|
191
191
|
});
|
|
192
|
-
const resp = handleViewNote(store, "private");
|
|
192
|
+
const resp = await handleViewNote(store, "private");
|
|
193
193
|
expect(resp.status).toBe(404);
|
|
194
194
|
});
|
|
195
195
|
|
|
@@ -198,17 +198,17 @@ describe("handleViewNote", () => {
|
|
|
198
198
|
const store = makeStore({
|
|
199
199
|
"n1": { content: "public content", tags: ["public"] },
|
|
200
200
|
});
|
|
201
|
-
const resp = handleViewNote(store, "n1", { publishedTag: "public" });
|
|
201
|
+
const resp = await handleViewNote(store, "n1", { publishedTag: "public" });
|
|
202
202
|
expect(resp.status).toBe(200);
|
|
203
203
|
const html = await resp.text();
|
|
204
204
|
expect(html).toContain("<p>public content</p>");
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
it("rejects note with default tag when custom tag is configured", () => {
|
|
207
|
+
it("rejects note with default tag when custom tag is configured", async () => {
|
|
208
208
|
const store = makeStore({
|
|
209
209
|
"n1": { content: "content", tags: ["publish"] },
|
|
210
210
|
});
|
|
211
|
-
const resp = handleViewNote(store, "n1", { publishedTag: "public" });
|
|
211
|
+
const resp = await handleViewNote(store, "n1", { publishedTag: "public" });
|
|
212
212
|
expect(resp.status).toBe(404);
|
|
213
213
|
});
|
|
214
214
|
});
|
package/src/routes.ts
CHANGED
|
@@ -16,6 +16,13 @@ import { listUnresolvedWikilinks } from "../core/src/wikilinks.ts";
|
|
|
16
16
|
import { toNoteIndex, filterMetadata } from "../core/src/notes.ts";
|
|
17
17
|
import * as linkOps from "../core/src/links.ts";
|
|
18
18
|
import * as tagSchemaOps from "../core/src/tag-schemas.ts";
|
|
19
|
+
import {
|
|
20
|
+
expandContent,
|
|
21
|
+
DEFAULT_EXPAND_DEPTH,
|
|
22
|
+
MAX_EXPAND_DEPTH,
|
|
23
|
+
type ExpandContext,
|
|
24
|
+
type ExpandMode,
|
|
25
|
+
} from "../core/src/expand.ts";
|
|
19
26
|
import { join, extname, normalize } from "path";
|
|
20
27
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
21
28
|
import { vaultDir } from "./config.ts";
|
|
@@ -65,18 +72,39 @@ function parseIncludeMetadata(url: URL): boolean | string[] | undefined {
|
|
|
65
72
|
return fields;
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Parse expand_links/expand_depth/expand_mode from query params, returning
|
|
77
|
+
* an (ExpandContext, depth) pair if expansion is requested, else null.
|
|
78
|
+
*/
|
|
79
|
+
function parseExpandParams(
|
|
80
|
+
url: URL,
|
|
81
|
+
db: any,
|
|
82
|
+
): { ctx: ExpandContext; depth: number } | null {
|
|
83
|
+
if (!parseBool(parseQuery(url, "expand_links"), false)) return null;
|
|
84
|
+
const modeRaw = parseQuery(url, "expand_mode");
|
|
85
|
+
const mode: ExpandMode = modeRaw === "summary" ? "summary" : "full";
|
|
86
|
+
const depth = Math.max(
|
|
87
|
+
0,
|
|
88
|
+
Math.min(
|
|
89
|
+
parseInt10(parseQuery(url, "expand_depth")) ?? DEFAULT_EXPAND_DEPTH,
|
|
90
|
+
MAX_EXPAND_DEPTH,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
return { ctx: { db, mode, expanded: new Set() }, depth };
|
|
94
|
+
}
|
|
95
|
+
|
|
68
96
|
|
|
69
97
|
/**
|
|
70
98
|
* Resolve a note by ID or path. Tries ID first, then case-insensitive path.
|
|
71
99
|
*/
|
|
72
|
-
function resolveNote(store: Store, idOrPath: string): Note | null {
|
|
73
|
-
const byId = store.getNote(idOrPath);
|
|
100
|
+
async function resolveNote(store: Store, idOrPath: string): Promise<Note | null> {
|
|
101
|
+
const byId = await store.getNote(idOrPath);
|
|
74
102
|
if (byId) return byId;
|
|
75
|
-
return store.getNoteByPath(idOrPath);
|
|
103
|
+
return await store.getNoteByPath(idOrPath);
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
function requireNote(store: Store, idOrPath: string): Note {
|
|
79
|
-
const note = resolveNote(store, idOrPath);
|
|
106
|
+
async function requireNote(store: Store, idOrPath: string): Promise<Note> {
|
|
107
|
+
const note = await resolveNote(store, idOrPath);
|
|
80
108
|
if (!note) throw new NotFoundError(`Note not found: "${idOrPath}"`);
|
|
81
109
|
return note;
|
|
82
110
|
}
|
|
@@ -110,16 +138,21 @@ export async function handleNotes(
|
|
|
110
138
|
|
|
111
139
|
// Single note by id/path
|
|
112
140
|
if (id) {
|
|
113
|
-
const note = resolveNote(store, id);
|
|
141
|
+
const note = await resolveNote(store, id);
|
|
114
142
|
if (!note) return json({ error: "Note not found", id }, 404);
|
|
115
143
|
const includeContent = parseBool(parseQuery(url, "include_content"), true);
|
|
116
144
|
let result: any = includeContent ? { ...note } : toNoteIndex(note);
|
|
145
|
+
const expand = parseExpandParams(url, db);
|
|
146
|
+
if (expand && includeContent && typeof result.content === "string") {
|
|
147
|
+
expand.ctx.expanded.add(note.id);
|
|
148
|
+
result.content = expandContent(result.content, expand.ctx, expand.depth);
|
|
149
|
+
}
|
|
117
150
|
result = filterMetadata(result, parseIncludeMetadata(url));
|
|
118
151
|
if (parseBool(parseQuery(url, "include_links"), false)) {
|
|
119
152
|
result.links = linkOps.getLinksHydrated(db, note.id);
|
|
120
153
|
}
|
|
121
154
|
if (parseBool(parseQuery(url, "include_attachments"), false)) {
|
|
122
|
-
result.attachments = store.getAttachments(note.id);
|
|
155
|
+
result.attachments = await store.getAttachments(note.id);
|
|
123
156
|
}
|
|
124
157
|
return json(result);
|
|
125
158
|
}
|
|
@@ -128,10 +161,19 @@ export async function handleNotes(
|
|
|
128
161
|
if (search) {
|
|
129
162
|
const searchTags = parseQueryList(url, "tag");
|
|
130
163
|
const limit = parseInt10(parseQuery(url, "limit")) ?? 50;
|
|
131
|
-
const results = store.searchNotes(search, { tags: searchTags, limit });
|
|
164
|
+
const results = await store.searchNotes(search, { tags: searchTags, limit });
|
|
132
165
|
const includeContent = parseBool(parseQuery(url, "include_content"), false);
|
|
133
166
|
const inclMeta = parseIncludeMetadata(url);
|
|
134
|
-
let output = includeContent ? results : results.map(toNoteIndex);
|
|
167
|
+
let output: any[] = includeContent ? results.map((n) => ({ ...n })) : results.map(toNoteIndex);
|
|
168
|
+
const expand = parseExpandParams(url, db);
|
|
169
|
+
if (expand && includeContent) {
|
|
170
|
+
for (const n of output) expand.ctx.expanded.add(n.id);
|
|
171
|
+
for (const n of output) {
|
|
172
|
+
if (typeof n.content === "string") {
|
|
173
|
+
n.content = expandContent(n.content, expand.ctx, expand.depth);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
135
177
|
if (inclMeta !== undefined && inclMeta !== true) {
|
|
136
178
|
output = output.map((n: any) => filterMetadata(n, inclMeta));
|
|
137
179
|
}
|
|
@@ -140,7 +182,7 @@ export async function handleNotes(
|
|
|
140
182
|
|
|
141
183
|
// Structured query
|
|
142
184
|
const tags = parseQueryList(url, "tag");
|
|
143
|
-
let results: Note[] = store.queryNotes({
|
|
185
|
+
let results: Note[] = await store.queryNotes({
|
|
144
186
|
tags,
|
|
145
187
|
tagMatch: (parseQuery(url, "tag_match") as "all" | "any") ?? (tags && tags.length > 1 ? "any" : undefined),
|
|
146
188
|
excludeTags: parseQueryList(url, "exclude_tag"),
|
|
@@ -157,7 +199,7 @@ export async function handleNotes(
|
|
|
157
199
|
// Near-scope filter (graph neighborhood)
|
|
158
200
|
const nearNoteId = parseQuery(url, "near[note_id]");
|
|
159
201
|
if (nearNoteId) {
|
|
160
|
-
const anchor = resolveNote(store, nearNoteId);
|
|
202
|
+
const anchor = await resolveNote(store, nearNoteId);
|
|
161
203
|
if (!anchor) return json({ error: "Anchor note not found", note_id: nearNoteId }, 404);
|
|
162
204
|
const depth = Math.min(parseInt10(parseQuery(url, "near[depth]")) ?? 2, 5);
|
|
163
205
|
const relationship = parseQuery(url, "near[relationship]") ?? undefined;
|
|
@@ -170,7 +212,16 @@ export async function handleNotes(
|
|
|
170
212
|
const includeLinks = parseBool(parseQuery(url, "include_links"), false);
|
|
171
213
|
const includeAttachments = parseBool(parseQuery(url, "include_attachments"), false);
|
|
172
214
|
const inclMeta = parseIncludeMetadata(url);
|
|
173
|
-
let output: any[] = includeContent ? results : results.map(toNoteIndex);
|
|
215
|
+
let output: any[] = includeContent ? results.map((n) => ({ ...n })) : results.map(toNoteIndex);
|
|
216
|
+
const expand = parseExpandParams(url, db);
|
|
217
|
+
if (expand && includeContent) {
|
|
218
|
+
for (const n of output) expand.ctx.expanded.add(n.id);
|
|
219
|
+
for (const n of output) {
|
|
220
|
+
if (typeof n.content === "string") {
|
|
221
|
+
n.content = expandContent(n.content, expand.ctx, expand.depth);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
174
225
|
if (inclMeta !== undefined && inclMeta !== true) {
|
|
175
226
|
output = output.map((n: any) => filterMetadata(n, inclMeta));
|
|
176
227
|
}
|
|
@@ -194,12 +245,14 @@ export async function handleNotes(
|
|
|
194
245
|
}
|
|
195
246
|
|
|
196
247
|
if (includeLinks || includeAttachments) {
|
|
197
|
-
|
|
198
|
-
|
|
248
|
+
const enrichedOut: any[] = [];
|
|
249
|
+
for (const n of output) {
|
|
250
|
+
const enriched: any = { ...n };
|
|
199
251
|
if (includeLinks) enriched.links = linkOps.getLinksHydrated(db, n.id);
|
|
200
|
-
if (includeAttachments) enriched.attachments = store.getAttachments(n.id);
|
|
201
|
-
|
|
202
|
-
}
|
|
252
|
+
if (includeAttachments) enriched.attachments = await store.getAttachments(n.id);
|
|
253
|
+
enrichedOut.push(enriched);
|
|
254
|
+
}
|
|
255
|
+
return json(enrichedOut);
|
|
203
256
|
}
|
|
204
257
|
|
|
205
258
|
return json(output);
|
|
@@ -212,7 +265,7 @@ export async function handleNotes(
|
|
|
212
265
|
|
|
213
266
|
const created: Note[] = [];
|
|
214
267
|
for (const item of items) {
|
|
215
|
-
const note = store.createNote(item.content ?? "", {
|
|
268
|
+
const note = await store.createNote(item.content ?? "", {
|
|
216
269
|
id: item.id,
|
|
217
270
|
path: item.path,
|
|
218
271
|
tags: item.tags,
|
|
@@ -223,18 +276,18 @@ export async function handleNotes(
|
|
|
223
276
|
// Create explicit links
|
|
224
277
|
if (item.links) {
|
|
225
278
|
for (const link of item.links as { target: string; relationship: string }[]) {
|
|
226
|
-
const target = resolveNote(store, link.target);
|
|
227
|
-
if (target) store.createLink(note.id, target.id, link.relationship);
|
|
279
|
+
const target = await resolveNote(store, link.target);
|
|
280
|
+
if (target) await store.createLink(note.id, target.id, link.relationship);
|
|
228
281
|
}
|
|
229
282
|
}
|
|
230
283
|
|
|
231
|
-
created.push(store.getNote(note.id) ?? note);
|
|
284
|
+
created.push((await store.getNote(note.id)) ?? note);
|
|
232
285
|
}
|
|
233
286
|
|
|
234
287
|
// Apply tag schema defaults
|
|
235
288
|
for (const note of created) {
|
|
236
289
|
if (note.tags?.length) {
|
|
237
|
-
applySchemaDefaults(store, db, [note.id], note.tags);
|
|
290
|
+
await applySchemaDefaults(store, db, [note.id], note.tags);
|
|
238
291
|
}
|
|
239
292
|
}
|
|
240
293
|
|
|
@@ -254,16 +307,16 @@ export async function handleNotes(
|
|
|
254
307
|
// Attachments sub-routes (keep as-is — Daily needs them)
|
|
255
308
|
if (sub === "/attachments") {
|
|
256
309
|
if (method === "POST") {
|
|
257
|
-
const note = resolveNote(store, idOrPath);
|
|
310
|
+
const note = await resolveNote(store, idOrPath);
|
|
258
311
|
if (!note) return json({ error: "Not found" }, 404);
|
|
259
312
|
const body = await req.json() as { path: string; mimeType: string };
|
|
260
313
|
if (!body.path || !body.mimeType) return json({ error: "path and mimeType are required" }, 400);
|
|
261
|
-
return json(store.addAttachment(note.id, body.path, body.mimeType), 201);
|
|
314
|
+
return json(await store.addAttachment(note.id, body.path, body.mimeType), 201);
|
|
262
315
|
}
|
|
263
316
|
if (method === "GET") {
|
|
264
|
-
const note = resolveNote(store, idOrPath);
|
|
317
|
+
const note = await resolveNote(store, idOrPath);
|
|
265
318
|
if (!note) return json({ error: "Not found" }, 404);
|
|
266
|
-
return json(store.getAttachments(note.id));
|
|
319
|
+
return json(await store.getAttachments(note.id));
|
|
267
320
|
}
|
|
268
321
|
return json({ error: "Method not allowed" }, 405);
|
|
269
322
|
}
|
|
@@ -272,16 +325,21 @@ export async function handleNotes(
|
|
|
272
325
|
|
|
273
326
|
// GET /notes/:idOrPath — single note
|
|
274
327
|
if (method === "GET") {
|
|
275
|
-
const note = resolveNote(store, idOrPath);
|
|
328
|
+
const note = await resolveNote(store, idOrPath);
|
|
276
329
|
if (!note) return json({ error: "Not found" }, 404);
|
|
277
330
|
const includeContent = parseBool(parseQuery(url, "include_content"), true);
|
|
278
331
|
let result: any = includeContent ? { ...note } : toNoteIndex(note);
|
|
332
|
+
const expand = parseExpandParams(url, db);
|
|
333
|
+
if (expand && includeContent && typeof result.content === "string") {
|
|
334
|
+
expand.ctx.expanded.add(note.id);
|
|
335
|
+
result.content = expandContent(result.content, expand.ctx, expand.depth);
|
|
336
|
+
}
|
|
279
337
|
result = filterMetadata(result, parseIncludeMetadata(url));
|
|
280
338
|
if (parseBool(parseQuery(url, "include_links"), false)) {
|
|
281
339
|
result.links = linkOps.getLinksHydrated(db, note.id);
|
|
282
340
|
}
|
|
283
341
|
if (parseBool(parseQuery(url, "include_attachments"), false)) {
|
|
284
|
-
result.attachments = store.getAttachments(note.id);
|
|
342
|
+
result.attachments = await store.getAttachments(note.id);
|
|
285
343
|
}
|
|
286
344
|
return json(result);
|
|
287
345
|
}
|
|
@@ -289,28 +347,30 @@ export async function handleNotes(
|
|
|
289
347
|
// PATCH /notes/:idOrPath — update (content, path, metadata, tags, links)
|
|
290
348
|
if (method === "PATCH") {
|
|
291
349
|
try {
|
|
292
|
-
const note = resolveNote(store, idOrPath);
|
|
350
|
+
const note = await resolveNote(store, idOrPath);
|
|
293
351
|
if (!note) throw new NotFoundError(`Note not found: "${idOrPath}"`);
|
|
294
352
|
const body = await req.json() as any;
|
|
295
353
|
|
|
296
|
-
//
|
|
297
|
-
|
|
354
|
+
// --- Plan bracket cleanup for wikilink removals (no DB writes yet) ---
|
|
355
|
+
// The actual link deletions happen only after the core UPDATE succeeds,
|
|
356
|
+
// so a conflict leaves the note untouched.
|
|
298
357
|
let contentOverride = body.content as string | undefined;
|
|
358
|
+
const linksRemove = body.links?.remove as { target: string; relationship: string }[] | undefined;
|
|
359
|
+
const resolvedLinksToRemove: { targetId: string; relationship: string }[] = [];
|
|
299
360
|
if (linksRemove) {
|
|
300
361
|
for (const link of linksRemove) {
|
|
301
|
-
const target = resolveNote(store, link.target);
|
|
302
|
-
if (target)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
362
|
+
const target = await resolveNote(store, link.target);
|
|
363
|
+
if (!target) continue;
|
|
364
|
+
resolvedLinksToRemove.push({ targetId: target.id, relationship: link.relationship });
|
|
365
|
+
if (link.relationship === "wikilink" && target.path) {
|
|
366
|
+
const current = contentOverride ?? note.content;
|
|
367
|
+
const cleaned = removeWikilinkBrackets(current, target.path);
|
|
368
|
+
if (cleaned !== current) contentOverride = cleaned;
|
|
309
369
|
}
|
|
310
370
|
}
|
|
311
371
|
}
|
|
312
372
|
|
|
313
|
-
// Core update
|
|
373
|
+
// --- Core update (runs the if_updated_at check atomically) ---
|
|
314
374
|
const updates: any = {};
|
|
315
375
|
if (contentOverride !== undefined) updates.content = contentOverride;
|
|
316
376
|
if (body.path !== undefined) updates.path = body.path;
|
|
@@ -321,40 +381,63 @@ export async function handleNotes(
|
|
|
321
381
|
if (body.created_at !== undefined || body.createdAt !== undefined) {
|
|
322
382
|
updates.created_at = body.created_at ?? body.createdAt;
|
|
323
383
|
}
|
|
384
|
+
if (body.if_updated_at !== undefined) {
|
|
385
|
+
updates.if_updated_at = body.if_updated_at;
|
|
386
|
+
}
|
|
324
387
|
|
|
325
388
|
if (Object.keys(updates).length > 0) {
|
|
326
|
-
store.updateNote(note.id, updates);
|
|
389
|
+
await store.updateNote(note.id, updates);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// --- Remove links (after core UPDATE; conflict would have thrown already) ---
|
|
393
|
+
for (const { targetId, relationship } of resolvedLinksToRemove) {
|
|
394
|
+
await store.deleteLink(note.id, targetId, relationship);
|
|
327
395
|
}
|
|
328
396
|
|
|
329
397
|
// Tags
|
|
330
398
|
if (body.tags?.add?.length) {
|
|
331
|
-
store.tagNote(note.id, body.tags.add);
|
|
332
|
-
applySchemaDefaults(store, db, [note.id], body.tags.add);
|
|
399
|
+
await store.tagNote(note.id, body.tags.add);
|
|
400
|
+
await applySchemaDefaults(store, db, [note.id], body.tags.add);
|
|
333
401
|
}
|
|
334
402
|
if (body.tags?.remove?.length) {
|
|
335
|
-
store.untagNote(note.id, body.tags.remove);
|
|
403
|
+
await store.untagNote(note.id, body.tags.remove);
|
|
336
404
|
}
|
|
337
405
|
|
|
338
406
|
// Add links
|
|
339
407
|
if (body.links?.add) {
|
|
340
408
|
for (const link of body.links.add as { target: string; relationship: string; metadata?: Record<string, unknown> }[]) {
|
|
341
|
-
const target = resolveNote(store, link.target);
|
|
342
|
-
if (target) store.createLink(note.id, target.id, link.relationship, link.metadata);
|
|
409
|
+
const target = await resolveNote(store, link.target);
|
|
410
|
+
if (target) await store.createLink(note.id, target.id, link.relationship, link.metadata);
|
|
343
411
|
}
|
|
344
412
|
}
|
|
345
413
|
|
|
346
|
-
return json(store.getNote(note.id));
|
|
414
|
+
return json(await store.getNote(note.id));
|
|
347
415
|
} catch (e: any) {
|
|
348
416
|
if (e instanceof NotFoundError) return json({ error: e.message }, 404);
|
|
417
|
+
// Duck-type on `code` rather than `instanceof ConflictError`: this
|
|
418
|
+
// error originates in the core package and survives any future
|
|
419
|
+
// bundling / module-boundary split more robustly than a prototype check.
|
|
420
|
+
if (e && e.code === "CONFLICT") {
|
|
421
|
+
return json(
|
|
422
|
+
{
|
|
423
|
+
error: "conflict",
|
|
424
|
+
message: e.message,
|
|
425
|
+
note_id: e.note_id,
|
|
426
|
+
current_updated_at: e.current_updated_at ?? null,
|
|
427
|
+
expected_updated_at: e.expected_updated_at,
|
|
428
|
+
},
|
|
429
|
+
409,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
349
432
|
throw e;
|
|
350
433
|
}
|
|
351
434
|
}
|
|
352
435
|
|
|
353
436
|
// DELETE /notes/:idOrPath — admin only (enforced at server level)
|
|
354
437
|
if (method === "DELETE") {
|
|
355
|
-
const note = resolveNote(store, idOrPath);
|
|
438
|
+
const note = await resolveNote(store, idOrPath);
|
|
356
439
|
if (!note) return json({ error: "Not found" }, 404);
|
|
357
|
-
store.deleteNote(note.id);
|
|
440
|
+
await store.deleteNote(note.id);
|
|
358
441
|
return json({ deleted: true, id: note.id });
|
|
359
442
|
}
|
|
360
443
|
|
|
@@ -367,16 +450,15 @@ export async function handleNotes(
|
|
|
367
450
|
|
|
368
451
|
export async function handleTags(req: Request, store: Store, subpath = ""): Promise<Response> {
|
|
369
452
|
const url = new URL(req.url);
|
|
370
|
-
const db = (store as any).db;
|
|
371
453
|
|
|
372
454
|
// GET /tags — list all, or get single tag detail
|
|
373
455
|
if (req.method === "GET" && subpath === "") {
|
|
374
456
|
const singleTag = parseQuery(url, "tag");
|
|
375
457
|
|
|
376
458
|
if (singleTag) {
|
|
377
|
-
const allTags = store.listTags();
|
|
459
|
+
const allTags = await store.listTags();
|
|
378
460
|
const found = allTags.find((t) => t.name === singleTag);
|
|
379
|
-
const schema = store.getTagSchema(singleTag);
|
|
461
|
+
const schema = await store.getTagSchema(singleTag);
|
|
380
462
|
return json({
|
|
381
463
|
name: singleTag,
|
|
382
464
|
count: found?.count ?? 0,
|
|
@@ -385,9 +467,9 @@ export async function handleTags(req: Request, store: Store, subpath = ""): Prom
|
|
|
385
467
|
});
|
|
386
468
|
}
|
|
387
469
|
|
|
388
|
-
const tags = store.listTags();
|
|
470
|
+
const tags = await store.listTags();
|
|
389
471
|
if (parseBool(parseQuery(url, "include_schema"), false)) {
|
|
390
|
-
const schemas = store.getTagSchemaMap();
|
|
472
|
+
const schemas = await store.getTagSchemaMap();
|
|
391
473
|
return json(tags.map((t) => ({
|
|
392
474
|
...t,
|
|
393
475
|
description: schemas[t.name]?.description ?? null,
|
|
@@ -404,9 +486,9 @@ export async function handleTags(req: Request, store: Store, subpath = ""): Prom
|
|
|
404
486
|
|
|
405
487
|
// GET /tags/:name — single tag detail
|
|
406
488
|
if (req.method === "GET") {
|
|
407
|
-
const allTags = store.listTags();
|
|
489
|
+
const allTags = await store.listTags();
|
|
408
490
|
const found = allTags.find((t) => t.name === tagName);
|
|
409
|
-
const schema = store.getTagSchema(tagName);
|
|
491
|
+
const schema = await store.getTagSchema(tagName);
|
|
410
492
|
return json({
|
|
411
493
|
name: tagName,
|
|
412
494
|
count: found?.count ?? 0,
|
|
@@ -418,9 +500,9 @@ export async function handleTags(req: Request, store: Store, subpath = ""): Prom
|
|
|
418
500
|
// PUT /tags/:name — upsert tag schema (description + fields)
|
|
419
501
|
if (req.method === "PUT") {
|
|
420
502
|
const body = await req.json() as { description?: string; fields?: Record<string, unknown> };
|
|
421
|
-
const existing = store.getTagSchema(tagName);
|
|
503
|
+
const existing = await store.getTagSchema(tagName);
|
|
422
504
|
const mergedFields = { ...existing?.fields, ...(body.fields as any) };
|
|
423
|
-
const schema = store.upsertTagSchema(tagName, {
|
|
505
|
+
const schema = await store.upsertTagSchema(tagName, {
|
|
424
506
|
description: body.description ?? existing?.description,
|
|
425
507
|
fields: Object.keys(mergedFields).length > 0 ? mergedFields : undefined,
|
|
426
508
|
});
|
|
@@ -429,8 +511,8 @@ export async function handleTags(req: Request, store: Store, subpath = ""): Prom
|
|
|
429
511
|
|
|
430
512
|
// DELETE /tags/:name — delete tag + schema from all notes
|
|
431
513
|
if (req.method === "DELETE") {
|
|
432
|
-
store.deleteTagSchema(tagName);
|
|
433
|
-
return json(store.deleteTag(tagName));
|
|
514
|
+
await store.deleteTagSchema(tagName);
|
|
515
|
+
return json(await store.deleteTag(tagName));
|
|
434
516
|
}
|
|
435
517
|
|
|
436
518
|
return json({ error: "Method not allowed" }, 405);
|
|
@@ -440,7 +522,7 @@ export async function handleTags(req: Request, store: Store, subpath = ""): Prom
|
|
|
440
522
|
// Find-path — GET /api/find-path?source=...&target=...
|
|
441
523
|
// ---------------------------------------------------------------------------
|
|
442
524
|
|
|
443
|
-
export function handleFindPath(req: Request, store: Store): Response {
|
|
525
|
+
export async function handleFindPath(req: Request, store: Store): Promise<Response> {
|
|
444
526
|
if (req.method !== "GET") return json({ error: "Method not allowed" }, 405);
|
|
445
527
|
|
|
446
528
|
const url = new URL(req.url);
|
|
@@ -450,9 +532,9 @@ export function handleFindPath(req: Request, store: Store): Response {
|
|
|
450
532
|
|
|
451
533
|
const db = (store as any).db;
|
|
452
534
|
try {
|
|
453
|
-
const sourceNote = resolveNote(store, source);
|
|
535
|
+
const sourceNote = await resolveNote(store, source);
|
|
454
536
|
if (!sourceNote) return json({ error: `Note not found: "${source}"` }, 404);
|
|
455
|
-
const targetNote = resolveNote(store, target);
|
|
537
|
+
const targetNote = await resolveNote(store, target);
|
|
456
538
|
if (!targetNote) return json({ error: `Note not found: "${target}"` }, 404);
|
|
457
539
|
const maxDepth = Math.min(parseInt10(parseQuery(url, "max_depth")) ?? 5, 10);
|
|
458
540
|
|
|
@@ -482,7 +564,7 @@ export async function handleVault(
|
|
|
482
564
|
description: vaultConfig.description ?? null,
|
|
483
565
|
};
|
|
484
566
|
if (parseBool(parseQuery(url, "include_stats"), false)) {
|
|
485
|
-
result.stats = store.getVaultStats();
|
|
567
|
+
result.stats = await store.getVaultStats();
|
|
486
568
|
}
|
|
487
569
|
return json(result);
|
|
488
570
|
}
|
|
@@ -611,13 +693,13 @@ function isNotePublished(note: { tags?: string[]; metadata?: unknown }, publishe
|
|
|
611
693
|
* GET /view/:idOrPath — serve a note as clean HTML.
|
|
612
694
|
* Supports ID or path resolution.
|
|
613
695
|
*/
|
|
614
|
-
export function handleViewNote(
|
|
696
|
+
export async function handleViewNote(
|
|
615
697
|
store: Store,
|
|
616
698
|
idOrPath: string,
|
|
617
699
|
options: { authenticated?: boolean; publishedTag?: string } = {},
|
|
618
|
-
): Response {
|
|
700
|
+
): Promise<Response> {
|
|
619
701
|
const { authenticated = false, publishedTag = "publish" } = options;
|
|
620
|
-
const note = resolveNote(store, idOrPath);
|
|
702
|
+
const note = await resolveNote(store, idOrPath);
|
|
621
703
|
if (!note) {
|
|
622
704
|
return new Response("Not Found", { status: 404, headers: { "Content-Type": "text/plain" } });
|
|
623
705
|
}
|
|
@@ -773,7 +855,7 @@ export async function handleStorage(req: Request, path: string, vault: string):
|
|
|
773
855
|
// Tag schema defaults — same logic as core/src/mcp.ts applySchemaDefaults
|
|
774
856
|
// ---------------------------------------------------------------------------
|
|
775
857
|
|
|
776
|
-
function applySchemaDefaults(store: Store, db: any, noteIds: string[], tags: string[]): void {
|
|
858
|
+
async function applySchemaDefaults(store: Store, db: any, noteIds: string[], tags: string[]): Promise<void> {
|
|
777
859
|
const schemas = tagSchemaOps.getTagSchemaMap(db);
|
|
778
860
|
if (Object.keys(schemas).length === 0) return;
|
|
779
861
|
|
|
@@ -790,7 +872,7 @@ function applySchemaDefaults(store: Store, db: any, noteIds: string[], tags: str
|
|
|
790
872
|
if (Object.keys(defaults).length === 0) return;
|
|
791
873
|
|
|
792
874
|
for (const noteId of noteIds) {
|
|
793
|
-
const note = store.getNote(noteId);
|
|
875
|
+
const note = await store.getNote(noteId);
|
|
794
876
|
if (!note) continue;
|
|
795
877
|
const existing = (note.metadata as Record<string, unknown>) ?? {};
|
|
796
878
|
const missing: Record<string, unknown> = {};
|
|
@@ -798,7 +880,7 @@ function applySchemaDefaults(store: Store, db: any, noteIds: string[], tags: str
|
|
|
798
880
|
if (!(field in existing)) missing[field] = value;
|
|
799
881
|
}
|
|
800
882
|
if (Object.keys(missing).length === 0) continue;
|
|
801
|
-
store.updateNote(noteId, {
|
|
883
|
+
await store.updateNote(noteId, {
|
|
802
884
|
metadata: { ...existing, ...missing },
|
|
803
885
|
skipUpdatedAt: true,
|
|
804
886
|
});
|