@openparachute/vault 0.2.4 → 0.3.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 +2 -25
- package/CHANGELOG.md +64 -0
- package/CLAUDE.md +17 -7
- package/README.md +169 -136
- package/core/src/core.test.ts +591 -19
- package/core/src/hooks.ts +111 -3
- package/core/src/indexed-fields.test.ts +285 -0
- package/core/src/indexed-fields.ts +238 -0
- package/core/src/mcp.ts +127 -6
- package/core/src/notes.ts +153 -11
- package/core/src/query-operators.ts +174 -0
- package/core/src/schema.ts +69 -2
- package/core/src/store.ts +95 -1
- package/core/src/tag-schemas.ts +5 -0
- package/core/src/types.ts +28 -1
- package/docs/HTTP_API.md +105 -1
- package/docs/auth-model.md +340 -0
- package/package/package.json +32 -0
- package/package.json +2 -2
- package/src/auth.test.ts +83 -114
- package/src/auth.ts +68 -6
- package/src/backup-launchd.ts +1 -1
- package/src/backup.test.ts +1 -1
- package/src/backup.ts +18 -17
- package/src/bind.test.ts +28 -0
- package/src/bind.ts +19 -0
- package/src/cli.ts +228 -133
- package/src/config-triggers.test.ts +49 -0
- package/src/config.test.ts +317 -2
- package/src/config.ts +420 -40
- package/src/context.test.ts +136 -0
- package/src/context.ts +115 -0
- package/src/daemon.ts +17 -16
- package/src/doctor.test.ts +9 -7
- package/src/launchd.test.ts +1 -1
- package/src/launchd.ts +6 -6
- package/src/mcp-http.ts +75 -21
- package/src/mcp-install.test.ts +125 -0
- package/src/mcp-install.ts +60 -0
- package/src/mcp-tools.ts +34 -96
- package/src/module-config.ts +109 -0
- package/src/oauth.test.ts +345 -57
- package/src/oauth.ts +155 -35
- package/src/published.test.ts +2 -2
- package/src/routes.ts +209 -33
- package/src/routing.test.ts +817 -300
- package/src/routing.ts +204 -202
- package/src/scopes.test.ts +294 -0
- package/src/scopes.ts +253 -0
- package/src/scribe-env.test.ts +49 -0
- package/src/scribe-env.ts +33 -0
- package/src/server.ts +73 -9
- package/src/services-manifest.test.ts +140 -0
- package/src/services-manifest.ts +99 -0
- package/src/systemd.ts +3 -3
- package/src/token-store.ts +42 -9
- package/src/transcription-worker.test.ts +864 -0
- package/src/transcription-worker.ts +501 -0
- package/src/triggers.test.ts +191 -1
- package/src/triggers.ts +17 -2
- package/src/vault.test.ts +693 -77
- package/src/version.test.ts +1 -1
- 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 -16
package/src/triggers.test.ts
CHANGED
|
@@ -129,7 +129,11 @@ describe("registerTriggers — dispatch modes", async () => {
|
|
|
129
129
|
webhookServer?.stop(true);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
function makeMockStore(
|
|
132
|
+
function makeMockStore(
|
|
133
|
+
note: Note,
|
|
134
|
+
attachments: Attachment[] = [],
|
|
135
|
+
contextNotesByTag: Record<string, Note[]> = {},
|
|
136
|
+
): Store {
|
|
133
137
|
const notes = new Map<string, Note>();
|
|
134
138
|
notes.set(note.id, { ...note });
|
|
135
139
|
const attachmentStore = new Map<string, Attachment[]>();
|
|
@@ -153,6 +157,14 @@ describe("registerTriggers — dispatch modes", async () => {
|
|
|
153
157
|
attachmentStore.set(noteId, existing);
|
|
154
158
|
return att;
|
|
155
159
|
},
|
|
160
|
+
queryNotes: async ({ tags, excludeTags }: { tags?: string[]; excludeTags?: string[] }) => {
|
|
161
|
+
const tag = tags?.[0];
|
|
162
|
+
if (!tag) return [];
|
|
163
|
+
const pool = contextNotesByTag[tag] ?? [];
|
|
164
|
+
if (!excludeTags?.length) return pool;
|
|
165
|
+
const excluded = new Set(excludeTags);
|
|
166
|
+
return pool.filter((n) => !(n.tags ?? []).some((t) => excluded.has(t)));
|
|
167
|
+
},
|
|
156
168
|
} as unknown as Store;
|
|
157
169
|
}
|
|
158
170
|
|
|
@@ -334,6 +346,184 @@ describe("registerTriggers — dispatch modes", async () => {
|
|
|
334
346
|
const meta = updated?.metadata as Record<string, unknown>;
|
|
335
347
|
expect(meta.empty_test_skipped_reason).toBe("note has no content to synthesize");
|
|
336
348
|
});
|
|
349
|
+
|
|
350
|
+
it("send=attachment with include_context attaches context JSON part", async () => {
|
|
351
|
+
const hooks = new HookRegistry();
|
|
352
|
+
const note = makeNote({ id: "ctx1", content: "", tags: ["capture"] });
|
|
353
|
+
|
|
354
|
+
const tmpDir = `/tmp/trigger-ctx-att-${Date.now()}`;
|
|
355
|
+
const { mkdirSync, writeFileSync, rmSync } = await import("fs");
|
|
356
|
+
mkdirSync(`${tmpDir}/2026-04-11`, { recursive: true });
|
|
357
|
+
writeFileSync(`${tmpDir}/2026-04-11/recording.wav`, Buffer.from("fake-wav"));
|
|
358
|
+
|
|
359
|
+
const attachment: Attachment = {
|
|
360
|
+
id: "att-c1",
|
|
361
|
+
noteId: "ctx1",
|
|
362
|
+
path: "2026-04-11/recording.wav",
|
|
363
|
+
mimeType: "audio/wav",
|
|
364
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const person = makeNote({
|
|
368
|
+
id: "p1",
|
|
369
|
+
path: "People/Aaron.md",
|
|
370
|
+
tags: ["person"],
|
|
371
|
+
metadata: { summary: "founder", aliases: ["AG"], secret: "nope" },
|
|
372
|
+
});
|
|
373
|
+
const project = makeNote({
|
|
374
|
+
id: "pj1",
|
|
375
|
+
path: "Projects/Lens.md",
|
|
376
|
+
tags: ["project"],
|
|
377
|
+
metadata: { summary: "note app" },
|
|
378
|
+
});
|
|
379
|
+
const store = makeMockStore(note, [attachment], { person: [person], project: [project] });
|
|
380
|
+
|
|
381
|
+
const originalAssetsDir = process.env.ASSETS_DIR;
|
|
382
|
+
process.env.ASSETS_DIR = tmpDir;
|
|
383
|
+
|
|
384
|
+
webhookHandler = () => Response.json({ text: "transcribed" });
|
|
385
|
+
|
|
386
|
+
registerTriggers(hooks, [{
|
|
387
|
+
name: "ctx_attachment_test",
|
|
388
|
+
when: { tags: ["capture"], has_content: false },
|
|
389
|
+
action: {
|
|
390
|
+
webhook: `http://127.0.0.1:${webhookPort}/transcribe`,
|
|
391
|
+
send: "attachment",
|
|
392
|
+
include_context: [
|
|
393
|
+
{ tag: "person", include_metadata: ["summary", "aliases"] },
|
|
394
|
+
{ tag: "project", include_metadata: ["summary"] },
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
}], { error: () => {}, info: () => {} });
|
|
398
|
+
|
|
399
|
+
await hooks.dispatch("created", note, store);
|
|
400
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
401
|
+
|
|
402
|
+
expect(lastRequest?.formData).toBeDefined();
|
|
403
|
+
const part = lastRequest!.formData!.get("context");
|
|
404
|
+
expect(part).toBeInstanceOf(Blob);
|
|
405
|
+
const body = JSON.parse(await (part as Blob).text());
|
|
406
|
+
expect(body.entries.length).toBe(2);
|
|
407
|
+
expect(body.entries[0]).toEqual({ name: "Aaron", summary: "founder", aliases: ["AG"] });
|
|
408
|
+
expect(body.entries[1]).toEqual({ name: "Lens", summary: "note app" });
|
|
409
|
+
// Non-whitelisted metadata must not leak.
|
|
410
|
+
expect(body.entries[0].secret).toBeUndefined();
|
|
411
|
+
|
|
412
|
+
if (originalAssetsDir) process.env.ASSETS_DIR = originalAssetsDir;
|
|
413
|
+
else delete process.env.ASSETS_DIR;
|
|
414
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("send=attachment without include_context omits context part (no regression)", async () => {
|
|
418
|
+
const hooks = new HookRegistry();
|
|
419
|
+
const note = makeNote({ id: "nx", content: "", tags: ["capture"] });
|
|
420
|
+
|
|
421
|
+
const tmpDir = `/tmp/trigger-ctx-none-${Date.now()}`;
|
|
422
|
+
const { mkdirSync, writeFileSync, rmSync } = await import("fs");
|
|
423
|
+
mkdirSync(`${tmpDir}/2026-04-11`, { recursive: true });
|
|
424
|
+
writeFileSync(`${tmpDir}/2026-04-11/recording.wav`, Buffer.from("fake-wav"));
|
|
425
|
+
const attachment: Attachment = {
|
|
426
|
+
id: "att-x",
|
|
427
|
+
noteId: "nx",
|
|
428
|
+
path: "2026-04-11/recording.wav",
|
|
429
|
+
mimeType: "audio/wav",
|
|
430
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
431
|
+
};
|
|
432
|
+
const store = makeMockStore(note, [attachment]);
|
|
433
|
+
const originalAssetsDir = process.env.ASSETS_DIR;
|
|
434
|
+
process.env.ASSETS_DIR = tmpDir;
|
|
435
|
+
|
|
436
|
+
webhookHandler = () => Response.json({ text: "ok" });
|
|
437
|
+
|
|
438
|
+
registerTriggers(hooks, [{
|
|
439
|
+
name: "no_ctx",
|
|
440
|
+
when: { tags: ["capture"], has_content: false },
|
|
441
|
+
action: { webhook: `http://127.0.0.1:${webhookPort}/transcribe`, send: "attachment" },
|
|
442
|
+
}], { error: () => {}, info: () => {} });
|
|
443
|
+
|
|
444
|
+
await hooks.dispatch("created", note, store);
|
|
445
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
446
|
+
|
|
447
|
+
expect(lastRequest?.formData).toBeDefined();
|
|
448
|
+
expect(lastRequest!.formData!.get("context")).toBeNull();
|
|
449
|
+
|
|
450
|
+
if (originalAssetsDir) process.env.ASSETS_DIR = originalAssetsDir;
|
|
451
|
+
else delete process.env.ASSETS_DIR;
|
|
452
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("send=json with include_context inlines context field", async () => {
|
|
456
|
+
const hooks = new HookRegistry();
|
|
457
|
+
const note = makeNote({ id: "j1", content: "hello", tags: ["test"] });
|
|
458
|
+
const person = makeNote({
|
|
459
|
+
id: "p1",
|
|
460
|
+
path: "People/Aaron.md",
|
|
461
|
+
tags: ["person"],
|
|
462
|
+
metadata: { summary: "founder" },
|
|
463
|
+
});
|
|
464
|
+
const store = makeMockStore(note, [], { person: [person] });
|
|
465
|
+
|
|
466
|
+
webhookHandler = () => Response.json({});
|
|
467
|
+
|
|
468
|
+
registerTriggers(hooks, [{
|
|
469
|
+
name: "json_ctx",
|
|
470
|
+
when: { tags: ["test"] },
|
|
471
|
+
action: {
|
|
472
|
+
webhook: `http://127.0.0.1:${webhookPort}/hook`,
|
|
473
|
+
include_context: [{ tag: "person", include_metadata: ["summary"] }],
|
|
474
|
+
},
|
|
475
|
+
}], { error: () => {}, info: () => {} });
|
|
476
|
+
|
|
477
|
+
await hooks.dispatch("created", note, store);
|
|
478
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
479
|
+
|
|
480
|
+
const body = lastRequest!.body as Record<string, unknown>;
|
|
481
|
+
expect(body.context).toBeDefined();
|
|
482
|
+
const ctx = body.context as { entries: Array<Record<string, unknown>> };
|
|
483
|
+
expect(ctx.entries).toEqual([{ name: "Aaron", summary: "founder" }]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("send=content ignores include_context (TTS-out has no use for it)", async () => {
|
|
487
|
+
const hooks = new HookRegistry();
|
|
488
|
+
const note = makeNote({ id: "c1", content: "speak", tags: ["reader"] });
|
|
489
|
+
const person = makeNote({
|
|
490
|
+
id: "p1",
|
|
491
|
+
path: "People/Aaron.md",
|
|
492
|
+
tags: ["person"],
|
|
493
|
+
metadata: { summary: "founder" },
|
|
494
|
+
});
|
|
495
|
+
const store = makeMockStore(note, [], { person: [person] });
|
|
496
|
+
|
|
497
|
+
const tmpDir = `/tmp/trigger-ctx-content-${Date.now()}`;
|
|
498
|
+
const { mkdirSync, rmSync } = await import("fs");
|
|
499
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
500
|
+
const originalAssetsDir = process.env.ASSETS_DIR;
|
|
501
|
+
process.env.ASSETS_DIR = tmpDir;
|
|
502
|
+
|
|
503
|
+
webhookHandler = () =>
|
|
504
|
+
new Response(Buffer.from("audio"), { headers: { "Content-Type": "audio/ogg" } });
|
|
505
|
+
|
|
506
|
+
registerTriggers(hooks, [{
|
|
507
|
+
name: "content_ctx",
|
|
508
|
+
when: { tags: ["reader"], has_content: true },
|
|
509
|
+
action: {
|
|
510
|
+
webhook: `http://127.0.0.1:${webhookPort}/speech`,
|
|
511
|
+
send: "content",
|
|
512
|
+
include_context: [{ tag: "person", include_metadata: ["summary"] }],
|
|
513
|
+
},
|
|
514
|
+
}], { error: () => {}, info: () => {} });
|
|
515
|
+
|
|
516
|
+
await hooks.dispatch("created", note, store);
|
|
517
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
518
|
+
|
|
519
|
+
const body = lastRequest!.body as Record<string, unknown>;
|
|
520
|
+
expect(body.context).toBeUndefined();
|
|
521
|
+
expect(body.input).toBe("speak");
|
|
522
|
+
|
|
523
|
+
if (originalAssetsDir) process.env.ASSETS_DIR = originalAssetsDir;
|
|
524
|
+
else delete process.env.ASSETS_DIR;
|
|
525
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
526
|
+
});
|
|
337
527
|
});
|
|
338
528
|
|
|
339
529
|
describe("registerTriggers — validation", () => {
|
package/src/triggers.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type { HookRegistry, HookEvent } from "../core/src/hooks.ts";
|
|
|
33
33
|
import type { TriggerConfig, TriggerWhen } from "./config.ts";
|
|
34
34
|
import { getVaultNameForStore } from "./vault-store.ts";
|
|
35
35
|
import { assetsDir } from "./routes.ts";
|
|
36
|
+
import { appendContextPart, fetchContextEntries, type ContextPayload } from "./context.ts";
|
|
36
37
|
|
|
37
38
|
const DEFAULT_TIMEOUT = 60_000;
|
|
38
39
|
|
|
@@ -157,6 +158,7 @@ async function dispatchJson(
|
|
|
157
158
|
attachments: Attachment[],
|
|
158
159
|
existingMeta: Record<string, unknown>,
|
|
159
160
|
hookEvent: HookEvent | undefined,
|
|
161
|
+
context: ContextPayload | null,
|
|
160
162
|
signal: AbortSignal,
|
|
161
163
|
): Promise<DispatchResult> {
|
|
162
164
|
const resp = await fetch(url, {
|
|
@@ -175,6 +177,10 @@ async function dispatchJson(
|
|
|
175
177
|
createdAt: note.createdAt,
|
|
176
178
|
updatedAt: note.updatedAt,
|
|
177
179
|
},
|
|
180
|
+
// Inline when include_context is configured and matched anything; the
|
|
181
|
+
// receiver can key off a top-level `context` field without having to
|
|
182
|
+
// parse multipart.
|
|
183
|
+
...(context && context.entries.length ? { context } : {}),
|
|
178
184
|
}),
|
|
179
185
|
signal,
|
|
180
186
|
});
|
|
@@ -196,6 +202,7 @@ async function dispatchAttachment(
|
|
|
196
202
|
note: Note,
|
|
197
203
|
attachments: Attachment[],
|
|
198
204
|
store: Store,
|
|
205
|
+
context: ContextPayload | null,
|
|
199
206
|
signal: AbortSignal,
|
|
200
207
|
): Promise<DispatchResult> {
|
|
201
208
|
const assetsRoot = resolveAssetsDir(store);
|
|
@@ -210,6 +217,7 @@ async function dispatchAttachment(
|
|
|
210
217
|
|
|
211
218
|
const form = new FormData();
|
|
212
219
|
form.append("file", file);
|
|
220
|
+
if (context) appendContextPart(form, context);
|
|
213
221
|
|
|
214
222
|
const resp = await fetch(url, { method: "POST", body: form, signal });
|
|
215
223
|
if (!resp.ok) {
|
|
@@ -325,20 +333,27 @@ export function registerTriggers(
|
|
|
325
333
|
// Fire the webhook using the configured send mode
|
|
326
334
|
let webhookResult: WebhookResponse;
|
|
327
335
|
const attachments = await store.getAttachments(note.id);
|
|
336
|
+
// Pre-fetch context once per fire. Predicate errors are logged and
|
|
337
|
+
// the fire continues — context is additive, never blocking.
|
|
338
|
+
const context = trigger.action.include_context?.length
|
|
339
|
+
? await fetchContextEntries(store, trigger.action.include_context, logger)
|
|
340
|
+
: null;
|
|
328
341
|
const controller = new AbortController();
|
|
329
342
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
330
343
|
try {
|
|
331
344
|
let result: DispatchResult;
|
|
332
345
|
switch (sendMode) {
|
|
333
346
|
case "attachment":
|
|
334
|
-
result = await dispatchAttachment(trigger.action.webhook, note, attachments, store, controller.signal);
|
|
347
|
+
result = await dispatchAttachment(trigger.action.webhook, note, attachments, store, context, controller.signal);
|
|
335
348
|
break;
|
|
336
349
|
case "content":
|
|
350
|
+
// send=content is pure TTS (audio out); vault context makes no
|
|
351
|
+
// sense here and would confuse the server contract.
|
|
337
352
|
result = await dispatchContent(trigger.action.webhook, note, store, controller.signal);
|
|
338
353
|
break;
|
|
339
354
|
case "json":
|
|
340
355
|
default:
|
|
341
|
-
result = await dispatchJson(trigger.action.webhook, trigger, note, attachments, existingMeta, hookEvent, controller.signal);
|
|
356
|
+
result = await dispatchJson(trigger.action.webhook, trigger, note, attachments, existingMeta, hookEvent, context, controller.signal);
|
|
342
357
|
break;
|
|
343
358
|
}
|
|
344
359
|
webhookResult = result.webhookResult;
|