@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +80 -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 +57 -12
  36. package/src/published.test.ts +21 -21
  37. package/src/routes.ts +152 -70
  38. package/src/routing.test.ts +347 -0
  39. package/src/routing.ts +365 -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/src/systemd.ts CHANGED
@@ -6,28 +6,33 @@
6
6
  */
7
7
 
8
8
  import { homedir } from "os";
9
- import { join, resolve } from "path";
9
+ import { join } from "path";
10
10
  import { writeFile, mkdir, unlink } from "fs/promises";
11
11
  import { existsSync } from "fs";
12
12
  import { $ } from "bun";
13
- import { CONFIG_DIR, ENV_PATH, LOG_PATH, ERR_PATH } from "./config.ts";
13
+ import { CONFIG_DIR, LOG_PATH, ERR_PATH } from "./config.ts";
14
+ import { WRAPPER_PATH, writeDaemonWrapper } from "./daemon.ts";
14
15
 
15
16
  const SERVICE_NAME = "parachute-vault";
16
17
  const SERVICE_DIR = join(homedir(), ".config", "systemd", "user");
17
18
  const SERVICE_PATH = join(SERVICE_DIR, `${SERVICE_NAME}.service`);
18
19
 
19
- function generateUnit(serverPath: string, bunPath: string): string {
20
+ /**
21
+ * systemd unit invokes the shared start.sh wrapper. Env + server path
22
+ * resolution lives in the wrapper (see daemon.ts) — keeping systemd and
23
+ * launchd aligned on a single source of truth.
24
+ */
25
+ export function generateUnit(): string {
20
26
  return `[Unit]
21
27
  Description=Parachute Vault
22
28
  After=network.target
23
29
 
24
30
  [Service]
25
31
  Type=simple
26
- WorkingDirectory=${resolve(serverPath, "..")}
27
- ExecStart=${bunPath} ${serverPath}
32
+ WorkingDirectory=${CONFIG_DIR}
33
+ ExecStart=/bin/bash ${WRAPPER_PATH}
28
34
  Restart=on-failure
29
35
  RestartSec=5
30
- EnvironmentFile=${ENV_PATH}
31
36
  StandardOutput=append:${LOG_PATH}
32
37
  StandardError=append:${ERR_PATH}
33
38
 
@@ -36,12 +41,11 @@ WantedBy=default.target
36
41
  `;
37
42
  }
38
43
 
39
- export async function installSystemdService(): Promise<void> {
40
- const serverPath = resolve(import.meta.dir, "server.ts");
41
- const bunPath = (await $`which bun`.text()).trim();
44
+ export async function installSystemdService(): Promise<{ serverPath: string }> {
45
+ const { serverPath } = await writeDaemonWrapper();
42
46
 
43
47
  await mkdir(SERVICE_DIR, { recursive: true });
44
- await writeFile(SERVICE_PATH, generateUnit(serverPath, bunPath));
48
+ await writeFile(SERVICE_PATH, generateUnit());
45
49
 
46
50
  // Enable lingering so user services run without login session
47
51
  try {
@@ -52,7 +56,10 @@ export async function installSystemdService(): Promise<void> {
52
56
 
53
57
  await $`systemctl --user daemon-reload`.quiet();
54
58
  await $`systemctl --user enable ${SERVICE_NAME}`.quiet();
55
- await $`systemctl --user start ${SERVICE_NAME}`.quiet();
59
+ // Idempotent: `restart` works whether or not the service was running.
60
+ await $`systemctl --user restart ${SERVICE_NAME}`.quiet();
61
+
62
+ return { serverPath };
56
63
  }
57
64
 
58
65
  export async function uninstallSystemdService(): Promise<void> {
@@ -97,13 +97,13 @@ describe("buildPredicate", () => {
97
97
  });
98
98
  });
99
99
 
100
- describe("registerTriggers — dispatch modes", () => {
100
+ describe("registerTriggers — dispatch modes", async () => {
101
101
  let webhookServer: ReturnType<typeof Bun.serve>;
102
102
  let webhookPort: number;
103
103
  let lastRequest: { method: string; url: string; headers: Headers; body: unknown; formData?: FormData } | null = null;
104
104
  let webhookHandler: (req: Request) => Response | Promise<Response>;
105
105
 
106
- beforeAll(() => {
106
+ beforeAll(async () => {
107
107
  webhookHandler = () => Response.json({});
108
108
  webhookServer = Bun.serve({
109
109
  hostname: "127.0.0.1",
@@ -224,7 +224,7 @@ describe("registerTriggers — dispatch modes", () => {
224
224
  expect((file as File).name).toBe("recording.wav");
225
225
 
226
226
  // Verify note content was updated
227
- const updated = store.getNote("n2");
227
+ const updated = await store.getNote("n2");
228
228
  expect(updated?.content).toBe("transcribed content");
229
229
 
230
230
  // Cleanup
@@ -272,12 +272,12 @@ describe("registerTriggers — dispatch modes", () => {
272
272
  expect(body.input).toBe("Hello world");
273
273
 
274
274
  // Verify attachment was created
275
- const attachments = store.getAttachments("n3");
275
+ const attachments = await store.getAttachments("n3");
276
276
  expect(attachments.length).toBe(1);
277
277
  expect(attachments[0].mimeType).toBe("audio/ogg");
278
278
 
279
279
  // Verify metadata includes provider info
280
- const updated = store.getNote("n3");
280
+ const updated = await store.getNote("n3");
281
281
  const meta = updated?.metadata as Record<string, unknown>;
282
282
  expect(meta.tts_provider).toBe("kokoro");
283
283
  expect(meta.tts_voice).toBe("af_heart");
@@ -307,7 +307,7 @@ describe("registerTriggers — dispatch modes", () => {
307
307
  await hooks.dispatch("created", note, store);
308
308
  await new Promise(r => setTimeout(r, 50));
309
309
 
310
- const updated = store.getNote("n4");
310
+ const updated = await store.getNote("n4");
311
311
  const meta = updated?.metadata as Record<string, unknown>;
312
312
  expect(meta.skip_test_skipped_reason).toBe("no audio attachment found");
313
313
  });
@@ -330,7 +330,7 @@ describe("registerTriggers — dispatch modes", () => {
330
330
  await hooks.dispatch("created", note, store);
331
331
  await new Promise(r => setTimeout(r, 50));
332
332
 
333
- const updated = store.getNote("n5");
333
+ const updated = await store.getNote("n5");
334
334
  const meta = updated?.metadata as Record<string, unknown>;
335
335
  expect(meta.empty_test_skipped_reason).toBe("note has no content to synthesize");
336
336
  });
package/src/triggers.ts CHANGED
@@ -313,7 +313,7 @@ export function registerTriggers(
313
313
 
314
314
  // Phase 1: claim
315
315
  try {
316
- store.updateNote(note.id, {
316
+ await store.updateNote(note.id, {
317
317
  metadata: { ...existingMeta, [pendingKey]: pendingAt },
318
318
  skipUpdatedAt: true,
319
319
  });
@@ -324,7 +324,7 @@ export function registerTriggers(
324
324
 
325
325
  // Fire the webhook using the configured send mode
326
326
  let webhookResult: WebhookResponse;
327
- const attachments = store.getAttachments(note.id);
327
+ const attachments = await store.getAttachments(note.id);
328
328
  const controller = new AbortController();
329
329
  const timer = setTimeout(() => controller.abort(), timeout);
330
330
  try {
@@ -358,7 +358,7 @@ export function registerTriggers(
358
358
  // trigger an infinite webhook loop on every update.
359
359
  if (webhookResult.skipped_reason) {
360
360
  try {
361
- store.updateNote(note.id, {
361
+ await store.updateNote(note.id, {
362
362
  metadata: {
363
363
  ...existingMeta,
364
364
  [pendingKey]: undefined,
@@ -378,16 +378,16 @@ export function registerTriggers(
378
378
  // Add attachments first
379
379
  if (webhookResult.attachments?.length) {
380
380
  for (const att of webhookResult.attachments) {
381
- store.addAttachment(note.id, att.path, att.mimeType, att.meta);
381
+ await store.addAttachment(note.id, att.path, att.mimeType, att.meta);
382
382
  }
383
383
  }
384
384
 
385
385
  // Read fresh metadata to avoid clobbering concurrent edits
386
- const fresh = store.getNote(note.id);
386
+ const fresh = await store.getNote(note.id);
387
387
  const freshMeta = (fresh?.metadata as Record<string, unknown> | undefined) ?? existingMeta;
388
388
  const { [pendingKey]: _drop, ...restMeta } = freshMeta;
389
389
 
390
- store.updateNote(note.id, {
390
+ await store.updateNote(note.id, {
391
391
  ...(webhookResult.content !== undefined ? { content: webhookResult.content } : {}),
392
392
  metadata: {
393
393
  ...restMeta,
@@ -38,10 +38,27 @@ export function getVaultNameForStore(store: SqliteStore): string | undefined {
38
38
  return storeToVault.get(store);
39
39
  }
40
40
 
41
- /** Close all open stores. */
42
- export function closeAllStores(): void {
41
+ /**
42
+ * Close all open stores. When `silent` is true, swallow errors from
43
+ * `db.close()` — used by test cleanup where the underlying DB files may
44
+ * already be gone.
45
+ */
46
+ export function closeAllStores(silent = false): void {
43
47
  for (const [, store] of stores) {
44
- store.db.close();
48
+ if (silent) {
49
+ try { store.db.close(); } catch {}
50
+ } else {
51
+ store.db.close();
52
+ }
45
53
  }
46
54
  stores.clear();
47
55
  }
56
+
57
+ /**
58
+ * Test-only: close and clear all cached stores. Used between tests that
59
+ * swap `PARACHUTE_HOME` out from under the cache, which would otherwise
60
+ * hold handles to DBs whose files no longer exist.
61
+ */
62
+ export function clearVaultStoreCache(): void {
63
+ closeAllStores(true);
64
+ }