@openparachute/vault 0.2.4 → 0.3.0-rc.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 (98) hide show
  1. package/.claude/settings.local.json +2 -25
  2. package/CHANGELOG.md +64 -0
  3. package/CLAUDE.md +17 -7
  4. package/README.md +169 -136
  5. package/core/src/core.test.ts +591 -19
  6. package/core/src/indexed-fields.test.ts +285 -0
  7. package/core/src/indexed-fields.ts +238 -0
  8. package/core/src/mcp.ts +127 -6
  9. package/core/src/notes.ts +153 -11
  10. package/core/src/query-operators.ts +174 -0
  11. package/core/src/schema.ts +69 -2
  12. package/core/src/store.ts +92 -0
  13. package/core/src/tag-schemas.ts +5 -0
  14. package/core/src/types.ts +28 -1
  15. package/docs/HTTP_API.md +105 -1
  16. package/package/package.json +32 -0
  17. package/package.json +2 -2
  18. package/src/auth.test.ts +83 -114
  19. package/src/auth.ts +68 -6
  20. package/src/backup-launchd.ts +1 -1
  21. package/src/backup.test.ts +1 -1
  22. package/src/backup.ts +18 -17
  23. package/src/cli.ts +179 -121
  24. package/src/config-triggers.test.ts +49 -0
  25. package/src/config.test.ts +317 -2
  26. package/src/config.ts +420 -40
  27. package/src/context.test.ts +136 -0
  28. package/src/context.ts +115 -0
  29. package/src/daemon.ts +17 -16
  30. package/src/doctor.test.ts +9 -7
  31. package/src/launchd.test.ts +1 -1
  32. package/src/launchd.ts +6 -6
  33. package/src/mcp-http.ts +75 -21
  34. package/src/mcp-install.test.ts +125 -0
  35. package/src/mcp-install.ts +60 -0
  36. package/src/mcp-tools.ts +34 -96
  37. package/src/module-config.ts +109 -0
  38. package/src/oauth.test.ts +345 -57
  39. package/src/oauth.ts +155 -35
  40. package/src/published.test.ts +2 -2
  41. package/src/routes.ts +209 -33
  42. package/src/routing.test.ts +817 -300
  43. package/src/routing.ts +204 -202
  44. package/src/scopes.test.ts +136 -0
  45. package/src/scopes.ts +105 -0
  46. package/src/scribe-env.test.ts +49 -0
  47. package/src/scribe-env.ts +33 -0
  48. package/src/server.ts +57 -5
  49. package/src/services-manifest.test.ts +140 -0
  50. package/src/services-manifest.ts +99 -0
  51. package/src/systemd.ts +3 -3
  52. package/src/token-store.ts +42 -9
  53. package/src/transcription-worker.test.ts +583 -0
  54. package/src/transcription-worker.ts +346 -0
  55. package/src/triggers.test.ts +191 -1
  56. package/src/triggers.ts +17 -2
  57. package/src/vault.test.ts +693 -77
  58. package/src/version.test.ts +1 -1
  59. package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +0 -2
  60. package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +0 -1
  61. package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +0 -2
  62. package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +0 -2
  63. package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +0 -1
  64. package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +0 -1
  65. package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +0 -211
  66. package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +0 -59
  67. package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +0 -232
  68. package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +0 -182
  69. package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +0 -91
  70. package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +0 -70
  71. package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +0 -59
  72. package/religions-abrahamic-filter.png +0 -0
  73. package/religions-buddhism-v2.png +0 -0
  74. package/religions-buddhism.png +0 -0
  75. package/religions-final.png +0 -0
  76. package/religions-v1.png +0 -0
  77. package/religions-v2.png +0 -0
  78. package/religions-zen.png +0 -0
  79. package/web/README.md +0 -73
  80. package/web/bun.lock +0 -827
  81. package/web/eslint.config.js +0 -23
  82. package/web/index.html +0 -15
  83. package/web/package.json +0 -36
  84. package/web/public/favicon.svg +0 -1
  85. package/web/public/icons.svg +0 -24
  86. package/web/src/App.tsx +0 -149
  87. package/web/src/Graph.tsx +0 -200
  88. package/web/src/NoteView.tsx +0 -155
  89. package/web/src/Sidebar.tsx +0 -186
  90. package/web/src/api.ts +0 -21
  91. package/web/src/index.css +0 -50
  92. package/web/src/main.tsx +0 -10
  93. package/web/src/types.ts +0 -37
  94. package/web/src/utils.ts +0 -107
  95. package/web/tsconfig.app.json +0 -25
  96. package/web/tsconfig.json +0 -7
  97. package/web/tsconfig.node.json +0 -24
  98. package/web/vite.config.ts +0 -16
