@batalabs/virlow-mcp-core 3.11.4

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/api.d.ts +118 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +109 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/embedding-cache.d.ts +34 -0
  8. package/dist/embedding-cache.d.ts.map +1 -0
  9. package/dist/embedding-cache.js +108 -0
  10. package/dist/embedding-cache.js.map +1 -0
  11. package/dist/exposure.d.ts +42 -0
  12. package/dist/exposure.d.ts.map +1 -0
  13. package/dist/exposure.js +74 -0
  14. package/dist/exposure.js.map +1 -0
  15. package/dist/index.d.ts +16 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +10 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/memories-enabled.d.ts +16 -0
  20. package/dist/memories-enabled.d.ts.map +1 -0
  21. package/dist/memories-enabled.js +29 -0
  22. package/dist/memories-enabled.js.map +1 -0
  23. package/dist/memories.d.ts +86 -0
  24. package/dist/memories.d.ts.map +1 -0
  25. package/dist/memories.js +350 -0
  26. package/dist/memories.js.map +1 -0
  27. package/dist/memory-note.d.ts +45 -0
  28. package/dist/memory-note.d.ts.map +1 -0
  29. package/dist/memory-note.js +144 -0
  30. package/dist/memory-note.js.map +1 -0
  31. package/dist/memory-tree.d.ts +28 -0
  32. package/dist/memory-tree.d.ts.map +1 -0
  33. package/dist/memory-tree.js +68 -0
  34. package/dist/memory-tree.js.map +1 -0
  35. package/dist/notes.d.ts +109 -0
  36. package/dist/notes.d.ts.map +1 -0
  37. package/dist/notes.js +234 -0
  38. package/dist/notes.js.map +1 -0
  39. package/dist/vault.d.ts +35 -0
  40. package/dist/vault.d.ts.map +1 -0
  41. package/dist/vault.js +73 -0
  42. package/dist/vault.js.map +1 -0
  43. package/package.json +43 -0
