@openparachute/vault 0.4.8 → 0.4.9-rc.11
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/core/src/core.test.ts +4 -1
- package/core/src/hooks.test.ts +320 -1
- package/core/src/hooks.ts +243 -38
- package/core/src/indexed-fields.test.ts +151 -0
- package/core/src/indexed-fields.ts +98 -0
- package/core/src/mcp.ts +99 -41
- package/core/src/notes.ts +26 -2
- package/core/src/portable-md.test.ts +304 -1
- package/core/src/portable-md.ts +418 -2
- package/core/src/schema.ts +114 -2
- package/core/src/store.ts +185 -2
- package/core/src/types.ts +28 -0
- package/package.json +2 -2
- package/src/auth-hub-jwt.test.ts +147 -0
- package/src/auth.ts +121 -1
- package/src/auto-transcribe.test.ts +7 -2
- package/src/auto-transcribe.ts +6 -2
- package/src/cli.ts +131 -36
- package/src/config.ts +12 -4
- package/src/export-watch.test.ts +74 -0
- package/src/export-watch.ts +108 -7
- package/src/github-device-flow.test.ts +404 -0
- package/src/github-device-flow.ts +415 -0
- package/src/hub-jwt.test.ts +27 -2
- package/src/hub-jwt.ts +10 -0
- package/src/mcp-http.ts +48 -39
- package/src/mcp-install-interactive.test.ts +10 -21
- package/src/mcp-install-interactive.ts +12 -21
- package/src/mcp-install.test.ts +141 -30
- package/src/mcp-install.ts +109 -3
- package/src/mcp-tools.ts +460 -3
- package/src/mirror-config.test.ts +277 -14
- package/src/mirror-config.ts +482 -31
- package/src/mirror-credentials.test.ts +601 -0
- package/src/mirror-credentials.ts +700 -0
- package/src/mirror-deps.ts +67 -17
- package/src/mirror-import.test.ts +550 -0
- package/src/mirror-import.ts +487 -0
- package/src/mirror-manager.test.ts +423 -12
- package/src/mirror-manager.ts +621 -72
- package/src/mirror-per-vault.test.ts +519 -0
- package/src/mirror-registry.ts +91 -14
- package/src/mirror-routes.test.ts +966 -10
- package/src/mirror-routes.ts +1111 -7
- package/src/module-config.ts +11 -5
- package/src/routes.ts +38 -1
- package/src/routing.test.ts +92 -1
- package/src/routing.ts +193 -20
- package/src/server.ts +116 -35
- package/src/storage.test.ts +132 -7
- package/src/token-store.ts +300 -5
- package/src/transcription-worker.ts +9 -4
- package/src/triggers.ts +16 -3
- package/src/vault.test.ts +681 -2
- package/web/ui/dist/assets/index-Cn-PPMRv.js +60 -0
- package/web/ui/dist/assets/{index-BOa-JJtV.css → index-DBe8Xiah.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-BzA5LgE3.js +0 -60
package/core/src/core.test.ts
CHANGED
|
@@ -1944,6 +1944,9 @@ describe("MCP tools", async () => {
|
|
|
1944
1944
|
expect(names).toContain("delete-tag");
|
|
1945
1945
|
expect(names).toContain("find-path");
|
|
1946
1946
|
expect(names).toContain("vault-info");
|
|
1947
|
+
// prune-schema (admin) — drops orphaned indexed-field columns whose
|
|
1948
|
+
// declaring tags are gone. The gitcoin orphaned-fields fix.
|
|
1949
|
+
expect(names).toContain("prune-schema");
|
|
1947
1950
|
// Six note-schema tools (list/update/delete-note-schema +
|
|
1948
1951
|
// list/set/delete-schema-mapping) retired in v17 — the standalone
|
|
1949
1952
|
// note_schemas + schema_mappings subsystem was a parallel path to
|
|
@@ -1957,7 +1960,7 @@ describe("MCP tools", async () => {
|
|
|
1957
1960
|
// synthesize-notes retired in v17 — replicable with query-notes(near=) +
|
|
1958
1961
|
// find-path + agent-side aggregation. See vault#268.
|
|
1959
1962
|
expect(names).not.toContain("synthesize-notes");
|
|
1960
|
-
expect(tools).toHaveLength(
|
|
1963
|
+
expect(tools).toHaveLength(10);
|
|
1961
1964
|
});
|
|
1962
1965
|
|
|
1963
1966
|
it("create-note tool works", async () => {
|
package/core/src/hooks.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
3
|
import { SqliteStore } from "./store.js";
|
|
4
|
-
import { HookRegistry } from "./hooks.js";
|
|
4
|
+
import { HookRegistry, type DeletedNoteRef, type DeletedAttachmentRef } from "./hooks.js";
|
|
5
5
|
import type { Note } from "./types.js";
|
|
6
6
|
|
|
7
7
|
let db: Database;
|
|
@@ -359,3 +359,322 @@ describe("HookRegistry — HOOK_CONCURRENCY env var parsing", async () => {
|
|
|
359
359
|
restore();
|
|
360
360
|
});
|
|
361
361
|
});
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// New event types — deleted notes, tag mutations, deleted attachments
|
|
365
|
+
// (vault#382 — event-driven git-mirror foundation)
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
describe("HookRegistry — deleted note events", () => {
|
|
369
|
+
let db: Database;
|
|
370
|
+
let hooks: HookRegistry;
|
|
371
|
+
let store: SqliteStore;
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
db = new Database(":memory:");
|
|
374
|
+
hooks = new HookRegistry({ concurrency: 4, logger: silentLogger });
|
|
375
|
+
store = new SqliteStore(db, { hooks });
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
async function settle(): Promise<void> {
|
|
379
|
+
await Promise.resolve();
|
|
380
|
+
await Promise.resolve();
|
|
381
|
+
await hooks.drain();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
it("default subscription doesn't include 'deleted'", async () => {
|
|
385
|
+
// Existing hooks pre-deleted-event don't suddenly start receiving
|
|
386
|
+
// delete shapes they weren't typed for.
|
|
387
|
+
const fired: string[] = [];
|
|
388
|
+
hooks.onNote({
|
|
389
|
+
handler: (note) => {
|
|
390
|
+
fired.push(note.id);
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const note = await store.createNote("hi");
|
|
395
|
+
await settle();
|
|
396
|
+
fired.length = 0;
|
|
397
|
+
await store.deleteNote(note.id);
|
|
398
|
+
await settle();
|
|
399
|
+
expect(fired).toEqual([]);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("explicit 'deleted' subscription receives DeletedNoteRef on store.deleteNote", async () => {
|
|
403
|
+
const seen: Array<{ event: string; id: string; path?: string }> = [];
|
|
404
|
+
hooks.onNote({
|
|
405
|
+
event: "deleted",
|
|
406
|
+
handler: (payload, _store, event) => {
|
|
407
|
+
// payload here is a DeletedNoteRef (we subscribed to deleted only).
|
|
408
|
+
const ref = payload as DeletedNoteRef;
|
|
409
|
+
seen.push({ event: event ?? "deleted", id: ref.id, path: ref.path });
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const note = await store.createNote("doomed", { path: "to-delete" });
|
|
414
|
+
await settle();
|
|
415
|
+
await store.deleteNote(note.id);
|
|
416
|
+
await settle();
|
|
417
|
+
expect(seen).toHaveLength(1);
|
|
418
|
+
expect(seen[0]!.event).toBe("deleted");
|
|
419
|
+
expect(seen[0]!.id).toBe(note.id);
|
|
420
|
+
expect(seen[0]!.path).toBe("to-delete");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("DeletedNoteRef has no metadata/tags/content (predicate authors take note)", async () => {
|
|
424
|
+
const observedPayloads: unknown[] = [];
|
|
425
|
+
hooks.onNote({
|
|
426
|
+
event: "deleted",
|
|
427
|
+
handler: (payload) => {
|
|
428
|
+
observedPayloads.push(payload);
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
const n = await store.createNote("with tags", { tags: ["one", "two"], metadata: { color: "blue" } });
|
|
432
|
+
await settle();
|
|
433
|
+
await store.deleteNote(n.id);
|
|
434
|
+
await settle();
|
|
435
|
+
expect(observedPayloads).toHaveLength(1);
|
|
436
|
+
const p = observedPayloads[0] as Record<string, unknown>;
|
|
437
|
+
expect(p.id).toBe(n.id);
|
|
438
|
+
// Strictly minimal shape — no full Note fields leak through.
|
|
439
|
+
expect(p.content).toBeUndefined();
|
|
440
|
+
expect(p.metadata).toBeUndefined();
|
|
441
|
+
expect(p.tags).toBeUndefined();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("array events allow subscribing to created+updated+deleted in one hook", async () => {
|
|
445
|
+
const events: string[] = [];
|
|
446
|
+
hooks.onNote({
|
|
447
|
+
event: ["created", "updated", "deleted"],
|
|
448
|
+
handler: (_n, _s, ev) => {
|
|
449
|
+
events.push(ev ?? "?");
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
const n = await store.createNote("lifecycle");
|
|
453
|
+
await settle();
|
|
454
|
+
await store.updateNote(n.id, { content: "v2" });
|
|
455
|
+
await settle();
|
|
456
|
+
await store.deleteNote(n.id);
|
|
457
|
+
await settle();
|
|
458
|
+
expect(events).toEqual(["created", "updated", "deleted"]);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("predicate on a deleted event receives DeletedNoteRef and can filter on path", async () => {
|
|
462
|
+
const fired: string[] = [];
|
|
463
|
+
hooks.onNote({
|
|
464
|
+
event: "deleted",
|
|
465
|
+
when: (payload) => {
|
|
466
|
+
const ref = payload as DeletedNoteRef;
|
|
467
|
+
return ref.path?.startsWith("keep/") === true;
|
|
468
|
+
},
|
|
469
|
+
handler: (payload) => {
|
|
470
|
+
fired.push((payload as DeletedNoteRef).id);
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
const a = await store.createNote("a", { path: "keep/one" });
|
|
474
|
+
const b = await store.createNote("b", { path: "skip/two" });
|
|
475
|
+
await settle();
|
|
476
|
+
await store.deleteNote(a.id);
|
|
477
|
+
await store.deleteNote(b.id);
|
|
478
|
+
await settle();
|
|
479
|
+
expect(fired).toEqual([a.id]);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("HookRegistry — tag events", () => {
|
|
484
|
+
let db: Database;
|
|
485
|
+
let hooks: HookRegistry;
|
|
486
|
+
let store: SqliteStore;
|
|
487
|
+
beforeEach(() => {
|
|
488
|
+
db = new Database(":memory:");
|
|
489
|
+
hooks = new HookRegistry({ concurrency: 4, logger: silentLogger });
|
|
490
|
+
store = new SqliteStore(db, { hooks });
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
async function settle(): Promise<void> {
|
|
494
|
+
await Promise.resolve();
|
|
495
|
+
await Promise.resolve();
|
|
496
|
+
await hooks.drain();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
it("upsertTagRecord dispatches 'upserted'", async () => {
|
|
500
|
+
const seen: Array<{ tag: string; event: string }> = [];
|
|
501
|
+
hooks.onTag({
|
|
502
|
+
handler: (tag, _store, event) => {
|
|
503
|
+
seen.push({ tag, event: event ?? "?" });
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
await store.upsertTagRecord("project", { description: "a project" });
|
|
507
|
+
await settle();
|
|
508
|
+
expect(seen).toEqual([{ tag: "project", event: "upserted" }]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("upsertTagSchema dispatches 'upserted'", async () => {
|
|
512
|
+
const seen: string[] = [];
|
|
513
|
+
hooks.onTag({
|
|
514
|
+
event: "upserted",
|
|
515
|
+
handler: (tag) => {
|
|
516
|
+
seen.push(tag);
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
await store.upsertTagSchema("meeting", {
|
|
520
|
+
fields: { duration: { type: "number" } },
|
|
521
|
+
});
|
|
522
|
+
await settle();
|
|
523
|
+
expect(seen).toEqual(["meeting"]);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("deleteTag dispatches 'deleted' only when the tag actually existed", async () => {
|
|
527
|
+
const seen: string[] = [];
|
|
528
|
+
hooks.onTag({
|
|
529
|
+
event: "deleted",
|
|
530
|
+
handler: (tag) => {
|
|
531
|
+
seen.push(tag);
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
const note = await store.createNote("tagged", { tags: ["mytag"] });
|
|
535
|
+
await settle();
|
|
536
|
+
|
|
537
|
+
// Delete the tag — fires.
|
|
538
|
+
await store.deleteTag("mytag");
|
|
539
|
+
await settle();
|
|
540
|
+
expect(seen).toEqual(["mytag"]);
|
|
541
|
+
|
|
542
|
+
// Delete a tag that doesn't exist — no fire.
|
|
543
|
+
await store.deleteTag("never-existed");
|
|
544
|
+
await settle();
|
|
545
|
+
expect(seen).toEqual(["mytag"]);
|
|
546
|
+
void note; // silence unused
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("renameTag dispatches deleted(old) + upserted(new)", async () => {
|
|
550
|
+
const events: Array<{ tag: string; event: string }> = [];
|
|
551
|
+
hooks.onTag({
|
|
552
|
+
handler: (tag, _store, event) => {
|
|
553
|
+
events.push({ tag, event: event ?? "?" });
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
await store.createNote("tagged", { tags: ["old-name"] });
|
|
557
|
+
await settle();
|
|
558
|
+
await store.renameTag("old-name", "new-name");
|
|
559
|
+
await settle();
|
|
560
|
+
// Order is implementation-defined; assert the set instead.
|
|
561
|
+
expect(events).toContainEqual({ tag: "old-name", event: "deleted" });
|
|
562
|
+
expect(events).toContainEqual({ tag: "new-name", event: "upserted" });
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("mergeTags dispatches deleted for each source + upserted for target", async () => {
|
|
566
|
+
const events: Array<{ tag: string; event: string }> = [];
|
|
567
|
+
hooks.onTag({
|
|
568
|
+
handler: (tag, _store, event) => {
|
|
569
|
+
events.push({ tag, event: event ?? "?" });
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
await store.createNote("a", { tags: ["src1"] });
|
|
573
|
+
await store.createNote("b", { tags: ["src2"] });
|
|
574
|
+
await settle();
|
|
575
|
+
events.length = 0;
|
|
576
|
+
await store.mergeTags(["src1", "src2"], "target");
|
|
577
|
+
await settle();
|
|
578
|
+
expect(events).toContainEqual({ tag: "src1", event: "deleted" });
|
|
579
|
+
expect(events).toContainEqual({ tag: "src2", event: "deleted" });
|
|
580
|
+
expect(events).toContainEqual({ tag: "target", event: "upserted" });
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("deleteTagSchema fires 'deleted' for the schema slot", async () => {
|
|
584
|
+
const seen: string[] = [];
|
|
585
|
+
hooks.onTag({
|
|
586
|
+
event: "deleted",
|
|
587
|
+
handler: (tag) => {
|
|
588
|
+
seen.push(tag);
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
await store.upsertTagSchema("foo", { description: "x" });
|
|
592
|
+
await settle();
|
|
593
|
+
const ok = await store.deleteTagSchema("foo");
|
|
594
|
+
await settle();
|
|
595
|
+
expect(ok).toBe(true);
|
|
596
|
+
expect(seen).toEqual(["foo"]);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("predicate filters tag events by name", async () => {
|
|
600
|
+
const seen: string[] = [];
|
|
601
|
+
hooks.onTag({
|
|
602
|
+
when: (tag) => tag.startsWith("_"),
|
|
603
|
+
handler: (tag) => {
|
|
604
|
+
seen.push(tag);
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
await store.upsertTagRecord("public-tag", { description: "x" });
|
|
608
|
+
await store.upsertTagRecord("_private-tag", { description: "y" });
|
|
609
|
+
await settle();
|
|
610
|
+
expect(seen).toEqual(["_private-tag"]);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe("HookRegistry — deleted attachment events", () => {
|
|
615
|
+
let db: Database;
|
|
616
|
+
let hooks: HookRegistry;
|
|
617
|
+
let store: SqliteStore;
|
|
618
|
+
beforeEach(() => {
|
|
619
|
+
db = new Database(":memory:");
|
|
620
|
+
hooks = new HookRegistry({ concurrency: 4, logger: silentLogger });
|
|
621
|
+
store = new SqliteStore(db, { hooks });
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
async function settle(): Promise<void> {
|
|
625
|
+
await Promise.resolve();
|
|
626
|
+
await Promise.resolve();
|
|
627
|
+
await hooks.drain();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
it("deleteAttachment dispatches 'deleted' with DeletedAttachmentRef", async () => {
|
|
631
|
+
const seen: DeletedAttachmentRef[] = [];
|
|
632
|
+
hooks.onAttachment({
|
|
633
|
+
event: "deleted",
|
|
634
|
+
handler: (payload) => {
|
|
635
|
+
seen.push(payload as DeletedAttachmentRef);
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
const note = await store.createNote("with-att");
|
|
639
|
+
const att = await store.addAttachment(note.id, "audio/voice.m4a", "audio/mp4");
|
|
640
|
+
await settle();
|
|
641
|
+
await store.deleteAttachment(note.id, att.id);
|
|
642
|
+
await settle();
|
|
643
|
+
expect(seen).toHaveLength(1);
|
|
644
|
+
expect(seen[0]!.id).toBe(att.id);
|
|
645
|
+
expect(seen[0]!.noteId).toBe(note.id);
|
|
646
|
+
expect(seen[0]!.path).toBe("audio/voice.m4a");
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("default attachment subscription doesn't include 'deleted' (back-compat)", async () => {
|
|
650
|
+
const fired: string[] = [];
|
|
651
|
+
hooks.onAttachment({
|
|
652
|
+
handler: (att) => {
|
|
653
|
+
// payload is Attachment | DeletedAttachmentRef; we know default
|
|
654
|
+
// events doesn't include deleted, so this only sees created.
|
|
655
|
+
fired.push(att.id);
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
const note = await store.createNote("hi");
|
|
659
|
+
const att = await store.addAttachment(note.id, "f.txt", "text/plain");
|
|
660
|
+
await settle();
|
|
661
|
+
fired.length = 0;
|
|
662
|
+
await store.deleteAttachment(note.id, att.id);
|
|
663
|
+
await settle();
|
|
664
|
+
expect(fired).toEqual([]);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("non-existent attachment delete does not dispatch", async () => {
|
|
668
|
+
const fired: string[] = [];
|
|
669
|
+
hooks.onAttachment({
|
|
670
|
+
event: "deleted",
|
|
671
|
+
handler: (a) => fired.push(a.id),
|
|
672
|
+
});
|
|
673
|
+
const note = await store.createNote("hi");
|
|
674
|
+
await settle();
|
|
675
|
+
const result = await store.deleteAttachment(note.id, "never-existed-id");
|
|
676
|
+
await settle();
|
|
677
|
+
expect(result.deleted).toBe(false);
|
|
678
|
+
expect(fired).toEqual([]);
|
|
679
|
+
});
|
|
680
|
+
});
|