@@ -1,7 +1,8 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { normalizePath } from "./paths.js";
3
+ import { rebuildIndexes } from "./indexed-fields.js";
3
4
 
4
- export const SCHEMA_VERSION = 9;
5
+ export const SCHEMA_VERSION = 12;
5
6
 
6
7
  export const SCHEMA_SQL = `
7
8
  -- Notes: the universal record
@@ -53,11 +54,32 @@ CREATE TABLE IF NOT EXISTS tag_schemas (
53
54
  fields TEXT -- JSON: { "field_name": { "type": "string", "description": "..." }, ... }
54
55
  );
55
56
 
56
- -- Tokens: API authentication with scoped permissions
57
+ -- Indexed fields: SSOT for generated columns and indexes on notes derived
58
+ -- from tag-declared fields with indexed=true. One row per indexed metadata
59
+ -- field; declarer_tags is a JSON array of tags that currently declare it.
60
+ -- See core/src/indexed-fields.ts.
61
+ CREATE TABLE IF NOT EXISTS indexed_fields (
62
+ field TEXT PRIMARY KEY,
63
+ sqlite_type TEXT NOT NULL,
64
+ declarer_tags TEXT NOT NULL DEFAULT '[]'
65
+ );
66
+
67
+ -- Tokens: API authentication with OAuth-standard scopes.
68
+ --
69
+ -- scopes is a whitespace-separated list of granted scopes (OAuth 2.0 §3.3)
70
+ -- — e.g. "vault:read vault:write". Introduced in v12 alongside enforcement;
71
+ -- NULL rows are pre-v12 tokens which fall back to deriving scopes from the
72
+ -- legacy permission column (see src/scopes.ts). permission is kept for the
73
+ -- one-release-cycle back-compat window and will be dropped in a future
74
+ -- migration.
75
+ --
76
+ -- scope_tag / scope_path_prefix are deprecated Phase-0 columns — never
77
+ -- enforced at runtime, kept only for schema stability.
57
78
  CREATE TABLE IF NOT EXISTS tokens (
58
79
  token_hash TEXT PRIMARY KEY,
59
80
  label TEXT NOT NULL,
60
81
  permission TEXT NOT NULL DEFAULT 'admin',
82
+ scopes TEXT,
61
83
  scope_tag TEXT,
62
84
  scope_path_prefix TEXT,
63
85
  expires_at TEXT,
@@ -163,6 +185,19 @@ export function initSchema(db: Database): void {
163
185
  // Migrate v8 → v9: add vault_name column to oauth_codes
164
186
  migrateToV9(db);
165
187
 
188
+ // Migrate v9 → v10: indexed_fields table (created by SCHEMA_SQL above).
189
+ migrateToV10(db);
190
+
191
+ // Migrate v10 → v11: backfill updated_at = created_at for legacy rows.
192
+ migrateToV11(db);
193
+
194
+ // Migrate v11 → v12: add `scopes` column to tokens for Phase 2 enforcement.
195
+ migrateToV12(db);
196
+
197
+ // Rebuild any generated columns + indexes declared in indexed_fields.
198
+ // No-op for a fresh vault; idempotent on existing vaults.
199
+ rebuildIndexes(db);
200
+
166
201
  // Record schema version
167
202
  db.prepare("INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)").run(
168
203
  SCHEMA_VERSION,
@@ -270,6 +305,38 @@ function migrateToV9(db: Database): void {
270
305
  }
271
306
  }
272
307
 
308
+ function migrateToV10(db: Database): void {
309
+ // SCHEMA_SQL's CREATE TABLE IF NOT EXISTS covers fresh vaults; this
310
+ // ensures indexed_fields exists on vaults created before v10. No data
311
+ // migration — rebuildIndexes() downstream handles column/index creation
312
+ // if any rows are already present.
313
+ }
314
+
315
+ /**
316
+ * Migrate v10 → v11: backfill `updated_at = created_at` for notes that never
317
+ * received an update. Pre-v11 inserts left `updated_at` NULL, which broke
318
+ * optimistic concurrency for clients that fall back to `createdAt` (the
319
+ * common `updatedAt ?? createdAt` pattern) — the `updated_at IS ?` guard
320
+ * never matched. From v11 onward, `createNote` sets both columns at insert.
321
+ * Idempotent — safe to run on every boot.
322
+ */
323
+ function migrateToV11(db: Database): void {
324
+ if (!hasTable(db, "notes")) return;
325
+ db.exec("UPDATE notes SET updated_at = created_at WHERE updated_at IS NULL");
326
+ }
327
+
328
+ /**
329
+ * Migrate v11 → v12: add `scopes` column to tokens. Existing rows get NULL
330
+ * and fall back to legacy `permission` → scopes derivation at read time
331
+ * (see src/scopes.ts:legacyPermissionToScopes). New tokens minted on v12+
332
+ * populate the column explicitly.
333
+ */
334
+ function migrateToV12(db: Database): void {
335
+ if (hasTable(db, "tokens") && !hasColumn(db, "tokens", "scopes")) {
336
+ db.exec("ALTER TABLE tokens ADD COLUMN scopes TEXT");
337
+ }
338
+ }
339
+
273
340
  function hasTable(db: Database, name: string): boolean {
274
341
  const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
275
342
  return !!row;
package/core/src/store.ts CHANGED
@@ -151,6 +151,17 @@ export class BunSqliteStore implements Store {
151
151
  return noteOps.deleteTag(this.db, name);
152
152
  }
153
153
 
154
+ async renameTag(oldName: string, newName: string): Promise<noteOps.RenameTagResult> {
155
+ return noteOps.renameTag(this.db, oldName, newName);
156
+ }
157
+
158
+ async mergeTags(
159
+ sources: string[],
160
+ target: string,
161
+ ): Promise<{ merged: Record<string, number>; target: string }> {
162
+ return noteOps.mergeTags(this.db, sources, target);
163
+ }
164
+
154
165
  // ---- Vault Stats ----
155
166
 
156
167
  async getVaultStats(opts?: { topTagsLimit?: number }) {
@@ -291,6 +302,87 @@ export class BunSqliteStore implements Store {
291
302
  };
292
303
  });
293
304
  }
305
+
306
+ async deleteAttachment(
307
+ noteId: string,
308
+ attachmentId: string,
309
+ ): Promise<{ deleted: boolean; path: string | null; orphaned: boolean }> {
310
+ // Scope by noteId so a token authorized for note A can't delete note B's attachments.
311
+ const row = this.db.prepare(
312
+ "SELECT path FROM attachments WHERE id = ? AND note_id = ?",
313
+ ).get(attachmentId, noteId) as { path: string } | undefined;
314
+ if (!row) return { deleted: false, path: null, orphaned: false };
315
+
316
+ this.db.prepare("DELETE FROM attachments WHERE id = ? AND note_id = ?").run(attachmentId, noteId);
317
+
318
+ // Orphan check: caller uses this to decide whether to unlink the file on disk.
319
+ const other = this.db.prepare(
320
+ "SELECT 1 FROM attachments WHERE path = ? LIMIT 1",
321
+ ).get(row.path);
322
+ return { deleted: true, path: row.path, orphaned: !other };
323
+ }
324
+
325
+ async getAttachment(attachmentId: string): Promise<Attachment | null> {
326
+ const row = this.db.prepare(
327
+ "SELECT * FROM attachments WHERE id = ?",
328
+ ).get(attachmentId) as { id: string; note_id: string; path: string; mime_type: string; metadata: string | null; created_at: string } | undefined;
329
+ if (!row) return null;
330
+ let metadata: Record<string, unknown> | undefined;
331
+ if (row.metadata && row.metadata !== "{}") {
332
+ try { metadata = JSON.parse(row.metadata); } catch {}
333
+ }
334
+ return {
335
+ id: row.id,
336
+ noteId: row.note_id,
337
+ path: row.path,
338
+ mimeType: row.mime_type,
339
+ metadata,
340
+ createdAt: row.created_at,
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Replace the attachment's metadata JSON blob. The caller passes the full
346
+ * merged object — this is a set, not a patch, so partial-field updates
347
+ * don't silently drop other keys.
348
+ */
349
+ async setAttachmentMetadata(attachmentId: string, metadata: Record<string, unknown>): Promise<void> {
350
+ const json = JSON.stringify(metadata);
351
+ this.db.prepare("UPDATE attachments SET metadata = ? WHERE id = ?").run(json, attachmentId);
352
+ }
353
+
354
+ /**
355
+ * Return attachments whose metadata.transcribe_status matches the given
356
+ * status, oldest first (FIFO). Used by the transcription worker to drain
357
+ * the queue. `status = "pending"` is the queue; `"failed"` feeds a retry
358
+ * sweep; `"done"` is only useful for tests and diagnostics.
359
+ */
360
+ async listAttachmentsByTranscribeStatus(
361
+ status: "pending" | "failed" | "done",
362
+ limit = 50,
363
+ ): Promise<Attachment[]> {
364
+ const rows = this.db.prepare(
365
+ `SELECT * FROM attachments
366
+ WHERE json_extract(metadata, '$.transcribe_status') = ?
367
+ ORDER BY created_at ASC
368
+ LIMIT ?`,
369
+ ).all(status, limit) as { id: string; note_id: string; path: string; mime_type: string; metadata: string | null; created_at: string }[];
370
+
371
+ return rows.map((r) => {
372
+ let metadata: Record<string, unknown> | undefined;
373
+ if (r.metadata && r.metadata !== "{}") {
374
+ try { metadata = JSON.parse(r.metadata); } catch {}
375
+ }
376
+ return {
377
+ id: r.id,
378
+ noteId: r.note_id,
379
+ path: r.path,
380
+ mimeType: r.mime_type,
381
+ metadata,
382
+ createdAt: r.created_at,
383
+ };
384
+ });
385
+ }
294
386
  }
295
387
 
296
388
  /** @deprecated Renamed to `BunSqliteStore` to make the runtime split explicit. Kept as an alias for backward compatibility. */
@@ -16,6 +16,11 @@ export interface TagFieldSchema {
16
16
  type: string;
17
17
  description?: string;
18
18
  enum?: string[];
19
+ // When true, a generated column + index are maintained on `notes` for this
20
+ // field, making it available for operator queries and `order_by`. Global
21
+ // across declarers — all tags declaring this field must agree on both
22
+ // `type` and `indexed`. See core/src/indexed-fields.ts for lifecycle.
23
+ indexed?: boolean;
19
24
  }
20
25
 
21
26
  export interface TagSchema {
package/core/src/types.ts CHANGED
@@ -50,12 +50,27 @@ export interface QueryOpts {
50
50
  tags?: string[];
51
51
  tagMatch?: "all" | "any"; // "all" = must have ALL tags (default), "any" = must have ANY tag
52
52
  excludeTags?: string[];
53
+ // Presence filters. `true` → has at least one; `false` → has none.
54
+ // When `tags` is also set, `hasTags` is ignored (the tag filter already constrains the set).
55
+ // `hasLinks` checks both directions — inbound or outbound counts as "has links".
56
+ hasTags?: boolean;
57
+ hasLinks?: boolean;
53
58
  path?: string; // exact path match (case-insensitive)
54
59
  pathPrefix?: string; // e.g., "Projects/Parachute" matches "Projects/Parachute/README"
55
- metadata?: Record<string, unknown>; // filter by metadata values (exact match on each key)
60
+ // Per-field metadata filter. Each value is either a primitive (exact
61
+ // match, today's behavior) or an operator object — `{ eq, ne, gt, gte, lt,
62
+ // lte, in, not_in, exists }` — which routes through the generated column
63
+ // for the field. Operator queries require the field to be declared
64
+ // `indexed: true` in a tag schema; undeclared fields error loudly.
65
+ metadata?: Record<string, unknown>;
56
66
  dateFrom?: string; // ISO date
57
67
  dateTo?: string; // ISO date
58
68
  sort?: "asc" | "desc";
69
+ // Sort by an indexed metadata field instead of `created_at`. Must be
70
+ // declared `indexed: true`; errors loudly otherwise. Direction is taken
71
+ // from `sort` (default "asc") and `created_at` is appended as a stable
72
+ // tiebreaker.
73
+ orderBy?: string;
59
74
  limit?: number;
60
75
  offset?: number;
61
76
  }
