@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/src/systemd.ts
CHANGED
|
@@ -6,28 +6,33 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { homedir } from "os";
|
|
9
|
-
import { join
|
|
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,
|
|
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
|
-
|
|
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=${
|
|
27
|
-
ExecStart
|
|
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<
|
|
40
|
-
const serverPath =
|
|
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(
|
|
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
|
-
|
|
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> {
|
package/src/triggers.test.ts
CHANGED
|
@@ -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,
|
package/src/vault-store.ts
CHANGED
|
@@ -38,10 +38,27 @@ export function getVaultNameForStore(store: SqliteStore): string | undefined {
|
|
|
38
38
|
return storeToVault.get(store);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
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
|
-
|
|
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
|
+
}
|