@openparachute/vault 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.
- package/CHANGELOG.md +80 -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 +57 -12
- package/src/published.test.ts +21 -21
- package/src/routes.ts +152 -70
- package/src/routing.test.ts +347 -0
- package/src/routing.ts +365 -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/core/src/store.ts
CHANGED
|
@@ -5,13 +5,15 @@ import * as noteOps from "./notes.js";
|
|
|
5
5
|
import * as linkOps from "./links.js";
|
|
6
6
|
import * as tagSchemaOps from "./tag-schemas.js";
|
|
7
7
|
import { syncWikilinks, resolveUnresolvedWikilinks } from "./wikilinks.js";
|
|
8
|
-
import {
|
|
8
|
+
import { pathTitle } from "./paths.js";
|
|
9
9
|
import { HookRegistry } from "./hooks.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* bun:sqlite-backed Store implementation. Internally everything is
|
|
13
|
+
* synchronous; the public Store API is async so the same interface
|
|
14
|
+
* can back an async runtime (e.g. Cloudflare Durable Objects SQLite).
|
|
13
15
|
*/
|
|
14
|
-
export class
|
|
16
|
+
export class BunSqliteStore implements Store {
|
|
15
17
|
public readonly hooks: HookRegistry;
|
|
16
18
|
|
|
17
19
|
constructor(public readonly db: Database, opts?: { hooks?: HookRegistry }) {
|
|
@@ -21,39 +23,45 @@ export class SqliteStore implements Store {
|
|
|
21
23
|
|
|
22
24
|
// ---- Notes ----
|
|
23
25
|
|
|
24
|
-
createNote(content: string, opts?: { id?: string; path?: string; tags?: string[]; metadata?: Record<string, unknown>; created_at?: string }): Note {
|
|
26
|
+
async createNote(content: string, opts?: { id?: string; path?: string; tags?: string[]; metadata?: Record<string, unknown>; created_at?: string }): Promise<Note> {
|
|
25
27
|
const note = noteOps.createNote(this.db, content, opts);
|
|
26
28
|
|
|
27
|
-
// Auto-sync wikilinks from content
|
|
28
29
|
if (content) {
|
|
29
30
|
syncWikilinks(this.db, note.id, content);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// If this note has a path, resolve any pending wikilinks targeting it
|
|
33
33
|
if (note.path) {
|
|
34
34
|
resolveUnresolvedWikilinks(this.db, note.path, note.id);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Dispatch async hooks post-commit. Never blocks the caller.
|
|
38
37
|
this.hooks.dispatch("created", note, this);
|
|
39
38
|
|
|
40
39
|
return note;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
getNote(id: string): Note | null {
|
|
42
|
+
async getNote(id: string): Promise<Note | null> {
|
|
44
43
|
return noteOps.getNote(this.db, id);
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
getNoteByPath(path: string): Note | null {
|
|
46
|
+
async getNoteByPath(path: string): Promise<Note | null> {
|
|
48
47
|
return noteOps.getNoteByPath(this.db, path);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
getNotes(ids: string[]): Note[] {
|
|
50
|
+
async getNotes(ids: string[]): Promise<Note[]> {
|
|
52
51
|
return noteOps.getNotes(this.db, ids);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
updateNote(
|
|
56
|
-
|
|
54
|
+
async updateNote(
|
|
55
|
+
id: string,
|
|
56
|
+
updates: {
|
|
57
|
+
content?: string;
|
|
58
|
+
path?: string;
|
|
59
|
+
metadata?: Record<string, unknown>;
|
|
60
|
+
created_at?: string;
|
|
61
|
+
skipUpdatedAt?: boolean;
|
|
62
|
+
if_updated_at?: string;
|
|
63
|
+
},
|
|
64
|
+
): Promise<Note> {
|
|
57
65
|
let oldPath: string | undefined;
|
|
58
66
|
if (updates.path !== undefined) {
|
|
59
67
|
const existing = noteOps.getNote(this.db, id);
|
|
@@ -62,12 +70,10 @@ export class SqliteStore implements Store {
|
|
|
62
70
|
|
|
63
71
|
const note = noteOps.updateNote(this.db, id, updates);
|
|
64
72
|
|
|
65
|
-
// Re-sync wikilinks if content changed
|
|
66
73
|
if (updates.content !== undefined) {
|
|
67
74
|
syncWikilinks(this.db, id, updates.content);
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
// If path changed, cascade rename through wikilinks in other notes
|
|
71
77
|
if (updates.path !== undefined && note.path) {
|
|
72
78
|
if (oldPath && oldPath !== note.path) {
|
|
73
79
|
this.cascadeRename(oldPath, note.path);
|
|
@@ -75,7 +81,6 @@ export class SqliteStore implements Store {
|
|
|
75
81
|
resolveUnresolvedWikilinks(this.db, note.path, id);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
// Dispatch async hooks post-commit. Never blocks the caller.
|
|
79
84
|
this.hooks.dispatch("updated", note, this);
|
|
80
85
|
|
|
81
86
|
return note;
|
|
@@ -89,8 +94,6 @@ export class SqliteStore implements Store {
|
|
|
89
94
|
const oldTitle = pathTitle(oldPath);
|
|
90
95
|
const newTitle = pathTitle(newPath);
|
|
91
96
|
|
|
92
|
-
// Find notes whose content contains a likely wikilink to the old path
|
|
93
|
-
// Search for both the full old path and just the old basename
|
|
94
97
|
const candidates = this.db.prepare(`
|
|
95
98
|
SELECT id, content FROM notes
|
|
96
99
|
WHERE content LIKE ? OR content LIKE ?
|
|
@@ -99,14 +102,11 @@ export class SqliteStore implements Store {
|
|
|
99
102
|
for (const row of candidates) {
|
|
100
103
|
let updated = row.content;
|
|
101
104
|
|
|
102
|
-
// Replace [[OldPath...]] with [[NewPath...]] (preserving aliases and anchors)
|
|
103
105
|
updated = updated.replace(
|
|
104
106
|
new RegExp(`\\[\\[${escapeRegex(oldPath)}([#|\\]])`, "g"),
|
|
105
107
|
`[[${newPath}$1`,
|
|
106
108
|
);
|
|
107
109
|
|
|
108
|
-
// Replace [[OldTitle...]] with [[NewTitle...]] (basename references)
|
|
109
|
-
// Only if old title !== new title and old title !== old path (avoid double-replace)
|
|
110
110
|
if (oldTitle !== newTitle && oldTitle !== oldPath) {
|
|
111
111
|
updated = updated.replace(
|
|
112
112
|
new RegExp(`\\[\\[${escapeRegex(oldTitle)}([#|\\]])`, "g"),
|
|
@@ -115,118 +115,113 @@ export class SqliteStore implements Store {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (updated !== row.content) {
|
|
118
|
-
// Call noteOps directly (not this.updateNote) to avoid recursive cascading.
|
|
119
|
-
// Only content changes here, so path normalization/cascading aren't needed.
|
|
120
118
|
noteOps.updateNote(this.db, row.id, { content: updated });
|
|
121
119
|
syncWikilinks(this.db, row.id, updated);
|
|
122
120
|
}
|
|
123
121
|
}
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
deleteNote(id: string): void {
|
|
124
|
+
async deleteNote(id: string): Promise<void> {
|
|
127
125
|
noteOps.deleteNote(this.db, id);
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
queryNotes(opts: QueryOpts): Note[] {
|
|
128
|
+
async queryNotes(opts: QueryOpts): Promise<Note[]> {
|
|
131
129
|
return noteOps.queryNotes(this.db, opts);
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
searchNotes(query: string, opts?: { tags?: string[]; limit?: number }): Note[] {
|
|
132
|
+
async searchNotes(query: string, opts?: { tags?: string[]; limit?: number }): Promise<Note[]> {
|
|
135
133
|
return noteOps.searchNotes(this.db, query, opts);
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
// ---- Tags ----
|
|
139
137
|
|
|
140
|
-
tagNote(noteId: string, tags: string[]): void {
|
|
138
|
+
async tagNote(noteId: string, tags: string[]): Promise<void> {
|
|
141
139
|
noteOps.tagNote(this.db, noteId, tags);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
|
-
untagNote(noteId: string, tags: string[]): void {
|
|
142
|
+
async untagNote(noteId: string, tags: string[]): Promise<void> {
|
|
145
143
|
noteOps.untagNote(this.db, noteId, tags);
|
|
146
144
|
}
|
|
147
145
|
|
|
148
|
-
listTags(): { name: string; count: number }[] {
|
|
146
|
+
async listTags(): Promise<{ name: string; count: number }[]> {
|
|
149
147
|
return noteOps.listTags(this.db);
|
|
150
148
|
}
|
|
151
149
|
|
|
152
|
-
deleteTag(name: string): { deleted: boolean; notes_untagged: number } {
|
|
150
|
+
async deleteTag(name: string): Promise<{ deleted: boolean; notes_untagged: number }> {
|
|
153
151
|
return noteOps.deleteTag(this.db, name);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
// ---- Vault Stats ----
|
|
157
155
|
|
|
158
|
-
getVaultStats(opts?: { topTagsLimit?: number }) {
|
|
156
|
+
async getVaultStats(opts?: { topTagsLimit?: number }) {
|
|
159
157
|
return noteOps.getVaultStats(this.db, opts);
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
// ---- Links ----
|
|
163
161
|
|
|
164
|
-
createLink(sourceId: string, targetId: string, relationship: string, metadata?: Record<string, unknown>): Link {
|
|
162
|
+
async createLink(sourceId: string, targetId: string, relationship: string, metadata?: Record<string, unknown>): Promise<Link> {
|
|
165
163
|
return linkOps.createLink(this.db, sourceId, targetId, relationship, metadata);
|
|
166
164
|
}
|
|
167
165
|
|
|
168
|
-
deleteLink(sourceId: string, targetId: string, relationship: string): void {
|
|
166
|
+
async deleteLink(sourceId: string, targetId: string, relationship: string): Promise<void> {
|
|
169
167
|
linkOps.deleteLink(this.db, sourceId, targetId, relationship);
|
|
170
168
|
}
|
|
171
169
|
|
|
172
|
-
getLinks(noteId: string, opts?: { direction?: "outbound" | "inbound" | "both" }): Link[] {
|
|
170
|
+
async getLinks(noteId: string, opts?: { direction?: "outbound" | "inbound" | "both" }): Promise<Link[]> {
|
|
173
171
|
return linkOps.getLinks(this.db, noteId, opts);
|
|
174
172
|
}
|
|
175
173
|
|
|
176
|
-
listLinks(opts?: { noteId?: string; direction?: "outbound" | "inbound" | "both"; relationship?: string }): Link[] {
|
|
174
|
+
async listLinks(opts?: { noteId?: string; direction?: "outbound" | "inbound" | "both"; relationship?: string }): Promise<Link[]> {
|
|
177
175
|
return linkOps.listLinks(this.db, opts);
|
|
178
176
|
}
|
|
179
177
|
|
|
180
178
|
// ---- Bulk Operations ----
|
|
181
179
|
|
|
182
|
-
createNotes(inputs: noteOps.BulkNoteInput[]): Note[] {
|
|
180
|
+
async createNotes(inputs: noteOps.BulkNoteInput[]): Promise<Note[]> {
|
|
183
181
|
const notes = noteOps.createNotes(this.db, inputs);
|
|
184
|
-
// Dispatch hooks for each created note — AFTER the bulk transaction
|
|
185
|
-
// commits inside noteOps.createNotes. Dispatch itself is async-safe
|
|
186
|
-
// (queueMicrotask), so the loop returns immediately.
|
|
187
182
|
for (const note of notes) {
|
|
188
183
|
this.hooks.dispatch("created", note, this);
|
|
189
184
|
}
|
|
190
185
|
return notes;
|
|
191
186
|
}
|
|
192
187
|
|
|
193
|
-
batchTag(noteIds: string[], tags: string[]): number {
|
|
188
|
+
async batchTag(noteIds: string[], tags: string[]): Promise<number> {
|
|
194
189
|
return noteOps.batchTag(this.db, noteIds, tags);
|
|
195
190
|
}
|
|
196
191
|
|
|
197
|
-
batchUntag(noteIds: string[], tags: string[]): number {
|
|
192
|
+
async batchUntag(noteIds: string[], tags: string[]): Promise<number> {
|
|
198
193
|
return noteOps.batchUntag(this.db, noteIds, tags);
|
|
199
194
|
}
|
|
200
195
|
|
|
201
196
|
// ---- Deeper Link Queries ----
|
|
202
197
|
|
|
203
|
-
traverseLinks(noteId: string, opts?: { max_depth?: number; relationship?: string }) {
|
|
198
|
+
async traverseLinks(noteId: string, opts?: { max_depth?: number; relationship?: string }) {
|
|
204
199
|
return linkOps.traverseLinks(this.db, noteId, opts);
|
|
205
200
|
}
|
|
206
201
|
|
|
207
|
-
findPath(sourceId: string, targetId: string, opts?: { max_depth?: number }) {
|
|
202
|
+
async findPath(sourceId: string, targetId: string, opts?: { max_depth?: number }) {
|
|
208
203
|
return linkOps.findPath(this.db, sourceId, targetId, opts);
|
|
209
204
|
}
|
|
210
205
|
|
|
211
206
|
// ---- Tag Schemas ----
|
|
212
207
|
|
|
213
|
-
listTagSchemas() {
|
|
208
|
+
async listTagSchemas() {
|
|
214
209
|
return tagSchemaOps.listTagSchemas(this.db);
|
|
215
210
|
}
|
|
216
211
|
|
|
217
|
-
getTagSchema(tag: string) {
|
|
212
|
+
async getTagSchema(tag: string) {
|
|
218
213
|
return tagSchemaOps.getTagSchema(this.db, tag);
|
|
219
214
|
}
|
|
220
215
|
|
|
221
|
-
upsertTagSchema(tag: string, schema: { description?: string; fields?: Record<string, tagSchemaOps.TagFieldSchema> }) {
|
|
216
|
+
async upsertTagSchema(tag: string, schema: { description?: string; fields?: Record<string, tagSchemaOps.TagFieldSchema> }) {
|
|
222
217
|
return tagSchemaOps.upsertTagSchema(this.db, tag, schema);
|
|
223
218
|
}
|
|
224
219
|
|
|
225
|
-
deleteTagSchema(tag: string) {
|
|
220
|
+
async deleteTagSchema(tag: string) {
|
|
226
221
|
return tagSchemaOps.deleteTagSchema(this.db, tag);
|
|
227
222
|
}
|
|
228
223
|
|
|
229
|
-
getTagSchemaMap() {
|
|
224
|
+
async getTagSchemaMap() {
|
|
230
225
|
return tagSchemaOps.getTagSchemaMap(this.db);
|
|
231
226
|
}
|
|
232
227
|
|
|
@@ -236,7 +231,7 @@ export class SqliteStore implements Store {
|
|
|
236
231
|
* Create a note without triggering wikilink sync.
|
|
237
232
|
* Use this during bulk imports, then call syncAllWikilinks() after.
|
|
238
233
|
*/
|
|
239
|
-
createNoteRaw(content: string, opts?: { id?: string; path?: string; tags?: string[]; metadata?: Record<string, unknown>; created_at?: string }): Note {
|
|
234
|
+
async createNoteRaw(content: string, opts?: { id?: string; path?: string; tags?: string[]; metadata?: Record<string, unknown>; created_at?: string }): Promise<Note> {
|
|
240
235
|
return noteOps.createNote(this.db, content, opts);
|
|
241
236
|
}
|
|
242
237
|
|
|
@@ -244,7 +239,7 @@ export class SqliteStore implements Store {
|
|
|
244
239
|
* Sync wikilinks for all notes in the vault.
|
|
245
240
|
* Efficient for bulk imports — call once after importing all notes.
|
|
246
241
|
*/
|
|
247
|
-
syncAllWikilinks(): { synced: number; totalAdded: number; totalRemoved: number } {
|
|
242
|
+
async syncAllWikilinks(): Promise<{ synced: number; totalAdded: number; totalRemoved: number }> {
|
|
248
243
|
const allNotes = noteOps.queryNotes(this.db, { limit: 1000000 });
|
|
249
244
|
let synced = 0;
|
|
250
245
|
let totalAdded = 0;
|
|
@@ -265,7 +260,7 @@ export class SqliteStore implements Store {
|
|
|
265
260
|
|
|
266
261
|
// ---- Attachments ----
|
|
267
262
|
|
|
268
|
-
addAttachment(noteId: string, filePath: string, mimeType: string, metadata?: Record<string, unknown>): Attachment {
|
|
263
|
+
async addAttachment(noteId: string, filePath: string, mimeType: string, metadata?: Record<string, unknown>): Promise<Attachment> {
|
|
269
264
|
const id = noteOps.generateId();
|
|
270
265
|
const now = new Date().toISOString();
|
|
271
266
|
const metadataJson = metadata ? JSON.stringify(metadata) : "{}";
|
|
@@ -276,7 +271,7 @@ export class SqliteStore implements Store {
|
|
|
276
271
|
return { id, noteId, path: filePath, mimeType, metadata, createdAt: now };
|
|
277
272
|
}
|
|
278
273
|
|
|
279
|
-
getAttachments(noteId: string): Attachment[] {
|
|
274
|
+
async getAttachments(noteId: string): Promise<Attachment[]> {
|
|
280
275
|
const rows = this.db.prepare(
|
|
281
276
|
"SELECT * FROM attachments WHERE note_id = ? ORDER BY created_at",
|
|
282
277
|
).all(noteId) as { id: string; note_id: string; path: string; mime_type: string; metadata: string | null; created_at: string }[];
|
|
@@ -298,6 +293,11 @@ export class SqliteStore implements Store {
|
|
|
298
293
|
}
|
|
299
294
|
}
|
|
300
295
|
|
|
296
|
+
/** @deprecated Renamed to `BunSqliteStore` to make the runtime split explicit. Kept as an alias for backward compatibility. */
|
|
297
|
+
export const SqliteStore = BunSqliteStore;
|
|
298
|
+
/** @deprecated Renamed to `BunSqliteStore`. */
|
|
299
|
+
export type SqliteStore = BunSqliteStore;
|
|
300
|
+
|
|
301
301
|
function escapeRegex(s: string): string {
|
|
302
302
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
303
303
|
}
|
package/core/src/types.ts
CHANGED
|
@@ -94,47 +94,47 @@ export interface HydratedLink extends Link {
|
|
|
94
94
|
|
|
95
95
|
export interface Store {
|
|
96
96
|
// Notes
|
|
97
|
-
createNote(content: string, opts?: { id?: string; path?: string; tags?: string[]; metadata?: Record<string, unknown>; created_at?: string }): Note
|
|
98
|
-
getNote(id: string): Note | null
|
|
99
|
-
getNoteByPath(path: string): Note | null
|
|
100
|
-
getNotes(ids: string[]): Note[]
|
|
101
|
-
updateNote(id: string, updates: { content?: string; path?: string; metadata?: Record<string, unknown>; skipUpdatedAt?: boolean }): Note
|
|
102
|
-
deleteNote(id: string): void
|
|
103
|
-
queryNotes(opts: QueryOpts): Note[]
|
|
104
|
-
searchNotes(query: string, opts?: { tags?: string[]; limit?: number }): Note[]
|
|
97
|
+
createNote(content: string, opts?: { id?: string; path?: string; tags?: string[]; metadata?: Record<string, unknown>; created_at?: string }): Promise<Note>;
|
|
98
|
+
getNote(id: string): Promise<Note | null>;
|
|
99
|
+
getNoteByPath(path: string): Promise<Note | null>;
|
|
100
|
+
getNotes(ids: string[]): Promise<Note[]>;
|
|
101
|
+
updateNote(id: string, updates: { content?: string; path?: string; metadata?: Record<string, unknown>; created_at?: string; skipUpdatedAt?: boolean; if_updated_at?: string }): Promise<Note>;
|
|
102
|
+
deleteNote(id: string): Promise<void>;
|
|
103
|
+
queryNotes(opts: QueryOpts): Promise<Note[]>;
|
|
104
|
+
searchNotes(query: string, opts?: { tags?: string[]; limit?: number }): Promise<Note[]>;
|
|
105
105
|
|
|
106
106
|
// Tags
|
|
107
|
-
tagNote(noteId: string, tags: string[]): void
|
|
108
|
-
untagNote(noteId: string, tags: string[]): void
|
|
109
|
-
listTags(): { name: string; count: number }[]
|
|
110
|
-
deleteTag(name: string): { deleted: boolean; notes_untagged: number }
|
|
107
|
+
tagNote(noteId: string, tags: string[]): Promise<void>;
|
|
108
|
+
untagNote(noteId: string, tags: string[]): Promise<void>;
|
|
109
|
+
listTags(): Promise<{ name: string; count: number }[]>;
|
|
110
|
+
deleteTag(name: string): Promise<{ deleted: boolean; notes_untagged: number }>;
|
|
111
111
|
|
|
112
112
|
// Vault stats (aggregate, read-only)
|
|
113
|
-
getVaultStats(opts?: { topTagsLimit?: number }): VaultStats
|
|
113
|
+
getVaultStats(opts?: { topTagsLimit?: number }): Promise<VaultStats>;
|
|
114
114
|
|
|
115
115
|
// Links
|
|
116
|
-
createLink(sourceId: string, targetId: string, relationship: string, metadata?: Record<string, unknown>): Link
|
|
117
|
-
deleteLink(sourceId: string, targetId: string, relationship: string): void
|
|
118
|
-
getLinks(noteId: string, opts?: { direction?: "outbound" | "inbound" | "both" }): Link[]
|
|
119
|
-
listLinks(opts?: { noteId?: string; direction?: "outbound" | "inbound" | "both"; relationship?: string }): Link[]
|
|
116
|
+
createLink(sourceId: string, targetId: string, relationship: string, metadata?: Record<string, unknown>): Promise<Link>;
|
|
117
|
+
deleteLink(sourceId: string, targetId: string, relationship: string): Promise<void>;
|
|
118
|
+
getLinks(noteId: string, opts?: { direction?: "outbound" | "inbound" | "both" }): Promise<Link[]>;
|
|
119
|
+
listLinks(opts?: { noteId?: string; direction?: "outbound" | "inbound" | "both"; relationship?: string }): Promise<Link[]>;
|
|
120
120
|
|
|
121
121
|
// Bulk operations
|
|
122
|
-
createNotes(inputs: { content: string; id?: string; path?: string; tags?: string[] }[]): Note[]
|
|
123
|
-
batchTag(noteIds: string[], tags: string[]): number
|
|
124
|
-
batchUntag(noteIds: string[], tags: string[]): number
|
|
122
|
+
createNotes(inputs: { content: string; id?: string; path?: string; tags?: string[] }[]): Promise<Note[]>;
|
|
123
|
+
batchTag(noteIds: string[], tags: string[]): Promise<number>;
|
|
124
|
+
batchUntag(noteIds: string[], tags: string[]): Promise<number>;
|
|
125
125
|
|
|
126
126
|
// Deeper link queries
|
|
127
|
-
traverseLinks(noteId: string, opts?: { max_depth?: number; relationship?: string }): { noteId: string; depth: number; relationship: string; direction: "outbound" | "inbound" }[]
|
|
128
|
-
findPath(sourceId: string, targetId: string, opts?: { max_depth?: number }): { path: string[]; relationships: string[] } | null
|
|
127
|
+
traverseLinks(noteId: string, opts?: { max_depth?: number; relationship?: string }): Promise<{ noteId: string; depth: number; relationship: string; direction: "outbound" | "inbound" }[]>;
|
|
128
|
+
findPath(sourceId: string, targetId: string, opts?: { max_depth?: number }): Promise<{ path: string[]; relationships: string[] } | null>;
|
|
129
129
|
|
|
130
130
|
// Tag schemas
|
|
131
|
-
listTagSchemas(): { tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }[]
|
|
132
|
-
getTagSchema(tag: string): { tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> } | null
|
|
133
|
-
upsertTagSchema(tag: string, schema: { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }): { tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }
|
|
134
|
-
deleteTagSchema(tag: string): boolean
|
|
135
|
-
getTagSchemaMap(): Record<string, { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }
|
|
131
|
+
listTagSchemas(): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }[]>;
|
|
132
|
+
getTagSchema(tag: string): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> } | null>;
|
|
133
|
+
upsertTagSchema(tag: string, schema: { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }>;
|
|
134
|
+
deleteTagSchema(tag: string): Promise<boolean>;
|
|
135
|
+
getTagSchemaMap(): Promise<Record<string, { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }>>;
|
|
136
136
|
|
|
137
137
|
// Attachments
|
|
138
|
-
addAttachment(noteId: string, path: string, mimeType: string, metadata?: Record<string, unknown>): Attachment
|
|
139
|
-
getAttachments(noteId: string): Attachment[]
|
|
138
|
+
addAttachment(noteId: string, path: string, mimeType: string, metadata?: Record<string, unknown>): Promise<Attachment>;
|
|
139
|
+
getAttachments(noteId: string): Promise<Attachment[]>;
|
|
140
140
|
}
|
|
@@ -110,28 +110,28 @@ More text
|
|
|
110
110
|
// Resolution
|
|
111
111
|
// ---------------------------------------------------------------------------
|
|
112
112
|
|
|
113
|
-
describe("resolveWikilink", () => {
|
|
114
|
-
it("resolves exact path match", () => {
|
|
115
|
-
store.createNote("Target note", { path: "My Note" });
|
|
113
|
+
describe("resolveWikilink", async () => {
|
|
114
|
+
it("resolves exact path match", async () => {
|
|
115
|
+
await store.createNote("Target note", { path: "My Note" });
|
|
116
116
|
const id = resolveWikilink(db, "My Note");
|
|
117
117
|
expect(id).toBeTruthy();
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it("resolves case-insensitively", () => {
|
|
121
|
-
const note = store.createNote("Target", { path: "My Note" });
|
|
120
|
+
it("resolves case-insensitively", async () => {
|
|
121
|
+
const note = await store.createNote("Target", { path: "My Note" });
|
|
122
122
|
const id = resolveWikilink(db, "my note");
|
|
123
123
|
expect(id).toBe(note.id);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
it("resolves basename match", () => {
|
|
127
|
-
const note = store.createNote("Deep note", { path: "Projects/Parachute/README" });
|
|
126
|
+
it("resolves basename match", async () => {
|
|
127
|
+
const note = await store.createNote("Deep note", { path: "Projects/Parachute/README" });
|
|
128
128
|
const id = resolveWikilink(db, "README");
|
|
129
129
|
expect(id).toBe(note.id);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
it("returns null for ambiguous basename", () => {
|
|
133
|
-
store.createNote("A", { path: "Folder1/README" });
|
|
134
|
-
store.createNote("B", { path: "Folder2/README" });
|
|
132
|
+
it("returns null for ambiguous basename", async () => {
|
|
133
|
+
await store.createNote("A", { path: "Folder1/README" });
|
|
134
|
+
await store.createNote("B", { path: "Folder2/README" });
|
|
135
135
|
const id = resolveWikilink(db, "README");
|
|
136
136
|
expect(id).toBeNull();
|
|
137
137
|
});
|
|
@@ -146,21 +146,21 @@ describe("resolveWikilink", () => {
|
|
|
146
146
|
// Sync
|
|
147
147
|
// ---------------------------------------------------------------------------
|
|
148
148
|
|
|
149
|
-
describe("syncWikilinks", () => {
|
|
150
|
-
it("creates links for resolved wikilinks", () => {
|
|
151
|
-
const target = store.createNote("Target", { path: "Target Note" });
|
|
152
|
-
const source = store.createNote("See [[Target Note]]");
|
|
149
|
+
describe("syncWikilinks", async () => {
|
|
150
|
+
it("creates links for resolved wikilinks", async () => {
|
|
151
|
+
const target = await store.createNote("Target", { path: "Target Note" });
|
|
152
|
+
const source = await store.createNote("See [[Target Note]]");
|
|
153
153
|
|
|
154
|
-
const links = store.getLinks(source.id, { direction: "outbound" });
|
|
154
|
+
const links = await store.getLinks(source.id, { direction: "outbound" });
|
|
155
155
|
expect(links).toHaveLength(1);
|
|
156
156
|
expect(links[0].targetId).toBe(target.id);
|
|
157
157
|
expect(links[0].relationship).toBe("wikilink");
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
it("tracks unresolved wikilinks", () => {
|
|
161
|
-
const source = store.createNote("See [[Missing Note]]");
|
|
160
|
+
it("tracks unresolved wikilinks", async () => {
|
|
161
|
+
const source = await store.createNote("See [[Missing Note]]");
|
|
162
162
|
|
|
163
|
-
const links = store.getLinks(source.id, { direction: "outbound" });
|
|
163
|
+
const links = await store.getLinks(source.id, { direction: "outbound" });
|
|
164
164
|
expect(links).toHaveLength(0);
|
|
165
165
|
|
|
166
166
|
// Check unresolved table
|
|
@@ -171,85 +171,85 @@ describe("syncWikilinks", () => {
|
|
|
171
171
|
expect(unresolved[0].target_path).toBe("Missing Note");
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
it("resolves pending wikilinks when target note is created", () => {
|
|
175
|
-
const source = store.createNote("See [[Future Note]]");
|
|
174
|
+
it("resolves pending wikilinks when target note is created", async () => {
|
|
175
|
+
const source = await store.createNote("See [[Future Note]]");
|
|
176
176
|
|
|
177
177
|
// No link yet
|
|
178
|
-
expect(store.getLinks(source.id, { direction: "outbound" })).toHaveLength(0);
|
|
178
|
+
expect(await store.getLinks(source.id, { direction: "outbound" })).toHaveLength(0);
|
|
179
179
|
|
|
180
180
|
// Create the target note
|
|
181
|
-
const target = store.createNote("I exist now", { path: "Future Note" });
|
|
181
|
+
const target = await store.createNote("I exist now", { path: "Future Note" });
|
|
182
182
|
|
|
183
183
|
// Link should now exist
|
|
184
|
-
const links = store.getLinks(source.id, { direction: "outbound" });
|
|
184
|
+
const links = await store.getLinks(source.id, { direction: "outbound" });
|
|
185
185
|
expect(links).toHaveLength(1);
|
|
186
186
|
expect(links[0].targetId).toBe(target.id);
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
it("removes links when wikilinks are removed from content", () => {
|
|
190
|
-
const target = store.createNote("Target", { path: "Target" });
|
|
191
|
-
const source = store.createNote("See [[Target]]");
|
|
189
|
+
it("removes links when wikilinks are removed from content", async () => {
|
|
190
|
+
const target = await store.createNote("Target", { path: "Target" });
|
|
191
|
+
const source = await store.createNote("See [[Target]]");
|
|
192
192
|
|
|
193
|
-
expect(store.getLinks(source.id, { direction: "outbound" })).toHaveLength(1);
|
|
193
|
+
expect(await store.getLinks(source.id, { direction: "outbound" })).toHaveLength(1);
|
|
194
194
|
|
|
195
195
|
// Update content to remove the wikilink
|
|
196
|
-
store.updateNote(source.id, { content: "No more links here." });
|
|
196
|
+
await store.updateNote(source.id, { content: "No more links here." });
|
|
197
197
|
|
|
198
|
-
expect(store.getLinks(source.id, { direction: "outbound" })).toHaveLength(0);
|
|
198
|
+
expect(await store.getLinks(source.id, { direction: "outbound" })).toHaveLength(0);
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
-
it("adds new links when wikilinks are added to content", () => {
|
|
202
|
-
const a = store.createNote("A", { path: "Note A" });
|
|
203
|
-
const b = store.createNote("B", { path: "Note B" });
|
|
204
|
-
const source = store.createNote("See [[Note A]]");
|
|
201
|
+
it("adds new links when wikilinks are added to content", async () => {
|
|
202
|
+
const a = await store.createNote("A", { path: "Note A" });
|
|
203
|
+
const b = await store.createNote("B", { path: "Note B" });
|
|
204
|
+
const source = await store.createNote("See [[Note A]]");
|
|
205
205
|
|
|
206
|
-
expect(store.getLinks(source.id, { direction: "outbound" })).toHaveLength(1);
|
|
206
|
+
expect(await store.getLinks(source.id, { direction: "outbound" })).toHaveLength(1);
|
|
207
207
|
|
|
208
208
|
// Update to add another link
|
|
209
|
-
store.updateNote(source.id, { content: "See [[Note A]] and [[Note B]]" });
|
|
209
|
+
await store.updateNote(source.id, { content: "See [[Note A]] and [[Note B]]" });
|
|
210
210
|
|
|
211
|
-
const links = store.getLinks(source.id, { direction: "outbound" });
|
|
211
|
+
const links = await store.getLinks(source.id, { direction: "outbound" });
|
|
212
212
|
expect(links).toHaveLength(2);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
it("does not create self-links", () => {
|
|
216
|
-
const note = store.createNote("I link to [[Myself]]", { path: "Myself" });
|
|
217
|
-
const links = store.getLinks(note.id, { direction: "outbound" });
|
|
215
|
+
it("does not create self-links", async () => {
|
|
216
|
+
const note = await store.createNote("I link to [[Myself]]", { path: "Myself" });
|
|
217
|
+
const links = await store.getLinks(note.id, { direction: "outbound" });
|
|
218
218
|
expect(links.filter((l) => l.relationship === "wikilink")).toHaveLength(0);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
-
it("deduplicates multiple mentions of same target", () => {
|
|
222
|
-
const target = store.createNote("Target", { path: "Target" });
|
|
223
|
-
const source = store.createNote("See [[Target]] and again [[Target]]");
|
|
221
|
+
it("deduplicates multiple mentions of same target", async () => {
|
|
222
|
+
const target = await store.createNote("Target", { path: "Target" });
|
|
223
|
+
const source = await store.createNote("See [[Target]] and again [[Target]]");
|
|
224
224
|
|
|
225
|
-
const links = store.getLinks(source.id, { direction: "outbound" })
|
|
225
|
+
const links = (await store.getLinks(source.id, { direction: "outbound" }))
|
|
226
226
|
.filter((l) => l.relationship === "wikilink");
|
|
227
227
|
expect(links).toHaveLength(1);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
-
it("preserves non-wikilink links", () => {
|
|
231
|
-
const a = store.createNote("A", { id: "a", path: "Note A" });
|
|
232
|
-
const b = store.createNote("B", { id: "b", path: "Note B" });
|
|
230
|
+
it("preserves non-wikilink links", async () => {
|
|
231
|
+
const a = await store.createNote("A", { id: "a", path: "Note A" });
|
|
232
|
+
const b = await store.createNote("B", { id: "b", path: "Note B" });
|
|
233
233
|
|
|
234
234
|
// Manual semantic link
|
|
235
|
-
store.createLink("a", "b", "related-to");
|
|
235
|
+
await store.createLink("a", "b", "related-to");
|
|
236
236
|
|
|
237
237
|
// Create note with wikilink to B
|
|
238
|
-
const source = store.createNote("See [[Note B]]", { id: "source" });
|
|
238
|
+
const source = await store.createNote("See [[Note B]]", { id: "source" });
|
|
239
239
|
|
|
240
240
|
// Update content to remove wikilink
|
|
241
|
-
store.updateNote("source", { content: "No links" });
|
|
241
|
+
await store.updateNote("source", { content: "No links" });
|
|
242
242
|
|
|
243
243
|
// Semantic link between a and b should still exist
|
|
244
|
-
const links = store.getLinks("a", { direction: "outbound" });
|
|
244
|
+
const links = await store.getLinks("a", { direction: "outbound" });
|
|
245
245
|
expect(links.some((l) => l.relationship === "related-to")).toBe(true);
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
it("stores display text and anchor in link metadata", () => {
|
|
249
|
-
const target = store.createNote("Target", { path: "Target" });
|
|
250
|
-
const source = store.createNote("See [[Target#Introduction|intro]]");
|
|
248
|
+
it("stores display text and anchor in link metadata", async () => {
|
|
249
|
+
const target = await store.createNote("Target", { path: "Target" });
|
|
250
|
+
const source = await store.createNote("See [[Target#Introduction|intro]]");
|
|
251
251
|
|
|
252
|
-
const links = store.getLinks(source.id, { direction: "outbound" });
|
|
252
|
+
const links = await store.getLinks(source.id, { direction: "outbound" });
|
|
253
253
|
expect(links).toHaveLength(1);
|
|
254
254
|
expect(links[0].metadata?.display).toBe("intro");
|
|
255
255
|
expect(links[0].metadata?.anchor).toBe("Introduction");
|
|
@@ -260,17 +260,17 @@ describe("syncWikilinks", () => {
|
|
|
260
260
|
// Integration with path changes
|
|
261
261
|
// ---------------------------------------------------------------------------
|
|
262
262
|
|
|
263
|
-
describe("path-based resolution", () => {
|
|
264
|
-
it("resolves pending links when a note gets a path", () => {
|
|
265
|
-
const source = store.createNote("See [[Named Note]]");
|
|
266
|
-
expect(store.getLinks(source.id, { direction: "outbound" })).toHaveLength(0);
|
|
263
|
+
describe("path-based resolution", async () => {
|
|
264
|
+
it("resolves pending links when a note gets a path", async () => {
|
|
265
|
+
const source = await store.createNote("See [[Named Note]]");
|
|
266
|
+
expect(await store.getLinks(source.id, { direction: "outbound" })).toHaveLength(0);
|
|
267
267
|
|
|
268
268
|
// Create a note without a path, then give it one
|
|
269
|
-
const target = store.createNote("Unnamed");
|
|
270
|
-
store.updateNote(target.id, { path: "Named Note" });
|
|
269
|
+
const target = await store.createNote("Unnamed");
|
|
270
|
+
await store.updateNote(target.id, { path: "Named Note" });
|
|
271
271
|
|
|
272
272
|
// The pending link should be resolved
|
|
273
|
-
const links = store.getLinks(source.id, { direction: "outbound" });
|
|
273
|
+
const links = await store.getLinks(source.id, { direction: "outbound" });
|
|
274
274
|
expect(links).toHaveLength(1);
|
|
275
275
|
expect(links[0].targetId).toBe(target.id);
|
|
276
276
|
});
|
package/docs/HTTP_API.md
CHANGED
|
@@ -46,8 +46,10 @@ Authorization: Bearer <key>
|
|
|
46
46
|
X-API-Key: <key>
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
Every request is authenticated — localhost and remote traffic go through the
|
|
50
|
+
same path, there is no bypass. Local dev feels friction-free because you can
|
|
51
|
+
hand the CLI-generated API key to your script without exposing it to the
|
|
52
|
+
network, not because the auth check is skipped.
|
|
51
53
|
|
|
52
54
|
Keys have a **scope**:
|
|
53
55
|
|