@batalabs/virlow-mcp-core 3.11.4 → 3.11.6
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/dist/cjs/api.d.ts +117 -0
- package/dist/cjs/api.js +113 -0
- package/dist/cjs/embedding-cache.d.ts +33 -0
- package/dist/cjs/embedding-cache.js +114 -0
- package/dist/cjs/exposure.d.ts +41 -0
- package/dist/cjs/exposure.js +77 -0
- package/dist/cjs/index.d.ts +15 -0
- package/dist/cjs/index.js +32 -0
- package/dist/cjs/memories-enabled.d.ts +15 -0
- package/dist/cjs/memories-enabled.js +32 -0
- package/dist/cjs/memories.d.ts +85 -0
- package/dist/cjs/memories.js +353 -0
- package/dist/cjs/memory-note.d.ts +44 -0
- package/dist/cjs/memory-note.js +149 -0
- package/dist/cjs/memory-tree.d.ts +27 -0
- package/dist/cjs/memory-tree.js +72 -0
- package/dist/cjs/notes.d.ts +108 -0
- package/dist/cjs/notes.js +237 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/vault.d.ts +34 -0
- package/dist/cjs/vault.js +77 -0
- package/dist/exposure.d.ts +1 -1
- package/dist/exposure.d.ts.map +1 -1
- package/dist/index.d.ts +15 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/dist/memories-enabled.d.ts +1 -1
- package/dist/memories-enabled.d.ts.map +1 -1
- package/dist/memories.d.ts +4 -4
- package/dist/memories.d.ts.map +1 -1
- package/dist/memories.js +3 -3
- package/dist/memories.js.map +1 -1
- package/dist/memory-tree.d.ts +1 -1
- package/dist/memory-tree.d.ts.map +1 -1
- package/dist/memory-tree.js +1 -1
- package/dist/memory-tree.js.map +1 -1
- package/dist/notes.d.ts +3 -3
- package/dist/notes.d.ts.map +1 -1
- package/dist/notes.js +1 -1
- package/dist/notes.js.map +1 -1
- package/package.json +13 -7
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
}
|
package/dist/cjs/api.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VirlowApi = exports.ApiError = void 0;
|
|
4
|
+
class ApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
constructor(status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = 'ApiError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.ApiError = ApiError;
|
|
13
|
+
class VirlowApi {
|
|
14
|
+
baseUrl;
|
|
15
|
+
tokens = null;
|
|
16
|
+
refreshInFlight = null;
|
|
17
|
+
onTokensRotated;
|
|
18
|
+
constructor(baseUrl) {
|
|
19
|
+
this.baseUrl = baseUrl;
|
|
20
|
+
}
|
|
21
|
+
setTokens(tokens) {
|
|
22
|
+
this.tokens = tokens;
|
|
23
|
+
}
|
|
24
|
+
async request(method, path, body, opts = {}) {
|
|
25
|
+
const { auth = true, retryOn401 = true } = opts;
|
|
26
|
+
const headers = { 'content-type': 'application/json' };
|
|
27
|
+
if (auth && this.tokens)
|
|
28
|
+
headers.authorization = `Bearer ${this.tokens.accessToken}`;
|
|
29
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
30
|
+
method,
|
|
31
|
+
headers,
|
|
32
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
33
|
+
});
|
|
34
|
+
if (res.status === 401 && auth && retryOn401 && this.tokens) {
|
|
35
|
+
await this.refresh();
|
|
36
|
+
return this.request(method, path, body, { auth, retryOn401: false });
|
|
37
|
+
}
|
|
38
|
+
if (res.status === 204)
|
|
39
|
+
return undefined;
|
|
40
|
+
const data = await res.json().catch(() => ({}));
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const d = data;
|
|
43
|
+
throw new ApiError(res.status, d.error ?? d.message ?? res.statusText);
|
|
44
|
+
}
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
refresh() {
|
|
48
|
+
this.refreshInFlight ??= this.doRefresh().finally(() => {
|
|
49
|
+
this.refreshInFlight = null;
|
|
50
|
+
});
|
|
51
|
+
return this.refreshInFlight;
|
|
52
|
+
}
|
|
53
|
+
async doRefresh() {
|
|
54
|
+
if (!this.tokens)
|
|
55
|
+
throw new ApiError(401, 'Not authenticated');
|
|
56
|
+
const rotated = await this.request('POST', '/api/auth/refresh', { refreshToken: this.tokens.refreshToken }, { auth: false, retryOn401: false });
|
|
57
|
+
this.tokens = { accessToken: rotated.accessToken, refreshToken: rotated.refreshToken };
|
|
58
|
+
this.onTokensRotated?.(this.tokens);
|
|
59
|
+
}
|
|
60
|
+
login(email, password) {
|
|
61
|
+
return this.request('POST', '/api/auth/login', { email, password }, { auth: false });
|
|
62
|
+
}
|
|
63
|
+
verify2fa(mfaToken, codeOrRecovery) {
|
|
64
|
+
return this.request('POST', '/api/auth/2fa/verify', { mfaToken, ...codeOrRecovery }, { auth: false });
|
|
65
|
+
}
|
|
66
|
+
me() {
|
|
67
|
+
return this.request('GET', '/api/users/me');
|
|
68
|
+
}
|
|
69
|
+
encryptionStatus() {
|
|
70
|
+
return this.request('GET', '/api/users/me/encryption');
|
|
71
|
+
}
|
|
72
|
+
listNotes(params) {
|
|
73
|
+
const query = new URLSearchParams();
|
|
74
|
+
if (params.page !== undefined)
|
|
75
|
+
query.set('page', String(params.page));
|
|
76
|
+
if (params.limit !== undefined)
|
|
77
|
+
query.set('limit', String(params.limit));
|
|
78
|
+
if (params.folderId !== undefined)
|
|
79
|
+
query.set('folderId', params.folderId);
|
|
80
|
+
if (params.starred !== undefined)
|
|
81
|
+
query.set('starred', String(params.starred));
|
|
82
|
+
if (params.archived !== undefined)
|
|
83
|
+
query.set('archived', String(params.archived));
|
|
84
|
+
if (params.deleted !== undefined)
|
|
85
|
+
query.set('deleted', String(params.deleted));
|
|
86
|
+
const qs = query.toString();
|
|
87
|
+
return this.request('GET', `/api/notes${qs ? `?${qs}` : ''}`);
|
|
88
|
+
}
|
|
89
|
+
getNote(id) {
|
|
90
|
+
return this.request('GET', `/api/notes/${id}`);
|
|
91
|
+
}
|
|
92
|
+
createNote(body) {
|
|
93
|
+
return this.request('POST', '/api/notes', body);
|
|
94
|
+
}
|
|
95
|
+
updateNote(id, body) {
|
|
96
|
+
return this.request('PUT', `/api/notes/${id}`, body);
|
|
97
|
+
}
|
|
98
|
+
listFolders() {
|
|
99
|
+
return this.request('GET', '/api/folders');
|
|
100
|
+
}
|
|
101
|
+
createFolder(name, parentId, kind) {
|
|
102
|
+
// `kind` is omitted rather than sent as null: the API rejects unknown kinds
|
|
103
|
+
// and a namespace subfolder is an ordinary folder.
|
|
104
|
+
const body = {
|
|
105
|
+
name,
|
|
106
|
+
parentId,
|
|
107
|
+
};
|
|
108
|
+
if (kind !== undefined)
|
|
109
|
+
body.kind = kind;
|
|
110
|
+
return this.request('POST', '/api/folders', body);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.VirlowApi = VirlowApi;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EmbeddingCache = void 0;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const virlow_crypto_1 = require("@batalabs/virlow-crypto");
|
|
11
|
+
/** Bumped if the on-disk record shape ever changes; a mismatch is a cache miss. */
|
|
12
|
+
const CACHE_VERSION = 1;
|
|
13
|
+
/**
|
|
14
|
+
* Embedding vectors cached on disk, encrypted under the master key.
|
|
15
|
+
*
|
|
16
|
+
* Embedding a few hundred memories takes long enough to be felt at the start
|
|
17
|
+
* of every session, so the vectors are kept between runs. They are derived
|
|
18
|
+
* from plaintext memories, so the file is AES-GCM ciphertext and nothing —
|
|
19
|
+
* not the note ids, not the timestamps — is readable without the key.
|
|
20
|
+
*
|
|
21
|
+
* Every failure mode is a silent miss: a missing file, a file written under a
|
|
22
|
+
* different master password, a different embedding model, or corrupt bytes all
|
|
23
|
+
* yield an empty cache that gets rebuilt. The cache is an optimisation, never
|
|
24
|
+
* a source of truth.
|
|
25
|
+
*/
|
|
26
|
+
class EmbeddingCache {
|
|
27
|
+
file;
|
|
28
|
+
embeddingModel;
|
|
29
|
+
dir;
|
|
30
|
+
records = new Map();
|
|
31
|
+
dirty = false;
|
|
32
|
+
constructor(opts) {
|
|
33
|
+
this.dir = opts.dir;
|
|
34
|
+
this.embeddingModel = opts.embeddingModel;
|
|
35
|
+
// The user id is hashed so the filename itself identifies nobody.
|
|
36
|
+
const name = (0, node_crypto_1.createHash)('sha256').update(opts.userId).digest('hex');
|
|
37
|
+
this.file = node_path_1.default.join(opts.dir, `${name}.bin`);
|
|
38
|
+
}
|
|
39
|
+
async load(key) {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
const blob = await (0, promises_1.readFile)(this.file, 'utf8');
|
|
43
|
+
const bytes = await (0, virlow_crypto_1.decryptBytesBlob)(key, blob);
|
|
44
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Missing, undecryptable, or malformed — all just a cold cache.
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (parsed.version !== CACHE_VERSION)
|
|
51
|
+
return;
|
|
52
|
+
// Vectors from another model are meaningless against current ones: mixing
|
|
53
|
+
// them would produce similarity scores that silently misrank.
|
|
54
|
+
if (parsed.embeddingModel !== this.embeddingModel)
|
|
55
|
+
return;
|
|
56
|
+
if (!parsed.records || typeof parsed.records !== 'object')
|
|
57
|
+
return;
|
|
58
|
+
for (const [noteId, record] of Object.entries(parsed.records)) {
|
|
59
|
+
if (!record || typeof record.v !== 'string' || typeof record.u !== 'string') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const bytes = (0, virlow_crypto_1.base64ToBytes)(record.v);
|
|
63
|
+
if (bytes.byteLength % 4 !== 0)
|
|
64
|
+
continue;
|
|
65
|
+
const vector = new Float32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
66
|
+
this.records.set(noteId, { vector, updatedAt: record.u });
|
|
67
|
+
}
|
|
68
|
+
this.dirty = false;
|
|
69
|
+
}
|
|
70
|
+
/** The cached vector, but only if the note has not been edited since. */
|
|
71
|
+
get(noteId, updatedAt) {
|
|
72
|
+
const record = this.records.get(noteId);
|
|
73
|
+
return record && record.updatedAt === updatedAt ? record.vector : undefined;
|
|
74
|
+
}
|
|
75
|
+
set(noteId, updatedAt, vector) {
|
|
76
|
+
this.records.set(noteId, { vector, updatedAt });
|
|
77
|
+
this.dirty = true;
|
|
78
|
+
}
|
|
79
|
+
delete(noteId) {
|
|
80
|
+
if (this.records.delete(noteId))
|
|
81
|
+
this.dirty = true;
|
|
82
|
+
}
|
|
83
|
+
clear() {
|
|
84
|
+
if (this.records.size > 0)
|
|
85
|
+
this.dirty = true;
|
|
86
|
+
this.records.clear();
|
|
87
|
+
}
|
|
88
|
+
async save(key) {
|
|
89
|
+
if (!this.dirty)
|
|
90
|
+
return;
|
|
91
|
+
const records = {};
|
|
92
|
+
for (const [noteId, { vector, updatedAt }] of this.records) {
|
|
93
|
+
const bytes = new Uint8Array(vector.buffer.slice(vector.byteOffset, vector.byteOffset + vector.byteLength));
|
|
94
|
+
records[noteId] = { v: (0, virlow_crypto_1.bytesToBase64)(bytes), u: updatedAt };
|
|
95
|
+
}
|
|
96
|
+
const payload = {
|
|
97
|
+
version: CACHE_VERSION,
|
|
98
|
+
embeddingModel: this.embeddingModel,
|
|
99
|
+
records,
|
|
100
|
+
};
|
|
101
|
+
const blob = await (0, virlow_crypto_1.encryptBytesBlob)(key, new TextEncoder().encode(JSON.stringify(payload)));
|
|
102
|
+
await (0, promises_1.mkdir)(this.dir, { recursive: true, mode: 0o700 });
|
|
103
|
+
// Write-then-rename so an interrupted save cannot leave a half-written file
|
|
104
|
+
// where the next load expects ciphertext.
|
|
105
|
+
const tmp = `${this.file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
106
|
+
await (0, promises_1.writeFile)(tmp, blob, { mode: 0o600 });
|
|
107
|
+
await (0, promises_1.rename)(tmp, this.file);
|
|
108
|
+
this.dirty = false;
|
|
109
|
+
}
|
|
110
|
+
get size() {
|
|
111
|
+
return this.records.size;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.EmbeddingCache = EmbeddingCache;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ApiFolder } from './api.js';
|
|
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;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeExposure = computeExposure;
|
|
4
|
+
exports.filteredNotice = filteredNotice;
|
|
5
|
+
function computeExposure(folders, opts = {}) {
|
|
6
|
+
const byId = new Map(folders.map((f) => [f.id, f]));
|
|
7
|
+
const memoriesRootId = opts.memoriesRootId ?? folders.find((f) => f.kind === 'memories')?.id ?? null;
|
|
8
|
+
const resolved = new Map();
|
|
9
|
+
const isExposed = (folderId) => {
|
|
10
|
+
const cached = resolved.get(folderId);
|
|
11
|
+
if (cached !== undefined)
|
|
12
|
+
return cached;
|
|
13
|
+
// Walk to the root, stopping at the first switched-on ancestor. The seen
|
|
14
|
+
// set keeps a corrupt parent cycle from hanging the server rather than
|
|
15
|
+
// trusting the data to be a tree.
|
|
16
|
+
const chain = [];
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
let current = folderId;
|
|
19
|
+
let exposed = false;
|
|
20
|
+
while (current !== null && !seen.has(current)) {
|
|
21
|
+
seen.add(current);
|
|
22
|
+
const folder = byId.get(current);
|
|
23
|
+
if (folder === undefined)
|
|
24
|
+
break;
|
|
25
|
+
chain.push(folder.id);
|
|
26
|
+
if (folder.mcpEnabled === true || folder.id === memoriesRootId) {
|
|
27
|
+
exposed = true;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
current = folder.parentId;
|
|
31
|
+
}
|
|
32
|
+
for (const id of chain)
|
|
33
|
+
resolved.set(id, exposed);
|
|
34
|
+
return exposed;
|
|
35
|
+
};
|
|
36
|
+
let hiddenFolderCount = 0;
|
|
37
|
+
for (const folder of folders) {
|
|
38
|
+
if (!isExposed(folder.id))
|
|
39
|
+
hiddenFolderCount++;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
// A note in no folder is in nothing the user has opened, so it stays
|
|
43
|
+
// hidden. That is consistent with off-by-default, and the tools say so
|
|
44
|
+
// rather than letting it look like the note is gone.
|
|
45
|
+
allows: (folderId) => (folderId === null ? false : isExposed(folderId)),
|
|
46
|
+
hiddenFolderCount,
|
|
47
|
+
allHidden: folders.length > 0 && hiddenFolderCount === folders.length,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The sentence appended to any result that was filtered. Never omitted when
|
|
52
|
+
* something was withheld: a quietly shortened list is indistinguishable from
|
|
53
|
+
* an empty vault, and the user would have no reason to suspect a setting.
|
|
54
|
+
*/
|
|
55
|
+
function filteredNotice(hidden, exposure) {
|
|
56
|
+
const parts = [];
|
|
57
|
+
if (hidden.inClosedFolders > 0) {
|
|
58
|
+
const n = hidden.inClosedFolders;
|
|
59
|
+
const notes = `${n} note${n === 1 ? '' : 's'}`;
|
|
60
|
+
parts.push(exposure.allHidden
|
|
61
|
+
? `${notes} hidden: no folder is exposed to MCP yet. Open a folder in the ` +
|
|
62
|
+
'Virlow app, edit it, and turn on "Expose to MCP" to let AI tools read it.'
|
|
63
|
+
: `${notes} hidden in folders that are not exposed to MCP. Turn on ` +
|
|
64
|
+
'"Expose to MCP" in a folder\'s settings in the Virlow app to include it.');
|
|
65
|
+
}
|
|
66
|
+
// Worth saying separately: the remedy is different. There is no switch on a
|
|
67
|
+
// note that lives in no folder, so "turn on the folder" is useless advice —
|
|
68
|
+
// it has to be filed into an exposed folder first.
|
|
69
|
+
if (hidden.unfiled > 0) {
|
|
70
|
+
const n = hidden.unfiled;
|
|
71
|
+
parts.push(`${n} note${n === 1 ? '' : 's'} hidden because ${n === 1 ? 'it is' : 'they are'} ` +
|
|
72
|
+
'not in any folder, and exposure is granted per folder. Move ' +
|
|
73
|
+
`${n === 1 ? 'it' : 'them'} into a folder that is exposed to MCP to include ` +
|
|
74
|
+
`${n === 1 ? 'it' : 'them'}.`);
|
|
75
|
+
}
|
|
76
|
+
return parts.length === 0 ? '' : `\n\n(${parts.join(' ')})`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { ApiError, VirlowApi } from './api.js';
|
|
2
|
+
export type { ApiFolder, ApiNote, AuthTokens, LoginResult, NoteWrite } from './api.js';
|
|
3
|
+
export { LockedError, Vault } from './vault.js';
|
|
4
|
+
export type { UnlockedSession } from './vault.js';
|
|
5
|
+
export { NotesService, SEARCH_SCAN_CAP } from './notes.js';
|
|
6
|
+
export { MemoryStore, SYNC_PAGE_LIMIT } from './memories.js';
|
|
7
|
+
export type { CachedMemory, SimilarMemory, SyncResult } from './memories.js';
|
|
8
|
+
export { mergeMeta, parseMemoryNote, serializeMemoryBody, toCodeEnvelope, } from './memory-note.js';
|
|
9
|
+
export type { MemoryMeta, ParsedMemoryNote } from './memory-note.js';
|
|
10
|
+
export { MEMORIES_ROOT_NAME, ensureNamespace, resolveMemoryTree, } from './memory-tree.js';
|
|
11
|
+
export type { MemoryTree } from './memory-tree.js';
|
|
12
|
+
export { computeExposure, filteredNotice } from './exposure.js';
|
|
13
|
+
export type { Exposure } from './exposure.js';
|
|
14
|
+
export { EmbeddingCache } from './embedding-cache.js';
|
|
15
|
+
export { MEMORIES_DISABLED_MESSAGE, memoriesAllowed } from './memories-enabled.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.memoriesAllowed = exports.MEMORIES_DISABLED_MESSAGE = exports.EmbeddingCache = exports.filteredNotice = exports.computeExposure = exports.resolveMemoryTree = exports.ensureNamespace = exports.MEMORIES_ROOT_NAME = exports.toCodeEnvelope = exports.serializeMemoryBody = exports.parseMemoryNote = exports.mergeMeta = exports.SYNC_PAGE_LIMIT = exports.MemoryStore = exports.SEARCH_SCAN_CAP = exports.NotesService = exports.Vault = exports.LockedError = exports.VirlowApi = exports.ApiError = void 0;
|
|
4
|
+
var api_js_1 = require("./api.js");
|
|
5
|
+
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return api_js_1.ApiError; } });
|
|
6
|
+
Object.defineProperty(exports, "VirlowApi", { enumerable: true, get: function () { return api_js_1.VirlowApi; } });
|
|
7
|
+
var vault_js_1 = require("./vault.js");
|
|
8
|
+
Object.defineProperty(exports, "LockedError", { enumerable: true, get: function () { return vault_js_1.LockedError; } });
|
|
9
|
+
Object.defineProperty(exports, "Vault", { enumerable: true, get: function () { return vault_js_1.Vault; } });
|
|
10
|
+
var notes_js_1 = require("./notes.js");
|
|
11
|
+
Object.defineProperty(exports, "NotesService", { enumerable: true, get: function () { return notes_js_1.NotesService; } });
|
|
12
|
+
Object.defineProperty(exports, "SEARCH_SCAN_CAP", { enumerable: true, get: function () { return notes_js_1.SEARCH_SCAN_CAP; } });
|
|
13
|
+
var memories_js_1 = require("./memories.js");
|
|
14
|
+
Object.defineProperty(exports, "MemoryStore", { enumerable: true, get: function () { return memories_js_1.MemoryStore; } });
|
|
15
|
+
Object.defineProperty(exports, "SYNC_PAGE_LIMIT", { enumerable: true, get: function () { return memories_js_1.SYNC_PAGE_LIMIT; } });
|
|
16
|
+
var memory_note_js_1 = require("./memory-note.js");
|
|
17
|
+
Object.defineProperty(exports, "mergeMeta", { enumerable: true, get: function () { return memory_note_js_1.mergeMeta; } });
|
|
18
|
+
Object.defineProperty(exports, "parseMemoryNote", { enumerable: true, get: function () { return memory_note_js_1.parseMemoryNote; } });
|
|
19
|
+
Object.defineProperty(exports, "serializeMemoryBody", { enumerable: true, get: function () { return memory_note_js_1.serializeMemoryBody; } });
|
|
20
|
+
Object.defineProperty(exports, "toCodeEnvelope", { enumerable: true, get: function () { return memory_note_js_1.toCodeEnvelope; } });
|
|
21
|
+
var memory_tree_js_1 = require("./memory-tree.js");
|
|
22
|
+
Object.defineProperty(exports, "MEMORIES_ROOT_NAME", { enumerable: true, get: function () { return memory_tree_js_1.MEMORIES_ROOT_NAME; } });
|
|
23
|
+
Object.defineProperty(exports, "ensureNamespace", { enumerable: true, get: function () { return memory_tree_js_1.ensureNamespace; } });
|
|
24
|
+
Object.defineProperty(exports, "resolveMemoryTree", { enumerable: true, get: function () { return memory_tree_js_1.resolveMemoryTree; } });
|
|
25
|
+
var exposure_js_1 = require("./exposure.js");
|
|
26
|
+
Object.defineProperty(exports, "computeExposure", { enumerable: true, get: function () { return exposure_js_1.computeExposure; } });
|
|
27
|
+
Object.defineProperty(exports, "filteredNotice", { enumerable: true, get: function () { return exposure_js_1.filteredNotice; } });
|
|
28
|
+
var embedding_cache_js_1 = require("./embedding-cache.js");
|
|
29
|
+
Object.defineProperty(exports, "EmbeddingCache", { enumerable: true, get: function () { return embedding_cache_js_1.EmbeddingCache; } });
|
|
30
|
+
var memories_enabled_js_1 = require("./memories-enabled.js");
|
|
31
|
+
Object.defineProperty(exports, "MEMORIES_DISABLED_MESSAGE", { enumerable: true, get: function () { return memories_enabled_js_1.MEMORIES_DISABLED_MESSAGE; } });
|
|
32
|
+
Object.defineProperty(exports, "memoriesAllowed", { enumerable: true, get: function () { return memories_enabled_js_1.memoriesAllowed; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VirlowApi } from './api.js';
|
|
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;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MEMORIES_DISABLED_MESSAGE = void 0;
|
|
4
|
+
exports.memoriesAllowed = memoriesAllowed;
|
|
5
|
+
/**
|
|
6
|
+
* Whether the user has switched AI memories on, read fresh from the account.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately not cached for the session. Turning the switch *on* being slow
|
|
9
|
+
* is an inconvenience; turning it *off* being slow is a privacy control that
|
|
10
|
+
* does not work. A user who switches memories off in the app expects the next
|
|
11
|
+
* tool call to respect it, not the next time they happen to restart their
|
|
12
|
+
* editor — and they would have no way to discover the difference.
|
|
13
|
+
*
|
|
14
|
+
* The cost is one GET alongside operations that already list folders, page
|
|
15
|
+
* through notes, decrypt, and embed.
|
|
16
|
+
*/
|
|
17
|
+
async function memoriesAllowed(api) {
|
|
18
|
+
try {
|
|
19
|
+
const me = await api.me();
|
|
20
|
+
// An API that predates the column omits the field. Treat an unknown answer
|
|
21
|
+
// as "not enabled": this decides whether an AI may record facts about the
|
|
22
|
+
// user, and guessing yes is the expensive direction to be wrong in.
|
|
23
|
+
return me.memoriesEnabled === true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Fail closed. A network blip briefly disabling memories is a far better
|
|
27
|
+
// failure than a revoked setting continuing to work.
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.MEMORIES_DISABLED_MESSAGE = 'AI memories are switched off for this account. Turn on "AI memories" in ' +
|
|
32
|
+
'Settings → Security in the Virlow app to use this tool.';
|