@@ -109,6 +124,14 @@ export interface Store {
109
124
  untagNote(noteId: string, tags: string[]): Promise<void>;
110
125
  listTags(): Promise<{ name: string; count: number }[]>;
111
126
  deleteTag(name: string): Promise<{ deleted: boolean; notes_untagged: number }>;
127
+ renameTag(
128
+ oldName: string,
129
+ newName: string,
130
+ ): Promise<{ renamed: number } | { error: "not_found" } | { error: "target_exists" }>;
131
+ mergeTags(
132
+ sources: string[],
133
+ target: string,
134
+ ): Promise<{ merged: Record<string, number>; target: string }>;
112
135
 
113
136
  // Vault stats (aggregate, read-only)
114
137
  getVaultStats(opts?: { topTagsLimit?: number }): Promise<VaultStats>;
@@ -138,4 +161,8 @@ export interface Store {
138
161
  // Attachments
139
162
  addAttachment(noteId: string, path: string, mimeType: string, metadata?: Record<string, unknown>): Promise<Attachment>;
140
163
  getAttachments(noteId: string): Promise<Attachment[]>;
164
+ getAttachment(attachmentId: string): Promise<Attachment | null>;
165
+ setAttachmentMetadata(attachmentId: string, metadata: Record<string, unknown>): Promise<void>;
166
+ deleteAttachment(noteId: string, attachmentId: string): Promise<{ deleted: boolean; path: string | null; orphaned: boolean }>;
167
+ listAttachmentsByTranscribeStatus(status: "pending" | "failed" | "done", limit?: number): Promise<Attachment[]>;
141
168
  }
package/docs/HTTP_API.md CHANGED
@@ -204,11 +204,42 @@ Returns `{deleted: true}`.
204
204
  Body: `{"tags": ["a", "b"]}`.
205
205
 
206
206
  #### `POST /notes/{id}/attachments`
207
- Body: `{"path": "files/a.png", "mimeType": "image/png"}`.
207
+ Body: `{"path": "files/a.png", "mimeType": "image/png", "transcribe"?: boolean}`.
208
+
209
+ When `transcribe: true` and the file is audio, the server queues a
210
+ transcription job: `attachment.metadata.transcribe_status = "pending"` is
211
+ set, and `note.metadata.transcribe_stub = true` is written as the opt-in to
212
+ overwrite content when the transcript lands. A background worker (enabled
213
+ by setting `SCRIBE_URL` on the server) drains the queue FIFO, one at a
214
+ time, calling `${SCRIBE_URL}/v1/audio/transcriptions` with the audio as
215
+ multipart `file` and expecting `{ text: string }` back.
216
+
217
+ On success:
218
+ - If `note.metadata.transcribe_stub === true`, the worker replaces the
219
+ literal `_Transcript pending._` placeholder in the note body with the
220
+ transcript, or the whole body if the placeholder is absent. The stub
221
+ marker is cleared. A user edit clearing `transcribe_stub` before the
222
+ transcript arrives opts out of the overwrite.
223
+ - `attachment.metadata.transcribe_status` becomes `"done"` and
224
+ `transcript` + `transcribe_done_at` are recorded on the attachment even
225
+ when the note opted out, so the transcript is always addressable.
226
+
227
+ On failure, the worker retries with exponential backoff up to three
228
+ attempts before setting `transcribe_status = "failed"` and capturing
229
+ `transcribe_error`.
230
+
231
+ The queue lives in the DB (`attachments` table), so a server restart
232
+ resumes pending work without replay.
208
233
 
209
234
  #### `GET /notes/{id}/attachments`
210
235
  Returns `Attachment[]`.
211
236
 
237
+ #### `DELETE /notes/{id}/attachments/{attId}`
238
+ Returns `204 No Content`. The attachment record is removed and the underlying
239
+ storage file is unlinked when no other attachment still references the same
240
+ path (orphan-check). Returns `404` if the attachment doesn't exist or belongs
241
+ to a different note. Idempotent: a second delete of the same id returns `404`.
242
+
212
243
  ### Links
213
244
 
214
245
  #### `GET /links`
@@ -288,11 +319,84 @@ Query params:
288
319
  #### `GET /tags`
289
320
  Returns `[{name, count}]`.
290
321
 
322
+ #### `POST /tags/{name}/rename`
323
+ Body: `{ "new_name": string }`. Atomically renames the tag across `tags`,
324
+ `note_tags`, and `tag_schemas` in a single transaction.
325
+
326
+ Returns `{ "renamed": number }` on success — the number of note-tag rows
327
+ rewritten.
328
+
329
+ Errors:
330
+ - `404 { "error": "not_found" }` — source tag does not exist.
331
+ - `409 { "error": "target_exists", "target": string, "message": "..." }` —
332
+ `new_name` is already a tag. The client should call `POST /tags/merge`
333
+ instead if combining the two tags is the intent.
334
+
335
+ #### `POST /tags/merge`
336
+ Body: `{ "sources": string[], "target": string }`. Retags every note carrying
337
+ any of the `sources` tags with `target`, then drops the source tags (and
338
+ their schemas) in a single transaction. `target`'s own schema is preserved.
339
+
340
+ `target` is created if it doesn't exist yet. Sources that don't exist are
341
+ recorded with count `0`. Duplicate sources are deduped; `target` appearing
342
+ in `sources` is a no-op for that entry.
343
+
344
+ Returns `{ "merged": { [source]: count }, "target": string }`.
345
+
291
346
  ### Vault stats
292
347
 
293
348
  You usually want `GET /vaults/{name}` which bundles stats with vault metadata.
294
349
  If you only need the stats, call `GET /vaults/{name}` and read `.stats`.
295
350
 
351
+ ### Vault config
352
+
353
+ #### `GET /api/vault`
354
+ Returns the vault's identity plus a nested `config` block for mutable
355
+ settings.
356
+
357
+ ```json
358
+ {
359
+ "name": "default",
360
+ "description": "My knowledge graph",
361
+ "config": {
362
+ "audio_retention": "keep"
363
+ }
364
+ }
365
+ ```
366
+
367
+ `?include_stats=true` folds the same `VaultStats` shape into the response
368
+ under `stats`.
369
+
370
+ #### `PATCH /api/vault`
371
+ Update the description and/or nested `config` fields. Only the fields you
372
+ pass are changed; omitted fields are left alone.
373
+
374
+ ```json
375
+ {
376
+ "description": "new description",
377
+ "config": { "audio_retention": "until_transcribed" }
378
+ }
379
+ ```
380
+
381
+ Response echoes the full vault payload (same shape as `GET /api/vault`).
382
+
383
+ ##### `config.audio_retention`
384
+
385
+ Controls what the transcription worker does with the audio file on disk
386
+ once it reaches a terminal state. The attachment row (including any
387
+ recorded transcript) is always preserved — only the file on disk is
388
+ affected.
389
+
390
+ | Value | Behavior |
391
+ |---|---|
392
+ | `"keep"` (default) | Never unlink. The original audio stays on disk indefinitely. |
393
+ | `"until_transcribed"` | Unlink on successful transcription. On failure the file is kept so you can retry or re-upload. |
394
+ | `"never"` | Unlink on any terminal state — **including failure**. Users who opt in accept that losing a bad transcription also loses the source audio. |
395
+
396
+ Validation: `audio_retention` must be exactly one of those three strings.
397
+ Any other value returns `400 { "error": "invalid_audio_retention" }`.
398
+ Vaults created before this setting existed read back as `"keep"`.
399
+
296
400
  ### Storage
297
401
 
298
402
  #### `POST /storage/upload`
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@openparachute/vault",
3
+ "version": "0.2.4",
4
+ "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
+ "module": "src/cli.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "parachute-vault": "src/cli.ts"
9
+ },
10
+ "scripts": {
11
+ "start": "bun src/server.ts",
12
+ "cli": "bun src/cli.ts",
13
+ "test": "bun test src/",
14
+ "test:core": "cd core && node --experimental-vm-modules node_modules/vitest/dist/cli.js run"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.12.1",
18
+ "otpauth": "^9.5.0",
19
+ "qrcode-terminal": "^0.12.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "latest"
23
+ },
24
+ "peerDependencies": {
25
+ "typescript": "^5"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/ParachuteComputer/parachute-vault.git"
30
+ },
31
+ "license": "AGPL-3.0"
32
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.2.4",
3
+ "version": "0.3.0-rc.1",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
7
7
  "bin": {
8
- "parachute": "src/cli.ts"
8
+ "parachute-vault": "src/cli.ts"
9
9
  },
10
10
  "scripts": {
11
11
  "start": "bun src/server.ts",