@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/CLAUDE.md +2 -2
  3. package/README.md +289 -44
  4. package/core/src/core.test.ts +802 -346
  5. package/core/src/expand.ts +140 -0
  6. package/core/src/hooks.test.ts +27 -27
  7. package/core/src/hooks.ts +1 -1
  8. package/core/src/mcp.ts +102 -39
  9. package/core/src/notes.ts +82 -4
  10. package/core/src/obsidian.test.ts +11 -11
  11. package/core/src/paths.test.ts +46 -46
  12. package/core/src/schema.ts +18 -2
  13. package/core/src/store.ts +51 -51
  14. package/core/src/types.ts +29 -29
  15. package/core/src/wikilinks.test.ts +61 -61
  16. package/docs/HTTP_API.md +4 -2
  17. package/package.json +1 -1
  18. package/src/auth.test.ts +319 -0
  19. package/src/backup-launchd.test.ts +90 -0
  20. package/src/backup-launchd.ts +169 -0
  21. package/src/backup.test.ts +715 -0
  22. package/src/backup.ts +699 -0
  23. package/src/cli.ts +923 -31
  24. package/src/config.test.ts +173 -0
  25. package/src/config.ts +345 -15
  26. package/src/daemon.ts +136 -0
  27. package/src/doctor.test.ts +356 -0
  28. package/src/health.test.ts +201 -0
  29. package/src/health.ts +115 -0
  30. package/src/launchd.test.ts +91 -0
  31. package/src/launchd.ts +37 -40
  32. package/src/mcp-http.ts +1 -1
  33. package/src/mcp-tools.ts +7 -9
  34. package/src/oauth.test.ts +289 -8
  35. package/src/oauth.ts +66 -13
  36. package/src/published.test.ts +21 -21
  37. package/src/routes.ts +152 -70
  38. package/src/routing.test.ts +478 -0
  39. package/src/routing.ts +413 -0
  40. package/src/server.ts +7 -278
  41. package/src/systemd.test.ts +15 -0
  42. package/src/systemd.ts +18 -11
  43. package/src/triggers.test.ts +7 -7
  44. package/src/triggers.ts +6 -6
  45. package/src/vault-store.ts +20 -3
  46. package/src/vault.test.ts +356 -262
  47. package/.claude/settings.local.json +0 -31
  48. package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +0 -2
  49. package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +0 -1
  50. package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +0 -2
  51. package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +0 -2
  52. package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +0 -1
  53. package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +0 -1
  54. package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +0 -211
  55. package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +0 -59
  56. package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +0 -232
  57. package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +0 -182
  58. package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +0 -91
  59. package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +0 -70
  60. package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +0 -59
  61. package/religions-abrahamic-filter.png +0 -0
  62. package/religions-buddhism-v2.png +0 -0
  63. package/religions-buddhism.png +0 -0
  64. package/religions-final.png +0 -0
  65. package/religions-v1.png +0 -0
  66. package/religions-v2.png +0 -0
  67. package/religions-zen.png +0 -0
  68. package/web/README.md +0 -73
  69. package/web/bun.lock +0 -827
  70. package/web/eslint.config.js +0 -23
  71. package/web/index.html +0 -15
  72. package/web/package.json +0 -36
  73. package/web/public/favicon.svg +0 -1
  74. package/web/public/icons.svg +0 -24
  75. package/web/src/App.tsx +0 -149
  76. package/web/src/Graph.tsx +0 -200
  77. package/web/src/NoteView.tsx +0 -155
  78. package/web/src/Sidebar.tsx +0 -186
  79. package/web/src/api.ts +0 -21
  80. package/web/src/index.css +0 -50
  81. package/web/src/main.tsx +0 -10
  82. package/web/src/types.ts +0 -37
  83. package/web/src/utils.ts +0 -107
  84. package/web/tsconfig.app.json +0 -25
  85. package/web/tsconfig.json +0 -7
  86. package/web/tsconfig.node.json +0 -24
  87. 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 { normalizePath, pathTitle } from "./paths.js";
8
+ import { pathTitle } from "./paths.js";
9
9
  import { HookRegistry } from "./hooks.js";
10
10
 
11
11
  /**
12
- * SQLite-backed Store implementation.
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 SqliteStore implements Store {
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(id: string, updates: { content?: string; path?: string; metadata?: Record<string, unknown>; created_at?: string; skipUpdatedAt?: boolean }): Note {
56
- // Capture old path before update for rename cascading
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
- Requests from localhost bypass auth (you can hit the server directly without a
50
- key for local dev).
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",