mindcache 3.4.2 → 3.4.3
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/{CloudAdapter-D2xxVv4E.d.mts → CloudAdapter-CJS3Sh4f.d.mts} +4 -1
- package/dist/{CloudAdapter-D2xxVv4E.d.ts → CloudAdapter-CJS3Sh4f.d.ts} +4 -1
- package/dist/cloud/index.d.mts +2 -2
- package/dist/cloud/index.d.ts +2 -2
- package/dist/cloud/index.js +42 -7
- package/dist/cloud/index.js.map +1 -1
- package/dist/cloud/index.mjs +42 -7
- package/dist/cloud/index.mjs.map +1 -1
- package/dist/index.d.mts +4 -31
- package/dist/index.d.ts +4 -31
- package/dist/index.js +42 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -7
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +33 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.js +2280 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +2250 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +6 -1
package/dist/server.js
ADDED
|
@@ -0,0 +1,2280 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var syncProtocol = require('y-protocols/sync');
|
|
4
|
+
var encoding = require('lib0/encoding');
|
|
5
|
+
var decoding = require('lib0/decoding');
|
|
6
|
+
var Y = require('yjs');
|
|
7
|
+
var yIndexeddb = require('y-indexeddb');
|
|
8
|
+
var diff = require('fast-diff');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var syncProtocol__namespace = /*#__PURE__*/_interopNamespace(syncProtocol);
|
|
31
|
+
var encoding__namespace = /*#__PURE__*/_interopNamespace(encoding);
|
|
32
|
+
var decoding__namespace = /*#__PURE__*/_interopNamespace(decoding);
|
|
33
|
+
var Y__namespace = /*#__PURE__*/_interopNamespace(Y);
|
|
34
|
+
var diff__default = /*#__PURE__*/_interopDefault(diff);
|
|
35
|
+
|
|
36
|
+
var __defProp = Object.defineProperty;
|
|
37
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
38
|
+
var __esm = (fn, res) => function __init() {
|
|
39
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
40
|
+
};
|
|
41
|
+
var __export = (target, all) => {
|
|
42
|
+
for (var name in all)
|
|
43
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/local/IndexedDBAdapter.ts
|
|
47
|
+
var IndexedDBAdapter_exports = {};
|
|
48
|
+
__export(IndexedDBAdapter_exports, {
|
|
49
|
+
IndexedDBAdapter: () => exports.IndexedDBAdapter
|
|
50
|
+
});
|
|
51
|
+
exports.IndexedDBAdapter = void 0;
|
|
52
|
+
var init_IndexedDBAdapter = __esm({
|
|
53
|
+
"src/local/IndexedDBAdapter.ts"() {
|
|
54
|
+
exports.IndexedDBAdapter = class {
|
|
55
|
+
constructor(config = {}) {
|
|
56
|
+
this.config = config;
|
|
57
|
+
this.dbName = config.dbName || "mindcache_db";
|
|
58
|
+
this.storeName = config.storeName || "mindcache_store";
|
|
59
|
+
this.key = config.key || "mindcache_data";
|
|
60
|
+
}
|
|
61
|
+
mindcache = null;
|
|
62
|
+
unsubscribe = null;
|
|
63
|
+
saveTimeout = null;
|
|
64
|
+
db = null;
|
|
65
|
+
dbName;
|
|
66
|
+
storeName;
|
|
67
|
+
key;
|
|
68
|
+
async attach(mc) {
|
|
69
|
+
if (this.mindcache) {
|
|
70
|
+
this.detach();
|
|
71
|
+
}
|
|
72
|
+
this.mindcache = mc;
|
|
73
|
+
await this.initDB();
|
|
74
|
+
await this.load();
|
|
75
|
+
const listener = () => {
|
|
76
|
+
if (this.mindcache) {
|
|
77
|
+
this.scheduleSave();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
mc.subscribeToAll(listener);
|
|
81
|
+
this.unsubscribe = () => mc.unsubscribeFromAll(listener);
|
|
82
|
+
console.log("\u{1F5C4}\uFE0F IndexedDBAdapter: Attached to MindCache instance");
|
|
83
|
+
}
|
|
84
|
+
detach() {
|
|
85
|
+
if (this.unsubscribe) {
|
|
86
|
+
this.unsubscribe();
|
|
87
|
+
this.unsubscribe = null;
|
|
88
|
+
}
|
|
89
|
+
if (this.saveTimeout) {
|
|
90
|
+
clearTimeout(this.saveTimeout);
|
|
91
|
+
this.saveTimeout = null;
|
|
92
|
+
}
|
|
93
|
+
this.mindcache = null;
|
|
94
|
+
if (this.db) {
|
|
95
|
+
this.db.close();
|
|
96
|
+
this.db = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
initDB() {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const request = indexedDB.open(this.dbName);
|
|
102
|
+
request.onerror = () => {
|
|
103
|
+
console.error("MindCache: IndexedDB error:", request.error);
|
|
104
|
+
reject(request.error);
|
|
105
|
+
};
|
|
106
|
+
request.onsuccess = () => {
|
|
107
|
+
const db = request.result;
|
|
108
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
109
|
+
const currentVersion = db.version;
|
|
110
|
+
db.close();
|
|
111
|
+
const upgradeRequest = indexedDB.open(this.dbName, currentVersion + 1);
|
|
112
|
+
upgradeRequest.onerror = () => {
|
|
113
|
+
console.error("MindCache: IndexedDB upgrade error:", upgradeRequest.error);
|
|
114
|
+
reject(upgradeRequest.error);
|
|
115
|
+
};
|
|
116
|
+
upgradeRequest.onupgradeneeded = () => {
|
|
117
|
+
const upgradeDb = upgradeRequest.result;
|
|
118
|
+
if (!upgradeDb.objectStoreNames.contains(this.storeName)) {
|
|
119
|
+
upgradeDb.createObjectStore(this.storeName);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
upgradeRequest.onsuccess = () => {
|
|
123
|
+
this.db = upgradeRequest.result;
|
|
124
|
+
resolve();
|
|
125
|
+
};
|
|
126
|
+
} else {
|
|
127
|
+
this.db = db;
|
|
128
|
+
resolve();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
request.onupgradeneeded = () => {
|
|
132
|
+
const db = request.result;
|
|
133
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
134
|
+
db.createObjectStore(this.storeName);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
load() {
|
|
140
|
+
if (!this.db || !this.mindcache) {
|
|
141
|
+
return Promise.resolve();
|
|
142
|
+
}
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
try {
|
|
145
|
+
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
146
|
+
const store = transaction.objectStore(this.storeName);
|
|
147
|
+
const request = store.get(this.key);
|
|
148
|
+
request.onsuccess = () => {
|
|
149
|
+
if (request.result) {
|
|
150
|
+
this.mindcache.deserialize(request.result);
|
|
151
|
+
console.log("\u{1F5C4}\uFE0F IndexedDBAdapter: Loaded data from IndexedDB");
|
|
152
|
+
}
|
|
153
|
+
resolve();
|
|
154
|
+
};
|
|
155
|
+
request.onerror = () => {
|
|
156
|
+
console.error("MindCache: Failed to load from IndexedDB:", request.error);
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error("MindCache: Error accessing IndexedDB for load:", error);
|
|
161
|
+
resolve();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
scheduleSave() {
|
|
166
|
+
if (this.saveTimeout) {
|
|
167
|
+
clearTimeout(this.saveTimeout);
|
|
168
|
+
}
|
|
169
|
+
this.saveTimeout = setTimeout(() => {
|
|
170
|
+
this.save();
|
|
171
|
+
this.saveTimeout = null;
|
|
172
|
+
}, this.config.debounceMs ?? 1e3);
|
|
173
|
+
}
|
|
174
|
+
save() {
|
|
175
|
+
if (!this.db || !this.mindcache) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const data = this.mindcache.serialize();
|
|
180
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
181
|
+
const store = transaction.objectStore(this.storeName);
|
|
182
|
+
const request = store.put(data, this.key);
|
|
183
|
+
request.onsuccess = () => {
|
|
184
|
+
console.log("\u{1F5C4}\uFE0F IndexedDBAdapter: Saved to IndexedDB");
|
|
185
|
+
};
|
|
186
|
+
request.onerror = () => {
|
|
187
|
+
console.error("MindCache: Failed to save to IndexedDB:", request.error);
|
|
188
|
+
};
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error("MindCache: Error accessing IndexedDB for save:", error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// src/cloud/CloudAdapter.ts
|
|
198
|
+
var CloudAdapter_exports = {};
|
|
199
|
+
__export(CloudAdapter_exports, {
|
|
200
|
+
CloudAdapter: () => exports.CloudAdapter
|
|
201
|
+
});
|
|
202
|
+
var RECONNECT_DELAY, MAX_RECONNECT_DELAY; exports.CloudAdapter = void 0;
|
|
203
|
+
var init_CloudAdapter = __esm({
|
|
204
|
+
"src/cloud/CloudAdapter.ts"() {
|
|
205
|
+
RECONNECT_DELAY = 1e3;
|
|
206
|
+
MAX_RECONNECT_DELAY = 3e4;
|
|
207
|
+
exports.CloudAdapter = class {
|
|
208
|
+
// Track if initial sync is complete
|
|
209
|
+
constructor(config) {
|
|
210
|
+
this.config = config;
|
|
211
|
+
if (!config.baseUrl) {
|
|
212
|
+
throw new Error("MindCache Cloud: baseUrl is required. Please provide the cloud API URL in your configuration.");
|
|
213
|
+
}
|
|
214
|
+
this.setupNetworkDetection();
|
|
215
|
+
}
|
|
216
|
+
ws = null;
|
|
217
|
+
mindcache = null;
|
|
218
|
+
unsubscribe = null;
|
|
219
|
+
reconnectAttempts = 0;
|
|
220
|
+
reconnectTimeout = null;
|
|
221
|
+
_state = "disconnected";
|
|
222
|
+
_isOnline = true;
|
|
223
|
+
// Browser network status
|
|
224
|
+
listeners = {};
|
|
225
|
+
token = null;
|
|
226
|
+
handleOnline = null;
|
|
227
|
+
handleOffline = null;
|
|
228
|
+
_synced = false;
|
|
229
|
+
/** Browser network status - instantly updated via navigator.onLine */
|
|
230
|
+
get isOnline() {
|
|
231
|
+
return this._isOnline;
|
|
232
|
+
}
|
|
233
|
+
setupNetworkDetection() {
|
|
234
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this._isOnline = navigator.onLine;
|
|
238
|
+
this.handleOnline = () => {
|
|
239
|
+
console.log("\u2601\uFE0F CloudAdapter: Network is back online");
|
|
240
|
+
this._isOnline = true;
|
|
241
|
+
this.emit("network_online");
|
|
242
|
+
if (this._state === "disconnected" || this._state === "error") {
|
|
243
|
+
this.connect();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
this.handleOffline = () => {
|
|
247
|
+
console.log("\u2601\uFE0F CloudAdapter: Network went offline");
|
|
248
|
+
this._isOnline = false;
|
|
249
|
+
this.emit("network_offline");
|
|
250
|
+
if (this._state === "connected" || this._state === "connecting") {
|
|
251
|
+
this._state = "disconnected";
|
|
252
|
+
this.emit("disconnected");
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
window.addEventListener("online", this.handleOnline);
|
|
256
|
+
window.addEventListener("offline", this.handleOffline);
|
|
257
|
+
}
|
|
258
|
+
cleanupNetworkDetection() {
|
|
259
|
+
if (typeof window === "undefined") {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (this.handleOnline) {
|
|
263
|
+
window.removeEventListener("online", this.handleOnline);
|
|
264
|
+
}
|
|
265
|
+
if (this.handleOffline) {
|
|
266
|
+
window.removeEventListener("offline", this.handleOffline);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
setToken(token) {
|
|
270
|
+
this.token = token;
|
|
271
|
+
}
|
|
272
|
+
setTokenProvider(provider) {
|
|
273
|
+
this.config.tokenProvider = provider;
|
|
274
|
+
}
|
|
275
|
+
get state() {
|
|
276
|
+
return this._state;
|
|
277
|
+
}
|
|
278
|
+
attach(mc) {
|
|
279
|
+
if (this.mindcache) {
|
|
280
|
+
this.detach();
|
|
281
|
+
}
|
|
282
|
+
this.mindcache = mc;
|
|
283
|
+
mc.doc.on("update", (update, origin) => {
|
|
284
|
+
if (origin !== this && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
285
|
+
const encoder = encoding__namespace.createEncoder();
|
|
286
|
+
syncProtocol__namespace.writeUpdate(encoder, update);
|
|
287
|
+
this.sendBinary(encoding__namespace.toUint8Array(encoder));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
console.log("\u2601\uFE0F CloudAdapter: Attached to MindCache instance");
|
|
291
|
+
}
|
|
292
|
+
detach() {
|
|
293
|
+
if (this.unsubscribe) {
|
|
294
|
+
this.unsubscribe();
|
|
295
|
+
this.unsubscribe = null;
|
|
296
|
+
}
|
|
297
|
+
this.mindcache = null;
|
|
298
|
+
}
|
|
299
|
+
async fetchTokenWithApiKey() {
|
|
300
|
+
if (!this.config.apiKey) {
|
|
301
|
+
throw new Error("API key is required to fetch token");
|
|
302
|
+
}
|
|
303
|
+
const httpBaseUrl = this.config.baseUrl.replace("wss://", "https://").replace("ws://", "http://");
|
|
304
|
+
const isDelegate = this.config.apiKey.startsWith("del_") && this.config.apiKey.includes(":");
|
|
305
|
+
const authHeader = isDelegate ? `ApiKey ${this.config.apiKey}` : `Bearer ${this.config.apiKey}`;
|
|
306
|
+
const response = await fetch(`${httpBaseUrl}/api/ws-token`, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: {
|
|
309
|
+
"Content-Type": "application/json",
|
|
310
|
+
"Authorization": authHeader
|
|
311
|
+
},
|
|
312
|
+
body: JSON.stringify({
|
|
313
|
+
instanceId: this.config.instanceId,
|
|
314
|
+
permission: "write"
|
|
315
|
+
})
|
|
316
|
+
});
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
const error = await response.json().catch(() => ({ error: "Failed to get token" }));
|
|
319
|
+
throw new Error(error.error || `Failed to get WebSocket token: ${response.status}`);
|
|
320
|
+
}
|
|
321
|
+
const data = await response.json();
|
|
322
|
+
return data.token;
|
|
323
|
+
}
|
|
324
|
+
async connect() {
|
|
325
|
+
if (this._state === "connecting" || this._state === "connected") {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this._state = "connecting";
|
|
329
|
+
try {
|
|
330
|
+
if (!this.token) {
|
|
331
|
+
if (this.config.tokenProvider) {
|
|
332
|
+
this.token = await this.config.tokenProvider();
|
|
333
|
+
} else if (this.config.apiKey) {
|
|
334
|
+
this.token = await this.fetchTokenWithApiKey();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
let url = `${this.config.baseUrl}/sync/${this.config.instanceId}`;
|
|
338
|
+
if (this.token) {
|
|
339
|
+
url += `?token=${encodeURIComponent(this.token)}`;
|
|
340
|
+
this.token = null;
|
|
341
|
+
} else {
|
|
342
|
+
throw new Error("MindCache Cloud: No authentication method available. Provide apiKey or tokenProvider.");
|
|
343
|
+
}
|
|
344
|
+
this.ws = new WebSocket(url);
|
|
345
|
+
this.ws.binaryType = "arraybuffer";
|
|
346
|
+
this.setupWebSocket();
|
|
347
|
+
} catch (error) {
|
|
348
|
+
this._state = "error";
|
|
349
|
+
this.emit("error", error);
|
|
350
|
+
this.scheduleReconnect();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
disconnect() {
|
|
354
|
+
if (this.reconnectTimeout) {
|
|
355
|
+
clearTimeout(this.reconnectTimeout);
|
|
356
|
+
this.reconnectTimeout = null;
|
|
357
|
+
}
|
|
358
|
+
if (this.ws) {
|
|
359
|
+
this.ws.close();
|
|
360
|
+
this.ws = null;
|
|
361
|
+
}
|
|
362
|
+
this.cleanupNetworkDetection();
|
|
363
|
+
this._state = "disconnected";
|
|
364
|
+
this.emit("disconnected");
|
|
365
|
+
}
|
|
366
|
+
on(event, listener) {
|
|
367
|
+
if (!this.listeners[event]) {
|
|
368
|
+
this.listeners[event] = [];
|
|
369
|
+
}
|
|
370
|
+
this.listeners[event].push(listener);
|
|
371
|
+
}
|
|
372
|
+
off(event, listener) {
|
|
373
|
+
if (this.listeners[event]) {
|
|
374
|
+
this.listeners[event] = this.listeners[event].filter((l) => l !== listener);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
emit(event, ...args) {
|
|
378
|
+
if (this.listeners[event]) {
|
|
379
|
+
this.listeners[event].forEach((listener) => listener(...args));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
setupWebSocket() {
|
|
383
|
+
if (!this.ws) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
this.ws.onopen = () => {
|
|
387
|
+
if (this.mindcache) {
|
|
388
|
+
const encoder = encoding__namespace.createEncoder();
|
|
389
|
+
syncProtocol__namespace.writeSyncStep1(encoder, this.mindcache.doc);
|
|
390
|
+
this.sendBinary(encoding__namespace.toUint8Array(encoder));
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
this.ws.onmessage = (event) => {
|
|
394
|
+
try {
|
|
395
|
+
if (typeof event.data === "string") {
|
|
396
|
+
const msg = JSON.parse(event.data);
|
|
397
|
+
if (msg.type === "auth_success") {
|
|
398
|
+
this._state = "connected";
|
|
399
|
+
this.reconnectAttempts = 0;
|
|
400
|
+
this.emit("connected");
|
|
401
|
+
} else if (msg.type === "auth_error" || msg.type === "error") {
|
|
402
|
+
this._state = "error";
|
|
403
|
+
this.emit("error", new Error(msg.error));
|
|
404
|
+
} else {
|
|
405
|
+
console.debug("MindCache Cloud: Received message type:", msg.type, msg);
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
const encoder = encoding__namespace.createEncoder();
|
|
409
|
+
const decoder = decoding__namespace.createDecoder(new Uint8Array(event.data));
|
|
410
|
+
if (this.mindcache) {
|
|
411
|
+
const messageType = syncProtocol__namespace.readSyncMessage(decoder, encoder, this.mindcache.doc, this);
|
|
412
|
+
if (encoding__namespace.length(encoder) > 0) {
|
|
413
|
+
this.sendBinary(encoding__namespace.toUint8Array(encoder));
|
|
414
|
+
}
|
|
415
|
+
if (!this._synced && (messageType === 1 || messageType === 2)) {
|
|
416
|
+
this._synced = true;
|
|
417
|
+
this.emit("synced");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error("MindCache Cloud: Failed to handle message:", error);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
this.ws.onclose = () => {
|
|
426
|
+
this._state = "disconnected";
|
|
427
|
+
this.emit("disconnected");
|
|
428
|
+
this.scheduleReconnect();
|
|
429
|
+
};
|
|
430
|
+
this.ws.onerror = () => {
|
|
431
|
+
this._state = "error";
|
|
432
|
+
this.emit("error", new Error("WebSocket connection failed"));
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
sendBinary(data) {
|
|
436
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
437
|
+
this.ws.send(data);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
scheduleReconnect() {
|
|
441
|
+
if (this.reconnectTimeout) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const delay = Math.min(
|
|
445
|
+
RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts),
|
|
446
|
+
MAX_RECONNECT_DELAY
|
|
447
|
+
);
|
|
448
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
449
|
+
this.reconnectTimeout = null;
|
|
450
|
+
this.reconnectAttempts++;
|
|
451
|
+
this.connect();
|
|
452
|
+
}, delay);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// src/core/types.ts
|
|
459
|
+
var DEFAULT_KEY_ATTRIBUTES = {
|
|
460
|
+
type: "text",
|
|
461
|
+
contentTags: [],
|
|
462
|
+
systemTags: [],
|
|
463
|
+
// Keys are private by default - explicitly add SystemPrompt/LLMRead/LLMWrite to enable LLM access
|
|
464
|
+
zIndex: 0
|
|
465
|
+
};
|
|
466
|
+
var SystemTagHelpers = {
|
|
467
|
+
/** Check if key is writable by LLM */
|
|
468
|
+
isLLMWritable: (attrs) => attrs.systemTags.includes("LLMWrite"),
|
|
469
|
+
/** Check if key is readable by LLM (in context or via tools) */
|
|
470
|
+
isLLMReadable: (attrs) => attrs.systemTags.includes("SystemPrompt") || attrs.systemTags.includes("LLMRead"),
|
|
471
|
+
/** Check if key is included in system prompt */
|
|
472
|
+
isInSystemPrompt: (attrs) => attrs.systemTags.includes("SystemPrompt"),
|
|
473
|
+
/** Check if key is protected from deletion */
|
|
474
|
+
isProtected: (attrs) => attrs.systemTags.includes("protected"),
|
|
475
|
+
/** Check if key uses template injection */
|
|
476
|
+
hasTemplateInjection: (attrs) => attrs.systemTags.includes("ApplyTemplate")
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// src/core/MindCache.ts
|
|
480
|
+
var MindCache = class {
|
|
481
|
+
// Public doc for adapter access
|
|
482
|
+
doc;
|
|
483
|
+
rootMap;
|
|
484
|
+
// Key -> EntryMap({value, attributes})
|
|
485
|
+
// Cache listeners
|
|
486
|
+
listeners = {};
|
|
487
|
+
globalListeners = [];
|
|
488
|
+
// Metadata
|
|
489
|
+
version = "3.3.2";
|
|
490
|
+
// Internal flag to prevent sync loops when receiving remote updates
|
|
491
|
+
// (Less critical with Yjs but kept for API compat)
|
|
492
|
+
normalizeSystemTags(tags) {
|
|
493
|
+
const normalized = [];
|
|
494
|
+
const seen = /* @__PURE__ */ new Set();
|
|
495
|
+
for (const tag of tags) {
|
|
496
|
+
if (["SystemPrompt", "LLMRead", "LLMWrite", "protected", "ApplyTemplate"].includes(tag)) {
|
|
497
|
+
if (!seen.has(tag)) {
|
|
498
|
+
seen.add(tag);
|
|
499
|
+
normalized.push(tag);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return normalized;
|
|
504
|
+
}
|
|
505
|
+
// Cloud sync state
|
|
506
|
+
_cloudAdapter = null;
|
|
507
|
+
_connectionState = "disconnected";
|
|
508
|
+
_isLoaded = true;
|
|
509
|
+
// Default true for local mode
|
|
510
|
+
_cloudConfig = null;
|
|
511
|
+
// Access level for system operations
|
|
512
|
+
_accessLevel = "user";
|
|
513
|
+
_initPromise = null;
|
|
514
|
+
// Y-IndexedDB provider
|
|
515
|
+
_idbProvider = null;
|
|
516
|
+
// Undo Managers Cache
|
|
517
|
+
_undoManagers = /* @__PURE__ */ new Map();
|
|
518
|
+
// Global Undo Manager (watches entire rootMap)
|
|
519
|
+
_globalUndoManager = null;
|
|
520
|
+
// History tracking
|
|
521
|
+
_history = [];
|
|
522
|
+
_historyOptions = { maxEntries: 100, snapshotInterval: 10 };
|
|
523
|
+
_historyEnabled = false;
|
|
524
|
+
constructor(options) {
|
|
525
|
+
this.doc = options?.doc || new Y__namespace.Doc();
|
|
526
|
+
this.rootMap = this.doc.getMap("mindcache");
|
|
527
|
+
this.rootMap.observeDeep((events) => {
|
|
528
|
+
const keysAffected = /* @__PURE__ */ new Set();
|
|
529
|
+
events.forEach((event) => {
|
|
530
|
+
if (event.target === this.rootMap) {
|
|
531
|
+
const mapEvent = event;
|
|
532
|
+
mapEvent.keysChanged.forEach((key) => keysAffected.add(key));
|
|
533
|
+
} else if (event.target.parent === this.rootMap) {
|
|
534
|
+
for (const [key, val] of this.rootMap) {
|
|
535
|
+
if (val === event.target) {
|
|
536
|
+
keysAffected.add(key);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
let current = event.target;
|
|
542
|
+
while (current && current.parent) {
|
|
543
|
+
if (current.parent.parent === this.rootMap) {
|
|
544
|
+
for (const [key, val] of this.rootMap) {
|
|
545
|
+
if (val === current.parent) {
|
|
546
|
+
keysAffected.add(key);
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
current = current.parent;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
keysAffected.forEach((key) => {
|
|
557
|
+
const entryMap = this.rootMap.get(key);
|
|
558
|
+
if (entryMap) {
|
|
559
|
+
const value = entryMap.get("value");
|
|
560
|
+
const attrs = entryMap.get("attributes");
|
|
561
|
+
const resolvedValue = attrs?.type === "document" && value instanceof Y__namespace.Text ? value.toString() : value;
|
|
562
|
+
if (this.listeners[key]) {
|
|
563
|
+
this.listeners[key].forEach((l) => l(resolvedValue));
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
if (this.listeners[key]) {
|
|
567
|
+
this.listeners[key].forEach((l) => l(void 0));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
this.notifyGlobalListeners();
|
|
572
|
+
});
|
|
573
|
+
this.initGlobalUndoManager();
|
|
574
|
+
if (options?.accessLevel) {
|
|
575
|
+
this._accessLevel = options.accessLevel;
|
|
576
|
+
}
|
|
577
|
+
const initPromises = [];
|
|
578
|
+
if (options?.cloud) {
|
|
579
|
+
this._cloudConfig = options.cloud;
|
|
580
|
+
this._isLoaded = false;
|
|
581
|
+
this._connectionState = "disconnected";
|
|
582
|
+
initPromises.push(this._initCloud());
|
|
583
|
+
if (typeof window !== "undefined") {
|
|
584
|
+
const dbName = `mindcache_cloud_${options.cloud.instanceId}`;
|
|
585
|
+
initPromises.push(this._initYIndexedDB(dbName));
|
|
586
|
+
}
|
|
587
|
+
this.enableHistory(options.history);
|
|
588
|
+
}
|
|
589
|
+
if (options?.indexedDB && !options?.cloud) {
|
|
590
|
+
this._isLoaded = false;
|
|
591
|
+
initPromises.push(this._initYIndexedDB(options.indexedDB.dbName || "mindcache_yjs_db"));
|
|
592
|
+
this.enableHistory(options.history);
|
|
593
|
+
}
|
|
594
|
+
if (initPromises.length > 0) {
|
|
595
|
+
this._initPromise = Promise.all(initPromises).then(() => {
|
|
596
|
+
if (!this._cloudConfig) {
|
|
597
|
+
this._isLoaded = true;
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Helper: Get or Create UndoManager for a key
|
|
603
|
+
getUndoManager(key) {
|
|
604
|
+
const entryMap = this.rootMap.get(key);
|
|
605
|
+
if (!entryMap) {
|
|
606
|
+
return void 0;
|
|
607
|
+
}
|
|
608
|
+
if (!this._undoManagers.has(key)) {
|
|
609
|
+
const um = new Y__namespace.UndoManager(entryMap, {
|
|
610
|
+
captureTimeout: 500
|
|
611
|
+
});
|
|
612
|
+
this._undoManagers.set(key, um);
|
|
613
|
+
}
|
|
614
|
+
return this._undoManagers.get(key);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Undo changes for a specific key
|
|
618
|
+
*/
|
|
619
|
+
undo(key) {
|
|
620
|
+
const um = this.getUndoManager(key);
|
|
621
|
+
if (um) {
|
|
622
|
+
um.undo();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Redo changes for a specific key
|
|
627
|
+
*/
|
|
628
|
+
redo(key) {
|
|
629
|
+
const um = this.getUndoManager(key);
|
|
630
|
+
if (um) {
|
|
631
|
+
um.redo();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
getHistory(key) {
|
|
635
|
+
const um = this.getUndoManager(key);
|
|
636
|
+
if (!um) {
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
return um.undoStack;
|
|
640
|
+
}
|
|
641
|
+
// Initialize global undo manager (watches entire rootMap)
|
|
642
|
+
initGlobalUndoManager() {
|
|
643
|
+
if (!this._globalUndoManager) {
|
|
644
|
+
this._globalUndoManager = new Y__namespace.UndoManager(this.rootMap, {
|
|
645
|
+
captureTimeout: 500
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Undo all recent local changes (across all keys)
|
|
651
|
+
* Only undoes YOUR changes, not changes from other users in cloud mode
|
|
652
|
+
*/
|
|
653
|
+
undoAll() {
|
|
654
|
+
this.initGlobalUndoManager();
|
|
655
|
+
this._globalUndoManager?.undo();
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Redo previously undone local changes
|
|
659
|
+
*/
|
|
660
|
+
redoAll() {
|
|
661
|
+
this.initGlobalUndoManager();
|
|
662
|
+
this._globalUndoManager?.redo();
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Check if there are changes to undo globally
|
|
666
|
+
*/
|
|
667
|
+
canUndoAll() {
|
|
668
|
+
this.initGlobalUndoManager();
|
|
669
|
+
return (this._globalUndoManager?.undoStack.length ?? 0) > 0;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Check if there are changes to redo globally
|
|
673
|
+
*/
|
|
674
|
+
canRedoAll() {
|
|
675
|
+
this.initGlobalUndoManager();
|
|
676
|
+
return (this._globalUndoManager?.redoStack.length ?? 0) > 0;
|
|
677
|
+
}
|
|
678
|
+
// Enable history tracking (called for IndexedDB and Cloud modes)
|
|
679
|
+
enableHistory(options) {
|
|
680
|
+
if (this._historyEnabled) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this._historyEnabled = true;
|
|
684
|
+
if (options) {
|
|
685
|
+
this._historyOptions = { ...this._historyOptions, ...options };
|
|
686
|
+
}
|
|
687
|
+
this.rootMap.observeDeep((events) => {
|
|
688
|
+
const keysAffected = /* @__PURE__ */ new Set();
|
|
689
|
+
events.forEach((event) => {
|
|
690
|
+
if (event.target === this.rootMap) {
|
|
691
|
+
const mapEvent = event;
|
|
692
|
+
mapEvent.keysChanged.forEach((key) => keysAffected.add(key));
|
|
693
|
+
} else {
|
|
694
|
+
let current = event.target;
|
|
695
|
+
while (current && current.parent) {
|
|
696
|
+
if (current.parent === this.rootMap) {
|
|
697
|
+
for (const [key, val] of this.rootMap) {
|
|
698
|
+
if (val === current) {
|
|
699
|
+
keysAffected.add(key);
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
current = current.parent;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
if (keysAffected.size > 0) {
|
|
710
|
+
const entry = {
|
|
711
|
+
id: this.generateId(),
|
|
712
|
+
timestamp: Date.now(),
|
|
713
|
+
keysAffected: Array.from(keysAffected)
|
|
714
|
+
};
|
|
715
|
+
this._history.push(entry);
|
|
716
|
+
const max = this._historyOptions.maxEntries || 100;
|
|
717
|
+
if (this._history.length > max) {
|
|
718
|
+
this._history = this._history.slice(-max);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
generateId() {
|
|
724
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Get global history of all changes (available in IndexedDB and Cloud modes)
|
|
728
|
+
*/
|
|
729
|
+
getGlobalHistory() {
|
|
730
|
+
return [...this._history];
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Check if history tracking is enabled
|
|
734
|
+
*/
|
|
735
|
+
get historyEnabled() {
|
|
736
|
+
return this._historyEnabled;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Restore to a specific version (time travel)
|
|
740
|
+
* Note: Full implementation requires storing update binaries, which is not yet implemented.
|
|
741
|
+
* @returns false - not yet fully implemented
|
|
742
|
+
*/
|
|
743
|
+
restoreToVersion(_versionId) {
|
|
744
|
+
console.warn("restoreToVersion: Full implementation requires storing update binaries. Not yet implemented.");
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
get accessLevel() {
|
|
748
|
+
return this._accessLevel;
|
|
749
|
+
}
|
|
750
|
+
get hasSystemAccess() {
|
|
751
|
+
return this._accessLevel === "system";
|
|
752
|
+
}
|
|
753
|
+
async _initCloud() {
|
|
754
|
+
if (!this._cloudConfig) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const CloudAdapter2 = await this._getCloudAdapterClass();
|
|
758
|
+
if (!this._cloudConfig.baseUrl) ;
|
|
759
|
+
const baseUrl = (this._cloudConfig.baseUrl || "https://api.mindcache.dev").replace("https://", "wss://").replace("http://", "ws://");
|
|
760
|
+
const adapter = new CloudAdapter2({
|
|
761
|
+
instanceId: this._cloudConfig.instanceId,
|
|
762
|
+
projectId: this._cloudConfig.projectId || "default",
|
|
763
|
+
baseUrl,
|
|
764
|
+
apiKey: this._cloudConfig.apiKey
|
|
765
|
+
});
|
|
766
|
+
if (this._cloudConfig.tokenProvider) {
|
|
767
|
+
adapter.setTokenProvider(this._cloudConfig.tokenProvider);
|
|
768
|
+
} else if (this._cloudConfig.tokenEndpoint) {
|
|
769
|
+
const tokenEndpoint = this._cloudConfig.tokenEndpoint;
|
|
770
|
+
const instanceId = this._cloudConfig.instanceId;
|
|
771
|
+
let resolvedBaseUrl;
|
|
772
|
+
if (tokenEndpoint.startsWith("http://") || tokenEndpoint.startsWith("https://")) {
|
|
773
|
+
resolvedBaseUrl = tokenEndpoint;
|
|
774
|
+
} else if (typeof window !== "undefined" && window.location?.origin) {
|
|
775
|
+
resolvedBaseUrl = `${window.location.origin}${tokenEndpoint.startsWith("/") ? "" : "/"}${tokenEndpoint}`;
|
|
776
|
+
} else {
|
|
777
|
+
resolvedBaseUrl = tokenEndpoint;
|
|
778
|
+
}
|
|
779
|
+
adapter.setTokenProvider(async () => {
|
|
780
|
+
const url = resolvedBaseUrl.includes("?") ? `${resolvedBaseUrl}&instanceId=${instanceId}` : `${resolvedBaseUrl}?instanceId=${instanceId}`;
|
|
781
|
+
const response = await fetch(url);
|
|
782
|
+
if (!response.ok) {
|
|
783
|
+
throw new Error("Failed to get token");
|
|
784
|
+
}
|
|
785
|
+
const data = await response.json();
|
|
786
|
+
return data.token;
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
adapter.on("connected", () => {
|
|
790
|
+
this._connectionState = "connected";
|
|
791
|
+
this.notifyGlobalListeners();
|
|
792
|
+
});
|
|
793
|
+
adapter.on("disconnected", () => {
|
|
794
|
+
this._connectionState = "disconnected";
|
|
795
|
+
this.notifyGlobalListeners();
|
|
796
|
+
});
|
|
797
|
+
adapter.on("error", () => {
|
|
798
|
+
this._connectionState = "error";
|
|
799
|
+
this.notifyGlobalListeners();
|
|
800
|
+
});
|
|
801
|
+
adapter.on("synced", () => {
|
|
802
|
+
this._isLoaded = true;
|
|
803
|
+
this.notifyGlobalListeners();
|
|
804
|
+
});
|
|
805
|
+
adapter.attach(this);
|
|
806
|
+
this._cloudAdapter = adapter;
|
|
807
|
+
this._connectionState = "connecting";
|
|
808
|
+
adapter.connect();
|
|
809
|
+
}
|
|
810
|
+
async _initYIndexedDB(dbName) {
|
|
811
|
+
if (typeof window === "undefined") {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
this._idbProvider = new yIndexeddb.IndexeddbPersistence(dbName, this.doc);
|
|
815
|
+
return new Promise((resolve) => {
|
|
816
|
+
if (!this._idbProvider) {
|
|
817
|
+
return resolve();
|
|
818
|
+
}
|
|
819
|
+
this._idbProvider.on("synced", () => {
|
|
820
|
+
this._isLoaded = true;
|
|
821
|
+
resolve();
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
// Legacy IndexedDB method stub
|
|
826
|
+
async _initIndexedDB(_config) {
|
|
827
|
+
}
|
|
828
|
+
async _getIndexedDBAdapterClass() {
|
|
829
|
+
const { IndexedDBAdapter: IndexedDBAdapter2 } = await Promise.resolve().then(() => (init_IndexedDBAdapter(), IndexedDBAdapter_exports));
|
|
830
|
+
return IndexedDBAdapter2;
|
|
831
|
+
}
|
|
832
|
+
get connectionState() {
|
|
833
|
+
return this._connectionState;
|
|
834
|
+
}
|
|
835
|
+
get isLoaded() {
|
|
836
|
+
return this._isLoaded;
|
|
837
|
+
}
|
|
838
|
+
async _getCloudAdapterClass() {
|
|
839
|
+
const { CloudAdapter: CloudAdapter2 } = await Promise.resolve().then(() => (init_CloudAdapter(), CloudAdapter_exports));
|
|
840
|
+
return CloudAdapter2;
|
|
841
|
+
}
|
|
842
|
+
get isCloud() {
|
|
843
|
+
return this._cloudConfig !== null;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Browser network status. Returns true if online or in local-only mode.
|
|
847
|
+
* In cloud mode, this updates instantly when network status changes.
|
|
848
|
+
*/
|
|
849
|
+
get isOnline() {
|
|
850
|
+
if (!this._cloudAdapter) {
|
|
851
|
+
if (typeof navigator !== "undefined") {
|
|
852
|
+
return navigator.onLine;
|
|
853
|
+
}
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
return this._cloudAdapter.isOnline;
|
|
857
|
+
}
|
|
858
|
+
async waitForSync() {
|
|
859
|
+
if (this._isLoaded) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (this._initPromise) {
|
|
863
|
+
await this._initPromise;
|
|
864
|
+
}
|
|
865
|
+
if (this._isLoaded) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
return new Promise((resolve) => {
|
|
869
|
+
if (this._isLoaded) {
|
|
870
|
+
return resolve();
|
|
871
|
+
}
|
|
872
|
+
const interval = setInterval(() => {
|
|
873
|
+
if (this._isLoaded) {
|
|
874
|
+
clearInterval(interval);
|
|
875
|
+
resolve();
|
|
876
|
+
}
|
|
877
|
+
}, 100);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
disconnect() {
|
|
881
|
+
if (this._cloudAdapter) {
|
|
882
|
+
this._cloudAdapter.disconnect();
|
|
883
|
+
this._cloudAdapter.detach();
|
|
884
|
+
this._cloudAdapter = null;
|
|
885
|
+
this._connectionState = "disconnected";
|
|
886
|
+
}
|
|
887
|
+
if (this._idbProvider) {
|
|
888
|
+
this._idbProvider.destroy();
|
|
889
|
+
this._idbProvider = null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// Serialize state
|
|
893
|
+
serialize() {
|
|
894
|
+
const json = {};
|
|
895
|
+
for (const [key, val] of this.rootMap) {
|
|
896
|
+
const entryMap = val;
|
|
897
|
+
const attrs = entryMap.get("attributes");
|
|
898
|
+
let value = entryMap.get("value");
|
|
899
|
+
if (attrs?.type === "document" && value instanceof Y__namespace.Text) {
|
|
900
|
+
value = value.toString();
|
|
901
|
+
}
|
|
902
|
+
json[key] = {
|
|
903
|
+
value,
|
|
904
|
+
attributes: attrs
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
return json;
|
|
908
|
+
}
|
|
909
|
+
// Deserialize state (for IndexedDBAdapter compatibility)
|
|
910
|
+
deserialize(data) {
|
|
911
|
+
if (!data || typeof data !== "object") {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
this.doc.transact(() => {
|
|
915
|
+
for (const key of this.rootMap.keys()) {
|
|
916
|
+
this.rootMap.delete(key);
|
|
917
|
+
}
|
|
918
|
+
for (const [key, entry] of Object.entries(data)) {
|
|
919
|
+
if (key.startsWith("$")) {
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const entryMap = new Y__namespace.Map();
|
|
923
|
+
this.rootMap.set(key, entryMap);
|
|
924
|
+
entryMap.set("value", entry.value);
|
|
925
|
+
const attrs = entry.attributes || {};
|
|
926
|
+
const normalizedAttrs = {
|
|
927
|
+
type: attrs.type || "text",
|
|
928
|
+
contentType: attrs.contentType,
|
|
929
|
+
contentTags: attrs.contentTags || [],
|
|
930
|
+
systemTags: this.normalizeSystemTags(attrs.systemTags || []),
|
|
931
|
+
zIndex: attrs.zIndex ?? 0
|
|
932
|
+
};
|
|
933
|
+
entryMap.set("attributes", normalizedAttrs);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
encodeFileToBase64(file) {
|
|
938
|
+
return new Promise((resolve, reject) => {
|
|
939
|
+
if (typeof FileReader !== "undefined") {
|
|
940
|
+
const reader = new FileReader();
|
|
941
|
+
reader.onload = () => {
|
|
942
|
+
const result = reader.result;
|
|
943
|
+
const base64Data = result.split(",")[1];
|
|
944
|
+
resolve(base64Data);
|
|
945
|
+
};
|
|
946
|
+
reader.onerror = reject;
|
|
947
|
+
reader.readAsDataURL(file);
|
|
948
|
+
} else {
|
|
949
|
+
reject(new Error("FileReader not available in Node.js environment. Use set_base64() method instead."));
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
createDataUrl(base64Data, contentType) {
|
|
954
|
+
return `data:${contentType};base64,${base64Data}`;
|
|
955
|
+
}
|
|
956
|
+
validateContentType(type, contentType) {
|
|
957
|
+
if (type === "text" || type === "json") {
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
if (!contentType) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
if (type === "image") {
|
|
964
|
+
return contentType.startsWith("image/");
|
|
965
|
+
}
|
|
966
|
+
if (type === "file") {
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
// InjectSTM replacement (private helper)
|
|
972
|
+
_injectSTMInternal(template, _processingStack) {
|
|
973
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
|
974
|
+
const val = this.get_value(key.trim(), _processingStack);
|
|
975
|
+
return val !== void 0 ? String(val) : `{{${key}}}`;
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Replace {{key}} placeholders in a template string with values from MindCache.
|
|
980
|
+
* @param template The template string with {{key}} placeholders
|
|
981
|
+
* @returns The template with placeholders replaced by values
|
|
982
|
+
*/
|
|
983
|
+
injectSTM(template) {
|
|
984
|
+
return this._injectSTMInternal(template, /* @__PURE__ */ new Set());
|
|
985
|
+
}
|
|
986
|
+
// Public API Methods
|
|
987
|
+
getAll() {
|
|
988
|
+
const result = {};
|
|
989
|
+
for (const [key] of this.rootMap) {
|
|
990
|
+
result[key] = this.get_value(key);
|
|
991
|
+
}
|
|
992
|
+
result["$date"] = this.get_value("$date");
|
|
993
|
+
result["$time"] = this.get_value("$time");
|
|
994
|
+
return result;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Get all entries with their full structure (value + attributes).
|
|
998
|
+
* Use this for UI/admin interfaces that need to display key properties.
|
|
999
|
+
* Unlike serialize(), this format is stable and won't change.
|
|
1000
|
+
*/
|
|
1001
|
+
getAllEntries() {
|
|
1002
|
+
const result = {};
|
|
1003
|
+
for (const [key] of this.rootMap) {
|
|
1004
|
+
const value = this.get_value(key);
|
|
1005
|
+
const attributes = this.get_attributes(key);
|
|
1006
|
+
if (attributes) {
|
|
1007
|
+
result[key] = { value, attributes };
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return result;
|
|
1011
|
+
}
|
|
1012
|
+
get_value(key, _processingStack) {
|
|
1013
|
+
if (key === "$date") {
|
|
1014
|
+
const today = /* @__PURE__ */ new Date();
|
|
1015
|
+
return today.toISOString().split("T")[0];
|
|
1016
|
+
}
|
|
1017
|
+
if (key === "$time") {
|
|
1018
|
+
const now = /* @__PURE__ */ new Date();
|
|
1019
|
+
return now.toTimeString().split(" ")[0];
|
|
1020
|
+
}
|
|
1021
|
+
if (key === "$version") {
|
|
1022
|
+
return this.version;
|
|
1023
|
+
}
|
|
1024
|
+
const entryMap = this.rootMap.get(key);
|
|
1025
|
+
if (!entryMap) {
|
|
1026
|
+
return void 0;
|
|
1027
|
+
}
|
|
1028
|
+
const attributes = entryMap.get("attributes");
|
|
1029
|
+
const value = entryMap.get("value");
|
|
1030
|
+
if (attributes?.type === "document" && value instanceof Y__namespace.Text) {
|
|
1031
|
+
return value.toString();
|
|
1032
|
+
}
|
|
1033
|
+
if (_processingStack && _processingStack.has(key)) {
|
|
1034
|
+
return `{{${key}}}`;
|
|
1035
|
+
}
|
|
1036
|
+
if (attributes?.systemTags?.includes("ApplyTemplate")) {
|
|
1037
|
+
if (typeof value === "string") {
|
|
1038
|
+
const stack = _processingStack || /* @__PURE__ */ new Set();
|
|
1039
|
+
stack.add(key);
|
|
1040
|
+
return this._injectSTMInternal(value, stack);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return value;
|
|
1044
|
+
}
|
|
1045
|
+
get_attributes(key) {
|
|
1046
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1047
|
+
return {
|
|
1048
|
+
type: "text",
|
|
1049
|
+
contentTags: [],
|
|
1050
|
+
systemTags: ["SystemPrompt", "protected"],
|
|
1051
|
+
zIndex: 999999
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const entryMap = this.rootMap.get(key);
|
|
1055
|
+
return entryMap ? entryMap.get("attributes") : void 0;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Update only the attributes of a key without modifying the value.
|
|
1059
|
+
* Useful for updating tags, permissions etc. on document type keys.
|
|
1060
|
+
*/
|
|
1061
|
+
set_attributes(key, attributes) {
|
|
1062
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const entryMap = this.rootMap.get(key);
|
|
1066
|
+
if (!entryMap) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
this.doc.transact(() => {
|
|
1070
|
+
const existingAttrs = entryMap.get("attributes");
|
|
1071
|
+
const mergedAttrs = { ...existingAttrs, ...attributes };
|
|
1072
|
+
if (mergedAttrs.systemTags) {
|
|
1073
|
+
mergedAttrs.systemTags = this.normalizeSystemTags(mergedAttrs.systemTags);
|
|
1074
|
+
}
|
|
1075
|
+
entryMap.set("attributes", mergedAttrs);
|
|
1076
|
+
const currentValue = entryMap.get("value");
|
|
1077
|
+
if (mergedAttrs.type === "document" && !(currentValue instanceof Y__namespace.Text)) {
|
|
1078
|
+
const strValue = typeof currentValue === "string" ? currentValue : String(currentValue ?? "");
|
|
1079
|
+
entryMap.set("value", new Y__namespace.Text(strValue));
|
|
1080
|
+
this.getUndoManager(key);
|
|
1081
|
+
} else if (mergedAttrs.type !== "document" && currentValue instanceof Y__namespace.Text) {
|
|
1082
|
+
entryMap.set("value", currentValue.toString());
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
set_value(key, value, attributes) {
|
|
1087
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const existingEntry = this.rootMap.get(key);
|
|
1091
|
+
if (existingEntry) {
|
|
1092
|
+
const existingAttrs = existingEntry.get("attributes");
|
|
1093
|
+
if (existingAttrs?.type === "document") {
|
|
1094
|
+
if (!attributes?.type || attributes.type === "document") {
|
|
1095
|
+
if (typeof value === "string") {
|
|
1096
|
+
this._replaceDocumentText(key, value);
|
|
1097
|
+
}
|
|
1098
|
+
if (attributes) {
|
|
1099
|
+
this.set_attributes(key, attributes);
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (!existingEntry && attributes?.type === "document") {
|
|
1106
|
+
this.set_document(key, typeof value === "string" ? value : "", attributes);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
let entryMap = this.rootMap.get(key);
|
|
1110
|
+
const isNewEntry = !entryMap;
|
|
1111
|
+
if (isNewEntry) {
|
|
1112
|
+
entryMap = new Y__namespace.Map();
|
|
1113
|
+
this.rootMap.set(key, entryMap);
|
|
1114
|
+
}
|
|
1115
|
+
this.getUndoManager(key);
|
|
1116
|
+
this.doc.transact(() => {
|
|
1117
|
+
const oldAttributes = isNewEntry ? {
|
|
1118
|
+
...DEFAULT_KEY_ATTRIBUTES
|
|
1119
|
+
} : entryMap.get("attributes");
|
|
1120
|
+
const finalAttributes = attributes ? { ...oldAttributes, ...attributes } : oldAttributes;
|
|
1121
|
+
let normalizedAttributes = { ...finalAttributes };
|
|
1122
|
+
if (finalAttributes.systemTags) {
|
|
1123
|
+
normalizedAttributes.systemTags = this.normalizeSystemTags(finalAttributes.systemTags);
|
|
1124
|
+
}
|
|
1125
|
+
let valueToSet = value;
|
|
1126
|
+
if (normalizedAttributes.type === "document" && !(valueToSet instanceof Y__namespace.Text)) {
|
|
1127
|
+
valueToSet = new Y__namespace.Text(typeof value === "string" ? value : String(value ?? ""));
|
|
1128
|
+
} else if (normalizedAttributes.type !== "document" && valueToSet instanceof Y__namespace.Text) {
|
|
1129
|
+
valueToSet = valueToSet.toString();
|
|
1130
|
+
}
|
|
1131
|
+
entryMap.set("value", valueToSet);
|
|
1132
|
+
entryMap.set("attributes", normalizedAttributes);
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* LLM-safe method to write a value to a key.
|
|
1137
|
+
* This method:
|
|
1138
|
+
* - Only updates the value, never modifies attributes/systemTags
|
|
1139
|
+
* - Checks LLMWrite permission before writing
|
|
1140
|
+
* - Returns false if key doesn't exist or lacks LLMWrite permission
|
|
1141
|
+
*
|
|
1142
|
+
* Used by create_vercel_ai_tools() to prevent LLMs from escalating privileges.
|
|
1143
|
+
*/
|
|
1144
|
+
llm_set_key(key, value) {
|
|
1145
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
const entryMap = this.rootMap.get(key);
|
|
1149
|
+
if (!entryMap) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
const attributes = entryMap.get("attributes");
|
|
1153
|
+
if (!attributes?.systemTags?.includes("LLMWrite")) {
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
if (attributes.type === "document") {
|
|
1157
|
+
if (typeof value === "string") {
|
|
1158
|
+
this._replaceDocumentText(key, value);
|
|
1159
|
+
}
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
this.doc.transact(() => {
|
|
1163
|
+
entryMap.set("value", value);
|
|
1164
|
+
});
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
delete_key(key) {
|
|
1168
|
+
if (key === "$date" || key === "$time") {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
this.rootMap.delete(key);
|
|
1172
|
+
}
|
|
1173
|
+
clear() {
|
|
1174
|
+
const keys = Array.from(this.rootMap.keys());
|
|
1175
|
+
this.doc.transact(() => {
|
|
1176
|
+
keys.forEach((k) => this.rootMap.delete(k));
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
// ============================================
|
|
1180
|
+
// Restored Methods (from v2.x)
|
|
1181
|
+
// ============================================
|
|
1182
|
+
/**
|
|
1183
|
+
* Check if a key exists in MindCache.
|
|
1184
|
+
*/
|
|
1185
|
+
has(key) {
|
|
1186
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
return this.rootMap.has(key);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Delete a key from MindCache.
|
|
1193
|
+
* @returns true if the key existed and was deleted
|
|
1194
|
+
*/
|
|
1195
|
+
delete(key) {
|
|
1196
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
if (!this.rootMap.has(key)) {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
this.rootMap.delete(key);
|
|
1203
|
+
this.notifyGlobalListeners();
|
|
1204
|
+
if (this.listeners[key]) {
|
|
1205
|
+
this.listeners[key].forEach((listener) => listener(void 0));
|
|
1206
|
+
}
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
/** @deprecated Use get_value instead */
|
|
1210
|
+
get(key) {
|
|
1211
|
+
return this.get_value(key);
|
|
1212
|
+
}
|
|
1213
|
+
/** @deprecated Use set_value instead */
|
|
1214
|
+
set(key, value) {
|
|
1215
|
+
this.set_value(key, value);
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Update multiple values at once from an object.
|
|
1219
|
+
* @deprecated Use set_value for individual keys
|
|
1220
|
+
*/
|
|
1221
|
+
update(data) {
|
|
1222
|
+
this.doc.transact(() => {
|
|
1223
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1224
|
+
if (key !== "$date" && key !== "$time" && key !== "$version") {
|
|
1225
|
+
this.set_value(key, value);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
this.notifyGlobalListeners();
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Get the number of keys in MindCache.
|
|
1233
|
+
*/
|
|
1234
|
+
size() {
|
|
1235
|
+
return this.rootMap.size + 2;
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Get all keys in MindCache (including temporal keys).
|
|
1239
|
+
*/
|
|
1240
|
+
keys() {
|
|
1241
|
+
const keys = Array.from(this.rootMap.keys());
|
|
1242
|
+
keys.push("$date", "$time");
|
|
1243
|
+
return keys;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Get all values in MindCache (including temporal values).
|
|
1247
|
+
*/
|
|
1248
|
+
values() {
|
|
1249
|
+
const result = [];
|
|
1250
|
+
for (const [key] of this.rootMap) {
|
|
1251
|
+
result.push(this.get_value(key));
|
|
1252
|
+
}
|
|
1253
|
+
result.push(this.get_value("$date"));
|
|
1254
|
+
result.push(this.get_value("$time"));
|
|
1255
|
+
return result;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Get all key-value entries (including temporal entries).
|
|
1259
|
+
*/
|
|
1260
|
+
entries() {
|
|
1261
|
+
const result = [];
|
|
1262
|
+
for (const [key] of this.rootMap) {
|
|
1263
|
+
result.push([key, this.get_value(key)]);
|
|
1264
|
+
}
|
|
1265
|
+
result.push(["$date", this.get_value("$date")]);
|
|
1266
|
+
result.push(["$time", this.get_value("$time")]);
|
|
1267
|
+
return result;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Unsubscribe from key changes.
|
|
1271
|
+
* @deprecated Use the cleanup function returned by subscribe() instead
|
|
1272
|
+
*/
|
|
1273
|
+
unsubscribe(key, listener) {
|
|
1274
|
+
if (this.listeners[key]) {
|
|
1275
|
+
this.listeners[key] = this.listeners[key].filter((l) => l !== listener);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Get the STM as a formatted string for LLM context.
|
|
1280
|
+
* @deprecated Use get_system_prompt() instead
|
|
1281
|
+
*/
|
|
1282
|
+
getSTM() {
|
|
1283
|
+
return this.get_system_prompt();
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Get the STM as an object with values directly (no attributes).
|
|
1287
|
+
* Includes system keys ($date, $time).
|
|
1288
|
+
* @deprecated Use getAll() for full STM format
|
|
1289
|
+
*/
|
|
1290
|
+
getSTMObject() {
|
|
1291
|
+
const result = {};
|
|
1292
|
+
for (const [key] of this.rootMap) {
|
|
1293
|
+
result[key] = this.get_value(key);
|
|
1294
|
+
}
|
|
1295
|
+
result["$date"] = this.get_value("$date");
|
|
1296
|
+
result["$time"] = this.get_value("$time");
|
|
1297
|
+
return result;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Add a content tag to a key.
|
|
1301
|
+
* @returns true if the tag was added, false if key doesn't exist or tag already exists
|
|
1302
|
+
*/
|
|
1303
|
+
addTag(key, tag) {
|
|
1304
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
const entryMap = this.rootMap.get(key);
|
|
1308
|
+
if (!entryMap) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
const attributes = entryMap.get("attributes");
|
|
1312
|
+
const contentTags = attributes?.contentTags || [];
|
|
1313
|
+
if (contentTags.includes(tag)) {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
this.doc.transact(() => {
|
|
1317
|
+
const newContentTags = [...contentTags, tag];
|
|
1318
|
+
entryMap.set("attributes", {
|
|
1319
|
+
...attributes,
|
|
1320
|
+
contentTags: newContentTags,
|
|
1321
|
+
tags: newContentTags
|
|
1322
|
+
// Sync legacy tags array
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
this.notifyGlobalListeners();
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Remove a content tag from a key.
|
|
1330
|
+
* @returns true if the tag was removed
|
|
1331
|
+
*/
|
|
1332
|
+
removeTag(key, tag) {
|
|
1333
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
const entryMap = this.rootMap.get(key);
|
|
1337
|
+
if (!entryMap) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
const attributes = entryMap.get("attributes");
|
|
1341
|
+
const contentTags = attributes?.contentTags || [];
|
|
1342
|
+
const tagIndex = contentTags.indexOf(tag);
|
|
1343
|
+
if (tagIndex === -1) {
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
this.doc.transact(() => {
|
|
1347
|
+
const newContentTags = contentTags.filter((t) => t !== tag);
|
|
1348
|
+
entryMap.set("attributes", {
|
|
1349
|
+
...attributes,
|
|
1350
|
+
contentTags: newContentTags,
|
|
1351
|
+
tags: newContentTags
|
|
1352
|
+
// Sync legacy tags array
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
this.notifyGlobalListeners();
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Get all content tags for a key.
|
|
1360
|
+
*/
|
|
1361
|
+
getTags(key) {
|
|
1362
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1363
|
+
return [];
|
|
1364
|
+
}
|
|
1365
|
+
const entryMap = this.rootMap.get(key);
|
|
1366
|
+
if (!entryMap) {
|
|
1367
|
+
return [];
|
|
1368
|
+
}
|
|
1369
|
+
const attributes = entryMap.get("attributes");
|
|
1370
|
+
return attributes?.contentTags || [];
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Get all unique content tags across all keys.
|
|
1374
|
+
*/
|
|
1375
|
+
getAllTags() {
|
|
1376
|
+
const allTags = /* @__PURE__ */ new Set();
|
|
1377
|
+
for (const [, val] of this.rootMap) {
|
|
1378
|
+
const entryMap = val;
|
|
1379
|
+
const attributes = entryMap.get("attributes");
|
|
1380
|
+
if (attributes?.contentTags) {
|
|
1381
|
+
attributes.contentTags.forEach((tag) => allTags.add(tag));
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return Array.from(allTags);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Check if a key has a specific content tag.
|
|
1388
|
+
*/
|
|
1389
|
+
hasTag(key, tag) {
|
|
1390
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1391
|
+
return false;
|
|
1392
|
+
}
|
|
1393
|
+
const entryMap = this.rootMap.get(key);
|
|
1394
|
+
if (!entryMap) {
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
const attributes = entryMap.get("attributes");
|
|
1398
|
+
return attributes?.contentTags?.includes(tag) || false;
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Get all keys with a specific content tag as formatted string.
|
|
1402
|
+
*/
|
|
1403
|
+
getTagged(tag) {
|
|
1404
|
+
const entries = [];
|
|
1405
|
+
const keys = this.getSortedKeys();
|
|
1406
|
+
keys.forEach((key) => {
|
|
1407
|
+
if (this.hasTag(key, tag)) {
|
|
1408
|
+
entries.push([key, this.get_value(key)]);
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
return entries.map(([key, value]) => `${key}: ${value}`).join(", ");
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Get array of keys with a specific content tag.
|
|
1415
|
+
*/
|
|
1416
|
+
getKeysByTag(tag) {
|
|
1417
|
+
const keys = this.getSortedKeys();
|
|
1418
|
+
return keys.filter((key) => this.hasTag(key, tag));
|
|
1419
|
+
}
|
|
1420
|
+
// ============================================
|
|
1421
|
+
// System Tag Methods (requires system access level)
|
|
1422
|
+
// ============================================
|
|
1423
|
+
/**
|
|
1424
|
+
* Add a system tag to a key (requires system access).
|
|
1425
|
+
* System tags: 'SystemPrompt', 'LLMRead', 'LLMWrite', 'readonly', 'protected', 'ApplyTemplate'
|
|
1426
|
+
*/
|
|
1427
|
+
systemAddTag(key, tag) {
|
|
1428
|
+
if (!this.hasSystemAccess) {
|
|
1429
|
+
console.warn("MindCache: systemAddTag requires system access level");
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
const entryMap = this.rootMap.get(key);
|
|
1436
|
+
if (!entryMap) {
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
const attributes = entryMap.get("attributes");
|
|
1440
|
+
const systemTags = attributes?.systemTags || [];
|
|
1441
|
+
if (systemTags.includes(tag)) {
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
this.doc.transact(() => {
|
|
1445
|
+
const newSystemTags = [...systemTags, tag];
|
|
1446
|
+
const normalizedTags = this.normalizeSystemTags(newSystemTags);
|
|
1447
|
+
entryMap.set("attributes", {
|
|
1448
|
+
...attributes,
|
|
1449
|
+
systemTags: normalizedTags
|
|
1450
|
+
});
|
|
1451
|
+
});
|
|
1452
|
+
this.notifyGlobalListeners();
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Remove a system tag from a key (requires system access).
|
|
1457
|
+
*/
|
|
1458
|
+
systemRemoveTag(key, tag) {
|
|
1459
|
+
if (!this.hasSystemAccess) {
|
|
1460
|
+
console.warn("MindCache: systemRemoveTag requires system access level");
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
const entryMap = this.rootMap.get(key);
|
|
1467
|
+
if (!entryMap) {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
const attributes = entryMap.get("attributes");
|
|
1471
|
+
const systemTags = attributes?.systemTags || [];
|
|
1472
|
+
const tagIndex = systemTags.indexOf(tag);
|
|
1473
|
+
if (tagIndex === -1) {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
this.doc.transact(() => {
|
|
1477
|
+
const newSystemTags = systemTags.filter((t) => t !== tag);
|
|
1478
|
+
entryMap.set("attributes", {
|
|
1479
|
+
...attributes,
|
|
1480
|
+
systemTags: newSystemTags
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
this.notifyGlobalListeners();
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Get all system tags for a key (requires system access).
|
|
1488
|
+
*/
|
|
1489
|
+
systemGetTags(key) {
|
|
1490
|
+
if (!this.hasSystemAccess) {
|
|
1491
|
+
console.warn("MindCache: systemGetTags requires system access level");
|
|
1492
|
+
return [];
|
|
1493
|
+
}
|
|
1494
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1495
|
+
return [];
|
|
1496
|
+
}
|
|
1497
|
+
const entryMap = this.rootMap.get(key);
|
|
1498
|
+
if (!entryMap) {
|
|
1499
|
+
return [];
|
|
1500
|
+
}
|
|
1501
|
+
const attributes = entryMap.get("attributes");
|
|
1502
|
+
return attributes?.systemTags || [];
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Check if a key has a specific system tag (requires system access).
|
|
1506
|
+
*/
|
|
1507
|
+
systemHasTag(key, tag) {
|
|
1508
|
+
if (!this.hasSystemAccess) {
|
|
1509
|
+
console.warn("MindCache: systemHasTag requires system access level");
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
const entryMap = this.rootMap.get(key);
|
|
1516
|
+
if (!entryMap) {
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
const attributes = entryMap.get("attributes");
|
|
1520
|
+
return attributes?.systemTags?.includes(tag) || false;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Set all system tags for a key at once (requires system access).
|
|
1524
|
+
*/
|
|
1525
|
+
systemSetTags(key, tags) {
|
|
1526
|
+
if (!this.hasSystemAccess) {
|
|
1527
|
+
console.warn("MindCache: systemSetTags requires system access level");
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
const entryMap = this.rootMap.get(key);
|
|
1534
|
+
if (!entryMap) {
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1537
|
+
this.doc.transact(() => {
|
|
1538
|
+
const attributes = entryMap.get("attributes");
|
|
1539
|
+
entryMap.set("attributes", {
|
|
1540
|
+
...attributes,
|
|
1541
|
+
systemTags: [...tags]
|
|
1542
|
+
});
|
|
1543
|
+
});
|
|
1544
|
+
this.notifyGlobalListeners();
|
|
1545
|
+
return true;
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Get all keys with a specific system tag (requires system access).
|
|
1549
|
+
*/
|
|
1550
|
+
systemGetKeysByTag(tag) {
|
|
1551
|
+
if (!this.hasSystemAccess) {
|
|
1552
|
+
console.warn("MindCache: systemGetKeysByTag requires system access level");
|
|
1553
|
+
return [];
|
|
1554
|
+
}
|
|
1555
|
+
const keys = this.getSortedKeys();
|
|
1556
|
+
return keys.filter((key) => this.systemHasTag(key, tag));
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Helper to get sorted keys (by zIndex).
|
|
1560
|
+
*/
|
|
1561
|
+
getSortedKeys() {
|
|
1562
|
+
const entries = [];
|
|
1563
|
+
for (const [key, val] of this.rootMap) {
|
|
1564
|
+
const entryMap = val;
|
|
1565
|
+
const attributes = entryMap.get("attributes");
|
|
1566
|
+
entries.push({ key, zIndex: attributes?.zIndex ?? 0 });
|
|
1567
|
+
}
|
|
1568
|
+
return entries.sort((a, b) => a.zIndex - b.zIndex).map((e) => e.key);
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Serialize to JSON string.
|
|
1572
|
+
*/
|
|
1573
|
+
toJSON() {
|
|
1574
|
+
return JSON.stringify(this.serialize());
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Deserialize from JSON string.
|
|
1578
|
+
*/
|
|
1579
|
+
fromJSON(jsonString) {
|
|
1580
|
+
try {
|
|
1581
|
+
const data = JSON.parse(jsonString);
|
|
1582
|
+
this.deserialize(data);
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
console.error("MindCache: Failed to deserialize JSON:", error);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Export to Markdown format.
|
|
1589
|
+
*/
|
|
1590
|
+
toMarkdown() {
|
|
1591
|
+
const now = /* @__PURE__ */ new Date();
|
|
1592
|
+
const lines = [];
|
|
1593
|
+
const appendixEntries = [];
|
|
1594
|
+
let appendixCounter = 0;
|
|
1595
|
+
lines.push("# MindCache STM Export");
|
|
1596
|
+
lines.push("");
|
|
1597
|
+
lines.push(`Export Date: ${now.toISOString().split("T")[0]}`);
|
|
1598
|
+
lines.push("");
|
|
1599
|
+
lines.push("---");
|
|
1600
|
+
lines.push("");
|
|
1601
|
+
lines.push("## STM Entries");
|
|
1602
|
+
lines.push("");
|
|
1603
|
+
const sortedKeys = this.getSortedKeys();
|
|
1604
|
+
sortedKeys.forEach((key) => {
|
|
1605
|
+
const entryMap = this.rootMap.get(key);
|
|
1606
|
+
if (!entryMap) {
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const attributes = entryMap.get("attributes");
|
|
1610
|
+
const value = entryMap.get("value");
|
|
1611
|
+
if (attributes?.systemTags?.includes("protected")) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
lines.push(`### ${key}`);
|
|
1615
|
+
const entryType = attributes?.type || "text";
|
|
1616
|
+
lines.push(`- **Type**: \`${entryType}\``);
|
|
1617
|
+
lines.push(`- **System Tags**: \`${attributes?.systemTags?.join(", ") || "none"}\``);
|
|
1618
|
+
lines.push(`- **Z-Index**: \`${attributes?.zIndex ?? 0}\``);
|
|
1619
|
+
if (attributes?.contentTags && attributes.contentTags.length > 0) {
|
|
1620
|
+
lines.push(`- **Tags**: \`${attributes.contentTags.join("`, `")}\``);
|
|
1621
|
+
}
|
|
1622
|
+
if (attributes?.contentType) {
|
|
1623
|
+
lines.push(`- **Content Type**: \`${attributes.contentType}\``);
|
|
1624
|
+
}
|
|
1625
|
+
if (entryType === "image" || entryType === "file") {
|
|
1626
|
+
const label = String.fromCharCode(65 + appendixCounter);
|
|
1627
|
+
appendixCounter++;
|
|
1628
|
+
lines.push(`- **Value**: [See Appendix ${label}]`);
|
|
1629
|
+
appendixEntries.push({
|
|
1630
|
+
key,
|
|
1631
|
+
type: entryType,
|
|
1632
|
+
contentType: attributes?.contentType || "application/octet-stream",
|
|
1633
|
+
base64: value,
|
|
1634
|
+
label
|
|
1635
|
+
});
|
|
1636
|
+
} else if (entryType === "json") {
|
|
1637
|
+
lines.push("- **Value**:");
|
|
1638
|
+
lines.push("```json");
|
|
1639
|
+
try {
|
|
1640
|
+
const jsonValue = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
1641
|
+
lines.push(jsonValue);
|
|
1642
|
+
} catch {
|
|
1643
|
+
lines.push(String(value));
|
|
1644
|
+
}
|
|
1645
|
+
lines.push("```");
|
|
1646
|
+
} else {
|
|
1647
|
+
lines.push("- **Value**:");
|
|
1648
|
+
lines.push("```");
|
|
1649
|
+
lines.push(String(value));
|
|
1650
|
+
lines.push("```");
|
|
1651
|
+
}
|
|
1652
|
+
lines.push("");
|
|
1653
|
+
});
|
|
1654
|
+
if (appendixEntries.length > 0) {
|
|
1655
|
+
lines.push("---");
|
|
1656
|
+
lines.push("");
|
|
1657
|
+
lines.push("## Appendix: Binary Data");
|
|
1658
|
+
lines.push("");
|
|
1659
|
+
appendixEntries.forEach((entry) => {
|
|
1660
|
+
lines.push(`### Appendix ${entry.label}: ${entry.key}`);
|
|
1661
|
+
lines.push(`- **Type**: \`${entry.type}\``);
|
|
1662
|
+
lines.push(`- **Content Type**: \`${entry.contentType}\``);
|
|
1663
|
+
lines.push("- **Base64 Data**:");
|
|
1664
|
+
lines.push("```");
|
|
1665
|
+
lines.push(entry.base64);
|
|
1666
|
+
lines.push("```");
|
|
1667
|
+
lines.push("");
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
return lines.join("\n");
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Import from Markdown format.
|
|
1674
|
+
*/
|
|
1675
|
+
fromMarkdown(markdown) {
|
|
1676
|
+
const lines = markdown.split("\n");
|
|
1677
|
+
let currentKey = null;
|
|
1678
|
+
let currentAttributes = {};
|
|
1679
|
+
let currentValue = null;
|
|
1680
|
+
let inCodeBlock = false;
|
|
1681
|
+
let codeBlockContent = [];
|
|
1682
|
+
for (const line of lines) {
|
|
1683
|
+
if (line.startsWith("### ") && !line.startsWith("### Appendix")) {
|
|
1684
|
+
if (currentKey && currentValue !== null) {
|
|
1685
|
+
this.set_value(currentKey, currentValue.trim(), currentAttributes);
|
|
1686
|
+
}
|
|
1687
|
+
currentKey = line.substring(4).trim();
|
|
1688
|
+
currentAttributes = {};
|
|
1689
|
+
currentValue = null;
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
if (line.startsWith("### Appendix ")) {
|
|
1693
|
+
const match = line.match(/### Appendix ([A-Z]): (.+)/);
|
|
1694
|
+
if (match) {
|
|
1695
|
+
currentKey = match[2];
|
|
1696
|
+
}
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (line.startsWith("- **Type**:")) {
|
|
1700
|
+
const type = line.match(/`(.+)`/)?.[1];
|
|
1701
|
+
if (type) {
|
|
1702
|
+
currentAttributes.type = type;
|
|
1703
|
+
}
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
if (line.startsWith("- **System Tags**:")) {
|
|
1707
|
+
const tagsStr = line.match(/`([^`]+)`/)?.[1] || "";
|
|
1708
|
+
if (tagsStr !== "none") {
|
|
1709
|
+
currentAttributes.systemTags = tagsStr.split(", ").filter((t) => t);
|
|
1710
|
+
}
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
if (line.startsWith("- **Z-Index**:")) {
|
|
1714
|
+
const zIndex = parseInt(line.match(/`(\d+)`/)?.[1] || "0", 10);
|
|
1715
|
+
currentAttributes.zIndex = zIndex;
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
if (line.startsWith("- **Tags**:")) {
|
|
1719
|
+
const tags = line.match(/`([^`]+)`/g)?.map((t) => t.slice(1, -1)) || [];
|
|
1720
|
+
currentAttributes.contentTags = tags;
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
if (line.startsWith("- **Content Type**:")) {
|
|
1724
|
+
currentAttributes.contentType = line.match(/`(.+)`/)?.[1];
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (line.startsWith("- **Value**:") && !line.includes("[See Appendix")) {
|
|
1728
|
+
const afterValue = line.substring(12).trim();
|
|
1729
|
+
if (afterValue === "") {
|
|
1730
|
+
currentValue = "";
|
|
1731
|
+
} else if (afterValue === "```" || afterValue === "```json") {
|
|
1732
|
+
inCodeBlock = true;
|
|
1733
|
+
codeBlockContent = [];
|
|
1734
|
+
currentValue = "";
|
|
1735
|
+
} else if (afterValue.startsWith("```")) {
|
|
1736
|
+
inCodeBlock = true;
|
|
1737
|
+
codeBlockContent = [afterValue.substring(3)];
|
|
1738
|
+
currentValue = "";
|
|
1739
|
+
} else {
|
|
1740
|
+
currentValue = afterValue;
|
|
1741
|
+
}
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
const trimmedLine = line.trim();
|
|
1745
|
+
if (trimmedLine === "```json" || trimmedLine === "```") {
|
|
1746
|
+
if (inCodeBlock) {
|
|
1747
|
+
inCodeBlock = false;
|
|
1748
|
+
if (currentKey && codeBlockContent.length > 0) {
|
|
1749
|
+
currentValue = codeBlockContent.join("\n");
|
|
1750
|
+
}
|
|
1751
|
+
codeBlockContent = [];
|
|
1752
|
+
} else {
|
|
1753
|
+
inCodeBlock = true;
|
|
1754
|
+
codeBlockContent = [];
|
|
1755
|
+
}
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
if (inCodeBlock) {
|
|
1759
|
+
codeBlockContent.push(line);
|
|
1760
|
+
} else if (currentKey && currentValue !== null) {
|
|
1761
|
+
currentValue += "\n" + line;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (currentKey && currentValue !== null) {
|
|
1765
|
+
this.set_value(currentKey, currentValue.trim(), currentAttributes);
|
|
1766
|
+
}
|
|
1767
|
+
const hasParsedKeys = lines.some((line) => line.startsWith("### ") && !line.startsWith("### Appendix"));
|
|
1768
|
+
if (!hasParsedKeys && markdown.trim().length > 0) {
|
|
1769
|
+
this.set_value("imported_content", markdown.trim(), {
|
|
1770
|
+
type: "text",
|
|
1771
|
+
systemTags: ["SystemPrompt", "LLMWrite"],
|
|
1772
|
+
// Default assumptions
|
|
1773
|
+
zIndex: 0
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Set base64 binary data.
|
|
1779
|
+
*/
|
|
1780
|
+
set_base64(key, base64Data, contentType, type = "file", attributes) {
|
|
1781
|
+
if (!this.validateContentType(type, contentType)) {
|
|
1782
|
+
throw new Error(`Invalid content type ${contentType} for type ${type}`);
|
|
1783
|
+
}
|
|
1784
|
+
const fileAttributes = {
|
|
1785
|
+
type,
|
|
1786
|
+
contentType,
|
|
1787
|
+
...attributes
|
|
1788
|
+
};
|
|
1789
|
+
this.set_value(key, base64Data, fileAttributes);
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Add an image from base64 data.
|
|
1793
|
+
*/
|
|
1794
|
+
add_image(key, base64Data, contentType = "image/jpeg", attributes) {
|
|
1795
|
+
if (!contentType.startsWith("image/")) {
|
|
1796
|
+
throw new Error(`Invalid image content type: ${contentType}. Must start with 'image/'`);
|
|
1797
|
+
}
|
|
1798
|
+
this.set_base64(key, base64Data, contentType, "image", attributes);
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Get the data URL for an image or file key.
|
|
1802
|
+
*/
|
|
1803
|
+
get_data_url(key) {
|
|
1804
|
+
const entryMap = this.rootMap.get(key);
|
|
1805
|
+
if (!entryMap) {
|
|
1806
|
+
return void 0;
|
|
1807
|
+
}
|
|
1808
|
+
const attributes = entryMap.get("attributes");
|
|
1809
|
+
if (attributes?.type !== "image" && attributes?.type !== "file") {
|
|
1810
|
+
return void 0;
|
|
1811
|
+
}
|
|
1812
|
+
if (!attributes?.contentType) {
|
|
1813
|
+
return void 0;
|
|
1814
|
+
}
|
|
1815
|
+
const value = entryMap.get("value");
|
|
1816
|
+
return this.createDataUrl(value, attributes.contentType);
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Get the base64 data for an image or file key.
|
|
1820
|
+
*/
|
|
1821
|
+
get_base64(key) {
|
|
1822
|
+
const entryMap = this.rootMap.get(key);
|
|
1823
|
+
if (!entryMap) {
|
|
1824
|
+
return void 0;
|
|
1825
|
+
}
|
|
1826
|
+
const attributes = entryMap.get("attributes");
|
|
1827
|
+
if (attributes?.type !== "image" && attributes?.type !== "file") {
|
|
1828
|
+
return void 0;
|
|
1829
|
+
}
|
|
1830
|
+
return entryMap.get("value");
|
|
1831
|
+
}
|
|
1832
|
+
// File methods
|
|
1833
|
+
async set_file(key, file, attributes) {
|
|
1834
|
+
const base64 = await this.encodeFileToBase64(file);
|
|
1835
|
+
const dataUrl = this.createDataUrl(base64, file.type);
|
|
1836
|
+
this.set_value(key, dataUrl, {
|
|
1837
|
+
...attributes,
|
|
1838
|
+
type: "file",
|
|
1839
|
+
contentType: file.type
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
set_image(key, file, attributes) {
|
|
1843
|
+
return this.set_file(key, file, {
|
|
1844
|
+
...attributes,
|
|
1845
|
+
type: "image"
|
|
1846
|
+
// Override to image
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
// Document methods for collaborative editing
|
|
1850
|
+
/**
|
|
1851
|
+
* Create or get a collaborative document key.
|
|
1852
|
+
* Uses Y.Text for character-level concurrent editing.
|
|
1853
|
+
*
|
|
1854
|
+
* Note: This exposes Yjs Y.Text directly for editor bindings (y-quill, y-codemirror, etc.)
|
|
1855
|
+
*/
|
|
1856
|
+
set_document(key, initialText, attributes) {
|
|
1857
|
+
if (key === "$date" || key === "$time" || key === "$version") {
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
let entryMap = this.rootMap.get(key);
|
|
1861
|
+
if (!entryMap) {
|
|
1862
|
+
entryMap = new Y__namespace.Map();
|
|
1863
|
+
this.rootMap.set(key, entryMap);
|
|
1864
|
+
const yText = new Y__namespace.Text(initialText || "");
|
|
1865
|
+
entryMap.set("value", yText);
|
|
1866
|
+
entryMap.set("attributes", {
|
|
1867
|
+
...DEFAULT_KEY_ATTRIBUTES,
|
|
1868
|
+
type: "document",
|
|
1869
|
+
contentTags: [],
|
|
1870
|
+
systemTags: ["SystemPrompt", "LLMWrite"],
|
|
1871
|
+
tags: [],
|
|
1872
|
+
zIndex: 0,
|
|
1873
|
+
...attributes
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
this.getUndoManager(key);
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Get the Y.Text object for a document key.
|
|
1880
|
+
* Use this to bind to editors (Quill, CodeMirror, Monaco, etc.)
|
|
1881
|
+
*
|
|
1882
|
+
* @returns Y.Text or undefined if key doesn't exist or isn't a document
|
|
1883
|
+
*/
|
|
1884
|
+
get_document(key) {
|
|
1885
|
+
const entryMap = this.rootMap.get(key);
|
|
1886
|
+
if (!entryMap) {
|
|
1887
|
+
return void 0;
|
|
1888
|
+
}
|
|
1889
|
+
const attrs = entryMap.get("attributes");
|
|
1890
|
+
if (attrs?.type !== "document") {
|
|
1891
|
+
return void 0;
|
|
1892
|
+
}
|
|
1893
|
+
const value = entryMap.get("value");
|
|
1894
|
+
if (value instanceof Y__namespace.Text) {
|
|
1895
|
+
return value;
|
|
1896
|
+
}
|
|
1897
|
+
return void 0;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Insert text at a position in a document key.
|
|
1901
|
+
*/
|
|
1902
|
+
insert_text(key, index, text) {
|
|
1903
|
+
const yText = this.get_document(key);
|
|
1904
|
+
if (yText) {
|
|
1905
|
+
yText.insert(index, text);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Delete text from a document key.
|
|
1910
|
+
*/
|
|
1911
|
+
delete_text(key, index, length2) {
|
|
1912
|
+
const yText = this.get_document(key);
|
|
1913
|
+
if (yText) {
|
|
1914
|
+
yText.delete(index, length2);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Replace all text in a document key (private - use set_value for public API).
|
|
1919
|
+
* Uses diff-based updates when changes are < diffThreshold (default 80%).
|
|
1920
|
+
* This preserves concurrent edits and provides better undo granularity.
|
|
1921
|
+
*/
|
|
1922
|
+
_replaceDocumentText(key, newText, diffThreshold = 0.8) {
|
|
1923
|
+
const yText = this.get_document(key);
|
|
1924
|
+
if (!yText) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const oldText = yText.toString();
|
|
1928
|
+
if (oldText === newText) {
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (oldText.length === 0) {
|
|
1932
|
+
yText.insert(0, newText);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const diffs = diff__default.default(oldText, newText);
|
|
1936
|
+
let changedChars = 0;
|
|
1937
|
+
for (const [op, text] of diffs) {
|
|
1938
|
+
if (op !== 0) {
|
|
1939
|
+
changedChars += text.length;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
const changeRatio = changedChars / Math.max(oldText.length, newText.length);
|
|
1943
|
+
if (changeRatio > diffThreshold) {
|
|
1944
|
+
this.doc.transact(() => {
|
|
1945
|
+
yText.delete(0, yText.length);
|
|
1946
|
+
yText.insert(0, newText);
|
|
1947
|
+
});
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
this.doc.transact(() => {
|
|
1951
|
+
let cursor = 0;
|
|
1952
|
+
for (const [op, text] of diffs) {
|
|
1953
|
+
if (op === 0) {
|
|
1954
|
+
cursor += text.length;
|
|
1955
|
+
} else if (op === -1) {
|
|
1956
|
+
yText.delete(cursor, text.length);
|
|
1957
|
+
} else if (op === 1) {
|
|
1958
|
+
yText.insert(cursor, text);
|
|
1959
|
+
cursor += text.length;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
// ... (subscribe methods)
|
|
1965
|
+
subscribe(key, listener) {
|
|
1966
|
+
if (!this.listeners[key]) {
|
|
1967
|
+
this.listeners[key] = [];
|
|
1968
|
+
}
|
|
1969
|
+
this.listeners[key].push(listener);
|
|
1970
|
+
return () => {
|
|
1971
|
+
this.listeners[key] = this.listeners[key].filter((l) => l !== listener);
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
subscribeToAll(listener) {
|
|
1975
|
+
this.globalListeners.push(listener);
|
|
1976
|
+
return () => {
|
|
1977
|
+
this.globalListeners = this.globalListeners.filter((l) => l !== listener);
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
unsubscribeFromAll(listener) {
|
|
1981
|
+
this.globalListeners = this.globalListeners.filter((l) => l !== listener);
|
|
1982
|
+
}
|
|
1983
|
+
notifyGlobalListeners() {
|
|
1984
|
+
this.globalListeners.forEach((l) => l());
|
|
1985
|
+
}
|
|
1986
|
+
// Sanitize key name for use in tool names
|
|
1987
|
+
sanitizeKeyForTool(key) {
|
|
1988
|
+
return key.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1989
|
+
}
|
|
1990
|
+
// Find original key from sanitized tool name
|
|
1991
|
+
findKeyFromSanitizedTool(sanitizedKey) {
|
|
1992
|
+
for (const [key] of this.rootMap) {
|
|
1993
|
+
if (this.sanitizeKeyForTool(key) === sanitizedKey) {
|
|
1994
|
+
return key;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return void 0;
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Generate Vercel AI SDK compatible tools for writable keys.
|
|
2001
|
+
* For document type keys, generates additional tools: append_, insert_, edit_
|
|
2002
|
+
*
|
|
2003
|
+
* Security: All tools use llm_set_key internally which:
|
|
2004
|
+
* - Only modifies VALUES, never attributes/systemTags
|
|
2005
|
+
* - Prevents LLMs from escalating privileges
|
|
2006
|
+
*/
|
|
2007
|
+
create_vercel_ai_tools() {
|
|
2008
|
+
const tools = {};
|
|
2009
|
+
for (const [key, val] of this.rootMap) {
|
|
2010
|
+
if (key.startsWith("$")) {
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
const entryMap = val;
|
|
2014
|
+
const attributes = entryMap.get("attributes");
|
|
2015
|
+
const isWritable = attributes?.systemTags?.includes("LLMWrite");
|
|
2016
|
+
if (!isWritable) {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
const sanitizedKey = this.sanitizeKeyForTool(key);
|
|
2020
|
+
const isDocument = attributes?.type === "document";
|
|
2021
|
+
tools[`write_${sanitizedKey}`] = {
|
|
2022
|
+
description: isDocument ? `Rewrite the entire "${key}" document` : `Write a value to the STM key: ${key}`,
|
|
2023
|
+
inputSchema: {
|
|
2024
|
+
type: "object",
|
|
2025
|
+
properties: {
|
|
2026
|
+
value: { type: "string", description: isDocument ? "New document content" : "The value to write" }
|
|
2027
|
+
},
|
|
2028
|
+
required: ["value"]
|
|
2029
|
+
},
|
|
2030
|
+
execute: async ({ value }) => {
|
|
2031
|
+
const success = this.llm_set_key(key, value);
|
|
2032
|
+
if (success) {
|
|
2033
|
+
return {
|
|
2034
|
+
result: `Successfully wrote "${value}" to ${key}`,
|
|
2035
|
+
key,
|
|
2036
|
+
value
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
return {
|
|
2040
|
+
result: `Failed to write to ${key} - permission denied or key not found`,
|
|
2041
|
+
key,
|
|
2042
|
+
error: true
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
if (isDocument) {
|
|
2047
|
+
tools[`append_${sanitizedKey}`] = {
|
|
2048
|
+
description: `Append text to the end of "${key}" document`,
|
|
2049
|
+
inputSchema: {
|
|
2050
|
+
type: "object",
|
|
2051
|
+
properties: {
|
|
2052
|
+
text: { type: "string", description: "Text to append" }
|
|
2053
|
+
},
|
|
2054
|
+
required: ["text"]
|
|
2055
|
+
},
|
|
2056
|
+
execute: async ({ text }) => {
|
|
2057
|
+
if (!attributes?.systemTags?.includes("LLMWrite")) {
|
|
2058
|
+
return { result: `Permission denied for ${key}`, key, error: true };
|
|
2059
|
+
}
|
|
2060
|
+
const yText = this.get_document(key);
|
|
2061
|
+
if (yText) {
|
|
2062
|
+
yText.insert(yText.length, text);
|
|
2063
|
+
return {
|
|
2064
|
+
result: `Successfully appended to ${key}`,
|
|
2065
|
+
key,
|
|
2066
|
+
appended: text
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
return { result: `Document ${key} not found`, key };
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
tools[`insert_${sanitizedKey}`] = {
|
|
2073
|
+
description: `Insert text at a position in "${key}" document`,
|
|
2074
|
+
inputSchema: {
|
|
2075
|
+
type: "object",
|
|
2076
|
+
properties: {
|
|
2077
|
+
index: { type: "number", description: "Position to insert at (0 = start)" },
|
|
2078
|
+
text: { type: "string", description: "Text to insert" }
|
|
2079
|
+
},
|
|
2080
|
+
required: ["index", "text"]
|
|
2081
|
+
},
|
|
2082
|
+
execute: async ({ index, text }) => {
|
|
2083
|
+
if (!attributes?.systemTags?.includes("LLMWrite")) {
|
|
2084
|
+
return { result: `Permission denied for ${key}`, key, error: true };
|
|
2085
|
+
}
|
|
2086
|
+
this.insert_text(key, index, text);
|
|
2087
|
+
return {
|
|
2088
|
+
result: `Successfully inserted text at position ${index} in ${key}`,
|
|
2089
|
+
key,
|
|
2090
|
+
index,
|
|
2091
|
+
inserted: text
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
tools[`edit_${sanitizedKey}`] = {
|
|
2096
|
+
description: `Find and replace text in "${key}" document`,
|
|
2097
|
+
inputSchema: {
|
|
2098
|
+
type: "object",
|
|
2099
|
+
properties: {
|
|
2100
|
+
find: { type: "string", description: "Text to find" },
|
|
2101
|
+
replace: { type: "string", description: "Replacement text" }
|
|
2102
|
+
},
|
|
2103
|
+
required: ["find", "replace"]
|
|
2104
|
+
},
|
|
2105
|
+
execute: async ({ find, replace }) => {
|
|
2106
|
+
if (!attributes?.systemTags?.includes("LLMWrite")) {
|
|
2107
|
+
return { result: `Permission denied for ${key}`, key, error: true };
|
|
2108
|
+
}
|
|
2109
|
+
const yText = this.get_document(key);
|
|
2110
|
+
if (yText) {
|
|
2111
|
+
const text = yText.toString();
|
|
2112
|
+
const idx = text.indexOf(find);
|
|
2113
|
+
if (idx !== -1) {
|
|
2114
|
+
yText.delete(idx, find.length);
|
|
2115
|
+
yText.insert(idx, replace);
|
|
2116
|
+
return {
|
|
2117
|
+
result: `Successfully replaced "${find}" with "${replace}" in ${key}`,
|
|
2118
|
+
key,
|
|
2119
|
+
find,
|
|
2120
|
+
replace,
|
|
2121
|
+
index: idx
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
return { result: `Text "${find}" not found in ${key}`, key };
|
|
2125
|
+
}
|
|
2126
|
+
return { result: `Document ${key} not found`, key };
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return tools;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* @deprecated Use create_vercel_ai_tools() instead
|
|
2135
|
+
*/
|
|
2136
|
+
get_aisdk_tools() {
|
|
2137
|
+
return this.create_vercel_ai_tools();
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Generate a system prompt containing all visible STM keys and their values.
|
|
2141
|
+
* Indicates which tools can be used to modify writable keys.
|
|
2142
|
+
*/
|
|
2143
|
+
get_system_prompt() {
|
|
2144
|
+
const lines = [];
|
|
2145
|
+
for (const [key, val] of this.rootMap) {
|
|
2146
|
+
if (key.startsWith("$")) {
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
const entryMap = val;
|
|
2150
|
+
const attributes = entryMap.get("attributes");
|
|
2151
|
+
const isVisible = attributes?.systemTags?.includes("SystemPrompt") || attributes?.systemTags?.includes("LLMRead");
|
|
2152
|
+
if (!isVisible) {
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
const value = this.get_value(key);
|
|
2156
|
+
const displayValue = typeof value === "object" ? JSON.stringify(value) : value;
|
|
2157
|
+
const isWritable = attributes?.systemTags?.includes("LLMWrite");
|
|
2158
|
+
const isDocument = attributes?.type === "document";
|
|
2159
|
+
const sanitizedKey = this.sanitizeKeyForTool(key);
|
|
2160
|
+
if (isWritable) {
|
|
2161
|
+
if (isDocument) {
|
|
2162
|
+
lines.push(
|
|
2163
|
+
`${key}: ${displayValue}. Document tools: write_${sanitizedKey}, append_${sanitizedKey}, edit_${sanitizedKey}`
|
|
2164
|
+
);
|
|
2165
|
+
} else {
|
|
2166
|
+
const oldValueHint = displayValue ? ` This tool DOES NOT append \u2014 start your response with the old value (${displayValue})` : "";
|
|
2167
|
+
lines.push(
|
|
2168
|
+
`${key}: ${displayValue}. You can rewrite "${key}" by using the write_${sanitizedKey} tool.${oldValueHint}`
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
} else {
|
|
2172
|
+
lines.push(`${key}: ${displayValue}`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
lines.push(`$date: ${this.get_value("$date")}`);
|
|
2176
|
+
lines.push(`$time: ${this.get_value("$time")}`);
|
|
2177
|
+
return lines.join(", ");
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Execute a tool call by name with the given value.
|
|
2181
|
+
* Returns the result or null if tool not found.
|
|
2182
|
+
*/
|
|
2183
|
+
executeToolCall(toolName, value) {
|
|
2184
|
+
const match = toolName.match(/^(write|append|insert|edit)_(.+)$/);
|
|
2185
|
+
if (!match) {
|
|
2186
|
+
return null;
|
|
2187
|
+
}
|
|
2188
|
+
const [, action, sanitizedKey] = match;
|
|
2189
|
+
const key = this.findKeyFromSanitizedTool(sanitizedKey);
|
|
2190
|
+
if (!key) {
|
|
2191
|
+
return null;
|
|
2192
|
+
}
|
|
2193
|
+
const entryMap = this.rootMap.get(key);
|
|
2194
|
+
if (!entryMap) {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
const attributes = entryMap.get("attributes");
|
|
2198
|
+
const isWritable = attributes?.systemTags?.includes("LLMWrite");
|
|
2199
|
+
if (!isWritable) {
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
const isDocument = attributes?.type === "document";
|
|
2203
|
+
switch (action) {
|
|
2204
|
+
case "write":
|
|
2205
|
+
if (isDocument) {
|
|
2206
|
+
this._replaceDocumentText(key, value);
|
|
2207
|
+
} else {
|
|
2208
|
+
this.set_value(key, value);
|
|
2209
|
+
}
|
|
2210
|
+
return {
|
|
2211
|
+
result: `Successfully wrote "${value}" to ${key}`,
|
|
2212
|
+
key,
|
|
2213
|
+
value
|
|
2214
|
+
};
|
|
2215
|
+
case "append":
|
|
2216
|
+
if (isDocument) {
|
|
2217
|
+
const yText = this.get_document(key);
|
|
2218
|
+
if (yText) {
|
|
2219
|
+
yText.insert(yText.length, value);
|
|
2220
|
+
return {
|
|
2221
|
+
result: `Successfully appended to ${key}`,
|
|
2222
|
+
key,
|
|
2223
|
+
value
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return null;
|
|
2228
|
+
case "insert":
|
|
2229
|
+
if (isDocument && typeof value === "object" && value.index !== void 0 && value.text) {
|
|
2230
|
+
this.insert_text(key, value.index, value.text);
|
|
2231
|
+
return {
|
|
2232
|
+
result: `Successfully inserted at position ${value.index} in ${key}`,
|
|
2233
|
+
key,
|
|
2234
|
+
value: value.text
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
return null;
|
|
2238
|
+
case "edit":
|
|
2239
|
+
if (isDocument && typeof value === "object" && value.find && value.replace !== void 0) {
|
|
2240
|
+
const yText = this.get_document(key);
|
|
2241
|
+
if (yText) {
|
|
2242
|
+
const text = yText.toString();
|
|
2243
|
+
const idx = text.indexOf(value.find);
|
|
2244
|
+
if (idx !== -1) {
|
|
2245
|
+
yText.delete(idx, value.find.length);
|
|
2246
|
+
yText.insert(idx, value.replace);
|
|
2247
|
+
return {
|
|
2248
|
+
result: `Successfully replaced "${value.find}" with "${value.replace}" in ${key}`,
|
|
2249
|
+
key,
|
|
2250
|
+
value: value.replace
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return null;
|
|
2256
|
+
default:
|
|
2257
|
+
return null;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
// Internal method stub for legacy compatibility
|
|
2261
|
+
_setFromRemote(_key, _value, _attributes) {
|
|
2262
|
+
}
|
|
2263
|
+
_deleteFromRemote(_key) {
|
|
2264
|
+
}
|
|
2265
|
+
_clearFromRemote() {
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
// src/cloud/index.ts
|
|
2270
|
+
init_CloudAdapter();
|
|
2271
|
+
init_CloudAdapter();
|
|
2272
|
+
|
|
2273
|
+
// src/local/index.ts
|
|
2274
|
+
init_IndexedDBAdapter();
|
|
2275
|
+
|
|
2276
|
+
exports.DEFAULT_KEY_ATTRIBUTES = DEFAULT_KEY_ATTRIBUTES;
|
|
2277
|
+
exports.MindCache = MindCache;
|
|
2278
|
+
exports.SystemTagHelpers = SystemTagHelpers;
|
|
2279
|
+
//# sourceMappingURL=server.js.map
|
|
2280
|
+
//# sourceMappingURL=server.js.map
|