@openparachute/vault 0.1.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/.claude/settings.local.json +31 -0
- package/.dockerignore +8 -0
- package/.env.example +9 -0
- package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +2 -0
- package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +1 -0
- package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +2 -0
- package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +2 -0
- package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +211 -0
- package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +59 -0
- package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +232 -0
- package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +182 -0
- package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +91 -0
- package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +70 -0
- package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +59 -0
- package/CLAUDE.md +115 -0
- package/Caddyfile +3 -0
- package/Dockerfile +22 -0
- package/LICENSE +661 -0
- package/README.md +356 -0
- package/bun.lock +219 -0
- package/bunfig.toml +2 -0
- package/core/package.json +7 -0
- package/core/src/core.test.ts +940 -0
- package/core/src/hooks.test.ts +361 -0
- package/core/src/hooks.ts +234 -0
- package/core/src/links.ts +352 -0
- package/core/src/mcp.ts +672 -0
- package/core/src/notes.ts +520 -0
- package/core/src/obsidian.test.ts +380 -0
- package/core/src/obsidian.ts +322 -0
- package/core/src/paths.test.ts +197 -0
- package/core/src/paths.ts +53 -0
- package/core/src/schema.ts +331 -0
- package/core/src/store.ts +303 -0
- package/core/src/tag-schemas.ts +104 -0
- package/core/src/test-preload.ts +8 -0
- package/core/src/types.ts +140 -0
- package/core/src/wikilinks.test.ts +277 -0
- package/core/src/wikilinks.ts +402 -0
- package/deploy/parachute-vault.service +20 -0
- package/docker-compose.yml +50 -0
- package/docs/HTTP_API.md +328 -0
- package/fly.toml +24 -0
- package/package.json +32 -0
- package/railway.json +14 -0
- 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/scripts/migrate-audio-to-opus.test.ts +237 -0
- package/scripts/migrate-audio-to-opus.ts +499 -0
- package/src/auth.ts +170 -0
- package/src/cli.ts +1131 -0
- package/src/config-triggers.test.ts +83 -0
- package/src/config.test.ts +125 -0
- package/src/config.ts +716 -0
- package/src/db.ts +14 -0
- package/src/launchd.ts +109 -0
- package/src/mcp-http.ts +113 -0
- package/src/mcp-tools.ts +155 -0
- package/src/oauth.test.ts +1242 -0
- package/src/oauth.ts +729 -0
- package/src/owner-auth.ts +159 -0
- package/src/prompt.ts +141 -0
- package/src/published.test.ts +214 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/routes.ts +822 -0
- package/src/server.ts +450 -0
- package/src/systemd.ts +84 -0
- package/src/token-store.test.ts +174 -0
- package/src/token-store.ts +241 -0
- package/src/triggers.test.ts +397 -0
- package/src/triggers.ts +412 -0
- package/src/two-factor.test.ts +246 -0
- package/src/two-factor.ts +222 -0
- package/src/vault-store.ts +47 -0
- package/src/vault.test.ts +1309 -0
- package/tsconfig.json +29 -0
- package/web/README.md +73 -0
- package/web/bun.lock +827 -0
- package/web/eslint.config.js +23 -0
- package/web/index.html +15 -0
- package/web/package.json +36 -0
- package/web/public/favicon.svg +1 -0
- package/web/public/icons.svg +24 -0
- package/web/src/App.tsx +149 -0
- package/web/src/Graph.tsx +200 -0
- package/web/src/NoteView.tsx +155 -0
- package/web/src/Sidebar.tsx +186 -0
- package/web/src/api.ts +21 -0
- package/web/src/index.css +50 -0
- package/web/src/main.tsx +10 -0
- package/web/src/types.ts +37 -0
- package/web/src/utils.ts +107 -0
- package/web/tsconfig.app.json +25 -0
- package/web/tsconfig.json +7 -0
- package/web/tsconfig.node.json +24 -0
- package/web/vite.config.ts +15 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import type { Link, NoteSummary, HydratedLink } from "./types.js";
|
|
3
|
+
import { getNoteTags } from "./notes.js";
|
|
4
|
+
|
|
5
|
+
export function createLink(
|
|
6
|
+
db: Database,
|
|
7
|
+
sourceId: string,
|
|
8
|
+
targetId: string,
|
|
9
|
+
relationship: string,
|
|
10
|
+
metadata?: Record<string, unknown>,
|
|
11
|
+
): Link {
|
|
12
|
+
const now = new Date().toISOString();
|
|
13
|
+
const metadataJson = metadata ? JSON.stringify(metadata) : "{}";
|
|
14
|
+
|
|
15
|
+
db.prepare(
|
|
16
|
+
`INSERT OR IGNORE INTO links (source_id, target_id, relationship, metadata, created_at)
|
|
17
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
18
|
+
).run(sourceId, targetId, relationship, metadataJson, now);
|
|
19
|
+
|
|
20
|
+
const row = db.prepare(
|
|
21
|
+
`SELECT * FROM links WHERE source_id = ? AND target_id = ? AND relationship = ?`,
|
|
22
|
+
).get(sourceId, targetId, relationship) as LinkRow;
|
|
23
|
+
return rowToLink(row);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function deleteLink(
|
|
27
|
+
db: Database,
|
|
28
|
+
sourceId: string,
|
|
29
|
+
targetId: string,
|
|
30
|
+
relationship: string,
|
|
31
|
+
): void {
|
|
32
|
+
db.prepare(
|
|
33
|
+
"DELETE FROM links WHERE source_id = ? AND target_id = ? AND relationship = ?",
|
|
34
|
+
).run(sourceId, targetId, relationship);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getLinks(
|
|
38
|
+
db: Database,
|
|
39
|
+
noteId: string,
|
|
40
|
+
opts?: { direction?: "outbound" | "inbound" | "both" },
|
|
41
|
+
): Link[] {
|
|
42
|
+
return listLinks(db, { noteId, direction: opts?.direction });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List links with optional filters.
|
|
47
|
+
* - If `noteId` is provided: restricts to links touching that note
|
|
48
|
+
* (respects `direction`: outbound, inbound, or both).
|
|
49
|
+
* - If `relationship` is provided: restricts to links of that type.
|
|
50
|
+
* - Without filters: returns every link in the vault.
|
|
51
|
+
*
|
|
52
|
+
* Returns bare `Link[]` (no hydration). Callers that need note details
|
|
53
|
+
* should pair the result with `getNote` / `getNotes`.
|
|
54
|
+
*/
|
|
55
|
+
export function listLinks(
|
|
56
|
+
db: Database,
|
|
57
|
+
opts?: {
|
|
58
|
+
noteId?: string;
|
|
59
|
+
direction?: "outbound" | "inbound" | "both";
|
|
60
|
+
relationship?: string;
|
|
61
|
+
},
|
|
62
|
+
): Link[] {
|
|
63
|
+
const conditions: string[] = [];
|
|
64
|
+
const params: unknown[] = [];
|
|
65
|
+
|
|
66
|
+
if (opts?.noteId) {
|
|
67
|
+
const direction = opts.direction ?? "both";
|
|
68
|
+
if (direction === "outbound") {
|
|
69
|
+
conditions.push("source_id = ?");
|
|
70
|
+
params.push(opts.noteId);
|
|
71
|
+
} else if (direction === "inbound") {
|
|
72
|
+
conditions.push("target_id = ?");
|
|
73
|
+
params.push(opts.noteId);
|
|
74
|
+
} else {
|
|
75
|
+
conditions.push("(source_id = ? OR target_id = ?)");
|
|
76
|
+
params.push(opts.noteId, opts.noteId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (opts?.relationship) {
|
|
81
|
+
conditions.push("relationship = ?");
|
|
82
|
+
params.push(opts.relationship);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
86
|
+
const sql = `SELECT * FROM links ${where} ORDER BY created_at DESC`;
|
|
87
|
+
const rows = db.prepare(sql).all(...params) as LinkRow[];
|
|
88
|
+
return rows.map(rowToLink);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- Note Summaries (for hydrated results) ----
|
|
92
|
+
|
|
93
|
+
interface SummaryRow {
|
|
94
|
+
id: string;
|
|
95
|
+
path: string | null;
|
|
96
|
+
metadata: string | null;
|
|
97
|
+
created_at: string;
|
|
98
|
+
updated_at: string | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseMetadata(raw: string | null): Record<string, unknown> | undefined {
|
|
102
|
+
if (!raw || raw === "{}") return undefined;
|
|
103
|
+
try { return JSON.parse(raw); } catch { return undefined; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getNoteSummary(db: Database, noteId: string): NoteSummary | undefined {
|
|
107
|
+
const row = db.prepare(
|
|
108
|
+
"SELECT id, path, metadata, created_at, updated_at FROM notes WHERE id = ?",
|
|
109
|
+
).get(noteId) as SummaryRow | undefined;
|
|
110
|
+
if (!row) return undefined;
|
|
111
|
+
return {
|
|
112
|
+
id: row.id,
|
|
113
|
+
path: row.path ?? undefined,
|
|
114
|
+
metadata: parseMetadata(row.metadata),
|
|
115
|
+
createdAt: row.created_at,
|
|
116
|
+
updatedAt: row.updated_at ?? undefined,
|
|
117
|
+
tags: getNoteTags(db, row.id),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getNoteSummaries(db: Database, noteIds: string[]): Map<string, NoteSummary> {
|
|
122
|
+
const map = new Map<string, NoteSummary>();
|
|
123
|
+
if (noteIds.length === 0) return map;
|
|
124
|
+
const placeholders = noteIds.map(() => "?").join(", ");
|
|
125
|
+
const rows = db.prepare(
|
|
126
|
+
`SELECT id, path, metadata, created_at, updated_at FROM notes WHERE id IN (${placeholders})`,
|
|
127
|
+
).all(...noteIds) as SummaryRow[];
|
|
128
|
+
for (const row of rows) {
|
|
129
|
+
map.set(row.id, {
|
|
130
|
+
id: row.id,
|
|
131
|
+
path: row.path ?? undefined,
|
|
132
|
+
metadata: parseMetadata(row.metadata),
|
|
133
|
+
createdAt: row.created_at,
|
|
134
|
+
updatedAt: row.updated_at ?? undefined,
|
|
135
|
+
tags: getNoteTags(db, row.id),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return map;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get links for a note with hydrated note summaries.
|
|
143
|
+
* Always includes note path/tags. Optionally includes content.
|
|
144
|
+
*/
|
|
145
|
+
export function getLinksHydrated(
|
|
146
|
+
db: Database,
|
|
147
|
+
noteId: string,
|
|
148
|
+
opts?: { direction?: "outbound" | "inbound" | "both"; include_content?: boolean },
|
|
149
|
+
): HydratedLink[] {
|
|
150
|
+
const links = getLinks(db, noteId, opts);
|
|
151
|
+
|
|
152
|
+
// Collect all note IDs we need to hydrate
|
|
153
|
+
const noteIds = new Set<string>();
|
|
154
|
+
for (const link of links) {
|
|
155
|
+
noteIds.add(link.sourceId);
|
|
156
|
+
noteIds.add(link.targetId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const summaries = getNoteSummaries(db, [...noteIds]);
|
|
160
|
+
|
|
161
|
+
return links.map((link) => ({
|
|
162
|
+
...link,
|
|
163
|
+
sourceNote: summaries.get(link.sourceId),
|
|
164
|
+
targetNote: summaries.get(link.targetId),
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- Deeper Link Queries ----
|
|
169
|
+
|
|
170
|
+
export interface TraversalNode {
|
|
171
|
+
noteId: string;
|
|
172
|
+
depth: number;
|
|
173
|
+
relationship: string;
|
|
174
|
+
direction: "outbound" | "inbound";
|
|
175
|
+
note?: NoteSummary;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Traverse the link graph from a starting note up to `maxDepth` hops.
|
|
180
|
+
* Returns all reachable notes with their depth and how they were reached.
|
|
181
|
+
*/
|
|
182
|
+
export function traverseLinks(
|
|
183
|
+
db: Database,
|
|
184
|
+
noteId: string,
|
|
185
|
+
opts?: { max_depth?: number; relationship?: string },
|
|
186
|
+
): TraversalNode[] {
|
|
187
|
+
const maxDepth = opts?.max_depth ?? 2;
|
|
188
|
+
const relFilter = opts?.relationship;
|
|
189
|
+
const visited = new Set<string>([noteId]);
|
|
190
|
+
const results: TraversalNode[] = [];
|
|
191
|
+
let frontier = [noteId];
|
|
192
|
+
|
|
193
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
194
|
+
const nextFrontier: string[] = [];
|
|
195
|
+
|
|
196
|
+
for (const currentId of frontier) {
|
|
197
|
+
// Outbound links
|
|
198
|
+
let outbound: LinkRow[];
|
|
199
|
+
if (relFilter) {
|
|
200
|
+
outbound = db.prepare(
|
|
201
|
+
"SELECT * FROM links WHERE source_id = ? AND relationship = ?",
|
|
202
|
+
).all(currentId, relFilter) as LinkRow[];
|
|
203
|
+
} else {
|
|
204
|
+
outbound = db.prepare(
|
|
205
|
+
"SELECT * FROM links WHERE source_id = ?",
|
|
206
|
+
).all(currentId) as LinkRow[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const row of outbound) {
|
|
210
|
+
if (!visited.has(row.target_id)) {
|
|
211
|
+
visited.add(row.target_id);
|
|
212
|
+
nextFrontier.push(row.target_id);
|
|
213
|
+
results.push({
|
|
214
|
+
noteId: row.target_id,
|
|
215
|
+
depth,
|
|
216
|
+
relationship: row.relationship,
|
|
217
|
+
direction: "outbound",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Inbound links
|
|
223
|
+
let inbound: LinkRow[];
|
|
224
|
+
if (relFilter) {
|
|
225
|
+
inbound = db.prepare(
|
|
226
|
+
"SELECT * FROM links WHERE target_id = ? AND relationship = ?",
|
|
227
|
+
).all(currentId, relFilter) as LinkRow[];
|
|
228
|
+
} else {
|
|
229
|
+
inbound = db.prepare(
|
|
230
|
+
"SELECT * FROM links WHERE target_id = ?",
|
|
231
|
+
).all(currentId) as LinkRow[];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const row of inbound) {
|
|
235
|
+
if (!visited.has(row.source_id)) {
|
|
236
|
+
visited.add(row.source_id);
|
|
237
|
+
nextFrontier.push(row.source_id);
|
|
238
|
+
results.push({
|
|
239
|
+
noteId: row.source_id,
|
|
240
|
+
depth,
|
|
241
|
+
relationship: row.relationship,
|
|
242
|
+
direction: "inbound",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
frontier = nextFrontier;
|
|
249
|
+
if (frontier.length === 0) break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Hydrate with note summaries
|
|
253
|
+
const noteIds = results.map((r) => r.noteId);
|
|
254
|
+
const summaries = getNoteSummaries(db, noteIds);
|
|
255
|
+
for (const result of results) {
|
|
256
|
+
result.note = summaries.get(result.noteId);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return results;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Find a path between two notes in the link graph.
|
|
264
|
+
* Returns the sequence of note IDs from source to target, or null if no path exists.
|
|
265
|
+
*/
|
|
266
|
+
export function findPath(
|
|
267
|
+
db: Database,
|
|
268
|
+
sourceId: string,
|
|
269
|
+
targetId: string,
|
|
270
|
+
opts?: { max_depth?: number },
|
|
271
|
+
): { path: string[]; relationships: string[] } | null {
|
|
272
|
+
const maxDepth = opts?.max_depth ?? 5;
|
|
273
|
+
|
|
274
|
+
if (sourceId === targetId) {
|
|
275
|
+
return { path: [sourceId], relationships: [] };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// BFS from source
|
|
279
|
+
const visited = new Map<string, { parent: string; relationship: string }>();
|
|
280
|
+
visited.set(sourceId, { parent: "", relationship: "" });
|
|
281
|
+
let frontier = [sourceId];
|
|
282
|
+
|
|
283
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
284
|
+
const nextFrontier: string[] = [];
|
|
285
|
+
|
|
286
|
+
for (const currentId of frontier) {
|
|
287
|
+
// Check all neighbors (both directions)
|
|
288
|
+
const outbound = db.prepare(
|
|
289
|
+
"SELECT * FROM links WHERE source_id = ?",
|
|
290
|
+
).all(currentId) as LinkRow[];
|
|
291
|
+
|
|
292
|
+
const inbound = db.prepare(
|
|
293
|
+
"SELECT * FROM links WHERE target_id = ?",
|
|
294
|
+
).all(currentId) as LinkRow[];
|
|
295
|
+
|
|
296
|
+
const neighbors: { id: string; rel: string }[] = [
|
|
297
|
+
...outbound.map((r) => ({ id: r.target_id, rel: r.relationship })),
|
|
298
|
+
...inbound.map((r) => ({ id: r.source_id, rel: r.relationship })),
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
for (const neighbor of neighbors) {
|
|
302
|
+
if (visited.has(neighbor.id)) continue;
|
|
303
|
+
visited.set(neighbor.id, { parent: currentId, relationship: neighbor.rel });
|
|
304
|
+
nextFrontier.push(neighbor.id);
|
|
305
|
+
|
|
306
|
+
if (neighbor.id === targetId) {
|
|
307
|
+
// Reconstruct path
|
|
308
|
+
const path: string[] = [];
|
|
309
|
+
const relationships: string[] = [];
|
|
310
|
+
let current = targetId;
|
|
311
|
+
while (current !== sourceId) {
|
|
312
|
+
path.unshift(current);
|
|
313
|
+
const entry = visited.get(current)!;
|
|
314
|
+
relationships.unshift(entry.relationship);
|
|
315
|
+
current = entry.parent;
|
|
316
|
+
}
|
|
317
|
+
path.unshift(sourceId);
|
|
318
|
+
return { path, relationships };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
frontier = nextFrontier;
|
|
324
|
+
if (frontier.length === 0) break;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---- Internal ----
|
|
331
|
+
|
|
332
|
+
interface LinkRow {
|
|
333
|
+
source_id: string;
|
|
334
|
+
target_id: string;
|
|
335
|
+
relationship: string;
|
|
336
|
+
metadata: string | null;
|
|
337
|
+
created_at: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function rowToLink(row: LinkRow): Link {
|
|
341
|
+
let metadata: Record<string, unknown> | undefined;
|
|
342
|
+
if (row.metadata && row.metadata !== "{}") {
|
|
343
|
+
try { metadata = JSON.parse(row.metadata); } catch {}
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
sourceId: row.source_id,
|
|
347
|
+
targetId: row.target_id,
|
|
348
|
+
relationship: row.relationship,
|
|
349
|
+
metadata,
|
|
350
|
+
createdAt: row.created_at,
|
|
351
|
+
};
|
|
352
|
+
}
|