@@ -0,0 +1,144 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ /** Serialisation order, so a rewrite of unchanged metadata is a no-op diff. */
3
+ const KEY_ORDER = ['source', 'tags', 'confidence', 'created'];
4
+ const FENCE = '---';
5
+ /** The app's code editor stores content as this envelope; `code` is what the
6
+ * user sees and edits in Monaco. */
7
+ export function toCodeEnvelope(body) {
8
+ return JSON.stringify({ language: 'markdown', code: body, lastExecution: null });
9
+ }
10
+ function unwrapEnvelope(content) {
11
+ try {
12
+ const parsed = JSON.parse(content);
13
+ if (parsed !== null &&
14
+ typeof parsed === 'object' &&
15
+ typeof parsed.code === 'string') {
16
+ return parsed.code;
17
+ }
18
+ }
19
+ catch {
20
+ // Not JSON at all — a hand-written note, which is a perfectly good memory.
21
+ }
22
+ return content;
23
+ }
24
+ /** Keep only keys we understand, and only when their value has the right
25
+ * shape. One malformed key never discards its neighbours. */
26
+ function coerceMeta(raw) {
27
+ const meta = {};
28
+ if (typeof raw.source === 'string')
29
+ meta.source = raw.source;
30
+ if (Array.isArray(raw.tags) &&
31
+ raw.tags.every((t) => typeof t === 'string')) {
32
+ meta.tags = [...raw.tags];
33
+ }
34
+ // YAML turns `confidence: 0.8` into a number; the field is free-form text.
35
+ if (typeof raw.confidence === 'string') {
36
+ meta.confidence = raw.confidence;
37
+ }
38
+ else if (typeof raw.confidence === 'number') {
39
+ meta.confidence = String(raw.confidence);
40
+ }
41
+ if (typeof raw.created === 'string') {
42
+ meta.created = raw.created;
43
+ }
44
+ else if (raw.created instanceof Date) {
45
+ // YAML parses unquoted ISO timestamps into Dates.
46
+ meta.created = raw.created.toISOString();
47
+ }
48
+ return meta;
49
+ }
50
+ /**
51
+ * Split a decrypted note body into its fact and metadata.
52
+ *
53
+ * Total by construction: a missing fence, unparseable YAML, frontmatter that
54
+ * is not a mapping, or a note that was never a memory note at all all yield
55
+ * `{ fact: <the whole body>, meta: {} }`. A human edits these by hand, so a
56
+ * syntax error must cost them a bit of metadata, never the memory.
57
+ */
58
+ export function parseMemoryNote(decryptedContent) {
59
+ // The note's `type` is deliberately not consulted: an envelope is accepted
60
+ // wherever one is found, so a memory whose type was changed in the app — or
61
+ // a plain note dropped into the folder by hand — still reads correctly.
62
+ const body = unwrapEnvelope(decryptedContent);
63
+ if (!body.startsWith(`${FENCE}\n`))
64
+ return { fact: body, meta: {} };
65
+ // Only the first fence pair is frontmatter; --- lines inside the fact are
66
+ // ordinary content.
67
+ const close = body.indexOf(`\n${FENCE}\n`, FENCE.length);
68
+ if (close === -1)
69
+ return { fact: body, meta: {} };
70
+ const front = body.slice(FENCE.length + 1, close);
71
+ const fact = body.slice(close + FENCE.length + 2);
72
+ // An empty fence is the marker `serializeMemoryBody` writes when a fact
73
+ // would otherwise be misread as its own frontmatter. Strip it and keep the
74
+ // fact whole.
75
+ if (front.trim() === '')
76
+ return { fact, meta: {} };
77
+ let raw;
78
+ try {
79
+ raw = parseYaml(front);
80
+ }
81
+ catch {
82
+ return { fact: body, meta: {} };
83
+ }
84
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
85
+ return { fact: body, meta: {} };
86
+ }
87
+ return { fact, meta: coerceMeta(raw) };
88
+ }
89
+ function yamlScalar(value) {
90
+ // Quote anything YAML would otherwise reinterpret (or that would break the
91
+ // line), so a round-trip returns the same string.
92
+ return /^[A-Za-z0-9][\w .:@/+-]*$/.test(value) && !value.includes(': ')
93
+ ? value
94
+ : JSON.stringify(value);
95
+ }
96
+ /**
97
+ * Canonical memory body: frontmatter then the fact, or just the fact when
98
+ * there is no metadata to record.
99
+ */
100
+ export function serializeMemoryBody(fact, meta) {
101
+ const lines = [];
102
+ for (const key of KEY_ORDER) {
103
+ const value = meta[key];
104
+ if (value === undefined)
105
+ continue;
106
+ if (key === 'tags') {
107
+ const tags = value;
108
+ if (tags.length === 0)
109
+ continue;
110
+ lines.push(`tags: [${tags.map(yamlScalar).join(', ')}]`);
111
+ }
112
+ else {
113
+ lines.push(`${key}: ${yamlScalar(value)}`);
114
+ }
115
+ }
116
+ if (lines.length === 0) {
117
+ // A fact that opens with its own fence (a pasted document with
118
+ // frontmatter, say) would be read back as metadata and lose its first
119
+ // lines. An empty fence disambiguates it.
120
+ return fact.startsWith(`${FENCE}\n`) ? `${FENCE}\n${FENCE}\n${fact}` : fact;
121
+ }
122
+ return `${FENCE}\n${lines.join('\n')}\n${FENCE}\n${fact}`;
123
+ }
124
+ /**
125
+ * Metadata for an update: named keys win, unnamed keys carry forward, and
126
+ * `created` is pinned to the first write so an in-place dedupe merge cannot
127
+ * make an old memory look new.
128
+ */
129
+ export function mergeMeta(existing, changes) {
130
+ const merged = { ...existing };
131
+ if (changes.source !== undefined)
132
+ merged.source = changes.source;
133
+ if (changes.tags !== undefined)
134
+ merged.tags = [...changes.tags];
135
+ if (changes.confidence !== undefined)
136
+ merged.confidence = changes.confidence;
137
+ // Pinned to the first write, so an in-place dedupe merge cannot make an old
138
+ // memory look newly created.
139
+ if (changes.created !== undefined && existing.created === undefined) {
140
+ merged.created = changes.created;
141
+ }
142
+ return merged;
143
+ }
144
+ //# sourceMappingURL=memory-note.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-note.js","sourceRoot":"","sources":["../src/memory-note.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAyB1C,+EAA+E;AAC/E,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,CAAU,CAAC;AAEvE,MAAM,KAAK,GAAG,KAAK,CAAC;AAEpB;oCACoC;AACpC,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;AACnF,CAAC;AAED,SAAS,cAAc,CAAC,OAAe;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5C,IACE,MAAM,KAAK,IAAI;YACf,OAAO,MAAM,KAAK,QAAQ;YAC1B,OAAQ,MAA6B,CAAC,IAAI,KAAK,QAAQ,EACvD,CAAC;YACD,OAAQ,MAA2B,CAAC,IAAI,CAAC;QAC3C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;IAC7E,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;6DAC6D;AAC7D,SAAS,UAAU,CAAC,GAA4B;IAC9C,MAAM,IAAI,GAAe,EAAE,CAAC;IAE5B,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;QAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IAE7D,IACE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QACvB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,EACzD,CAAC;QACD,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,2EAA2E;IAC3E,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;QACvC,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC;IACnC,CAAC;SAAM,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;QAC9C,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC3C,CAAC;IAED,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC7B,CAAC;SAAM,IAAI,GAAG,CAAC,OAAO,YAAY,IAAI,EAAE,CAAC;QACvC,kDAAkD;QAClD,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IAC3C,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,gBAAwB;IACtD,2EAA2E;IAC3E,4EAA4E;IAC5E,wEAAwE;IACxE,MAAM,IAAI,GAAG,cAAc,CAAC,gBAAgB,CAAC,CAAC;IAE9C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,KAAK,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAEpE,0EAA0E;IAC1E,oBAAoB;IACpB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,KAAK,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAElD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAElD,wEAAwE;IACxE,2EAA2E;IAC3E,cAAc;IACd,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAEnD,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAClC,CAAC;IACD,IAAI,GAAG,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAClC,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,GAA8B,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,2EAA2E;IAC3E,kDAAkD;IAClD,OAAO,2BAA2B,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QACrE,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,IAAgB;IAChE,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,KAAiB,CAAC;YAC/B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAChC,KAAK,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3D,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,UAAU,CAAC,KAAe,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,+DAA+D;QAC/D,sEAAsE;QACtE,0CAA0C;QAC1C,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,KAAK,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9E,CAAC;IACD,OAAO,GAAG,KAAK,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK,KAAK,IAAI,EAAE,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,QAAoB,EAAE,OAAmB;IACjE,MAAM,MAAM,GAAe,EAAE,GAAG,QAAQ,EAAE,CAAC;IAC3C,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACjE,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;QAAE,MAAM,CAAC,IAAI,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS;QAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAC7E,4EAA4E;IAC5E,6BAA6B;IAC7B,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACpE,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACnC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { type VirlowApi } from './api';
2
+ /** Name given to the memories root when the server has to create it. The root
3
+ * is identified by `kind`, never by this name — the user is free to rename it. */
4
+ export declare const MEMORIES_ROOT_NAME = "Memories";
5
+ export interface MemoryTree {
6
+ rootId: string;
7
+ /** namespace name -> subfolder id (direct children of the root) */
8
+ namespaces: Map<string, string>;
9
+ /** folder id -> namespace name; undefined for the root itself */
10
+ namespaceOf(folderId: string): string | undefined;
11
+ /** every folder id whose notes are memories, root first */
12
+ folderIds(): string[];
13
+ /** Record a subfolder created after the tree was resolved. Used by
14
+ * `ensureNamespace`; callers outside this module have no reason to. */
15
+ attach(folderId: string, name: string): void;
16
+ }
17
+ /**
18
+ * Resolve the memories folder tree, creating the root if the user has none.
19
+ * The root is whichever folder carries `kind: 'memories'` — the API allows
20
+ * only one per user.
21
+ */
22
+ export declare function resolveMemoryTree(api: VirlowApi): Promise<MemoryTree>;
23
+ /**
24
+ * Folder id for `name`, creating the namespace subfolder if it does not exist
25
+ * yet. Mutates `tree` so a later call in the same session is a cache hit.
26
+ */
27
+ export declare function ensureNamespace(api: VirlowApi, tree: MemoryTree, name: string): Promise<string>;
28
+ //# sourceMappingURL=memory-tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-tree.d.ts","sourceRoot":"","sources":["../src/memory-tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEjD;kFACkF;AAClF,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAE7C,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,iEAAiE;IACjE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAClD,2DAA2D;IAC3D,SAAS,IAAI,MAAM,EAAE,CAAC;IACtB;2EACuE;IACvE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9C;AAgCD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAiB3E;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CAOjB"}
@@ -0,0 +1,68 @@
1
+ import { ApiError } from './api';
2
+ /** Name given to the memories root when the server has to create it. The root
3
+ * is identified by `kind`, never by this name — the user is free to rename it. */
4
+ export const MEMORIES_ROOT_NAME = 'Memories';
5
+ function buildTree(rootId, folders) {
6
+ const namespaces = new Map();
7
+ const byFolderId = new Map();
8
+ const attach = (folderId, name) => {
9
+ byFolderId.set(folderId, name);
10
+ // Two subfolders may share a name — that is the user's organising choice.
11
+ // Both count as that namespace; writes go to the first.
12
+ if (!namespaces.has(name))
13
+ namespaces.set(name, folderId);
14
+ };
15
+ // Only direct children of the root are namespaces. Anything deeper is an
16
+ // ordinary folder the user nested for their own reasons; its notes are not
17
+ // memories, so it never reaches byFolderId and the sync walk skips it.
18
+ for (const folder of folders) {
19
+ if (folder.parentId === rootId)
20
+ attach(folder.id, folder.name);
21
+ }
22
+ return {
23
+ rootId,
24
+ namespaces,
25
+ namespaceOf: (folderId) => byFolderId.get(folderId),
26
+ folderIds: () => [rootId, ...byFolderId.keys()],
27
+ attach,
28
+ };
29
+ }
30
+ /**
31
+ * Resolve the memories folder tree, creating the root if the user has none.
32
+ * The root is whichever folder carries `kind: 'memories'` — the API allows
33
+ * only one per user.
34
+ */
35
+ export async function resolveMemoryTree(api) {
36
+ const { folders } = await api.listFolders();
37
+ const existing = folders.find((f) => f.kind === 'memories');
38
+ if (existing)
39
+ return buildTree(existing.id, folders);
40
+ try {
41
+ const created = await api.createFolder(MEMORIES_ROOT_NAME, null, 'memories');
42
+ return buildTree(created.id, [...folders, created]);
43
+ }
44
+ catch (err) {
45
+ // 409 MEMORIES_FOLDER_EXISTS: another client created the root between our
46
+ // listing and our POST. Re-list and use the winner.
47
+ if (!(err instanceof ApiError) || err.status !== 409)
48
+ throw err;
49
+ const retry = await api.listFolders();
50
+ const winner = retry.folders.find((f) => f.kind === 'memories');
51
+ if (!winner)
52
+ throw err;
53
+ return buildTree(winner.id, retry.folders);
54
+ }
55
+ }
56
+ /**
57
+ * Folder id for `name`, creating the namespace subfolder if it does not exist
58
+ * yet. Mutates `tree` so a later call in the same session is a cache hit.
59
+ */
60
+ export async function ensureNamespace(api, tree, name) {
61
+ const existing = tree.namespaces.get(name);
62
+ if (existing)
63
+ return existing;
64
+ const created = await api.createFolder(name, tree.rootId);
65
+ tree.attach(created.id, name);
66
+ return created.id;
67
+ }
68
+ //# sourceMappingURL=memory-tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-tree.js","sourceRoot":"","sources":["../src/memory-tree.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ import { type Exposure } from './exposure';
2
+ import type { VirlowApi } from './api';
3
+ import type { Vault } from './vault';
4
+ /** Hard ceiling on how many notes searchNotes() will decrypt+scan in a
5
+ * single call. Keeps a single search bounded even for very large accounts;
6
+ * `truncated` tells the caller when the account holds more than this. */
7
+ export declare const SEARCH_SCAN_CAP = 500;
8
+ export declare class NotesService {
9
+ private readonly api;
10
+ private readonly vault;
11
+ /** Per-unlock cache of decrypted (title, content) keyed by `id:updatedAt`
12
+ * so re-reading/re-scanning the same note twice in one unlock doesn't
13
+ * re-run AES-GCM decryption. Cleared whenever the vault locks. */
14
+ private readonly decryptCache;
15
+ /** Bumped every time the vault locks (see handleLock()). decryptRow()
16
+ * captures this before its await on decryptNoteFields() and re-checks it
17
+ * afterward, so a decrypt that was still in flight when the vault locked
18
+ * never writes decrypted plaintext into the cache after the lock. */
19
+ private epoch;
20
+ /** Serializes updateNote() calls so two overlapping partial edits to the
21
+ * same note (e.g. one changing title, one changing content) never race
22
+ * their read-merge-encrypt-write cycle — without this, both could read
23
+ * the same pre-edit row and the second write would silently clobber the
24
+ * first's change. Mirrors MemoryStore's enqueue()/mutationChain pattern. */
25
+ private mutationChain;
26
+ constructor(api: VirlowApi, vault: Vault);
27
+ private handleLock;
28
+ listNotes(params: {
29
+ folderId?: string;
30
+ starred?: boolean;
31
+ archived?: boolean;
32
+ page?: number;
33
+ limit?: number;
34
+ }): Promise<{
35
+ notes: Array<{
36
+ id: string;
37
+ title: string;
38
+ folderId: string | null;
39
+ starred: boolean;
40
+ updatedAt: string;
41
+ }>;
42
+ pagination: {
43
+ page: number;
44
+ limit: number;
45
+ total: number;
46
+ pages: number;
47
+ };
48
+ /** How many otherwise-visible notes were withheld, split by why — the
49
+ * remedy differs. Reported so a filtered list can never be mistaken for an
50
+ * empty one. */
51
+ withheld: {
52
+ inClosedFolders: number;
53
+ unfiled: number;
54
+ };
55
+ exposure: Exposure;
56
+ }>;
57
+ readNote(id: string): Promise<{
58
+ id: string;
59
+ title: string;
60
+ content: string;
61
+ folderId: string | null;
62
+ }>;
63
+ createNote(title: string, content: string, folderId?: string | null, type?: string): Promise<{
64
+ id: string;
65
+ }>;
66
+ updateNote(id: string, changes: {
67
+ title?: string;
68
+ content?: string;
69
+ }): Promise<void>;
70
+ searchNotes(query: string, limit?: number): Promise<{
71
+ results: Array<{
72
+ id: string;
73
+ title: string;
74
+ snippet: string;
75
+ }>;
76
+ scanned: number;
77
+ truncated: boolean;
78
+ }>;
79
+ moveNote(id: string, folderId: string | null): Promise<void>;
80
+ setStar(id: string, starred: boolean): Promise<void>;
81
+ archiveNote(id: string, archived: boolean): Promise<void>;
82
+ trashNote(id: string): Promise<void>;
83
+ listFolders(): Promise<{
84
+ folders: Array<{
85
+ id: string;
86
+ name: string;
87
+ parentId: string | null;
88
+ }>;
89
+ withheld: number;
90
+ }>;
91
+ /** Folder exposure for this request. Not cached across calls: the user can
92
+ * flip the switch in the app at any moment and the next tool call must
93
+ * respect it. */
94
+ private exposure;
95
+ createFolder(name: string, parentId?: string | null): Promise<{
96
+ id: string;
97
+ }>;
98
+ /** Decrypt (with per-unlock caching) or pass through a legacy plaintext
99
+ * row. Legacy rows (no encryptedTitle) carry their real content directly
100
+ * in the plaintext title/content columns. */
101
+ private decryptRow;
102
+ private buildSnippet;
103
+ private requireSession;
104
+ /** Chains `fn` onto the mutation queue so overlapping calls run one at a
105
+ * time; the `.catch(() => {})` guard keeps the internal chain alive after
106
+ * a rejection while the promise returned to the caller still carries it. */
107
+ private enqueue;
108
+ }
109
+ //# sourceMappingURL=notes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notes.d.ts","sourceRoot":"","sources":["../src/notes.ts"],"names":[],"mappings":"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"}
package/dist/notes.js ADDED
@@ -0,0 +1,234 @@
1
+ import { bytesToBase64, decryptNoteFields, encryptNoteFields } from '@batalabs/virlow-crypto';
2
+ import { computeExposure } from './exposure';
3
+ /** Hard ceiling on how many notes searchNotes() will decrypt+scan in a
4
+ * single call. Keeps a single search bounded even for very large accounts;
5
+ * `truncated` tells the caller when the account holds more than this. */
6
+ export const SEARCH_SCAN_CAP = 500;
7
+ const SEARCH_PAGE_LIMIT = 50;
8
+ /** Window radius (chars) either side of the match start when building a
9
+ * content snippet; total window is up to 2*SNIPPET_RADIUS. */
10
+ const SNIPPET_RADIUS = 80;
11
+ export class NotesService {
12
+ api;
13
+ vault;
14
+ /** Per-unlock cache of decrypted (title, content) keyed by `id:updatedAt`
15
+ * so re-reading/re-scanning the same note twice in one unlock doesn't
16
+ * re-run AES-GCM decryption. Cleared whenever the vault locks. */
17
+ decryptCache = new Map();
18
+ /** Bumped every time the vault locks (see handleLock()). decryptRow()
19
+ * captures this before its await on decryptNoteFields() and re-checks it
20
+ * afterward, so a decrypt that was still in flight when the vault locked
21
+ * never writes decrypted plaintext into the cache after the lock. */
22
+ epoch = 0;
23
+ /** Serializes updateNote() calls so two overlapping partial edits to the
24
+ * same note (e.g. one changing title, one changing content) never race
25
+ * their read-merge-encrypt-write cycle — without this, both could read
26
+ * the same pre-edit row and the second write would silently clobber the
27
+ * first's change. Mirrors MemoryStore's enqueue()/mutationChain pattern. */
28
+ mutationChain = Promise.resolve();
29
+ constructor(api, vault) {
30
+ this.api = api;
31
+ this.vault = vault;
32
+ this.vault.onLock = () => this.handleLock();
33
+ }
34
+ handleLock() {
35
+ this.decryptCache.clear();
36
+ this.epoch++;
37
+ }
38
+ async listNotes(params) {
39
+ const key = this.requireSession();
40
+ const { notes, pagination } = await this.api.listNotes({ ...params, deleted: false });
41
+ // Hidden notes are segregated client-side by the web app (see ApiNote's
42
+ // `hidden` doc comment); the MCP server mirrors that here so they never
43
+ // surface in list/search results. readNote() still allows fetching one
44
+ // by exact id — that's a deliberate, explicit request.
45
+ const exposure = await this.exposure();
46
+ const notHidden = notes.filter((row) => row.hidden !== true);
47
+ // Folders the user has not opened to AI tools are filtered here rather
48
+ // than at the API, so the count of what was withheld can be reported.
49
+ const visible = notHidden.filter((row) => exposure.allows(row.folderId));
50
+ const withheld = {
51
+ inClosedFolders: notHidden.filter((row) => row.folderId !== null && !exposure.allows(row.folderId)).length,
52
+ unfiled: notHidden.filter((row) => row.folderId === null).length,
53
+ };
54
+ const mapped = await Promise.all(visible.map(async (row) => {
55
+ const { title } = await this.decryptRow(key, row);
56
+ return {
57
+ id: row.id,
58
+ title,
59
+ folderId: row.folderId,
60
+ starred: row.starred === true,
61
+ updatedAt: row.updatedAt,
62
+ };
63
+ }));
64
+ return { notes: mapped, pagination, withheld, exposure };
65
+ }
66
+ async readNote(id) {
67
+ const key = this.requireSession();
68
+ const row = await this.api.getNote(id);
69
+ // Reading by exact id is still a read: the switch says the assistant may
70
+ // not read this folder, and knowing the id does not change that.
71
+ const exposure = await this.exposure();
72
+ if (!exposure.allows(row.folderId)) {
73
+ throw new Error(`Note ${id} is in a folder that is not exposed to MCP. Turn on "Expose to MCP" in that folder's settings in the Virlow app to read it here.`);
74
+ }
75
+ const { title, content } = await this.decryptRow(key, row);
76
+ return { id: row.id, title, content, folderId: row.folderId };
77
+ }
78
+ async createNote(title, content, folderId, type) {
79
+ const key = this.requireSession();
80
+ const salt = bytesToBase64(crypto.getRandomValues(new Uint8Array(32)));
81
+ const { encryptedTitle, encryptedContent, iv } = await encryptNoteFields(key, title, content);
82
+ const body = {
83
+ title: '[ENCRYPTED]',
84
+ content: '[ENCRYPTED]',
85
+ encryptedTitle,
86
+ encryptedContent,
87
+ iv,
88
+ salt,
89
+ };
90
+ if (folderId !== undefined)
91
+ body.folderId = folderId;
92
+ if (type !== undefined)
93
+ body.type = type;
94
+ const row = await this.api.createNote(body);
95
+ return { id: row.id };
96
+ }
97
+ updateNote(id, changes) {
98
+ return this.enqueue(async () => {
99
+ const key = this.requireSession();
100
+ const existingRow = await this.api.getNote(id);
101
+ const current = await this.decryptRow(key, existingRow);
102
+ const title = changes.title ?? current.title;
103
+ const content = changes.content ?? current.content;
104
+ const salt = bytesToBase64(crypto.getRandomValues(new Uint8Array(32)));
105
+ const { encryptedTitle, encryptedContent, iv } = await encryptNoteFields(key, title, content);
106
+ await this.api.updateNote(id, {
107
+ title: '[ENCRYPTED]',
108
+ content: '[ENCRYPTED]',
109
+ encryptedTitle,
110
+ encryptedContent,
111
+ iv,
112
+ salt,
113
+ });
114
+ });
115
+ }
116
+ async searchNotes(query, limit = 20) {
117
+ const key = this.requireSession();
118
+ const q = query.toLowerCase();
119
+ const results = [];
120
+ let scanned = 0;
121
+ let total = 0;
122
+ let page = 1;
123
+ scan: for (;;) {
124
+ const { notes, pagination } = await this.api.listNotes({ page, limit: SEARCH_PAGE_LIMIT, deleted: false });
125
+ total = pagination.total;
126
+ for (const row of notes) {
127
+ if (row.hidden === true)
128
+ continue;
129
+ if (scanned >= SEARCH_SCAN_CAP)
130
+ break scan;
131
+ scanned++;
132
+ const { title, content } = await this.decryptRow(key, row);
133
+ const contentIdx = content.toLowerCase().indexOf(q);
134
+ const titleMatch = title.toLowerCase().includes(q);
135
+ if (titleMatch || contentIdx !== -1) {
136
+ results.push({ id: row.id, title, snippet: this.buildSnippet(title, content, contentIdx) });
137
+ }
138
+ }
139
+ if (scanned >= SEARCH_SCAN_CAP)
140
+ break;
141
+ if (notes.length === 0 || page >= pagination.pages)
142
+ break;
143
+ page++;
144
+ }
145
+ return { results: results.slice(0, limit), scanned, truncated: total > SEARCH_SCAN_CAP };
146
+ }
147
+ async moveNote(id, folderId) {
148
+ this.requireSession();
149
+ await this.api.updateNote(id, { folderId });
150
+ }
151
+ async setStar(id, starred) {
152
+ this.requireSession();
153
+ await this.api.updateNote(id, { starred });
154
+ }
155
+ async archiveNote(id, archived) {
156
+ this.requireSession();
157
+ await this.api.updateNote(id, { archived });
158
+ }
159
+ async trashNote(id) {
160
+ this.requireSession();
161
+ await this.api.updateNote(id, { deleted: true });
162
+ }
163
+ async listFolders() {
164
+ this.requireSession();
165
+ const { folders } = await this.api.listFolders();
166
+ const exposure = computeExposure(folders);
167
+ const visible = folders.filter((f) => exposure.allows(f.id));
168
+ return {
169
+ // Names are withheld along with contents: a folder the user has not
170
+ // opened should not be inventoried either, only counted.
171
+ folders: visible.map((f) => ({ id: f.id, name: f.name, parentId: f.parentId })),
172
+ withheld: folders.length - visible.length,
173
+ };
174
+ }
175
+ /** Folder exposure for this request. Not cached across calls: the user can
176
+ * flip the switch in the app at any moment and the next tool call must
177
+ * respect it. */
178
+ async exposure() {
179
+ const { folders } = await this.api.listFolders();
180
+ return computeExposure(folders);
181
+ }
182
+ async createFolder(name, parentId) {
183
+ this.requireSession();
184
+ const folder = await this.api.createFolder(name, parentId);
185
+ return { id: folder.id };
186
+ }
187
+ /** Decrypt (with per-unlock caching) or pass through a legacy plaintext
188
+ * row. Legacy rows (no encryptedTitle) carry their real content directly
189
+ * in the plaintext title/content columns. */
190
+ async decryptRow(key, row) {
191
+ if (!row.encryptedTitle) {
192
+ return { title: row.title, content: row.content };
193
+ }
194
+ const cacheKey = `${row.id}:${row.updatedAt}`;
195
+ const cached = this.decryptCache.get(cacheKey);
196
+ if (cached)
197
+ return cached;
198
+ const startEpoch = this.epoch;
199
+ const fields = await decryptNoteFields(key, row.encryptedTitle, row.encryptedContent ?? '', row.iv ?? '');
200
+ // If the vault locked while this decrypt was in flight, the epoch moved
201
+ // on and decryptCache was already cleared — don't write plaintext back
202
+ // into it after the fact.
203
+ if (this.epoch === startEpoch) {
204
+ this.decryptCache.set(cacheKey, fields);
205
+ }
206
+ return fields;
207
+ }
208
+ buildSnippet(title, content, contentMatchIndex) {
209
+ if (contentMatchIndex === -1) {
210
+ return title;
211
+ }
212
+ const windowSize = SNIPPET_RADIUS * 2;
213
+ let start = Math.max(0, contentMatchIndex - SNIPPET_RADIUS);
214
+ const end = Math.min(content.length, start + windowSize);
215
+ if (end - start < windowSize) {
216
+ start = Math.max(0, end - windowSize);
217
+ }
218
+ return content.slice(start, end);
219
+ }
220
+ requireSession() {
221
+ const key = this.vault.session.key;
222
+ this.vault.touch();
223
+ return key;
224
+ }
225
+ /** Chains `fn` onto the mutation queue so overlapping calls run one at a
226
+ * time; the `.catch(() => {})` guard keeps the internal chain alive after
227
+ * a rejection while the promise returned to the caller still carries it. */
228
+ enqueue(fn) {
229
+ const next = this.mutationChain.then(fn, fn);
230
+ this.mutationChain = next.catch(() => { });
231
+ return next;
232
+ }
233
+ }
234
+ //# sourceMappingURL=notes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notes.js","sourceRoot":"","sources":["../src/notes.ts"],"names":[],"mappings":"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"}