@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,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MEMORIES_ROOT_NAME = void 0;
|
|
4
|
+
exports.resolveMemoryTree = resolveMemoryTree;
|
|
5
|
+
exports.ensureNamespace = ensureNamespace;
|
|
6
|
+
const api_js_1 = require("./api.js");
|
|
7
|
+
/** Name given to the memories root when the server has to create it. The root
|
|
8
|
+
* is identified by `kind`, never by this name — the user is free to rename it. */
|
|
9
|
+
exports.MEMORIES_ROOT_NAME = 'Memories';
|
|
10
|
+
function buildTree(rootId, folders) {
|
|
11
|
+
const namespaces = new Map();
|
|
12
|
+
const byFolderId = new Map();
|
|
13
|
+
const attach = (folderId, name) => {
|
|
14
|
+
byFolderId.set(folderId, name);
|
|
15
|
+
// Two subfolders may share a name — that is the user's organising choice.
|
|
16
|
+
// Both count as that namespace; writes go to the first.
|
|
17
|
+
if (!namespaces.has(name))
|
|
18
|
+
namespaces.set(name, folderId);
|
|
19
|
+
};
|
|
20
|
+
// Only direct children of the root are namespaces. Anything deeper is an
|
|
21
|
+
// ordinary folder the user nested for their own reasons; its notes are not
|
|
22
|
+
// memories, so it never reaches byFolderId and the sync walk skips it.
|
|
23
|
+
for (const folder of folders) {
|
|
24
|
+
if (folder.parentId === rootId)
|
|
25
|
+
attach(folder.id, folder.name);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
rootId,
|
|
29
|
+
namespaces,
|
|
30
|
+
namespaceOf: (folderId) => byFolderId.get(folderId),
|
|
31
|
+
folderIds: () => [rootId, ...byFolderId.keys()],
|
|
32
|
+
attach,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the memories folder tree, creating the root if the user has none.
|
|
37
|
+
* The root is whichever folder carries `kind: 'memories'` — the API allows
|
|
38
|
+
* only one per user.
|
|
39
|
+
*/
|
|
40
|
+
async function resolveMemoryTree(api) {
|
|
41
|
+
const { folders } = await api.listFolders();
|
|
42
|
+
const existing = folders.find((f) => f.kind === 'memories');
|
|
43
|
+
if (existing)
|
|
44
|
+
return buildTree(existing.id, folders);
|
|
45
|
+
try {
|
|
46
|
+
const created = await api.createFolder(exports.MEMORIES_ROOT_NAME, null, 'memories');
|
|
47
|
+
return buildTree(created.id, [...folders, created]);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// 409 MEMORIES_FOLDER_EXISTS: another client created the root between our
|
|
51
|
+
// listing and our POST. Re-list and use the winner.
|
|
52
|
+
if (!(err instanceof api_js_1.ApiError) || err.status !== 409)
|
|
53
|
+
throw err;
|
|
54
|
+
const retry = await api.listFolders();
|
|
55
|
+
const winner = retry.folders.find((f) => f.kind === 'memories');
|
|
56
|
+
if (!winner)
|
|
57
|
+
throw err;
|
|
58
|
+
return buildTree(winner.id, retry.folders);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Folder id for `name`, creating the namespace subfolder if it does not exist
|
|
63
|
+
* yet. Mutates `tree` so a later call in the same session is a cache hit.
|
|
64
|
+
*/
|
|
65
|
+
async function ensureNamespace(api, tree, name) {
|
|
66
|
+
const existing = tree.namespaces.get(name);
|
|
67
|
+
if (existing)
|
|
68
|
+
return existing;
|
|
69
|
+
const created = await api.createFolder(name, tree.rootId);
|
|
70
|
+
tree.attach(created.id, name);
|
|
71
|
+
return created.id;
|
|
72
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type Exposure } from './exposure.js';
|
|
2
|
+
import type { VirlowApi } from './api.js';
|
|
3
|
+
import type { Vault } from './vault.js';
|
|
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
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotesService = exports.SEARCH_SCAN_CAP = void 0;
|
|
4
|
+
const virlow_crypto_1 = require("@batalabs/virlow-crypto");
|
|
5
|
+
const exposure_js_1 = require("./exposure.js");
|
|
6
|
+
/** Hard ceiling on how many notes searchNotes() will decrypt+scan in a
|
|
7
|
+
* single call. Keeps a single search bounded even for very large accounts;
|
|
8
|
+
* `truncated` tells the caller when the account holds more than this. */
|
|
9
|
+
exports.SEARCH_SCAN_CAP = 500;
|
|
10
|
+
const SEARCH_PAGE_LIMIT = 50;
|
|
11
|
+
/** Window radius (chars) either side of the match start when building a
|
|
12
|
+
* content snippet; total window is up to 2*SNIPPET_RADIUS. */
|
|
13
|
+
const SNIPPET_RADIUS = 80;
|
|
14
|
+
class NotesService {
|
|
15
|
+
api;
|
|
16
|
+
vault;
|
|
17
|
+
/** Per-unlock cache of decrypted (title, content) keyed by `id:updatedAt`
|
|
18
|
+
* so re-reading/re-scanning the same note twice in one unlock doesn't
|
|
19
|
+
* re-run AES-GCM decryption. Cleared whenever the vault locks. */
|
|
20
|
+
decryptCache = new Map();
|
|
21
|
+
/** Bumped every time the vault locks (see handleLock()). decryptRow()
|
|
22
|
+
* captures this before its await on decryptNoteFields() and re-checks it
|
|
23
|
+
* afterward, so a decrypt that was still in flight when the vault locked
|
|
24
|
+
* never writes decrypted plaintext into the cache after the lock. */
|
|
25
|
+
epoch = 0;
|
|
26
|
+
/** Serializes updateNote() calls so two overlapping partial edits to the
|
|
27
|
+
* same note (e.g. one changing title, one changing content) never race
|
|
28
|
+
* their read-merge-encrypt-write cycle — without this, both could read
|
|
29
|
+
* the same pre-edit row and the second write would silently clobber the
|
|
30
|
+
* first's change. Mirrors MemoryStore's enqueue()/mutationChain pattern. */
|
|
31
|
+
mutationChain = Promise.resolve();
|
|
32
|
+
constructor(api, vault) {
|
|
33
|
+
this.api = api;
|
|
34
|
+
this.vault = vault;
|
|
35
|
+
this.vault.onLock = () => this.handleLock();
|
|
36
|
+
}
|
|
37
|
+
handleLock() {
|
|
38
|
+
this.decryptCache.clear();
|
|
39
|
+
this.epoch++;
|
|
40
|
+
}
|
|
41
|
+
async listNotes(params) {
|
|
42
|
+
const key = this.requireSession();
|
|
43
|
+
const { notes, pagination } = await this.api.listNotes({ ...params, deleted: false });
|
|
44
|
+
// Hidden notes are segregated client-side by the web app (see ApiNote's
|
|
45
|
+
// `hidden` doc comment); the MCP server mirrors that here so they never
|
|
46
|
+
// surface in list/search results. readNote() still allows fetching one
|
|
47
|
+
// by exact id — that's a deliberate, explicit request.
|
|
48
|
+
const exposure = await this.exposure();
|
|
49
|
+
const notHidden = notes.filter((row) => row.hidden !== true);
|
|
50
|
+
// Folders the user has not opened to AI tools are filtered here rather
|
|
51
|
+
// than at the API, so the count of what was withheld can be reported.
|
|
52
|
+
const visible = notHidden.filter((row) => exposure.allows(row.folderId));
|
|
53
|
+
const withheld = {
|
|
54
|
+
inClosedFolders: notHidden.filter((row) => row.folderId !== null && !exposure.allows(row.folderId)).length,
|
|
55
|
+
unfiled: notHidden.filter((row) => row.folderId === null).length,
|
|
56
|
+
};
|
|
57
|
+
const mapped = await Promise.all(visible.map(async (row) => {
|
|
58
|
+
const { title } = await this.decryptRow(key, row);
|
|
59
|
+
return {
|
|
60
|
+
id: row.id,
|
|
61
|
+
title,
|
|
62
|
+
folderId: row.folderId,
|
|
63
|
+
starred: row.starred === true,
|
|
64
|
+
updatedAt: row.updatedAt,
|
|
65
|
+
};
|
|
66
|
+
}));
|
|
67
|
+
return { notes: mapped, pagination, withheld, exposure };
|
|
68
|
+
}
|
|
69
|
+
async readNote(id) {
|
|
70
|
+
const key = this.requireSession();
|
|
71
|
+
const row = await this.api.getNote(id);
|
|
72
|
+
// Reading by exact id is still a read: the switch says the assistant may
|
|
73
|
+
// not read this folder, and knowing the id does not change that.
|
|
74
|
+
const exposure = await this.exposure();
|
|
75
|
+
if (!exposure.allows(row.folderId)) {
|
|
76
|
+
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.`);
|
|
77
|
+
}
|
|
78
|
+
const { title, content } = await this.decryptRow(key, row);
|
|
79
|
+
return { id: row.id, title, content, folderId: row.folderId };
|
|
80
|
+
}
|
|
81
|
+
async createNote(title, content, folderId, type) {
|
|
82
|
+
const key = this.requireSession();
|
|
83
|
+
const salt = (0, virlow_crypto_1.bytesToBase64)(crypto.getRandomValues(new Uint8Array(32)));
|
|
84
|
+
const { encryptedTitle, encryptedContent, iv } = await (0, virlow_crypto_1.encryptNoteFields)(key, title, content);
|
|
85
|
+
const body = {
|
|
86
|
+
title: '[ENCRYPTED]',
|
|
87
|
+
content: '[ENCRYPTED]',
|
|
88
|
+
encryptedTitle,
|
|
89
|
+
encryptedContent,
|
|
90
|
+
iv,
|
|
91
|
+
salt,
|
|
92
|
+
};
|
|
93
|
+
if (folderId !== undefined)
|
|
94
|
+
body.folderId = folderId;
|
|
95
|
+
if (type !== undefined)
|
|
96
|
+
body.type = type;
|
|
97
|
+
const row = await this.api.createNote(body);
|
|
98
|
+
return { id: row.id };
|
|
99
|
+
}
|
|
100
|
+
updateNote(id, changes) {
|
|
101
|
+
return this.enqueue(async () => {
|
|
102
|
+
const key = this.requireSession();
|
|
103
|
+
const existingRow = await this.api.getNote(id);
|
|
104
|
+
const current = await this.decryptRow(key, existingRow);
|
|
105
|
+
const title = changes.title ?? current.title;
|
|
106
|
+
const content = changes.content ?? current.content;
|
|
107
|
+
const salt = (0, virlow_crypto_1.bytesToBase64)(crypto.getRandomValues(new Uint8Array(32)));
|
|
108
|
+
const { encryptedTitle, encryptedContent, iv } = await (0, virlow_crypto_1.encryptNoteFields)(key, title, content);
|
|
109
|
+
await this.api.updateNote(id, {
|
|
110
|
+
title: '[ENCRYPTED]',
|
|
111
|
+
content: '[ENCRYPTED]',
|
|
112
|
+
encryptedTitle,
|
|
113
|
+
encryptedContent,
|
|
114
|
+
iv,
|
|
115
|
+
salt,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async searchNotes(query, limit = 20) {
|
|
120
|
+
const key = this.requireSession();
|
|
121
|
+
const q = query.toLowerCase();
|
|
122
|
+
const results = [];
|
|
123
|
+
let scanned = 0;
|
|
124
|
+
let total = 0;
|
|
125
|
+
let page = 1;
|
|
126
|
+
scan: for (;;) {
|
|
127
|
+
const { notes, pagination } = await this.api.listNotes({ page, limit: SEARCH_PAGE_LIMIT, deleted: false });
|
|
128
|
+
total = pagination.total;
|
|
129
|
+
for (const row of notes) {
|
|
130
|
+
if (row.hidden === true)
|
|
131
|
+
continue;
|
|
132
|
+
if (scanned >= exports.SEARCH_SCAN_CAP)
|
|
133
|
+
break scan;
|
|
134
|
+
scanned++;
|
|
135
|
+
const { title, content } = await this.decryptRow(key, row);
|
|
136
|
+
const contentIdx = content.toLowerCase().indexOf(q);
|
|
137
|
+
const titleMatch = title.toLowerCase().includes(q);
|
|
138
|
+
if (titleMatch || contentIdx !== -1) {
|
|
139
|
+
results.push({ id: row.id, title, snippet: this.buildSnippet(title, content, contentIdx) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (scanned >= exports.SEARCH_SCAN_CAP)
|
|
143
|
+
break;
|
|
144
|
+
if (notes.length === 0 || page >= pagination.pages)
|
|
145
|
+
break;
|
|
146
|
+
page++;
|
|
147
|
+
}
|
|
148
|
+
return { results: results.slice(0, limit), scanned, truncated: total > exports.SEARCH_SCAN_CAP };
|
|
149
|
+
}
|
|
150
|
+
async moveNote(id, folderId) {
|
|
151
|
+
this.requireSession();
|
|
152
|
+
await this.api.updateNote(id, { folderId });
|
|
153
|
+
}
|
|
154
|
+
async setStar(id, starred) {
|
|
155
|
+
this.requireSession();
|
|
156
|
+
await this.api.updateNote(id, { starred });
|
|
157
|
+
}
|
|
158
|
+
async archiveNote(id, archived) {
|
|
159
|
+
this.requireSession();
|
|
160
|
+
await this.api.updateNote(id, { archived });
|
|
161
|
+
}
|
|
162
|
+
async trashNote(id) {
|
|
163
|
+
this.requireSession();
|
|
164
|
+
await this.api.updateNote(id, { deleted: true });
|
|
165
|
+
}
|
|
166
|
+
async listFolders() {
|
|
167
|
+
this.requireSession();
|
|
168
|
+
const { folders } = await this.api.listFolders();
|
|
169
|
+
const exposure = (0, exposure_js_1.computeExposure)(folders);
|
|
170
|
+
const visible = folders.filter((f) => exposure.allows(f.id));
|
|
171
|
+
return {
|
|
172
|
+
// Names are withheld along with contents: a folder the user has not
|
|
173
|
+
// opened should not be inventoried either, only counted.
|
|
174
|
+
folders: visible.map((f) => ({ id: f.id, name: f.name, parentId: f.parentId })),
|
|
175
|
+
withheld: folders.length - visible.length,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/** Folder exposure for this request. Not cached across calls: the user can
|
|
179
|
+
* flip the switch in the app at any moment and the next tool call must
|
|
180
|
+
* respect it. */
|
|
181
|
+
async exposure() {
|
|
182
|
+
const { folders } = await this.api.listFolders();
|
|
183
|
+
return (0, exposure_js_1.computeExposure)(folders);
|
|
184
|
+
}
|
|
185
|
+
async createFolder(name, parentId) {
|
|
186
|
+
this.requireSession();
|
|
187
|
+
const folder = await this.api.createFolder(name, parentId);
|
|
188
|
+
return { id: folder.id };
|
|
189
|
+
}
|
|
190
|
+
/** Decrypt (with per-unlock caching) or pass through a legacy plaintext
|
|
191
|
+
* row. Legacy rows (no encryptedTitle) carry their real content directly
|
|
192
|
+
* in the plaintext title/content columns. */
|
|
193
|
+
async decryptRow(key, row) {
|
|
194
|
+
if (!row.encryptedTitle) {
|
|
195
|
+
return { title: row.title, content: row.content };
|
|
196
|
+
}
|
|
197
|
+
const cacheKey = `${row.id}:${row.updatedAt}`;
|
|
198
|
+
const cached = this.decryptCache.get(cacheKey);
|
|
199
|
+
if (cached)
|
|
200
|
+
return cached;
|
|
201
|
+
const startEpoch = this.epoch;
|
|
202
|
+
const fields = await (0, virlow_crypto_1.decryptNoteFields)(key, row.encryptedTitle, row.encryptedContent ?? '', row.iv ?? '');
|
|
203
|
+
// If the vault locked while this decrypt was in flight, the epoch moved
|
|
204
|
+
// on and decryptCache was already cleared — don't write plaintext back
|
|
205
|
+
// into it after the fact.
|
|
206
|
+
if (this.epoch === startEpoch) {
|
|
207
|
+
this.decryptCache.set(cacheKey, fields);
|
|
208
|
+
}
|
|
209
|
+
return fields;
|
|
210
|
+
}
|
|
211
|
+
buildSnippet(title, content, contentMatchIndex) {
|
|
212
|
+
if (contentMatchIndex === -1) {
|
|
213
|
+
return title;
|
|
214
|
+
}
|
|
215
|
+
const windowSize = SNIPPET_RADIUS * 2;
|
|
216
|
+
let start = Math.max(0, contentMatchIndex - SNIPPET_RADIUS);
|
|
217
|
+
const end = Math.min(content.length, start + windowSize);
|
|
218
|
+
if (end - start < windowSize) {
|
|
219
|
+
start = Math.max(0, end - windowSize);
|
|
220
|
+
}
|
|
221
|
+
return content.slice(start, end);
|
|
222
|
+
}
|
|
223
|
+
requireSession() {
|
|
224
|
+
const key = this.vault.session.key;
|
|
225
|
+
this.vault.touch();
|
|
226
|
+
return key;
|
|
227
|
+
}
|
|
228
|
+
/** Chains `fn` onto the mutation queue so overlapping calls run one at a
|
|
229
|
+
* time; the `.catch(() => {})` guard keeps the internal chain alive after
|
|
230
|
+
* a rejection while the promise returned to the caller still carries it. */
|
|
231
|
+
enqueue(fn) {
|
|
232
|
+
const next = this.mutationChain.then(fn, fn);
|
|
233
|
+
this.mutationChain = next.catch(() => { });
|
|
234
|
+
return next;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
exports.NotesService = NotesService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare class LockedError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export interface UnlockedSession {
|
|
4
|
+
key: CryptoKey;
|
|
5
|
+
userId: string;
|
|
6
|
+
email: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class Vault {
|
|
9
|
+
private readonly autoLockMs;
|
|
10
|
+
private _session;
|
|
11
|
+
private _timerHandle;
|
|
12
|
+
/** Listeners registered via `onLock =` or `addLockListener()`. Multiple
|
|
13
|
+
* consumers (MemoryStore, NotesService, ...) each need their cache cleared
|
|
14
|
+
* on lock; a single-slot callback would let the second registration
|
|
15
|
+
* silently clobber the first. */
|
|
16
|
+
private lockListeners;
|
|
17
|
+
constructor(autoLockMs?: number);
|
|
18
|
+
/** Alias for `addLockListener` kept for call-site brevity (`vault.onLock = fn`).
|
|
19
|
+
* Assigning multiple times ADDS a listener each time — it does not replace
|
|
20
|
+
* a previous one. */
|
|
21
|
+
set onLock(fn: () => void);
|
|
22
|
+
addLockListener(fn: () => void): void;
|
|
23
|
+
setUnlocked(session: UnlockedSession): void;
|
|
24
|
+
lock(): void;
|
|
25
|
+
isUnlocked(): boolean;
|
|
26
|
+
status(): {
|
|
27
|
+
locked: boolean;
|
|
28
|
+
email?: string;
|
|
29
|
+
};
|
|
30
|
+
touch(): void;
|
|
31
|
+
get session(): UnlockedSession;
|
|
32
|
+
private _scheduleAutoLock;
|
|
33
|
+
private _clearTimer;
|
|
34
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Vault = exports.LockedError = void 0;
|
|
4
|
+
class LockedError extends Error {
|
|
5
|
+
}
|
|
6
|
+
exports.LockedError = LockedError;
|
|
7
|
+
class Vault {
|
|
8
|
+
autoLockMs;
|
|
9
|
+
_session = null;
|
|
10
|
+
_timerHandle = null;
|
|
11
|
+
/** Listeners registered via `onLock =` or `addLockListener()`. Multiple
|
|
12
|
+
* consumers (MemoryStore, NotesService, ...) each need their cache cleared
|
|
13
|
+
* on lock; a single-slot callback would let the second registration
|
|
14
|
+
* silently clobber the first. */
|
|
15
|
+
lockListeners = [];
|
|
16
|
+
constructor(autoLockMs = 4 * 60 * 60 * 1000) {
|
|
17
|
+
this.autoLockMs = autoLockMs;
|
|
18
|
+
}
|
|
19
|
+
/** Alias for `addLockListener` kept for call-site brevity (`vault.onLock = fn`).
|
|
20
|
+
* Assigning multiple times ADDS a listener each time — it does not replace
|
|
21
|
+
* a previous one. */
|
|
22
|
+
set onLock(fn) {
|
|
23
|
+
this.addLockListener(fn);
|
|
24
|
+
}
|
|
25
|
+
addLockListener(fn) {
|
|
26
|
+
this.lockListeners.push(fn);
|
|
27
|
+
}
|
|
28
|
+
setUnlocked(session) {
|
|
29
|
+
this._session = session;
|
|
30
|
+
this._scheduleAutoLock();
|
|
31
|
+
}
|
|
32
|
+
lock() {
|
|
33
|
+
if (this._session === null) {
|
|
34
|
+
return; // Already locked, idempotent
|
|
35
|
+
}
|
|
36
|
+
this._clearTimer();
|
|
37
|
+
this._session = null;
|
|
38
|
+
for (const listener of this.lockListeners) {
|
|
39
|
+
listener();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
isUnlocked() {
|
|
43
|
+
return this._session !== null;
|
|
44
|
+
}
|
|
45
|
+
status() {
|
|
46
|
+
if (this._session === null) {
|
|
47
|
+
return { locked: true };
|
|
48
|
+
}
|
|
49
|
+
return { locked: false, email: this._session.email };
|
|
50
|
+
}
|
|
51
|
+
touch() {
|
|
52
|
+
if (this._session === null) {
|
|
53
|
+
return; // Already locked, ignore
|
|
54
|
+
}
|
|
55
|
+
this._clearTimer();
|
|
56
|
+
this._scheduleAutoLock();
|
|
57
|
+
}
|
|
58
|
+
get session() {
|
|
59
|
+
if (this._session === null) {
|
|
60
|
+
throw new LockedError('Vault is locked. Ask the user to run the unlock tool to sign in and unlock their Virlow vault.');
|
|
61
|
+
}
|
|
62
|
+
return this._session;
|
|
63
|
+
}
|
|
64
|
+
_scheduleAutoLock() {
|
|
65
|
+
this._timerHandle = setTimeout(() => {
|
|
66
|
+
this.lock();
|
|
67
|
+
}, this.autoLockMs);
|
|
68
|
+
this._timerHandle.unref?.();
|
|
69
|
+
}
|
|
70
|
+
_clearTimer() {
|
|
71
|
+
if (this._timerHandle !== null) {
|
|
72
|
+
clearTimeout(this._timerHandle);
|
|
73
|
+
this._timerHandle = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.Vault = Vault;
|
package/dist/exposure.d.ts
CHANGED
package/dist/exposure.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exposure.d.ts","sourceRoot":"","sources":["../src/exposure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"exposure.d.ts","sourceRoot":"","sources":["../src/exposure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;;;;;;;;;;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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +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';
|
|
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';
|
|
16
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC/C,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EACL,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAChE,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +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';
|
|
1
|
+
export { ApiError, VirlowApi } from './api.js';
|
|
2
|
+
export { LockedError, Vault } from './vault.js';
|
|
3
|
+
export { NotesService, SEARCH_SCAN_CAP } from './notes.js';
|
|
4
|
+
export { MemoryStore, SYNC_PAGE_LIMIT } from './memories.js';
|
|
5
|
+
export { mergeMeta, parseMemoryNote, serializeMemoryBody, toCodeEnvelope, } from './memory-note.js';
|
|
6
|
+
export { MEMORIES_ROOT_NAME, ensureNamespace, resolveMemoryTree, } from './memory-tree.js';
|
|
7
|
+
export { computeExposure, filteredNotice } from './exposure.js';
|
|
8
|
+
export { EmbeddingCache } from './embedding-cache.js';
|
|
9
|
+
export { MEMORIES_DISABLED_MESSAGE, memoriesAllowed } from './memories-enabled.js';
|
|
10
10
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE7D,OAAO,EACL,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memories-enabled.d.ts","sourceRoot":"","sources":["../src/memories-enabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"memories-enabled.d.ts","sourceRoot":"","sources":["../src/memories-enabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAYtE;AAED,eAAO,MAAM,yBAAyB,QAEqB,CAAC"}
|
package/dist/memories.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type Embedder } from '@batalabs/virlow-memory';
|
|
2
|
-
import { type VirlowApi } from './api';
|
|
3
|
-
import type { EmbeddingCache } from './embedding-cache';
|
|
4
|
-
import { type MemoryMeta } from './memory-note';
|
|
5
|
-
import type { Vault } from './vault';
|
|
2
|
+
import { type VirlowApi } from './api.js';
|
|
3
|
+
import type { EmbeddingCache } from './embedding-cache.js';
|
|
4
|
+
import { type MemoryMeta } from './memory-note.js';
|
|
5
|
+
import type { Vault } from './vault.js';
|
|
6
6
|
export declare const SYNC_PAGE_LIMIT = 100;
|
|
7
7
|
export interface CachedMemory {
|
|
8
8
|
id: string;
|
package/dist/memories.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memories.d.ts","sourceRoot":"","sources":["../src/memories.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAA0C,KAAK,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"memories.d.ts","sourceRoot":"","sources":["../src/memories.ts"],"names":[],"mappings":"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"}
|