@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Typelets Secure Notes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @batalabs/virlow-mcp-core
2
+
3
+ The connector core behind [Virlow](https://virlow.com)'s MCP integration:
4
+ encrypted note and memory access, folder exposure, and the memories opt-in.
5
+
6
+ Shared deliberately. The local `virlow-mcp` CLI and the hosted connector both
7
+ decide what an AI tool may read, and those rules — `folders.mcpEnabled` and the
8
+ account's memories setting — must have one implementation. Two copies of the
9
+ code that decides who can read what would drift, and drift there is a security
10
+ bug rather than an inconsistency.
11
+
12
+ What it does *not* contain: transports, the CLI, and the unlock user interface.
13
+ Those differ between local and hosted and belong to their hosts.
14
+
15
+ Published from [batalabs/virlow-app](https://github.com/batalabs/virlow-app).
package/dist/api.d.ts ADDED
@@ -0,0 +1,118 @@
1
+ export declare class ApiError extends Error {
2
+ readonly status: number;
3
+ constructor(status: number, message: string);
4
+ }
5
+ export interface AuthTokens {
6
+ accessToken: string;
7
+ refreshToken: string;
8
+ }
9
+ export type LoginResult = {
10
+ mfaRequired: true;
11
+ mfaToken: string;
12
+ } | {
13
+ mfaRequired?: false;
14
+ user: {
15
+ id: string;
16
+ email: string;
17
+ };
18
+ accessToken: string;
19
+ refreshToken: string;
20
+ };
21
+ export interface ApiNote {
22
+ id: string;
23
+ folderId: string | null;
24
+ type: string;
25
+ /** Plaintext columns: the real content for legacy (unencrypted) notes,
26
+ * or the literal "[ENCRYPTED]" placeholder once a note carries ciphertext. */
27
+ title: string;
28
+ content: string;
29
+ encryptedTitle: string | null;
30
+ encryptedContent: string | null;
31
+ iv: string | null;
32
+ salt: string | null;
33
+ starred: boolean | null;
34
+ archived: boolean | null;
35
+ deleted: boolean | null;
36
+ /** The web app segregates hidden notes client-side; virlow-mcp filters
37
+ * them out of listNotes/searchNotes for the same reason (see notes.ts). */
38
+ hidden: boolean | null;
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ }
42
+ export interface NoteWrite {
43
+ title: string;
44
+ content: string;
45
+ encryptedTitle: string;
46
+ encryptedContent: string;
47
+ iv: string;
48
+ salt: string;
49
+ folderId?: string | null;
50
+ type?: string;
51
+ tags?: string[];
52
+ starred?: boolean;
53
+ archived?: boolean;
54
+ deleted?: boolean;
55
+ }
56
+ export interface ApiFolder {
57
+ id: string;
58
+ name: string;
59
+ parentId: string | null;
60
+ /** Marks the single root folder whose notes are AI memories; null for every
61
+ * ordinary folder, including the namespace subfolders under that root. */
62
+ kind: 'memories' | null;
63
+ /** The user's "Expose to MCP" switch. False by default — nothing in this
64
+ * folder is shown to AI tools until they turn it on. */
65
+ mcpEnabled: boolean;
66
+ }
67
+ export declare class VirlowApi {
68
+ private readonly baseUrl;
69
+ private tokens;
70
+ private refreshInFlight;
71
+ onTokensRotated?: (tokens: AuthTokens) => void;
72
+ constructor(baseUrl: string);
73
+ setTokens(tokens: AuthTokens | null): void;
74
+ private request;
75
+ private refresh;
76
+ private doRefresh;
77
+ login(email: string, password: string): Promise<LoginResult>;
78
+ verify2fa(mfaToken: string, codeOrRecovery: {
79
+ code?: string;
80
+ recoveryCode?: string;
81
+ }): Promise<AuthTokens>;
82
+ me(): Promise<{
83
+ id: string;
84
+ email: string;
85
+ memoriesEnabled?: boolean;
86
+ }>;
87
+ encryptionStatus(): Promise<{
88
+ verifier: {
89
+ ciphertext: string;
90
+ iv: string;
91
+ salt: string;
92
+ } | null;
93
+ }>;
94
+ listNotes(params: {
95
+ page?: number;
96
+ limit?: number;
97
+ folderId?: string;
98
+ starred?: boolean;
99
+ archived?: boolean;
100
+ deleted?: boolean;
101
+ }): Promise<{
102
+ notes: ApiNote[];
103
+ pagination: {
104
+ page: number;
105
+ limit: number;
106
+ total: number;
107
+ pages: number;
108
+ };
109
+ }>;
110
+ getNote(id: string): Promise<ApiNote>;
111
+ createNote(body: NoteWrite): Promise<ApiNote>;
112
+ updateNote(id: string, body: Partial<NoteWrite>): Promise<ApiNote>;
113
+ listFolders(): Promise<{
114
+ folders: ApiFolder[];
115
+ }>;
116
+ createFolder(name: string, parentId?: string | null, kind?: 'memories'): Promise<ApiFolder>;
117
+ }
118
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/api.js ADDED
@@ -0,0 +1,109 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ constructor(status, message) {
4
+ super(message);
5
+ this.status = status;
6
+ this.name = 'ApiError';
7
+ }
8
+ }
9
+ export class VirlowApi {
10
+ baseUrl;
11
+ tokens = null;
12
+ refreshInFlight = null;
13
+ onTokensRotated;
14
+ constructor(baseUrl) {
15
+ this.baseUrl = baseUrl;
16
+ }
17
+ setTokens(tokens) {
18
+ this.tokens = tokens;
19
+ }
20
+ async request(method, path, body, opts = {}) {
21
+ const { auth = true, retryOn401 = true } = opts;
22
+ const headers = { 'content-type': 'application/json' };
23
+ if (auth && this.tokens)
24
+ headers.authorization = `Bearer ${this.tokens.accessToken}`;
25
+ const res = await fetch(`${this.baseUrl}${path}`, {
26
+ method,
27
+ headers,
28
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
29
+ });
30
+ if (res.status === 401 && auth && retryOn401 && this.tokens) {
31
+ await this.refresh();
32
+ return this.request(method, path, body, { auth, retryOn401: false });
33
+ }
34
+ if (res.status === 204)
35
+ return undefined;
36
+ const data = await res.json().catch(() => ({}));
37
+ if (!res.ok) {
38
+ const d = data;
39
+ throw new ApiError(res.status, d.error ?? d.message ?? res.statusText);
40
+ }
41
+ return data;
42
+ }
43
+ refresh() {
44
+ this.refreshInFlight ??= this.doRefresh().finally(() => {
45
+ this.refreshInFlight = null;
46
+ });
47
+ return this.refreshInFlight;
48
+ }
49
+ async doRefresh() {
50
+ if (!this.tokens)
51
+ throw new ApiError(401, 'Not authenticated');
52
+ const rotated = await this.request('POST', '/api/auth/refresh', { refreshToken: this.tokens.refreshToken }, { auth: false, retryOn401: false });
53
+ this.tokens = { accessToken: rotated.accessToken, refreshToken: rotated.refreshToken };
54
+ this.onTokensRotated?.(this.tokens);
55
+ }
56
+ login(email, password) {
57
+ return this.request('POST', '/api/auth/login', { email, password }, { auth: false });
58
+ }
59
+ verify2fa(mfaToken, codeOrRecovery) {
60
+ return this.request('POST', '/api/auth/2fa/verify', { mfaToken, ...codeOrRecovery }, { auth: false });
61
+ }
62
+ me() {
63
+ return this.request('GET', '/api/users/me');
64
+ }
65
+ encryptionStatus() {
66
+ return this.request('GET', '/api/users/me/encryption');
67
+ }
68
+ listNotes(params) {
69
+ const query = new URLSearchParams();
70
+ if (params.page !== undefined)
71
+ query.set('page', String(params.page));
72
+ if (params.limit !== undefined)
73
+ query.set('limit', String(params.limit));
74
+ if (params.folderId !== undefined)
75
+ query.set('folderId', params.folderId);
76
+ if (params.starred !== undefined)
77
+ query.set('starred', String(params.starred));
78
+ if (params.archived !== undefined)
79
+ query.set('archived', String(params.archived));
80
+ if (params.deleted !== undefined)
81
+ query.set('deleted', String(params.deleted));
82
+ const qs = query.toString();
83
+ return this.request('GET', `/api/notes${qs ? `?${qs}` : ''}`);
84
+ }
85
+ getNote(id) {
86
+ return this.request('GET', `/api/notes/${id}`);
87
+ }
88
+ createNote(body) {
89
+ return this.request('POST', '/api/notes', body);
90
+ }
91
+ updateNote(id, body) {
92
+ return this.request('PUT', `/api/notes/${id}`, body);
93
+ }
94
+ listFolders() {
95
+ return this.request('GET', '/api/folders');
96
+ }
97
+ createFolder(name, parentId, kind) {
98
+ // `kind` is omitted rather than sent as null: the API rejects unknown kinds
99
+ // and a namespace subfolder is an ordinary folder.
100
+ const body = {
101
+ name,
102
+ parentId,
103
+ };
104
+ if (kind !== undefined)
105
+ body.kind = kind;
106
+ return this.request('POST', '/api/folders', body);
107
+ }
108
+ }
109
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Embedding vectors cached on disk, encrypted under the master key.
3
+ *
4
+ * Embedding a few hundred memories takes long enough to be felt at the start
5
+ * of every session, so the vectors are kept between runs. They are derived
6
+ * from plaintext memories, so the file is AES-GCM ciphertext and nothing —
7
+ * not the note ids, not the timestamps — is readable without the key.
8
+ *
9
+ * Every failure mode is a silent miss: a missing file, a file written under a
10
+ * different master password, a different embedding model, or corrupt bytes all
11
+ * yield an empty cache that gets rebuilt. The cache is an optimisation, never
12
+ * a source of truth.
13
+ */
14
+ export declare class EmbeddingCache {
15
+ private readonly file;
16
+ private readonly embeddingModel;
17
+ private readonly dir;
18
+ private records;
19
+ private dirty;
20
+ constructor(opts: {
21
+ dir: string;
22
+ userId: string;
23
+ embeddingModel: string;
24
+ });
25
+ load(key: CryptoKey): Promise<void>;
26
+ /** The cached vector, but only if the note has not been edited since. */
27
+ get(noteId: string, updatedAt: string): Float32Array | undefined;
28
+ set(noteId: string, updatedAt: string, vector: Float32Array): void;
29
+ delete(noteId: string): void;
30
+ clear(): void;
31
+ save(key: CryptoKey): Promise<void>;
32
+ get size(): number;
33
+ }
34
+ //# sourceMappingURL=embedding-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-cache.d.ts","sourceRoot":"","sources":["../src/embedding-cache.ts"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,OAAO,CAAkE;IACjF,OAAO,CAAC,KAAK,CAAS;gBAEV,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE;IAQnE,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BzC,yEAAyE;IACzE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAKhE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI;IAKlE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI5B,KAAK,IAAI,IAAI;IAKP,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BzC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,108 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { bytesToBase64, base64ToBytes, decryptBytesBlob, encryptBytesBlob, } from '@batalabs/virlow-crypto';
5
+ /** Bumped if the on-disk record shape ever changes; a mismatch is a cache miss. */
6
+ const CACHE_VERSION = 1;
7
+ /**
8
+ * Embedding vectors cached on disk, encrypted under the master key.
9
+ *
10
+ * Embedding a few hundred memories takes long enough to be felt at the start
11
+ * of every session, so the vectors are kept between runs. They are derived
12
+ * from plaintext memories, so the file is AES-GCM ciphertext and nothing —
13
+ * not the note ids, not the timestamps — is readable without the key.
14
+ *
15
+ * Every failure mode is a silent miss: a missing file, a file written under a
16
+ * different master password, a different embedding model, or corrupt bytes all
17
+ * yield an empty cache that gets rebuilt. The cache is an optimisation, never
18
+ * a source of truth.
19
+ */
20
+ export class EmbeddingCache {
21
+ file;
22
+ embeddingModel;
23
+ dir;
24
+ records = new Map();
25
+ dirty = false;
26
+ constructor(opts) {
27
+ this.dir = opts.dir;
28
+ this.embeddingModel = opts.embeddingModel;
29
+ // The user id is hashed so the filename itself identifies nobody.
30
+ const name = createHash('sha256').update(opts.userId).digest('hex');
31
+ this.file = path.join(opts.dir, `${name}.bin`);
32
+ }
33
+ async load(key) {
34
+ let parsed;
35
+ try {
36
+ const blob = await readFile(this.file, 'utf8');
37
+ const bytes = await decryptBytesBlob(key, blob);
38
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
39
+ }
40
+ catch {
41
+ // Missing, undecryptable, or malformed — all just a cold cache.
42
+ return;
43
+ }
44
+ if (parsed.version !== CACHE_VERSION)
45
+ return;
46
+ // Vectors from another model are meaningless against current ones: mixing
47
+ // them would produce similarity scores that silently misrank.
48
+ if (parsed.embeddingModel !== this.embeddingModel)
49
+ return;
50
+ if (!parsed.records || typeof parsed.records !== 'object')
51
+ return;
52
+ for (const [noteId, record] of Object.entries(parsed.records)) {
53
+ if (!record || typeof record.v !== 'string' || typeof record.u !== 'string') {
54
+ continue;
55
+ }
56
+ const bytes = base64ToBytes(record.v);
57
+ if (bytes.byteLength % 4 !== 0)
58
+ continue;
59
+ const vector = new Float32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
60
+ this.records.set(noteId, { vector, updatedAt: record.u });
61
+ }
62
+ this.dirty = false;
63
+ }
64
+ /** The cached vector, but only if the note has not been edited since. */
65
+ get(noteId, updatedAt) {
66
+ const record = this.records.get(noteId);
67
+ return record && record.updatedAt === updatedAt ? record.vector : undefined;
68
+ }
69
+ set(noteId, updatedAt, vector) {
70
+ this.records.set(noteId, { vector, updatedAt });
71
+ this.dirty = true;
72
+ }
73
+ delete(noteId) {
74
+ if (this.records.delete(noteId))
75
+ this.dirty = true;
76
+ }
77
+ clear() {
78
+ if (this.records.size > 0)
79
+ this.dirty = true;
80
+ this.records.clear();
81
+ }
82
+ async save(key) {
83
+ if (!this.dirty)
84
+ return;
85
+ const records = {};
86
+ for (const [noteId, { vector, updatedAt }] of this.records) {
87
+ const bytes = new Uint8Array(vector.buffer.slice(vector.byteOffset, vector.byteOffset + vector.byteLength));
88
+ records[noteId] = { v: bytesToBase64(bytes), u: updatedAt };
89
+ }
90
+ const payload = {
91
+ version: CACHE_VERSION,
92
+ embeddingModel: this.embeddingModel,
93
+ records,
94
+ };
95
+ const blob = await encryptBytesBlob(key, new TextEncoder().encode(JSON.stringify(payload)));
96
+ await mkdir(this.dir, { recursive: true, mode: 0o700 });
97
+ // Write-then-rename so an interrupted save cannot leave a half-written file
98
+ // where the next load expects ciphertext.
99
+ const tmp = `${this.file}.${randomUUID()}.tmp`;
100
+ await writeFile(tmp, blob, { mode: 0o600 });
101
+ await rename(tmp, this.file);
102
+ this.dirty = false;
103
+ }
104
+ get size() {
105
+ return this.records.size;
106
+ }
107
+ }
108
+ //# sourceMappingURL=embedding-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding-cache.js","sourceRoot":"","sources":["../src/embedding-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { ApiFolder } from './api';
2
+ /**
3
+ * Which folders the user has actually opened to AI tools.
4
+ *
5
+ * Every folder carries an "Expose to MCP" switch — *"Let the local AI
6
+ * assistant read & edit this folder and its subfolders. Off by default."* —
7
+ * and until now nothing read it, so the app was telling users something untrue
8
+ * about who could see their notes. This is the thing that makes it true.
9
+ *
10
+ * The rule follows the switch's own wording: a folder is exposed when it, or
11
+ * any ancestor, is switched on. Turning a parent on therefore opens everything
12
+ * beneath it, and a child cannot opt back out — matching what the label
13
+ * promises rather than inventing a precedence rule the UI never mentions.
14
+ */
15
+ export interface Exposure {
16
+ /** Whether notes in this folder may be shown. */
17
+ allows(folderId: string | null): boolean;
18
+ /** Folders the user has not opened, for reporting how much is hidden. */
19
+ hiddenFolderCount: number;
20
+ /** True when nothing at all is exposed — worth saying differently, since
21
+ * "no results" and "you have not opened anything yet" look identical
22
+ * otherwise. */
23
+ allHidden: boolean;
24
+ }
25
+ export interface ExposureOptions {
26
+ /** The memories root, if one exists. It and its namespace subfolders are
27
+ * exempt: the MCP server creates that tree and it exists solely for AI
28
+ * tools, so requiring the user to opt it in would be asking them to
29
+ * authorise a folder they never made for a purpose it already has. */
30
+ memoriesRootId?: string | null;
31
+ }
32
+ export declare function computeExposure(folders: ReadonlyArray<ApiFolder>, opts?: ExposureOptions): Exposure;
33
+ /**
34
+ * The sentence appended to any result that was filtered. Never omitted when
35
+ * something was withheld: a quietly shortened list is indistinguishable from
36
+ * an empty vault, and the user would have no reason to suspect a setting.
37
+ */
38
+ export declare function filteredNotice(hidden: {
39
+ inClosedFolders: number;
40
+ unfiled: number;
41
+ }, exposure: Exposure): string;
42
+ //# sourceMappingURL=exposure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exposure.d.ts","sourceRoot":"","sources":["../src/exposure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,QAAQ;IACvB,iDAAiD;IACjD,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC;IACzC,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;oBAEgB;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B;;;0EAGsE;IACtE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,EACjC,IAAI,GAAE,eAAoB,GACzB,QAAQ,CAgDV;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACpD,QAAQ,EAAE,QAAQ,GACjB,MAAM,CA6BR"}
@@ -0,0 +1,74 @@
1
+ export function computeExposure(folders, opts = {}) {
2
+ const byId = new Map(folders.map((f) => [f.id, f]));
3
+ const memoriesRootId = opts.memoriesRootId ?? folders.find((f) => f.kind === 'memories')?.id ?? null;
4
+ const resolved = new Map();
5
+ const isExposed = (folderId) => {
6
+ const cached = resolved.get(folderId);
7
+ if (cached !== undefined)
8
+ return cached;
9
+ // Walk to the root, stopping at the first switched-on ancestor. The seen
10
+ // set keeps a corrupt parent cycle from hanging the server rather than
11
+ // trusting the data to be a tree.
12
+ const chain = [];
13
+ const seen = new Set();
14
+ let current = folderId;
15
+ let exposed = false;
16
+ while (current !== null && !seen.has(current)) {
17
+ seen.add(current);
18
+ const folder = byId.get(current);
19
+ if (folder === undefined)
20
+ break;
21
+ chain.push(folder.id);
22
+ if (folder.mcpEnabled === true || folder.id === memoriesRootId) {
23
+ exposed = true;
24
+ break;
25
+ }
26
+ current = folder.parentId;
27
+ }
28
+ for (const id of chain)
29
+ resolved.set(id, exposed);
30
+ return exposed;
31
+ };
32
+ let hiddenFolderCount = 0;
33
+ for (const folder of folders) {
34
+ if (!isExposed(folder.id))
35
+ hiddenFolderCount++;
36
+ }
37
+ return {
38
+ // A note in no folder is in nothing the user has opened, so it stays
39
+ // hidden. That is consistent with off-by-default, and the tools say so
40
+ // rather than letting it look like the note is gone.
41
+ allows: (folderId) => (folderId === null ? false : isExposed(folderId)),
42
+ hiddenFolderCount,
43
+ allHidden: folders.length > 0 && hiddenFolderCount === folders.length,
44
+ };
45
+ }
46
+ /**
47
+ * The sentence appended to any result that was filtered. Never omitted when
48
+ * something was withheld: a quietly shortened list is indistinguishable from
49
+ * an empty vault, and the user would have no reason to suspect a setting.
50
+ */
51
+ export function filteredNotice(hidden, exposure) {
52
+ const parts = [];
53
+ if (hidden.inClosedFolders > 0) {
54
+ const n = hidden.inClosedFolders;
55
+ const notes = `${n} note${n === 1 ? '' : 's'}`;
56
+ parts.push(exposure.allHidden
57
+ ? `${notes} hidden: no folder is exposed to MCP yet. Open a folder in the ` +
58
+ 'Virlow app, edit it, and turn on "Expose to MCP" to let AI tools read it.'
59
+ : `${notes} hidden in folders that are not exposed to MCP. Turn on ` +
60
+ '"Expose to MCP" in a folder\'s settings in the Virlow app to include it.');
61
+ }
62
+ // Worth saying separately: the remedy is different. There is no switch on a
63
+ // note that lives in no folder, so "turn on the folder" is useless advice —
64
+ // it has to be filed into an exposed folder first.
65
+ if (hidden.unfiled > 0) {
66
+ const n = hidden.unfiled;
67
+ parts.push(`${n} note${n === 1 ? '' : 's'} hidden because ${n === 1 ? 'it is' : 'they are'} ` +
68
+ 'not in any folder, and exposure is granted per folder. Move ' +
69
+ `${n === 1 ? 'it' : 'them'} into a folder that is exposed to MCP to include ` +
70
+ `${n === 1 ? 'it' : 'them'}.`);
71
+ }
72
+ return parts.length === 0 ? '' : `\n\n(${parts.join(' ')})`;
73
+ }
74
+ //# sourceMappingURL=exposure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exposure.js","sourceRoot":"","sources":["../src/exposure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ export { ApiError, VirlowApi } from './api';
2
+ export type { ApiFolder, ApiNote, AuthTokens, LoginResult, NoteWrite } from './api';
3
+ export { LockedError, Vault } from './vault';
4
+ export type { UnlockedSession } from './vault';
5
+ export { NotesService, SEARCH_SCAN_CAP } from './notes';
6
+ export { MemoryStore, SYNC_PAGE_LIMIT } from './memories';
7
+ export type { CachedMemory, SimilarMemory, SyncResult } from './memories';
8
+ export { mergeMeta, parseMemoryNote, serializeMemoryBody, toCodeEnvelope, } from './memory-note';
9
+ export type { MemoryMeta, ParsedMemoryNote } from './memory-note';
10
+ export { MEMORIES_ROOT_NAME, ensureNamespace, resolveMemoryTree, } from './memory-tree';
11
+ export type { MemoryTree } from './memory-tree';
12
+ export { computeExposure, filteredNotice } from './exposure';
13
+ export type { Exposure } from './exposure';
14
+ export { EmbeddingCache } from './embedding-cache';
15
+ export { MEMORIES_DISABLED_MESSAGE, memoriesAllowed } from './memories-enabled';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5C,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAC7C,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EACL,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,GAClB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { ApiError, VirlowApi } from './api';
2
+ export { LockedError, Vault } from './vault';
3
+ export { NotesService, SEARCH_SCAN_CAP } from './notes';
4
+ export { MemoryStore, SYNC_PAGE_LIMIT } from './memories';
5
+ export { mergeMeta, parseMemoryNote, serializeMemoryBody, toCodeEnvelope, } from './memory-note';
6
+ export { MEMORIES_ROOT_NAME, ensureNamespace, resolveMemoryTree, } from './memory-tree';
7
+ export { computeExposure, filteredNotice } from './exposure';
8
+ export { EmbeddingCache } from './embedding-cache';
9
+ export { MEMORIES_DISABLED_MESSAGE, memoriesAllowed } from './memories-enabled';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1D,OAAO,EACL,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,cAAc,GACf,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { VirlowApi } from './api';
2
+ /**
3
+ * Whether the user has switched AI memories on, read fresh from the account.
4
+ *
5
+ * Deliberately not cached for the session. Turning the switch *on* being slow
6
+ * is an inconvenience; turning it *off* being slow is a privacy control that
7
+ * does not work. A user who switches memories off in the app expects the next
8
+ * tool call to respect it, not the next time they happen to restart their
9
+ * editor — and they would have no way to discover the difference.
10
+ *
11
+ * The cost is one GET alongside operations that already list folders, page
12
+ * through notes, decrypt, and embed.
13
+ */
14
+ export declare function memoriesAllowed(api: VirlowApi): Promise<boolean>;
15
+ export declare const MEMORIES_DISABLED_MESSAGE: string;
16
+ //# sourceMappingURL=memories-enabled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memories-enabled.d.ts","sourceRoot":"","sources":["../src/memories-enabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAYtE;AAED,eAAO,MAAM,yBAAyB,QAEqB,CAAC"}