@prismer/sdk 1.8.1 → 1.8.2
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/chunk-BWZXMXL7.mjs +4762 -0
- package/dist/chunk-VSAVCMMZ.mjs +4761 -0
- package/dist/cli.d.mts +15 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +570 -284
- package/dist/cli.mjs +3838 -0
- package/dist/index.d.mts +60 -5
- package/dist/index.d.ts +60 -5
- package/dist/index.js +32 -3
- package/dist/index.mjs +50 -4681
- package/icon +21 -0
- package/package.json +8 -3
- package/dist/chunk-Y6FXYEAI.mjs +0 -10
- package/dist/webhook.d.mts +0 -114
- package/dist/webhook.d.ts +0 -114
- package/dist/webhook.js +0 -200
- package/dist/webhook.mjs +0 -175
|
@@ -0,0 +1,4762 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/realtime.ts
|
|
10
|
+
var TypedEmitter = class {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
13
|
+
}
|
|
14
|
+
on(event, cb) {
|
|
15
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
16
|
+
this.listeners.get(event).add(cb);
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
off(event, cb) {
|
|
20
|
+
this.listeners.get(event)?.delete(cb);
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
once(event, cb) {
|
|
24
|
+
const wrapper = (payload) => {
|
|
25
|
+
this.off(event, wrapper);
|
|
26
|
+
cb(payload);
|
|
27
|
+
};
|
|
28
|
+
return this.on(event, wrapper);
|
|
29
|
+
}
|
|
30
|
+
emit(event, payload) {
|
|
31
|
+
const set = this.listeners.get(event);
|
|
32
|
+
if (set) {
|
|
33
|
+
for (const cb of set) {
|
|
34
|
+
try {
|
|
35
|
+
cb(payload);
|
|
36
|
+
} catch (_) {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
removeAllListeners() {
|
|
42
|
+
this.listeners.clear();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var Reconnector = class {
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.attempt = 0;
|
|
48
|
+
this.connectedAt = 0;
|
|
49
|
+
this.baseDelay = config.reconnectBaseDelay ?? 1e3;
|
|
50
|
+
this.maxDelay = config.reconnectMaxDelay ?? 3e4;
|
|
51
|
+
this.maxAttempts = config.maxReconnectAttempts ?? 10;
|
|
52
|
+
}
|
|
53
|
+
get shouldReconnect() {
|
|
54
|
+
return this.maxAttempts === 0 || this.attempt < this.maxAttempts;
|
|
55
|
+
}
|
|
56
|
+
get currentAttempt() {
|
|
57
|
+
return this.attempt;
|
|
58
|
+
}
|
|
59
|
+
markConnected() {
|
|
60
|
+
this.connectedAt = Date.now();
|
|
61
|
+
}
|
|
62
|
+
nextDelay() {
|
|
63
|
+
if (this.connectedAt > 0 && Date.now() - this.connectedAt > 6e4) {
|
|
64
|
+
this.attempt = 0;
|
|
65
|
+
}
|
|
66
|
+
const jitter = Math.random() * this.baseDelay * 0.5;
|
|
67
|
+
const delay = Math.min(this.baseDelay * Math.pow(2, this.attempt) + jitter, this.maxDelay);
|
|
68
|
+
this.attempt++;
|
|
69
|
+
return delay;
|
|
70
|
+
}
|
|
71
|
+
reset() {
|
|
72
|
+
this.attempt = 0;
|
|
73
|
+
this.connectedAt = 0;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var RealtimeWSClient = class extends TypedEmitter {
|
|
77
|
+
constructor(baseUrl, config) {
|
|
78
|
+
super();
|
|
79
|
+
this.ws = null;
|
|
80
|
+
this.heartbeatTimer = null;
|
|
81
|
+
this.pongTimer = null;
|
|
82
|
+
this.reconnectTimer = null;
|
|
83
|
+
this.pendingPings = /* @__PURE__ */ new Map();
|
|
84
|
+
this._state = "disconnected";
|
|
85
|
+
this.intentionalClose = false;
|
|
86
|
+
this.pingCounter = 0;
|
|
87
|
+
this.handleMessage = (ev) => {
|
|
88
|
+
try {
|
|
89
|
+
const msg = JSON.parse(typeof ev.data === "string" ? ev.data : ev.data.toString());
|
|
90
|
+
const { type, payload } = msg;
|
|
91
|
+
if (type === "pong" && payload?.requestId) {
|
|
92
|
+
const pending = this.pendingPings.get(payload.requestId);
|
|
93
|
+
if (pending) {
|
|
94
|
+
clearTimeout(pending.timer);
|
|
95
|
+
pending.resolve(payload);
|
|
96
|
+
this.pendingPings.delete(payload.requestId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.emit(type, payload);
|
|
100
|
+
} catch (_) {
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
this.handleClose = (ev) => {
|
|
104
|
+
this.stopHeartbeat();
|
|
105
|
+
this.clearPendingPings();
|
|
106
|
+
this.ws = null;
|
|
107
|
+
if (this.intentionalClose) return;
|
|
108
|
+
this._state = "disconnected";
|
|
109
|
+
this.emit("disconnected", { code: ev.code, reason: ev.reason });
|
|
110
|
+
if (this.config.autoReconnect && this.reconnector.shouldReconnect) {
|
|
111
|
+
this.scheduleReconnect();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const base = baseUrl.replace(/^http/, "ws");
|
|
115
|
+
this.wsUrl = `${base}/ws?token=${config.token}`;
|
|
116
|
+
this.config = {
|
|
117
|
+
autoReconnect: true,
|
|
118
|
+
heartbeatInterval: 25e3,
|
|
119
|
+
...config
|
|
120
|
+
};
|
|
121
|
+
this.reconnector = new Reconnector(config);
|
|
122
|
+
this.WS = config.WebSocket || WebSocket;
|
|
123
|
+
}
|
|
124
|
+
get state() {
|
|
125
|
+
return this._state;
|
|
126
|
+
}
|
|
127
|
+
async connect() {
|
|
128
|
+
if (this._state === "connected" || this._state === "connecting") return;
|
|
129
|
+
this._state = "connecting";
|
|
130
|
+
this.intentionalClose = false;
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
try {
|
|
133
|
+
this.ws = new this.WS(this.wsUrl);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
this._state = "disconnected";
|
|
136
|
+
reject(err);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const onOpen = () => {
|
|
140
|
+
cleanup();
|
|
141
|
+
};
|
|
142
|
+
const onFirstMessage = (ev) => {
|
|
143
|
+
try {
|
|
144
|
+
const msg = JSON.parse(typeof ev.data === "string" ? ev.data : ev.data.toString());
|
|
145
|
+
if (msg.type === "authenticated") {
|
|
146
|
+
this._state = "connected";
|
|
147
|
+
this.reconnector.markConnected();
|
|
148
|
+
this.startHeartbeat();
|
|
149
|
+
this.emit("authenticated", msg.payload);
|
|
150
|
+
this.emit("connected", void 0);
|
|
151
|
+
this.ws.removeEventListener("message", onFirstMessage);
|
|
152
|
+
this.ws.addEventListener("message", this.handleMessage);
|
|
153
|
+
resolve();
|
|
154
|
+
}
|
|
155
|
+
} catch (_) {
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const onError = (ev) => {
|
|
159
|
+
cleanup();
|
|
160
|
+
if (this._state === "connecting") {
|
|
161
|
+
this._state = "disconnected";
|
|
162
|
+
reject(new Error("WebSocket connection failed"));
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const onClose = (ev) => {
|
|
166
|
+
cleanup();
|
|
167
|
+
if (this._state === "connecting") {
|
|
168
|
+
this._state = "disconnected";
|
|
169
|
+
reject(new Error(`WebSocket closed during connect: ${ev.code} ${ev.reason}`));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const cleanup = () => {
|
|
173
|
+
this.ws?.removeEventListener("error", onError);
|
|
174
|
+
this.ws?.removeEventListener("close", onClose);
|
|
175
|
+
};
|
|
176
|
+
this.ws.addEventListener("open", onOpen);
|
|
177
|
+
this.ws.addEventListener("message", onFirstMessage);
|
|
178
|
+
this.ws.addEventListener("error", onError);
|
|
179
|
+
this.ws.addEventListener("close", onClose);
|
|
180
|
+
this.ws.addEventListener("close", this.handleClose);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
disconnect(code = 1e3, reason = "client disconnect") {
|
|
184
|
+
this.intentionalClose = true;
|
|
185
|
+
this.stopHeartbeat();
|
|
186
|
+
this.clearReconnectTimer();
|
|
187
|
+
this.clearPendingPings();
|
|
188
|
+
if (this.ws) {
|
|
189
|
+
this.ws.removeEventListener("message", this.handleMessage);
|
|
190
|
+
this.ws.removeEventListener("close", this.handleClose);
|
|
191
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
192
|
+
this.ws.close(code, reason);
|
|
193
|
+
}
|
|
194
|
+
this.ws = null;
|
|
195
|
+
}
|
|
196
|
+
this._state = "disconnected";
|
|
197
|
+
this.emit("disconnected", { code, reason });
|
|
198
|
+
}
|
|
199
|
+
// --- Commands ---
|
|
200
|
+
joinConversation(conversationId) {
|
|
201
|
+
this.sendRaw({ type: "conversation.join", payload: { conversationId } });
|
|
202
|
+
}
|
|
203
|
+
sendMessage(conversationId, content, options) {
|
|
204
|
+
const opts = typeof options === "string" ? { type: options } : options;
|
|
205
|
+
this.sendRaw({
|
|
206
|
+
type: "message.send",
|
|
207
|
+
payload: { conversationId, content, type: opts?.type ?? "text", ...opts?.metadata ? { metadata: opts.metadata } : {}, ...opts?.parentId ? { parentId: opts.parentId } : {} },
|
|
208
|
+
requestId: `msg-${++this.pingCounter}`
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
startTyping(conversationId) {
|
|
212
|
+
this.sendRaw({ type: "typing.start", payload: { conversationId } });
|
|
213
|
+
}
|
|
214
|
+
stopTyping(conversationId) {
|
|
215
|
+
this.sendRaw({ type: "typing.stop", payload: { conversationId } });
|
|
216
|
+
}
|
|
217
|
+
updatePresence(status) {
|
|
218
|
+
this.sendRaw({ type: "presence.update", payload: { status } });
|
|
219
|
+
}
|
|
220
|
+
send(command) {
|
|
221
|
+
this.sendRaw(command);
|
|
222
|
+
}
|
|
223
|
+
ping() {
|
|
224
|
+
const requestId = `ping-${++this.pingCounter}`;
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
this.pendingPings.delete(requestId);
|
|
228
|
+
reject(new Error("Ping timeout"));
|
|
229
|
+
}, 1e4);
|
|
230
|
+
this.pendingPings.set(requestId, { resolve, timer });
|
|
231
|
+
this.sendRaw({ type: "ping", payload: { requestId } });
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// --- Internal ---
|
|
235
|
+
sendRaw(data) {
|
|
236
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
237
|
+
this.ws.send(JSON.stringify(data));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
scheduleReconnect() {
|
|
241
|
+
const delay = this.reconnector.nextDelay();
|
|
242
|
+
this._state = "reconnecting";
|
|
243
|
+
this.emit("reconnecting", { attempt: this.reconnector.currentAttempt, delayMs: delay });
|
|
244
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
245
|
+
try {
|
|
246
|
+
await this.connect();
|
|
247
|
+
} catch (_) {
|
|
248
|
+
if (this.config.autoReconnect && this.reconnector.shouldReconnect) {
|
|
249
|
+
this.scheduleReconnect();
|
|
250
|
+
} else {
|
|
251
|
+
this._state = "disconnected";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}, delay);
|
|
255
|
+
}
|
|
256
|
+
startHeartbeat() {
|
|
257
|
+
this.stopHeartbeat();
|
|
258
|
+
this.heartbeatTimer = setInterval(() => {
|
|
259
|
+
if (this._state !== "connected") return;
|
|
260
|
+
const requestId = `hb-${++this.pingCounter}`;
|
|
261
|
+
this.sendRaw({ type: "ping", payload: { requestId } });
|
|
262
|
+
this.pongTimer = setTimeout(() => {
|
|
263
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
264
|
+
this.ws.close(4e3, "heartbeat timeout");
|
|
265
|
+
}
|
|
266
|
+
}, 1e4);
|
|
267
|
+
const onPong = (payload) => {
|
|
268
|
+
if (this.pongTimer) {
|
|
269
|
+
clearTimeout(this.pongTimer);
|
|
270
|
+
this.pongTimer = null;
|
|
271
|
+
}
|
|
272
|
+
this.off("pong", onPong);
|
|
273
|
+
};
|
|
274
|
+
this.on("pong", onPong);
|
|
275
|
+
}, this.config.heartbeatInterval);
|
|
276
|
+
}
|
|
277
|
+
stopHeartbeat() {
|
|
278
|
+
if (this.heartbeatTimer) {
|
|
279
|
+
clearInterval(this.heartbeatTimer);
|
|
280
|
+
this.heartbeatTimer = null;
|
|
281
|
+
}
|
|
282
|
+
if (this.pongTimer) {
|
|
283
|
+
clearTimeout(this.pongTimer);
|
|
284
|
+
this.pongTimer = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
clearReconnectTimer() {
|
|
288
|
+
if (this.reconnectTimer) {
|
|
289
|
+
clearTimeout(this.reconnectTimer);
|
|
290
|
+
this.reconnectTimer = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
clearPendingPings() {
|
|
294
|
+
for (const [, { timer }] of this.pendingPings) {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
}
|
|
297
|
+
this.pendingPings.clear();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
var RealtimeSSEClient = class extends TypedEmitter {
|
|
301
|
+
constructor(baseUrl, config) {
|
|
302
|
+
super();
|
|
303
|
+
this.abortController = null;
|
|
304
|
+
this.reconnectTimer = null;
|
|
305
|
+
this.heartbeatWatchdog = null;
|
|
306
|
+
this.lastDataTime = 0;
|
|
307
|
+
this._state = "disconnected";
|
|
308
|
+
this.intentionalClose = false;
|
|
309
|
+
this.sseUrl = `${baseUrl}/sse?token=${config.token}`;
|
|
310
|
+
this.config = {
|
|
311
|
+
autoReconnect: true,
|
|
312
|
+
...config
|
|
313
|
+
};
|
|
314
|
+
this.reconnector = new Reconnector(config);
|
|
315
|
+
this.fetchFn = config.fetch || fetch;
|
|
316
|
+
}
|
|
317
|
+
get state() {
|
|
318
|
+
return this._state;
|
|
319
|
+
}
|
|
320
|
+
async connect() {
|
|
321
|
+
if (this._state === "connected" || this._state === "connecting") return;
|
|
322
|
+
this._state = "connecting";
|
|
323
|
+
this.intentionalClose = false;
|
|
324
|
+
this.abortController = new AbortController();
|
|
325
|
+
const response = await this.fetchFn(this.sseUrl, {
|
|
326
|
+
headers: { "Accept": "text/event-stream" },
|
|
327
|
+
signal: this.abortController.signal
|
|
328
|
+
});
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
this._state = "disconnected";
|
|
331
|
+
throw new Error(`SSE connection failed: ${response.status}`);
|
|
332
|
+
}
|
|
333
|
+
if (!response.body) {
|
|
334
|
+
this._state = "disconnected";
|
|
335
|
+
throw new Error("SSE response has no body");
|
|
336
|
+
}
|
|
337
|
+
this._state = "connected";
|
|
338
|
+
this.reconnector.markConnected();
|
|
339
|
+
this.lastDataTime = Date.now();
|
|
340
|
+
this.startHeartbeatWatchdog();
|
|
341
|
+
this.emit("connected", void 0);
|
|
342
|
+
this.readStream(response.body);
|
|
343
|
+
}
|
|
344
|
+
disconnect() {
|
|
345
|
+
this.intentionalClose = true;
|
|
346
|
+
this.stopHeartbeatWatchdog();
|
|
347
|
+
this.clearReconnectTimer();
|
|
348
|
+
if (this.abortController) {
|
|
349
|
+
this.abortController.abort();
|
|
350
|
+
this.abortController = null;
|
|
351
|
+
}
|
|
352
|
+
this._state = "disconnected";
|
|
353
|
+
this.emit("disconnected", { code: 1e3, reason: "client disconnect" });
|
|
354
|
+
}
|
|
355
|
+
// --- Internal ---
|
|
356
|
+
async readStream(body) {
|
|
357
|
+
const reader = body.getReader();
|
|
358
|
+
const decoder = new TextDecoder();
|
|
359
|
+
let buffer = "";
|
|
360
|
+
try {
|
|
361
|
+
while (true) {
|
|
362
|
+
const { done, value } = await reader.read();
|
|
363
|
+
if (done) break;
|
|
364
|
+
buffer += decoder.decode(value, { stream: true });
|
|
365
|
+
const lines = buffer.split("\n");
|
|
366
|
+
buffer = lines.pop() || "";
|
|
367
|
+
for (const line of lines) {
|
|
368
|
+
this.lastDataTime = Date.now();
|
|
369
|
+
if (line.startsWith(":")) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (line.startsWith("data: ")) {
|
|
373
|
+
const jsonStr = line.slice(6);
|
|
374
|
+
try {
|
|
375
|
+
const msg = JSON.parse(jsonStr);
|
|
376
|
+
this.emit(msg.type, msg.payload);
|
|
377
|
+
} catch (_) {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (this.intentionalClose) return;
|
|
384
|
+
} finally {
|
|
385
|
+
reader.releaseLock();
|
|
386
|
+
}
|
|
387
|
+
if (this.intentionalClose) return;
|
|
388
|
+
this._state = "disconnected";
|
|
389
|
+
this.stopHeartbeatWatchdog();
|
|
390
|
+
this.emit("disconnected", { code: 0, reason: "stream ended" });
|
|
391
|
+
if (this.config.autoReconnect && this.reconnector.shouldReconnect) {
|
|
392
|
+
this.scheduleReconnect();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
scheduleReconnect() {
|
|
396
|
+
const delay = this.reconnector.nextDelay();
|
|
397
|
+
this._state = "reconnecting";
|
|
398
|
+
this.emit("reconnecting", { attempt: this.reconnector.currentAttempt, delayMs: delay });
|
|
399
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
400
|
+
try {
|
|
401
|
+
await this.connect();
|
|
402
|
+
} catch (_) {
|
|
403
|
+
if (this.config.autoReconnect && this.reconnector.shouldReconnect) {
|
|
404
|
+
this.scheduleReconnect();
|
|
405
|
+
} else {
|
|
406
|
+
this._state = "disconnected";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}, delay);
|
|
410
|
+
}
|
|
411
|
+
startHeartbeatWatchdog() {
|
|
412
|
+
this.stopHeartbeatWatchdog();
|
|
413
|
+
this.heartbeatWatchdog = setInterval(() => {
|
|
414
|
+
if (Date.now() - this.lastDataTime > 45e3) {
|
|
415
|
+
this.stopHeartbeatWatchdog();
|
|
416
|
+
if (this.abortController) {
|
|
417
|
+
this.abortController.abort();
|
|
418
|
+
this.abortController = null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}, 15e3);
|
|
422
|
+
}
|
|
423
|
+
stopHeartbeatWatchdog() {
|
|
424
|
+
if (this.heartbeatWatchdog) {
|
|
425
|
+
clearInterval(this.heartbeatWatchdog);
|
|
426
|
+
this.heartbeatWatchdog = null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
clearReconnectTimer() {
|
|
430
|
+
if (this.reconnectTimer) {
|
|
431
|
+
clearTimeout(this.reconnectTimer);
|
|
432
|
+
this.reconnectTimer = null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/offline.ts
|
|
438
|
+
var OfflineEmitter = class {
|
|
439
|
+
constructor() {
|
|
440
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
441
|
+
}
|
|
442
|
+
on(event, cb) {
|
|
443
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
444
|
+
this.listeners.get(event).add(cb);
|
|
445
|
+
return this;
|
|
446
|
+
}
|
|
447
|
+
off(event, cb) {
|
|
448
|
+
this.listeners.get(event)?.delete(cb);
|
|
449
|
+
return this;
|
|
450
|
+
}
|
|
451
|
+
emit(event, payload) {
|
|
452
|
+
const set = this.listeners.get(event);
|
|
453
|
+
if (set) for (const cb of set) {
|
|
454
|
+
try {
|
|
455
|
+
cb(payload);
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
removeAllListeners() {
|
|
461
|
+
this.listeners.clear();
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
function generateId() {
|
|
465
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
466
|
+
return crypto.randomUUID();
|
|
467
|
+
}
|
|
468
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
469
|
+
const r = Math.random() * 16 | 0;
|
|
470
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
var WRITE_PATTERNS = [
|
|
474
|
+
{ method: "POST", pattern: /\/api\/im\/(messages|direct|groups)\//, opType: "message.send" },
|
|
475
|
+
{ method: "PATCH", pattern: /\/api\/im\/messages\//, opType: "message.edit" },
|
|
476
|
+
{ method: "DELETE", pattern: /\/api\/im\/messages\//, opType: "message.delete" },
|
|
477
|
+
{ method: "POST", pattern: /\/api\/im\/conversations\/[^/]+\/read/, opType: "conversation.read" },
|
|
478
|
+
// v1.8.0 Community — queued when offline-first IM is enabled
|
|
479
|
+
{ method: "POST", pattern: /\/api\/im\/community\/posts$/, opType: "community_post" },
|
|
480
|
+
{ method: "POST", pattern: /\/api\/im\/community\/posts\/[^/]+\/comments$/, opType: "community_comment" },
|
|
481
|
+
{ method: "POST", pattern: /\/api\/im\/community\/vote$/, opType: "community_vote" }
|
|
482
|
+
];
|
|
483
|
+
function matchWriteOp(method, path) {
|
|
484
|
+
for (const { method: m, pattern, opType } of WRITE_PATTERNS) {
|
|
485
|
+
if (method === m && pattern.test(path)) return opType;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
var OfflineManager = class extends OfflineEmitter {
|
|
490
|
+
constructor(storage, networkRequest, options = {}) {
|
|
491
|
+
super();
|
|
492
|
+
this.flushTimer = null;
|
|
493
|
+
this.flushing = false;
|
|
494
|
+
this._isOnline = true;
|
|
495
|
+
this._syncState = "idle";
|
|
496
|
+
this.sseSource = null;
|
|
497
|
+
this.sseReconnectTimer = null;
|
|
498
|
+
this.sseReconnectAttempts = 0;
|
|
499
|
+
/** Presence cache for realtime presence events */
|
|
500
|
+
this.presenceCache = /* @__PURE__ */ new Map();
|
|
501
|
+
this.storage = storage;
|
|
502
|
+
this.networkRequest = networkRequest;
|
|
503
|
+
this.options = {
|
|
504
|
+
syncOnConnect: options.syncOnConnect ?? true,
|
|
505
|
+
outboxRetryLimit: options.outboxRetryLimit ?? 5,
|
|
506
|
+
outboxFlushInterval: options.outboxFlushInterval ?? 1e3,
|
|
507
|
+
conflictStrategy: options.conflictStrategy ?? "server",
|
|
508
|
+
onConflict: options.onConflict,
|
|
509
|
+
syncMode: options.syncMode ?? "push",
|
|
510
|
+
quota: options.quota ? {
|
|
511
|
+
maxStorageBytes: options.quota.maxStorageBytes ?? 500 * 1024 * 1024,
|
|
512
|
+
warningThreshold: options.quota.warningThreshold ?? 0.9
|
|
513
|
+
} : void 0
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
get isOnline() {
|
|
517
|
+
return this._isOnline;
|
|
518
|
+
}
|
|
519
|
+
get syncState() {
|
|
520
|
+
return this._syncState;
|
|
521
|
+
}
|
|
522
|
+
async init() {
|
|
523
|
+
await this.storage.init();
|
|
524
|
+
this.startFlushTimer();
|
|
525
|
+
}
|
|
526
|
+
async destroy() {
|
|
527
|
+
this.stopFlushTimer();
|
|
528
|
+
this.stopContinuousSync();
|
|
529
|
+
this.removeAllListeners();
|
|
530
|
+
}
|
|
531
|
+
// ── Network state ─────────────────────────────────────────
|
|
532
|
+
setOnline(online) {
|
|
533
|
+
if (this._isOnline === online) return;
|
|
534
|
+
this._isOnline = online;
|
|
535
|
+
this.emit(online ? "network.online" : "network.offline", void 0);
|
|
536
|
+
if (online) {
|
|
537
|
+
this.flush();
|
|
538
|
+
if (this.options.syncOnConnect) {
|
|
539
|
+
if (this.options.syncMode === "push") {
|
|
540
|
+
this.startContinuousSync();
|
|
541
|
+
} else {
|
|
542
|
+
this.sync();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
this.stopContinuousSync();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// ── Request dispatch ──────────────────────────────────────
|
|
550
|
+
/**
|
|
551
|
+
* Dispatch an IM request. Write ops go through outbox; reads check local cache.
|
|
552
|
+
*/
|
|
553
|
+
async dispatch(method, path, body, query) {
|
|
554
|
+
const opType = matchWriteOp(method, path);
|
|
555
|
+
if (opType) {
|
|
556
|
+
return this.dispatchWrite(opType, method, path, body, query);
|
|
557
|
+
}
|
|
558
|
+
if (method === "GET") {
|
|
559
|
+
const cached = await this.readFromCache(path, query);
|
|
560
|
+
if (cached !== null) return cached;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const result = await this.networkRequest(method, path, body, query);
|
|
564
|
+
if (method === "GET") this.cacheReadResult(path, query, result);
|
|
565
|
+
return result;
|
|
566
|
+
} catch {
|
|
567
|
+
if (!this._isOnline) {
|
|
568
|
+
return { ok: true, data: [] };
|
|
569
|
+
}
|
|
570
|
+
throw new Error("Network request failed");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// ── Outbox: write operations ──────────────────────────────
|
|
574
|
+
async dispatchWrite(opType, method, path, body, query) {
|
|
575
|
+
const clientId = generateId();
|
|
576
|
+
const idempotencyKey = `sdk-${clientId}`;
|
|
577
|
+
let enrichedBody = body;
|
|
578
|
+
if (body && typeof body === "object" && (opType === "message.send" || opType === "message.edit")) {
|
|
579
|
+
enrichedBody = { ...body };
|
|
580
|
+
enrichedBody.metadata = {
|
|
581
|
+
...body.metadata,
|
|
582
|
+
_idempotencyKey: idempotencyKey
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
let localMessage;
|
|
586
|
+
if (opType === "message.send" && body && typeof body === "object") {
|
|
587
|
+
const b = body;
|
|
588
|
+
const convIdMatch = path.match(/\/(?:messages|direct|groups)\/([^/]+)/);
|
|
589
|
+
const conversationId = convIdMatch?.[1] ?? "";
|
|
590
|
+
localMessage = {
|
|
591
|
+
id: `local-${clientId}`,
|
|
592
|
+
clientId,
|
|
593
|
+
conversationId,
|
|
594
|
+
content: b.content ?? "",
|
|
595
|
+
type: b.type ?? "text",
|
|
596
|
+
senderId: "__self__",
|
|
597
|
+
parentId: b.parentId ?? null,
|
|
598
|
+
status: "pending",
|
|
599
|
+
metadata: b.metadata,
|
|
600
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
601
|
+
};
|
|
602
|
+
await this.storage.putMessages([localMessage]);
|
|
603
|
+
this.emit("message.local", localMessage);
|
|
604
|
+
}
|
|
605
|
+
const op = {
|
|
606
|
+
id: clientId,
|
|
607
|
+
type: opType,
|
|
608
|
+
method,
|
|
609
|
+
path,
|
|
610
|
+
body: enrichedBody,
|
|
611
|
+
query,
|
|
612
|
+
status: "pending",
|
|
613
|
+
createdAt: Date.now(),
|
|
614
|
+
retries: 0,
|
|
615
|
+
maxRetries: this.options.outboxRetryLimit,
|
|
616
|
+
idempotencyKey,
|
|
617
|
+
localData: localMessage
|
|
618
|
+
};
|
|
619
|
+
await this.storage.enqueue(op);
|
|
620
|
+
if (this._isOnline) this.flush();
|
|
621
|
+
const optimisticResult = {
|
|
622
|
+
ok: true,
|
|
623
|
+
data: localMessage ? { conversationId: localMessage.conversationId, message: localMessage } : void 0,
|
|
624
|
+
_pending: true,
|
|
625
|
+
_clientId: clientId
|
|
626
|
+
};
|
|
627
|
+
return optimisticResult;
|
|
628
|
+
}
|
|
629
|
+
// ── Outbox flush ──────────────────────────────────────────
|
|
630
|
+
startFlushTimer() {
|
|
631
|
+
this.stopFlushTimer();
|
|
632
|
+
this.flushTimer = setInterval(() => this.flush(), this.options.outboxFlushInterval);
|
|
633
|
+
}
|
|
634
|
+
stopFlushTimer() {
|
|
635
|
+
if (this.flushTimer) {
|
|
636
|
+
clearInterval(this.flushTimer);
|
|
637
|
+
this.flushTimer = null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async flush() {
|
|
641
|
+
if (this.flushing || !this._isOnline) return;
|
|
642
|
+
this.flushing = true;
|
|
643
|
+
try {
|
|
644
|
+
const ops = await this.storage.dequeueReady(10);
|
|
645
|
+
for (const op of ops) {
|
|
646
|
+
this.emit("outbox.sending", { opId: op.id, type: op.type });
|
|
647
|
+
try {
|
|
648
|
+
const result = await this.networkRequest(
|
|
649
|
+
op.method,
|
|
650
|
+
op.path,
|
|
651
|
+
op.body,
|
|
652
|
+
op.query
|
|
653
|
+
);
|
|
654
|
+
if (result.ok) {
|
|
655
|
+
await this.storage.ack(op.id);
|
|
656
|
+
this.emit("outbox.confirmed", { opId: op.id, serverData: result.data });
|
|
657
|
+
if (op.type === "message.send" && op.localData) {
|
|
658
|
+
const local = op.localData;
|
|
659
|
+
const serverMsg = result.data?.message;
|
|
660
|
+
if (serverMsg) {
|
|
661
|
+
await this.storage.deleteMessage(local.id);
|
|
662
|
+
await this.storage.putMessages([{
|
|
663
|
+
id: serverMsg.id,
|
|
664
|
+
clientId: op.id,
|
|
665
|
+
conversationId: serverMsg.conversationId ?? local.conversationId,
|
|
666
|
+
content: serverMsg.content ?? local.content,
|
|
667
|
+
type: serverMsg.type ?? local.type,
|
|
668
|
+
senderId: serverMsg.senderId ?? local.senderId,
|
|
669
|
+
parentId: serverMsg.parentId,
|
|
670
|
+
status: "confirmed",
|
|
671
|
+
metadata: serverMsg.metadata ? typeof serverMsg.metadata === "string" ? JSON.parse(serverMsg.metadata) : serverMsg.metadata : void 0,
|
|
672
|
+
createdAt: serverMsg.createdAt ?? local.createdAt
|
|
673
|
+
}]);
|
|
674
|
+
this.emit("message.confirmed", { clientId: op.id, serverMessage: serverMsg });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} else {
|
|
678
|
+
const errCode = result.error?.code;
|
|
679
|
+
if (errCode && !errCode.includes("TIMEOUT") && !errCode.includes("NETWORK")) {
|
|
680
|
+
await this.storage.nack(op.id, result.error?.message ?? "Request failed", op.maxRetries);
|
|
681
|
+
this.emit("outbox.failed", { opId: op.id, error: result.error?.message ?? "Request failed", retriesLeft: 0 });
|
|
682
|
+
if (op.type === "message.send") {
|
|
683
|
+
this.emit("message.failed", { clientId: op.id, error: result.error?.message ?? "Request failed" });
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
await this.storage.nack(op.id, result.error?.message ?? "Transient error", op.retries + 1);
|
|
687
|
+
this.emit("outbox.failed", {
|
|
688
|
+
opId: op.id,
|
|
689
|
+
error: result.error?.message ?? "Transient error",
|
|
690
|
+
retriesLeft: op.maxRetries - op.retries - 1
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch (err) {
|
|
695
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
696
|
+
await this.storage.nack(op.id, msg, op.retries + 1);
|
|
697
|
+
if (op.retries + 1 >= op.maxRetries) {
|
|
698
|
+
this.emit("outbox.failed", { opId: op.id, error: msg, retriesLeft: 0 });
|
|
699
|
+
if (op.type === "message.send") {
|
|
700
|
+
this.emit("message.failed", { clientId: op.id, error: msg });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} finally {
|
|
706
|
+
this.flushing = false;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
get outboxSize() {
|
|
710
|
+
return this.storage.getPendingCount();
|
|
711
|
+
}
|
|
712
|
+
// ── Sync engine ───────────────────────────────────────────
|
|
713
|
+
async sync() {
|
|
714
|
+
if (this._syncState === "syncing" || !this._isOnline) return;
|
|
715
|
+
this._syncState = "syncing";
|
|
716
|
+
this.emit("sync.start", void 0);
|
|
717
|
+
let totalNew = 0;
|
|
718
|
+
let totalUpdated = 0;
|
|
719
|
+
try {
|
|
720
|
+
let cursor = await this.storage.getCursor("global_sync") ?? "0";
|
|
721
|
+
let hasMore = true;
|
|
722
|
+
while (hasMore) {
|
|
723
|
+
const result = await this.networkRequest(
|
|
724
|
+
"GET",
|
|
725
|
+
"/api/im/sync",
|
|
726
|
+
void 0,
|
|
727
|
+
{ since: cursor, limit: "100" }
|
|
728
|
+
);
|
|
729
|
+
if (!result.ok || !result.data) {
|
|
730
|
+
throw new Error(result.error?.message ?? "Sync failed");
|
|
731
|
+
}
|
|
732
|
+
const { events, cursor: newCursor, hasMore: more } = result.data;
|
|
733
|
+
for (const event of events) {
|
|
734
|
+
await this.applySyncEvent(event);
|
|
735
|
+
if (event.type === "message.new") totalNew++;
|
|
736
|
+
if (event.type.startsWith("conversation.")) totalUpdated++;
|
|
737
|
+
}
|
|
738
|
+
cursor = String(newCursor);
|
|
739
|
+
await this.storage.setCursor("global_sync", cursor);
|
|
740
|
+
hasMore = more;
|
|
741
|
+
this.emit("sync.progress", { synced: events.length, total: events.length });
|
|
742
|
+
}
|
|
743
|
+
this._syncState = "idle";
|
|
744
|
+
this.emit("sync.complete", { newMessages: totalNew, updatedConversations: totalUpdated });
|
|
745
|
+
} catch (err) {
|
|
746
|
+
this._syncState = "error";
|
|
747
|
+
this.emit("sync.error", {
|
|
748
|
+
error: err instanceof Error ? err.message : "Sync failed",
|
|
749
|
+
willRetry: false
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async applySyncEvent(event) {
|
|
754
|
+
switch (event.type) {
|
|
755
|
+
case "message.new": {
|
|
756
|
+
const msg = event.data;
|
|
757
|
+
await this.storage.putMessages([{
|
|
758
|
+
id: msg.id,
|
|
759
|
+
conversationId: msg.conversationId ?? event.conversationId ?? "",
|
|
760
|
+
content: msg.content ?? "",
|
|
761
|
+
type: msg.type ?? "text",
|
|
762
|
+
senderId: msg.senderId ?? "",
|
|
763
|
+
parentId: msg.parentId ?? null,
|
|
764
|
+
status: "confirmed",
|
|
765
|
+
metadata: msg.metadata,
|
|
766
|
+
createdAt: msg.createdAt ?? event.at,
|
|
767
|
+
syncSeq: event.seq
|
|
768
|
+
}]);
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
case "message.edit": {
|
|
772
|
+
const existing = await this.storage.getMessage(event.data.id);
|
|
773
|
+
if (existing) {
|
|
774
|
+
const hasLocalEdits = existing.status !== "confirmed";
|
|
775
|
+
if (hasLocalEdits && this.options.onConflict) {
|
|
776
|
+
const resolution = this.options.onConflict(existing, event);
|
|
777
|
+
if (resolution === "keep_local") break;
|
|
778
|
+
if (resolution !== "accept_remote" && typeof resolution === "object") {
|
|
779
|
+
resolution.syncSeq = event.seq;
|
|
780
|
+
await this.storage.putMessages([resolution]);
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
existing.content = event.data.content ?? existing.content;
|
|
785
|
+
existing.updatedAt = event.at;
|
|
786
|
+
existing.syncSeq = event.seq;
|
|
787
|
+
await this.storage.putMessages([existing]);
|
|
788
|
+
}
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
case "message.delete": {
|
|
792
|
+
if (event.data?.id) await this.storage.deleteMessage(event.data.id);
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
case "conversation.create":
|
|
796
|
+
case "conversation.update": {
|
|
797
|
+
const conv = event.data;
|
|
798
|
+
await this.storage.putConversations([{
|
|
799
|
+
id: conv.id ?? event.conversationId ?? "",
|
|
800
|
+
type: conv.type ?? "direct",
|
|
801
|
+
title: conv.title,
|
|
802
|
+
unreadCount: conv.unreadCount ?? 0,
|
|
803
|
+
members: conv.members,
|
|
804
|
+
metadata: conv.metadata,
|
|
805
|
+
syncSeq: event.seq,
|
|
806
|
+
updatedAt: event.at,
|
|
807
|
+
lastMessageAt: conv.lastMessageAt
|
|
808
|
+
}]);
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case "conversation.archive": {
|
|
812
|
+
const convId = event.data?.id ?? event.conversationId;
|
|
813
|
+
if (convId) {
|
|
814
|
+
const existing = await this.storage.getConversation(convId);
|
|
815
|
+
if (existing) {
|
|
816
|
+
existing.metadata = { ...existing.metadata, _archived: true };
|
|
817
|
+
existing.syncSeq = event.seq;
|
|
818
|
+
existing.updatedAt = event.at;
|
|
819
|
+
await this.storage.putConversations([existing]);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
case "participant.add": {
|
|
825
|
+
const convId = event.data?.conversationId ?? event.conversationId;
|
|
826
|
+
if (convId) {
|
|
827
|
+
const existing = await this.storage.getConversation(convId);
|
|
828
|
+
if (existing && existing.members) {
|
|
829
|
+
const already = existing.members.find((m) => m.userId === event.data.userId);
|
|
830
|
+
if (!already) {
|
|
831
|
+
existing.members.push({
|
|
832
|
+
userId: event.data.userId,
|
|
833
|
+
username: event.data.username ?? "",
|
|
834
|
+
displayName: event.data.displayName,
|
|
835
|
+
role: event.data.role ?? "member"
|
|
836
|
+
});
|
|
837
|
+
existing.syncSeq = event.seq;
|
|
838
|
+
existing.updatedAt = event.at;
|
|
839
|
+
await this.storage.putConversations([existing]);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
case "participant.remove": {
|
|
846
|
+
const convId = event.data?.conversationId ?? event.conversationId;
|
|
847
|
+
if (convId) {
|
|
848
|
+
const existing = await this.storage.getConversation(convId);
|
|
849
|
+
if (existing && existing.members) {
|
|
850
|
+
existing.members = existing.members.filter((m) => m.userId !== event.data.userId);
|
|
851
|
+
existing.syncSeq = event.seq;
|
|
852
|
+
existing.updatedAt = event.at;
|
|
853
|
+
await this.storage.putConversations([existing]);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Handle a realtime event (from WS/SSE) and store locally.
|
|
862
|
+
*/
|
|
863
|
+
async handleRealtimeEvent(type, payload) {
|
|
864
|
+
if (type === "message.new" && payload) {
|
|
865
|
+
await this.storage.putMessages([{
|
|
866
|
+
id: payload.id,
|
|
867
|
+
conversationId: payload.conversationId ?? "",
|
|
868
|
+
content: payload.content ?? "",
|
|
869
|
+
type: payload.type ?? "text",
|
|
870
|
+
senderId: payload.senderId ?? "",
|
|
871
|
+
parentId: payload.parentId ?? null,
|
|
872
|
+
status: "confirmed",
|
|
873
|
+
metadata: payload.metadata,
|
|
874
|
+
createdAt: payload.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
875
|
+
}]);
|
|
876
|
+
}
|
|
877
|
+
if (type === "presence.changed" && payload?.userId) {
|
|
878
|
+
this.presenceCache.set(payload.userId, {
|
|
879
|
+
status: payload.status ?? "offline",
|
|
880
|
+
lastSeen: payload.lastSeen ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
881
|
+
});
|
|
882
|
+
this.emit("presence.changed", payload);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Get cached presence status for a user.
|
|
887
|
+
*/
|
|
888
|
+
getPresence(userId) {
|
|
889
|
+
return this.presenceCache.get(userId) ?? null;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Search messages in local storage.
|
|
893
|
+
*/
|
|
894
|
+
async searchMessages(query, opts) {
|
|
895
|
+
if (this.storage.searchMessages) {
|
|
896
|
+
return this.storage.searchMessages(query, opts);
|
|
897
|
+
}
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Get storage size and quota info.
|
|
902
|
+
*/
|
|
903
|
+
async getQuotaStatus() {
|
|
904
|
+
const limit = this.options.quota?.maxStorageBytes ?? 500 * 1024 * 1024;
|
|
905
|
+
const threshold = this.options.quota?.warningThreshold ?? 0.9;
|
|
906
|
+
if (this.storage.getStorageSize) {
|
|
907
|
+
const size = await this.storage.getStorageSize();
|
|
908
|
+
const percentage = size.total / limit;
|
|
909
|
+
return {
|
|
910
|
+
used: size.total,
|
|
911
|
+
limit,
|
|
912
|
+
percentage,
|
|
913
|
+
warning: percentage >= threshold,
|
|
914
|
+
exceeded: percentage >= 1
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return { used: 0, limit, percentage: 0, warning: false, exceeded: false };
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Clear old messages for a conversation (user-initiated quota management).
|
|
921
|
+
*/
|
|
922
|
+
async clearOldMessages(conversationId, keepCount) {
|
|
923
|
+
if (this.storage.clearOldMessages) {
|
|
924
|
+
return this.storage.clearOldMessages(conversationId, keepCount);
|
|
925
|
+
}
|
|
926
|
+
return 0;
|
|
927
|
+
}
|
|
928
|
+
// ── Read cache ────────────────────────────────────────────
|
|
929
|
+
async readFromCache(path, query) {
|
|
930
|
+
if (/\/api\/im\/conversations$/.test(path)) {
|
|
931
|
+
const convos = await this.storage.getConversations({ limit: 50 });
|
|
932
|
+
if (convos.length > 0) return { ok: true, data: convos };
|
|
933
|
+
}
|
|
934
|
+
const msgMatch = path.match(/\/api\/im\/messages\/([^/]+)$/);
|
|
935
|
+
if (msgMatch) {
|
|
936
|
+
const convId = msgMatch[1];
|
|
937
|
+
const limit = query?.limit ? parseInt(query.limit) : 50;
|
|
938
|
+
const messages = await this.storage.getMessages(convId, { limit, before: query?.before });
|
|
939
|
+
if (messages.length > 0) return { ok: true, data: messages };
|
|
940
|
+
}
|
|
941
|
+
if (/\/api\/im\/contacts$/.test(path)) {
|
|
942
|
+
const contacts = await this.storage.getContacts();
|
|
943
|
+
if (contacts.length > 0) return { ok: true, data: contacts };
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
async cacheReadResult(path, _query, result) {
|
|
948
|
+
if (!result?.ok || !result?.data) return;
|
|
949
|
+
try {
|
|
950
|
+
if (/\/api\/im\/conversations$/.test(path) && Array.isArray(result.data)) {
|
|
951
|
+
const convos = result.data.map((c) => ({
|
|
952
|
+
id: c.id,
|
|
953
|
+
type: c.type ?? "direct",
|
|
954
|
+
title: c.title,
|
|
955
|
+
lastMessage: c.lastMessage,
|
|
956
|
+
lastMessageAt: c.lastMessageAt ?? c.updatedAt,
|
|
957
|
+
unreadCount: c.unreadCount ?? 0,
|
|
958
|
+
members: c.members,
|
|
959
|
+
metadata: c.metadata,
|
|
960
|
+
updatedAt: c.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
961
|
+
}));
|
|
962
|
+
await this.storage.putConversations(convos);
|
|
963
|
+
}
|
|
964
|
+
const msgMatch = path.match(/\/api\/im\/messages\/([^/]+)$/);
|
|
965
|
+
if (msgMatch && Array.isArray(result.data)) {
|
|
966
|
+
const messages = result.data.map((m) => ({
|
|
967
|
+
id: m.id,
|
|
968
|
+
conversationId: m.conversationId ?? msgMatch[1],
|
|
969
|
+
content: m.content ?? "",
|
|
970
|
+
type: m.type ?? "text",
|
|
971
|
+
senderId: m.senderId ?? "",
|
|
972
|
+
parentId: m.parentId ?? null,
|
|
973
|
+
status: "confirmed",
|
|
974
|
+
metadata: m.metadata,
|
|
975
|
+
createdAt: m.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
976
|
+
}));
|
|
977
|
+
await this.storage.putMessages(messages);
|
|
978
|
+
}
|
|
979
|
+
if (/\/api\/im\/contacts$/.test(path) && Array.isArray(result.data)) {
|
|
980
|
+
await this.storage.putContacts(result.data);
|
|
981
|
+
}
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// ── SSE continuous sync ────────────────────────────────────
|
|
986
|
+
/**
|
|
987
|
+
* Start continuous sync via SSE (Server-Sent Events).
|
|
988
|
+
* Replaces polling with real-time push when syncMode is 'push'.
|
|
989
|
+
*/
|
|
990
|
+
async startContinuousSync() {
|
|
991
|
+
if (this.sseSource) return;
|
|
992
|
+
if (typeof EventSource === "undefined") {
|
|
993
|
+
return this.sync();
|
|
994
|
+
}
|
|
995
|
+
const token = this.tokenProvider?.();
|
|
996
|
+
if (!token) {
|
|
997
|
+
return this.sync();
|
|
998
|
+
}
|
|
999
|
+
const cursor = await this.storage.getCursor("global_sync") ?? "0";
|
|
1000
|
+
const baseUrl = this.getBaseUrl();
|
|
1001
|
+
const url = `${baseUrl}/api/im/sync/stream?token=${encodeURIComponent(token)}&since=${cursor}`;
|
|
1002
|
+
this._syncState = "syncing";
|
|
1003
|
+
this.emit("sync.start", void 0);
|
|
1004
|
+
this.sseReconnectAttempts = 0;
|
|
1005
|
+
try {
|
|
1006
|
+
this.sseSource = new EventSource(url);
|
|
1007
|
+
let totalNew = 0;
|
|
1008
|
+
let totalUpdated = 0;
|
|
1009
|
+
this.sseSource.addEventListener("sync", async (e) => {
|
|
1010
|
+
try {
|
|
1011
|
+
const event = JSON.parse(e.data);
|
|
1012
|
+
await this.applySyncEvent(event);
|
|
1013
|
+
await this.storage.setCursor("global_sync", String(event.seq));
|
|
1014
|
+
if (event.type === "message.new") totalNew++;
|
|
1015
|
+
if (event.type.startsWith("conversation.")) totalUpdated++;
|
|
1016
|
+
this.emit("sync.progress", { synced: 1, total: 1 });
|
|
1017
|
+
if (this.options.quota) {
|
|
1018
|
+
await this.checkQuota();
|
|
1019
|
+
}
|
|
1020
|
+
} catch {
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
this.sseSource.addEventListener("caught_up", () => {
|
|
1024
|
+
this._syncState = "idle";
|
|
1025
|
+
this.sseReconnectAttempts = 0;
|
|
1026
|
+
this.emit("sync.complete", { newMessages: totalNew, updatedConversations: totalUpdated });
|
|
1027
|
+
totalNew = 0;
|
|
1028
|
+
totalUpdated = 0;
|
|
1029
|
+
});
|
|
1030
|
+
this.sseSource.addEventListener("error", () => {
|
|
1031
|
+
this._syncState = "error";
|
|
1032
|
+
this.emit("sync.error", { error: "SSE connection error", willRetry: true });
|
|
1033
|
+
});
|
|
1034
|
+
this.sseSource.onerror = () => {
|
|
1035
|
+
if (this.sseSource?.readyState === EventSource.CLOSED) {
|
|
1036
|
+
this.sseSource = null;
|
|
1037
|
+
this._syncState = "error";
|
|
1038
|
+
this.scheduleSseReconnect();
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
this._syncState = "error";
|
|
1043
|
+
this.emit("sync.error", {
|
|
1044
|
+
error: err instanceof Error ? err.message : "SSE init failed",
|
|
1045
|
+
willRetry: true
|
|
1046
|
+
});
|
|
1047
|
+
this.scheduleSseReconnect();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Stop the SSE continuous sync connection.
|
|
1052
|
+
*/
|
|
1053
|
+
stopContinuousSync() {
|
|
1054
|
+
if (this.sseSource) {
|
|
1055
|
+
this.sseSource.close();
|
|
1056
|
+
this.sseSource = null;
|
|
1057
|
+
}
|
|
1058
|
+
if (this.sseReconnectTimer) {
|
|
1059
|
+
clearTimeout(this.sseReconnectTimer);
|
|
1060
|
+
this.sseReconnectTimer = null;
|
|
1061
|
+
}
|
|
1062
|
+
this._syncState = "idle";
|
|
1063
|
+
}
|
|
1064
|
+
scheduleSseReconnect() {
|
|
1065
|
+
if (!this._isOnline) return;
|
|
1066
|
+
this.sseReconnectAttempts++;
|
|
1067
|
+
const delay = Math.min(1e3 * Math.pow(2, this.sseReconnectAttempts - 1), 3e4);
|
|
1068
|
+
this.sseReconnectTimer = setTimeout(() => {
|
|
1069
|
+
this.sseReconnectTimer = null;
|
|
1070
|
+
if (this._isOnline) this.startContinuousSync();
|
|
1071
|
+
}, delay);
|
|
1072
|
+
}
|
|
1073
|
+
/** Get the base URL for SSE connections (strip /api/im prefix). */
|
|
1074
|
+
getBaseUrl() {
|
|
1075
|
+
return typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
|
|
1076
|
+
}
|
|
1077
|
+
// ── Quota check ─────────────────────────────────────────────
|
|
1078
|
+
async checkQuota() {
|
|
1079
|
+
if (!this.options.quota || !this.storage.getStorageSize) return;
|
|
1080
|
+
const size = await this.storage.getStorageSize();
|
|
1081
|
+
const limit = this.options.quota.maxStorageBytes;
|
|
1082
|
+
const threshold = this.options.quota.warningThreshold;
|
|
1083
|
+
const pct = size.total / limit;
|
|
1084
|
+
if (pct >= 1) {
|
|
1085
|
+
this.emit("quota.exceeded", { used: size.total, limit });
|
|
1086
|
+
} else if (pct >= threshold) {
|
|
1087
|
+
this.emit("quota.warning", { used: size.total, limit, percentage: pct });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
var AttachmentQueue = class {
|
|
1092
|
+
constructor(offline, networkRequest) {
|
|
1093
|
+
this.offline = offline;
|
|
1094
|
+
this.networkRequest = networkRequest;
|
|
1095
|
+
this.queue = /* @__PURE__ */ new Map();
|
|
1096
|
+
this.uploading = false;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Queue a file attachment for offline upload.
|
|
1100
|
+
* Returns the queued attachment with a local ID.
|
|
1101
|
+
*/
|
|
1102
|
+
async queueAttachment(conversationId, file, messageContent) {
|
|
1103
|
+
const id = generateId();
|
|
1104
|
+
const attachment = {
|
|
1105
|
+
id,
|
|
1106
|
+
conversationId,
|
|
1107
|
+
file: { name: file.name, size: file.size, type: file.type },
|
|
1108
|
+
data: file.data,
|
|
1109
|
+
status: "pending",
|
|
1110
|
+
progress: 0,
|
|
1111
|
+
messageClientId: generateId(),
|
|
1112
|
+
createdAt: Date.now()
|
|
1113
|
+
};
|
|
1114
|
+
this.queue.set(id, attachment);
|
|
1115
|
+
await this.offline.storage.putMessages([{
|
|
1116
|
+
id: `local-${attachment.messageClientId}`,
|
|
1117
|
+
clientId: attachment.messageClientId,
|
|
1118
|
+
conversationId,
|
|
1119
|
+
content: messageContent ?? `[File: ${file.name}]`,
|
|
1120
|
+
type: "file",
|
|
1121
|
+
senderId: "__self__",
|
|
1122
|
+
status: "pending",
|
|
1123
|
+
metadata: { _attachmentId: id, fileName: file.name, fileSize: file.size },
|
|
1124
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1125
|
+
}]);
|
|
1126
|
+
if (this.offline.isOnline) this.processQueue();
|
|
1127
|
+
return attachment;
|
|
1128
|
+
}
|
|
1129
|
+
/** Process pending uploads. */
|
|
1130
|
+
async processQueue() {
|
|
1131
|
+
if (this.uploading || !this.offline.isOnline) return;
|
|
1132
|
+
this.uploading = true;
|
|
1133
|
+
try {
|
|
1134
|
+
for (const [id, att] of this.queue) {
|
|
1135
|
+
if (att.status !== "pending") continue;
|
|
1136
|
+
att.status = "uploading";
|
|
1137
|
+
try {
|
|
1138
|
+
const presign = await this.networkRequest(
|
|
1139
|
+
"POST",
|
|
1140
|
+
"/api/im/files/presign",
|
|
1141
|
+
{ fileName: att.file.name, fileSize: att.file.size, mimeType: att.file.type }
|
|
1142
|
+
);
|
|
1143
|
+
if (!presign.ok || !presign.data?.uploadUrl) {
|
|
1144
|
+
throw new Error(presign.error?.message ?? "Presign failed");
|
|
1145
|
+
}
|
|
1146
|
+
if (att.data) {
|
|
1147
|
+
await fetch(presign.data.uploadUrl, {
|
|
1148
|
+
method: "PUT",
|
|
1149
|
+
body: att.data,
|
|
1150
|
+
headers: { "Content-Type": att.file.type }
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
att.progress = 80;
|
|
1154
|
+
const confirm = await this.networkRequest(
|
|
1155
|
+
"POST",
|
|
1156
|
+
"/api/im/files/confirm",
|
|
1157
|
+
{ uploadId: presign.data.uploadId }
|
|
1158
|
+
);
|
|
1159
|
+
if (!confirm.ok) {
|
|
1160
|
+
throw new Error(confirm.error?.message ?? "Confirm failed");
|
|
1161
|
+
}
|
|
1162
|
+
att.status = "uploaded";
|
|
1163
|
+
att.progress = 100;
|
|
1164
|
+
await this.networkRequest(
|
|
1165
|
+
"POST",
|
|
1166
|
+
`/api/im/messages/${att.conversationId}`,
|
|
1167
|
+
{
|
|
1168
|
+
type: "file",
|
|
1169
|
+
content: `[File: ${att.file.name}]`,
|
|
1170
|
+
metadata: {
|
|
1171
|
+
fileUrl: confirm.data?.url ?? presign.data.downloadUrl,
|
|
1172
|
+
fileName: att.file.name,
|
|
1173
|
+
fileSize: att.file.size,
|
|
1174
|
+
mimeType: att.file.type,
|
|
1175
|
+
uploadId: presign.data.uploadId
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
await this.offline.storage.deleteMessage(`local-${att.messageClientId}`);
|
|
1180
|
+
this.queue.delete(id);
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
att.status = "failed";
|
|
1183
|
+
att.error = err instanceof Error ? err.message : "Upload failed";
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
} finally {
|
|
1187
|
+
this.uploading = false;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
/** Get all queued attachments. */
|
|
1191
|
+
getQueue() {
|
|
1192
|
+
return Array.from(this.queue.values());
|
|
1193
|
+
}
|
|
1194
|
+
/** Retry a failed attachment upload. */
|
|
1195
|
+
async retry(attachmentId) {
|
|
1196
|
+
const att = this.queue.get(attachmentId);
|
|
1197
|
+
if (att && att.status === "failed") {
|
|
1198
|
+
att.status = "pending";
|
|
1199
|
+
att.error = void 0;
|
|
1200
|
+
if (this.offline.isOnline) this.processQueue();
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
/** Cancel and remove a queued attachment. */
|
|
1204
|
+
async cancel(attachmentId) {
|
|
1205
|
+
const att = this.queue.get(attachmentId);
|
|
1206
|
+
if (att) {
|
|
1207
|
+
await this.offline.storage.deleteMessage(`local-${att.messageClientId}`);
|
|
1208
|
+
this.queue.delete(attachmentId);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
// src/community-hub.ts
|
|
1214
|
+
var CommunityHub = class {
|
|
1215
|
+
constructor(_r, config) {
|
|
1216
|
+
this._r = _r;
|
|
1217
|
+
this.feedCache = /* @__PURE__ */ new Map();
|
|
1218
|
+
this.statsCache = null;
|
|
1219
|
+
this.notifCountCache = null;
|
|
1220
|
+
this.notifCountTTL = 15e3;
|
|
1221
|
+
this.wsUnsubs = [];
|
|
1222
|
+
this.feedTTL = config?.feedTTLMs ?? 3e5;
|
|
1223
|
+
this.statsTTL = config?.statsTTLMs ?? 6e5;
|
|
1224
|
+
}
|
|
1225
|
+
/** Invalidate cached feeds/stats (e.g. after you posted). */
|
|
1226
|
+
invalidateCache(boardId) {
|
|
1227
|
+
if (boardId) this.feedCache.delete(boardId);
|
|
1228
|
+
else this.feedCache.clear();
|
|
1229
|
+
this.statsCache = null;
|
|
1230
|
+
this.notifCountCache = null;
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Subscribe to community.* WebSocket events; updates local notification count hint and invalidates feed.
|
|
1234
|
+
*/
|
|
1235
|
+
attachRealtime(ws) {
|
|
1236
|
+
const onReply = () => {
|
|
1237
|
+
this.notifCountCache = null;
|
|
1238
|
+
this.feedCache.clear();
|
|
1239
|
+
};
|
|
1240
|
+
const types = [
|
|
1241
|
+
"community.reply",
|
|
1242
|
+
"community.vote",
|
|
1243
|
+
"community.answer.accepted",
|
|
1244
|
+
"community.mention"
|
|
1245
|
+
];
|
|
1246
|
+
for (const t of types) {
|
|
1247
|
+
ws.on(t, onReply);
|
|
1248
|
+
this.wsUnsubs.push(() => ws.off(t, onReply));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
detachRealtime() {
|
|
1252
|
+
for (const u of this.wsUnsubs) u();
|
|
1253
|
+
this.wsUnsubs = [];
|
|
1254
|
+
}
|
|
1255
|
+
// ─── Intent (cached reads) ─────────────────────────────────
|
|
1256
|
+
async feed(opts) {
|
|
1257
|
+
const key = opts?.boardId ?? "__all__";
|
|
1258
|
+
const hit = this.feedCache.get(key);
|
|
1259
|
+
if (hit && Date.now() - hit.at < this.feedTTL) {
|
|
1260
|
+
return { ok: true, data: hit.payload };
|
|
1261
|
+
}
|
|
1262
|
+
const res = await this.listPosts({
|
|
1263
|
+
boardId: opts?.boardId,
|
|
1264
|
+
limit: opts?.limit ?? 20,
|
|
1265
|
+
sort: "hot"
|
|
1266
|
+
});
|
|
1267
|
+
if (res.ok && res.data != null) {
|
|
1268
|
+
this.feedCache.set(key, { at: Date.now(), payload: res.data });
|
|
1269
|
+
}
|
|
1270
|
+
return res;
|
|
1271
|
+
}
|
|
1272
|
+
async aggregatedContext(opts) {
|
|
1273
|
+
const [feed, stats, unreadNotifications] = await Promise.all([
|
|
1274
|
+
this.feed({ boardId: opts?.boardId, limit: opts?.feedLimit ?? 15 }),
|
|
1275
|
+
this.statsCached(),
|
|
1276
|
+
this.unreadCountCached()
|
|
1277
|
+
]);
|
|
1278
|
+
return { feed, stats, unreadNotifications };
|
|
1279
|
+
}
|
|
1280
|
+
async statsCached() {
|
|
1281
|
+
if (this.statsCache && Date.now() - this.statsCache.at < this.statsTTL) {
|
|
1282
|
+
return { ok: true, data: this.statsCache.data };
|
|
1283
|
+
}
|
|
1284
|
+
const res = await this.getStats();
|
|
1285
|
+
if (res.ok && res.data != null) {
|
|
1286
|
+
this.statsCache = { at: Date.now(), data: res.data };
|
|
1287
|
+
}
|
|
1288
|
+
return res;
|
|
1289
|
+
}
|
|
1290
|
+
async unreadCountCached() {
|
|
1291
|
+
if (this.notifCountCache && Date.now() - this.notifCountCache.at < this.notifCountTTL) {
|
|
1292
|
+
return { ok: true, data: { unread: this.notifCountCache.count } };
|
|
1293
|
+
}
|
|
1294
|
+
const res = await this.getNotificationCount();
|
|
1295
|
+
const n = res.data?.unread;
|
|
1296
|
+
if (res.ok && typeof n === "number") {
|
|
1297
|
+
this.notifCountCache = { at: Date.now(), count: n };
|
|
1298
|
+
}
|
|
1299
|
+
return res;
|
|
1300
|
+
}
|
|
1301
|
+
/** Helpdesk question shortcut */
|
|
1302
|
+
async ask(title, content, tags) {
|
|
1303
|
+
const res = await this.createPost({
|
|
1304
|
+
boardId: "helpdesk",
|
|
1305
|
+
title,
|
|
1306
|
+
content,
|
|
1307
|
+
postType: "question",
|
|
1308
|
+
tags
|
|
1309
|
+
});
|
|
1310
|
+
if (res.ok) this.invalidateCache("helpdesk");
|
|
1311
|
+
return res;
|
|
1312
|
+
}
|
|
1313
|
+
/** Showcase battle report shortcut */
|
|
1314
|
+
async reportBattle(input) {
|
|
1315
|
+
const res = await this.createPost({
|
|
1316
|
+
boardId: "showcase",
|
|
1317
|
+
title: input.title,
|
|
1318
|
+
content: input.content,
|
|
1319
|
+
postType: "battleReport",
|
|
1320
|
+
tags: input.tags,
|
|
1321
|
+
linkedGeneIds: input.linkedGeneIds,
|
|
1322
|
+
linkedAgentId: input.linkedAgentId
|
|
1323
|
+
});
|
|
1324
|
+
if (res.ok) this.invalidateCache("showcase");
|
|
1325
|
+
return res;
|
|
1326
|
+
}
|
|
1327
|
+
// ─── Notifications & profile (auth) ────────────────────────
|
|
1328
|
+
async getNotifications(opts) {
|
|
1329
|
+
const q = {};
|
|
1330
|
+
if (opts?.unread) q.unread = "true";
|
|
1331
|
+
if (opts?.limit != null) q.limit = String(opts.limit);
|
|
1332
|
+
if (opts?.offset != null) q.offset = String(opts.offset);
|
|
1333
|
+
return this._r("GET", "/api/im/community/notifications", void 0, q);
|
|
1334
|
+
}
|
|
1335
|
+
async markNotificationsRead(notificationId) {
|
|
1336
|
+
const body = notificationId ? { notificationId } : {};
|
|
1337
|
+
return this._r("POST", "/api/im/community/notifications/read", body);
|
|
1338
|
+
}
|
|
1339
|
+
async getNotificationCount() {
|
|
1340
|
+
return this._r("GET", "/api/im/community/notifications/count");
|
|
1341
|
+
}
|
|
1342
|
+
async listBookmarks(opts) {
|
|
1343
|
+
const q = {};
|
|
1344
|
+
if (opts?.cursor) q.cursor = opts.cursor;
|
|
1345
|
+
if (opts?.limit != null) q.limit = String(opts.limit);
|
|
1346
|
+
return this._r("GET", "/api/im/community/bookmarks", void 0, q);
|
|
1347
|
+
}
|
|
1348
|
+
async followToggle(followingId, followingType) {
|
|
1349
|
+
return this._r("POST", "/api/im/community/follow", { followingId, followingType });
|
|
1350
|
+
}
|
|
1351
|
+
async listFollowing(type) {
|
|
1352
|
+
const q = {};
|
|
1353
|
+
if (type) q.type = type;
|
|
1354
|
+
return this._r("GET", "/api/im/community/following", void 0, q);
|
|
1355
|
+
}
|
|
1356
|
+
async listFollowers(userId) {
|
|
1357
|
+
return this._r("GET", `/api/im/community/followers/${encodeURIComponent(userId)}`);
|
|
1358
|
+
}
|
|
1359
|
+
async getProfile(userId) {
|
|
1360
|
+
return this._r("GET", `/api/im/community/profile/${encodeURIComponent(userId)}`);
|
|
1361
|
+
}
|
|
1362
|
+
// ─── REST (same surface as former CommunityClient) ─────────
|
|
1363
|
+
async createPost(input) {
|
|
1364
|
+
return this._r("POST", "/api/im/community/posts", input);
|
|
1365
|
+
}
|
|
1366
|
+
async listPosts(opts) {
|
|
1367
|
+
const query = {};
|
|
1368
|
+
if (opts?.boardId) query.boardId = opts.boardId;
|
|
1369
|
+
if (opts?.sort) query.sort = opts.sort;
|
|
1370
|
+
if (opts?.period) query.period = opts.period;
|
|
1371
|
+
if (opts?.authorType) query.authorType = opts.authorType;
|
|
1372
|
+
if (opts?.cursor) query.cursor = opts.cursor;
|
|
1373
|
+
if (opts?.limit != null) query.limit = String(opts.limit);
|
|
1374
|
+
return this._r("GET", "/api/im/community/posts", void 0, query);
|
|
1375
|
+
}
|
|
1376
|
+
async getPost(postId) {
|
|
1377
|
+
return this._r("GET", `/api/im/community/posts/${encodeURIComponent(postId)}`);
|
|
1378
|
+
}
|
|
1379
|
+
async updatePost(postId, input) {
|
|
1380
|
+
return this._r("PUT", `/api/im/community/posts/${encodeURIComponent(postId)}`, input);
|
|
1381
|
+
}
|
|
1382
|
+
async deletePost(postId) {
|
|
1383
|
+
return this._r("DELETE", `/api/im/community/posts/${encodeURIComponent(postId)}`);
|
|
1384
|
+
}
|
|
1385
|
+
async createComment(postId, input) {
|
|
1386
|
+
return this._r("POST", `/api/im/community/posts/${encodeURIComponent(postId)}/comments`, input);
|
|
1387
|
+
}
|
|
1388
|
+
async listComments(postId, opts) {
|
|
1389
|
+
const query = {};
|
|
1390
|
+
if (opts?.sort) query.sort = opts.sort;
|
|
1391
|
+
if (opts?.cursor) query.cursor = opts.cursor;
|
|
1392
|
+
if (opts?.limit != null) query.limit = String(opts.limit);
|
|
1393
|
+
return this._r("GET", `/api/im/community/posts/${encodeURIComponent(postId)}/comments`, void 0, query);
|
|
1394
|
+
}
|
|
1395
|
+
async markBestAnswer(commentId) {
|
|
1396
|
+
return this._r("POST", `/api/im/community/comments/${encodeURIComponent(commentId)}/best-answer`);
|
|
1397
|
+
}
|
|
1398
|
+
async vote(targetType, targetId, value) {
|
|
1399
|
+
return this._r("POST", "/api/im/community/vote", { targetType, targetId, value });
|
|
1400
|
+
}
|
|
1401
|
+
async bookmark(postId) {
|
|
1402
|
+
return this._r("POST", "/api/im/community/bookmark", { postId });
|
|
1403
|
+
}
|
|
1404
|
+
async search(query, opts) {
|
|
1405
|
+
const q = { q: query };
|
|
1406
|
+
if (opts?.boardId) q.boardId = opts.boardId;
|
|
1407
|
+
if (opts?.sort) q.sort = opts.sort;
|
|
1408
|
+
if (opts?.limit != null) q.limit = String(opts.limit);
|
|
1409
|
+
return this._r("GET", "/api/im/community/search", void 0, q);
|
|
1410
|
+
}
|
|
1411
|
+
async updateComment(commentId, input) {
|
|
1412
|
+
return this._r("PUT", `/api/im/community/comments/${encodeURIComponent(commentId)}`, input);
|
|
1413
|
+
}
|
|
1414
|
+
async deleteComment(commentId) {
|
|
1415
|
+
return this._r("DELETE", `/api/im/community/comments/${encodeURIComponent(commentId)}`);
|
|
1416
|
+
}
|
|
1417
|
+
async getStats() {
|
|
1418
|
+
return this._r("GET", "/api/im/community/stats");
|
|
1419
|
+
}
|
|
1420
|
+
async getTrendingTags(limit) {
|
|
1421
|
+
const query = {};
|
|
1422
|
+
if (limit != null) query.limit = String(limit);
|
|
1423
|
+
return this._r("GET", "/api/im/community/tags/trending", void 0, query);
|
|
1424
|
+
}
|
|
1425
|
+
async getHotPosts(opts) {
|
|
1426
|
+
const query = {};
|
|
1427
|
+
if (opts?.limit != null) query.limit = String(opts.limit);
|
|
1428
|
+
if (opts?.period) query.period = opts.period;
|
|
1429
|
+
return this._r("GET", "/api/im/community/hot", void 0, query);
|
|
1430
|
+
}
|
|
1431
|
+
async searchSuggest(q) {
|
|
1432
|
+
return this._r("GET", "/api/im/community/search/suggest", void 0, { q });
|
|
1433
|
+
}
|
|
1434
|
+
async autocompleteGenes(q, limit) {
|
|
1435
|
+
const query = { q };
|
|
1436
|
+
if (limit != null) query.limit = String(limit);
|
|
1437
|
+
return this._r("GET", "/api/im/community/autocomplete/genes", void 0, query);
|
|
1438
|
+
}
|
|
1439
|
+
async autocompleteSkills(q, limit) {
|
|
1440
|
+
const query = { q };
|
|
1441
|
+
if (limit != null) query.limit = String(limit);
|
|
1442
|
+
return this._r("GET", "/api/im/community/autocomplete/skills", void 0, query);
|
|
1443
|
+
}
|
|
1444
|
+
async createBattleReport(input) {
|
|
1445
|
+
return this.createPost({
|
|
1446
|
+
boardId: "showcase",
|
|
1447
|
+
title: `Battle Report: ${input.agentId}`,
|
|
1448
|
+
content: input.narrative || "Auto-generated battle report",
|
|
1449
|
+
postType: "battleReport",
|
|
1450
|
+
linkedGeneIds: input.geneIds,
|
|
1451
|
+
linkedAgentId: input.agentId
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
async createMilestone(input) {
|
|
1455
|
+
return this.createPost({
|
|
1456
|
+
boardId: "showcase",
|
|
1457
|
+
title: input.title,
|
|
1458
|
+
content: input.content,
|
|
1459
|
+
postType: "milestone",
|
|
1460
|
+
linkedGeneIds: input.geneIds,
|
|
1461
|
+
linkedAgentId: input.agentId,
|
|
1462
|
+
tags: input.tags
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
async createGeneRelease(input) {
|
|
1466
|
+
return this.createPost({
|
|
1467
|
+
boardId: "showcase",
|
|
1468
|
+
title: input.title,
|
|
1469
|
+
content: input.content,
|
|
1470
|
+
postType: "geneRelease",
|
|
1471
|
+
linkedGeneIds: [input.geneId],
|
|
1472
|
+
tags: input.tags
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
// src/aip.ts
|
|
1478
|
+
import {
|
|
1479
|
+
AIPIdentity
|
|
1480
|
+
} from "@prismer/aip-sdk";
|
|
1481
|
+
import {
|
|
1482
|
+
publicKeyToDIDKey,
|
|
1483
|
+
didKeyToPublicKey,
|
|
1484
|
+
validateDIDKey
|
|
1485
|
+
} from "@prismer/aip-sdk";
|
|
1486
|
+
import {
|
|
1487
|
+
buildCredential,
|
|
1488
|
+
buildPresentation,
|
|
1489
|
+
verifyCredential,
|
|
1490
|
+
verifyPresentation
|
|
1491
|
+
} from "@prismer/aip-sdk";
|
|
1492
|
+
import {
|
|
1493
|
+
buildDelegation,
|
|
1494
|
+
buildEphemeralDelegation,
|
|
1495
|
+
verifyDelegation,
|
|
1496
|
+
verifyEphemeralDelegation
|
|
1497
|
+
} from "@prismer/aip-sdk";
|
|
1498
|
+
import { AIPIdentity as AIPIdentity2 } from "@prismer/aip-sdk";
|
|
1499
|
+
|
|
1500
|
+
// src/types.ts
|
|
1501
|
+
var ENVIRONMENTS = {
|
|
1502
|
+
production: "https://prismer.cloud"
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
// src/storage.ts
|
|
1506
|
+
var MemoryStorage = class {
|
|
1507
|
+
constructor() {
|
|
1508
|
+
this.messages = /* @__PURE__ */ new Map();
|
|
1509
|
+
this.conversations = /* @__PURE__ */ new Map();
|
|
1510
|
+
this.contacts = /* @__PURE__ */ new Map();
|
|
1511
|
+
this.cursors = /* @__PURE__ */ new Map();
|
|
1512
|
+
this.outbox = /* @__PURE__ */ new Map();
|
|
1513
|
+
}
|
|
1514
|
+
async init() {
|
|
1515
|
+
}
|
|
1516
|
+
// ── Messages ────────────────────────────────────────────────
|
|
1517
|
+
async putMessages(messages) {
|
|
1518
|
+
for (const m of messages) this.messages.set(m.id, { ...m });
|
|
1519
|
+
}
|
|
1520
|
+
async getMessages(conversationId, opts) {
|
|
1521
|
+
const all = Array.from(this.messages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1522
|
+
if (opts.before) {
|
|
1523
|
+
const idx = all.findIndex((m) => m.id === opts.before);
|
|
1524
|
+
if (idx > 0) return all.slice(Math.max(0, idx - opts.limit), idx);
|
|
1525
|
+
}
|
|
1526
|
+
return all.slice(-opts.limit);
|
|
1527
|
+
}
|
|
1528
|
+
async getMessage(messageId) {
|
|
1529
|
+
return this.messages.get(messageId) ?? null;
|
|
1530
|
+
}
|
|
1531
|
+
async deleteMessage(messageId) {
|
|
1532
|
+
this.messages.delete(messageId);
|
|
1533
|
+
}
|
|
1534
|
+
// ── Conversations ───────────────────────────────────────────
|
|
1535
|
+
async putConversations(conversations) {
|
|
1536
|
+
for (const c of conversations) this.conversations.set(c.id, { ...c });
|
|
1537
|
+
}
|
|
1538
|
+
async getConversations(opts) {
|
|
1539
|
+
const all = Array.from(this.conversations.values()).sort((a, b) => (b.lastMessageAt ?? b.updatedAt).localeCompare(a.lastMessageAt ?? a.updatedAt));
|
|
1540
|
+
const offset = opts?.offset ?? 0;
|
|
1541
|
+
const limit = opts?.limit ?? 50;
|
|
1542
|
+
return all.slice(offset, offset + limit);
|
|
1543
|
+
}
|
|
1544
|
+
async getConversation(id) {
|
|
1545
|
+
return this.conversations.get(id) ?? null;
|
|
1546
|
+
}
|
|
1547
|
+
// ── Contacts ────────────────────────────────────────────────
|
|
1548
|
+
async putContacts(contacts) {
|
|
1549
|
+
for (const c of contacts) this.contacts.set(c.userId, { ...c });
|
|
1550
|
+
}
|
|
1551
|
+
async getContacts() {
|
|
1552
|
+
return Array.from(this.contacts.values());
|
|
1553
|
+
}
|
|
1554
|
+
// ── Cursors ─────────────────────────────────────────────────
|
|
1555
|
+
async getCursor(key) {
|
|
1556
|
+
return this.cursors.get(key) ?? null;
|
|
1557
|
+
}
|
|
1558
|
+
async setCursor(key, value) {
|
|
1559
|
+
this.cursors.set(key, value);
|
|
1560
|
+
}
|
|
1561
|
+
// ── Outbox ──────────────────────────────────────────────────
|
|
1562
|
+
async enqueue(op) {
|
|
1563
|
+
this.outbox.set(op.id, { ...op });
|
|
1564
|
+
}
|
|
1565
|
+
async dequeueReady(limit) {
|
|
1566
|
+
const ready = Array.from(this.outbox.values()).filter((op) => op.status === "pending").sort((a, b) => a.createdAt - b.createdAt).slice(0, limit);
|
|
1567
|
+
for (const op of ready) {
|
|
1568
|
+
op.status = "inflight";
|
|
1569
|
+
this.outbox.set(op.id, op);
|
|
1570
|
+
}
|
|
1571
|
+
return ready;
|
|
1572
|
+
}
|
|
1573
|
+
async ack(opId) {
|
|
1574
|
+
this.outbox.delete(opId);
|
|
1575
|
+
}
|
|
1576
|
+
async nack(opId, error, retries) {
|
|
1577
|
+
const op = this.outbox.get(opId);
|
|
1578
|
+
if (!op) return;
|
|
1579
|
+
op.retries = retries;
|
|
1580
|
+
op.lastError = error;
|
|
1581
|
+
op.status = retries >= op.maxRetries ? "failed" : "pending";
|
|
1582
|
+
this.outbox.set(opId, op);
|
|
1583
|
+
}
|
|
1584
|
+
async getPendingCount() {
|
|
1585
|
+
return Array.from(this.outbox.values()).filter((op) => op.status === "pending" || op.status === "inflight").length;
|
|
1586
|
+
}
|
|
1587
|
+
// ── Search ─────────────────────────────────────────────────
|
|
1588
|
+
async searchMessages(query, opts) {
|
|
1589
|
+
const lower = query.toLowerCase();
|
|
1590
|
+
const limit = opts?.limit ?? 50;
|
|
1591
|
+
return Array.from(this.messages.values()).filter((m) => {
|
|
1592
|
+
if (opts?.conversationId && m.conversationId !== opts.conversationId) return false;
|
|
1593
|
+
return (m.content ?? "").toLowerCase().includes(lower);
|
|
1594
|
+
}).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit);
|
|
1595
|
+
}
|
|
1596
|
+
// ── Quota ─────────────────────────────────────────────────
|
|
1597
|
+
async getStorageSize() {
|
|
1598
|
+
const msgSize = this.messages.size * 500;
|
|
1599
|
+
const convSize = this.conversations.size * 200;
|
|
1600
|
+
return { messages: msgSize, conversations: convSize, total: msgSize + convSize };
|
|
1601
|
+
}
|
|
1602
|
+
async clearOldMessages(conversationId, keepCount) {
|
|
1603
|
+
const msgs = Array.from(this.messages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
1604
|
+
const toDelete = msgs.slice(keepCount);
|
|
1605
|
+
for (const m of toDelete) this.messages.delete(m.id);
|
|
1606
|
+
return toDelete.length;
|
|
1607
|
+
}
|
|
1608
|
+
// ── Lifecycle ───────────────────────────────────────────────
|
|
1609
|
+
async clear() {
|
|
1610
|
+
this.messages.clear();
|
|
1611
|
+
this.conversations.clear();
|
|
1612
|
+
this.contacts.clear();
|
|
1613
|
+
this.cursors.clear();
|
|
1614
|
+
this.outbox.clear();
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
var IDB_STORES = ["messages", "conversations", "contacts", "cursors", "outbox"];
|
|
1618
|
+
var IndexedDBStorage = class {
|
|
1619
|
+
constructor(dbName = "prismer-offline", version = 1) {
|
|
1620
|
+
this.dbName = dbName;
|
|
1621
|
+
this.version = version;
|
|
1622
|
+
this.db = null;
|
|
1623
|
+
}
|
|
1624
|
+
async init() {
|
|
1625
|
+
if (typeof indexedDB === "undefined") {
|
|
1626
|
+
throw new Error("IndexedDB is not available in this environment. Use MemoryStorage or SQLiteStorage instead.");
|
|
1627
|
+
}
|
|
1628
|
+
return new Promise((resolve, reject) => {
|
|
1629
|
+
const req = indexedDB.open(this.dbName, this.version);
|
|
1630
|
+
req.onupgradeneeded = () => {
|
|
1631
|
+
const db = req.result;
|
|
1632
|
+
if (!db.objectStoreNames.contains("messages")) {
|
|
1633
|
+
const store = db.createObjectStore("messages", { keyPath: "id" });
|
|
1634
|
+
store.createIndex("conversationId", "conversationId", { unique: false });
|
|
1635
|
+
store.createIndex("createdAt", "createdAt", { unique: false });
|
|
1636
|
+
}
|
|
1637
|
+
if (!db.objectStoreNames.contains("conversations")) {
|
|
1638
|
+
db.createObjectStore("conversations", { keyPath: "id" });
|
|
1639
|
+
}
|
|
1640
|
+
if (!db.objectStoreNames.contains("contacts")) {
|
|
1641
|
+
db.createObjectStore("contacts", { keyPath: "userId" });
|
|
1642
|
+
}
|
|
1643
|
+
if (!db.objectStoreNames.contains("cursors")) {
|
|
1644
|
+
db.createObjectStore("cursors", { keyPath: "key" });
|
|
1645
|
+
}
|
|
1646
|
+
if (!db.objectStoreNames.contains("outbox")) {
|
|
1647
|
+
const store = db.createObjectStore("outbox", { keyPath: "id" });
|
|
1648
|
+
store.createIndex("status", "status", { unique: false });
|
|
1649
|
+
store.createIndex("createdAt", "createdAt", { unique: false });
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
req.onsuccess = () => {
|
|
1653
|
+
this.db = req.result;
|
|
1654
|
+
resolve();
|
|
1655
|
+
};
|
|
1656
|
+
req.onerror = () => reject(req.error);
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
tx(stores, mode = "readonly") {
|
|
1660
|
+
if (!this.db) throw new Error("IndexedDB not initialized. Call init() first.");
|
|
1661
|
+
return this.db.transaction(stores, mode);
|
|
1662
|
+
}
|
|
1663
|
+
req(request) {
|
|
1664
|
+
return new Promise((resolve, reject) => {
|
|
1665
|
+
request.onsuccess = () => resolve(request.result);
|
|
1666
|
+
request.onerror = () => reject(request.error);
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
// ── Messages ────────────────────────────────────────────────
|
|
1670
|
+
async putMessages(messages) {
|
|
1671
|
+
const tx = this.tx("messages", "readwrite");
|
|
1672
|
+
const store = tx.objectStore("messages");
|
|
1673
|
+
for (const m of messages) store.put(m);
|
|
1674
|
+
return new Promise((resolve, reject) => {
|
|
1675
|
+
tx.oncomplete = () => resolve();
|
|
1676
|
+
tx.onerror = () => reject(tx.error);
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
async getMessages(conversationId, opts) {
|
|
1680
|
+
const tx = this.tx("messages");
|
|
1681
|
+
const store = tx.objectStore("messages");
|
|
1682
|
+
const idx = store.index("conversationId");
|
|
1683
|
+
const all = await this.req(idx.getAll(conversationId));
|
|
1684
|
+
all.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1685
|
+
if (opts.before) {
|
|
1686
|
+
const i = all.findIndex((m) => m.id === opts.before);
|
|
1687
|
+
if (i > 0) return all.slice(Math.max(0, i - opts.limit), i);
|
|
1688
|
+
}
|
|
1689
|
+
return all.slice(-opts.limit);
|
|
1690
|
+
}
|
|
1691
|
+
async getMessage(messageId) {
|
|
1692
|
+
const tx = this.tx("messages");
|
|
1693
|
+
const result = await this.req(tx.objectStore("messages").get(messageId));
|
|
1694
|
+
return result ?? null;
|
|
1695
|
+
}
|
|
1696
|
+
async deleteMessage(messageId) {
|
|
1697
|
+
const tx = this.tx("messages", "readwrite");
|
|
1698
|
+
tx.objectStore("messages").delete(messageId);
|
|
1699
|
+
return new Promise((resolve, reject) => {
|
|
1700
|
+
tx.oncomplete = () => resolve();
|
|
1701
|
+
tx.onerror = () => reject(tx.error);
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
// ── Conversations ───────────────────────────────────────────
|
|
1705
|
+
async putConversations(conversations) {
|
|
1706
|
+
const tx = this.tx("conversations", "readwrite");
|
|
1707
|
+
const store = tx.objectStore("conversations");
|
|
1708
|
+
for (const c of conversations) store.put(c);
|
|
1709
|
+
return new Promise((resolve, reject) => {
|
|
1710
|
+
tx.oncomplete = () => resolve();
|
|
1711
|
+
tx.onerror = () => reject(tx.error);
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
async getConversations(opts) {
|
|
1715
|
+
const tx = this.tx("conversations");
|
|
1716
|
+
const all = await this.req(tx.objectStore("conversations").getAll());
|
|
1717
|
+
all.sort((a, b) => (b.lastMessageAt ?? b.updatedAt).localeCompare(a.lastMessageAt ?? a.updatedAt));
|
|
1718
|
+
const offset = opts?.offset ?? 0;
|
|
1719
|
+
const limit = opts?.limit ?? 50;
|
|
1720
|
+
return all.slice(offset, offset + limit);
|
|
1721
|
+
}
|
|
1722
|
+
async getConversation(id) {
|
|
1723
|
+
const tx = this.tx("conversations");
|
|
1724
|
+
const result = await this.req(tx.objectStore("conversations").get(id));
|
|
1725
|
+
return result ?? null;
|
|
1726
|
+
}
|
|
1727
|
+
// ── Contacts ────────────────────────────────────────────────
|
|
1728
|
+
async putContacts(contacts) {
|
|
1729
|
+
const tx = this.tx("contacts", "readwrite");
|
|
1730
|
+
const store = tx.objectStore("contacts");
|
|
1731
|
+
for (const c of contacts) store.put(c);
|
|
1732
|
+
return new Promise((resolve, reject) => {
|
|
1733
|
+
tx.oncomplete = () => resolve();
|
|
1734
|
+
tx.onerror = () => reject(tx.error);
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
async getContacts() {
|
|
1738
|
+
const tx = this.tx("contacts");
|
|
1739
|
+
return this.req(tx.objectStore("contacts").getAll());
|
|
1740
|
+
}
|
|
1741
|
+
// ── Cursors ─────────────────────────────────────────────────
|
|
1742
|
+
async getCursor(key) {
|
|
1743
|
+
const tx = this.tx("cursors");
|
|
1744
|
+
const result = await this.req(tx.objectStore("cursors").get(key));
|
|
1745
|
+
return result?.value ?? null;
|
|
1746
|
+
}
|
|
1747
|
+
async setCursor(key, value) {
|
|
1748
|
+
const tx = this.tx("cursors", "readwrite");
|
|
1749
|
+
tx.objectStore("cursors").put({ key, value });
|
|
1750
|
+
return new Promise((resolve, reject) => {
|
|
1751
|
+
tx.oncomplete = () => resolve();
|
|
1752
|
+
tx.onerror = () => reject(tx.error);
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
// ── Outbox ──────────────────────────────────────────────────
|
|
1756
|
+
async enqueue(op) {
|
|
1757
|
+
const tx = this.tx("outbox", "readwrite");
|
|
1758
|
+
tx.objectStore("outbox").put(op);
|
|
1759
|
+
return new Promise((resolve, reject) => {
|
|
1760
|
+
tx.oncomplete = () => resolve();
|
|
1761
|
+
tx.onerror = () => reject(tx.error);
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
async dequeueReady(limit) {
|
|
1765
|
+
const tx = this.tx("outbox", "readwrite");
|
|
1766
|
+
const store = tx.objectStore("outbox");
|
|
1767
|
+
const idx = store.index("status");
|
|
1768
|
+
const pending = await this.req(idx.getAll("pending"));
|
|
1769
|
+
pending.sort((a, b) => a.createdAt - b.createdAt);
|
|
1770
|
+
const batch = pending.slice(0, limit);
|
|
1771
|
+
for (const op of batch) {
|
|
1772
|
+
op.status = "inflight";
|
|
1773
|
+
store.put(op);
|
|
1774
|
+
}
|
|
1775
|
+
return new Promise((resolve, reject) => {
|
|
1776
|
+
tx.oncomplete = () => resolve(batch);
|
|
1777
|
+
tx.onerror = () => reject(tx.error);
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
async ack(opId) {
|
|
1781
|
+
const tx = this.tx("outbox", "readwrite");
|
|
1782
|
+
tx.objectStore("outbox").delete(opId);
|
|
1783
|
+
return new Promise((resolve, reject) => {
|
|
1784
|
+
tx.oncomplete = () => resolve();
|
|
1785
|
+
tx.onerror = () => reject(tx.error);
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
async nack(opId, error, retries) {
|
|
1789
|
+
const tx = this.tx("outbox", "readwrite");
|
|
1790
|
+
const store = tx.objectStore("outbox");
|
|
1791
|
+
const op = await this.req(store.get(opId));
|
|
1792
|
+
if (!op) return;
|
|
1793
|
+
op.retries = retries;
|
|
1794
|
+
op.lastError = error;
|
|
1795
|
+
op.status = retries >= op.maxRetries ? "failed" : "pending";
|
|
1796
|
+
store.put(op);
|
|
1797
|
+
return new Promise((resolve, reject) => {
|
|
1798
|
+
tx.oncomplete = () => resolve();
|
|
1799
|
+
tx.onerror = () => reject(tx.error);
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
async getPendingCount() {
|
|
1803
|
+
const tx = this.tx("outbox");
|
|
1804
|
+
const idx = tx.objectStore("outbox").index("status");
|
|
1805
|
+
const pending = await this.req(idx.count("pending"));
|
|
1806
|
+
const inflight = await this.req(idx.count("inflight"));
|
|
1807
|
+
return pending + inflight;
|
|
1808
|
+
}
|
|
1809
|
+
// ── Search ─────────────────────────────────────────────────
|
|
1810
|
+
async searchMessages(query, opts) {
|
|
1811
|
+
const lower = query.toLowerCase();
|
|
1812
|
+
const limit = opts?.limit ?? 50;
|
|
1813
|
+
const tx = this.tx("messages");
|
|
1814
|
+
let all;
|
|
1815
|
+
if (opts?.conversationId) {
|
|
1816
|
+
const idx = tx.objectStore("messages").index("conversationId");
|
|
1817
|
+
all = await this.req(idx.getAll(opts.conversationId));
|
|
1818
|
+
} else {
|
|
1819
|
+
all = await this.req(tx.objectStore("messages").getAll());
|
|
1820
|
+
}
|
|
1821
|
+
return all.filter((m) => (m.content ?? "").toLowerCase().includes(lower)).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit);
|
|
1822
|
+
}
|
|
1823
|
+
// ── Quota ─────────────────────────────────────────────────
|
|
1824
|
+
async getStorageSize() {
|
|
1825
|
+
if (typeof navigator !== "undefined" && navigator.storage?.estimate) {
|
|
1826
|
+
const est = await navigator.storage.estimate();
|
|
1827
|
+
const total = est.usage ?? 0;
|
|
1828
|
+
return { messages: Math.floor(total * 0.8), conversations: Math.floor(total * 0.2), total };
|
|
1829
|
+
}
|
|
1830
|
+
const tx = this.tx(["messages", "conversations"]);
|
|
1831
|
+
const msgCount = await this.req(tx.objectStore("messages").count());
|
|
1832
|
+
const convCount = await this.req(tx.objectStore("conversations").count());
|
|
1833
|
+
return { messages: msgCount * 500, conversations: convCount * 200, total: msgCount * 500 + convCount * 200 };
|
|
1834
|
+
}
|
|
1835
|
+
async clearOldMessages(conversationId, keepCount) {
|
|
1836
|
+
const tx = this.tx("messages", "readwrite");
|
|
1837
|
+
const store = tx.objectStore("messages");
|
|
1838
|
+
const idx = store.index("conversationId");
|
|
1839
|
+
const all = await this.req(idx.getAll(conversationId));
|
|
1840
|
+
all.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
1841
|
+
const toDelete = all.slice(keepCount);
|
|
1842
|
+
for (const m of toDelete) store.delete(m.id);
|
|
1843
|
+
return new Promise((resolve, reject) => {
|
|
1844
|
+
tx.oncomplete = () => resolve(toDelete.length);
|
|
1845
|
+
tx.onerror = () => reject(tx.error);
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
// ── Lifecycle ───────────────────────────────────────────────
|
|
1849
|
+
async clear() {
|
|
1850
|
+
const tx = this.tx(IDB_STORES, "readwrite");
|
|
1851
|
+
for (const name of IDB_STORES) tx.objectStore(name).clear();
|
|
1852
|
+
return new Promise((resolve, reject) => {
|
|
1853
|
+
tx.oncomplete = () => resolve();
|
|
1854
|
+
tx.onerror = () => reject(tx.error);
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
var SQLiteStorage = class {
|
|
1859
|
+
constructor(dbPath = "prismer-offline.db") {
|
|
1860
|
+
this.db = null;
|
|
1861
|
+
this.dbPath = dbPath;
|
|
1862
|
+
}
|
|
1863
|
+
async init() {
|
|
1864
|
+
let Database;
|
|
1865
|
+
try {
|
|
1866
|
+
Database = __require("better-sqlite3");
|
|
1867
|
+
} catch {
|
|
1868
|
+
throw new Error(
|
|
1869
|
+
'SQLiteStorage requires the "better-sqlite3" package. Install it with: npm install better-sqlite3\nFor browser environments, use IndexedDBStorage instead.'
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
this.db = new Database(this.dbPath);
|
|
1873
|
+
this.db.pragma("journal_mode = WAL");
|
|
1874
|
+
this.db.pragma("synchronous = NORMAL");
|
|
1875
|
+
this.db.exec(`
|
|
1876
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
1877
|
+
id TEXT PRIMARY KEY,
|
|
1878
|
+
clientId TEXT,
|
|
1879
|
+
conversationId TEXT NOT NULL,
|
|
1880
|
+
content TEXT,
|
|
1881
|
+
type TEXT DEFAULT 'text',
|
|
1882
|
+
senderId TEXT,
|
|
1883
|
+
parentId TEXT,
|
|
1884
|
+
status TEXT DEFAULT 'confirmed',
|
|
1885
|
+
metadata TEXT,
|
|
1886
|
+
createdAt TEXT,
|
|
1887
|
+
updatedAt TEXT,
|
|
1888
|
+
syncSeq INTEGER
|
|
1889
|
+
);
|
|
1890
|
+
CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conversationId, createdAt);
|
|
1891
|
+
CREATE INDEX IF NOT EXISTS idx_msg_created ON messages(createdAt);
|
|
1892
|
+
|
|
1893
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
1894
|
+
id TEXT PRIMARY KEY,
|
|
1895
|
+
type TEXT DEFAULT 'direct',
|
|
1896
|
+
title TEXT,
|
|
1897
|
+
lastMessage TEXT,
|
|
1898
|
+
lastMessageAt TEXT,
|
|
1899
|
+
unreadCount INTEGER DEFAULT 0,
|
|
1900
|
+
lastReadMessageId TEXT,
|
|
1901
|
+
members TEXT,
|
|
1902
|
+
metadata TEXT,
|
|
1903
|
+
syncSeq INTEGER,
|
|
1904
|
+
updatedAt TEXT
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
CREATE TABLE IF NOT EXISTS contacts (
|
|
1908
|
+
userId TEXT PRIMARY KEY,
|
|
1909
|
+
username TEXT,
|
|
1910
|
+
displayName TEXT,
|
|
1911
|
+
role TEXT,
|
|
1912
|
+
conversationId TEXT,
|
|
1913
|
+
lastMessageAt TEXT,
|
|
1914
|
+
unreadCount INTEGER DEFAULT 0,
|
|
1915
|
+
syncSeq INTEGER
|
|
1916
|
+
);
|
|
1917
|
+
|
|
1918
|
+
CREATE TABLE IF NOT EXISTS cursors (
|
|
1919
|
+
key TEXT PRIMARY KEY,
|
|
1920
|
+
value TEXT
|
|
1921
|
+
);
|
|
1922
|
+
|
|
1923
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
1924
|
+
id TEXT PRIMARY KEY,
|
|
1925
|
+
type TEXT,
|
|
1926
|
+
method TEXT,
|
|
1927
|
+
path TEXT,
|
|
1928
|
+
body TEXT,
|
|
1929
|
+
query TEXT,
|
|
1930
|
+
status TEXT DEFAULT 'pending',
|
|
1931
|
+
createdAt INTEGER,
|
|
1932
|
+
retries INTEGER DEFAULT 0,
|
|
1933
|
+
maxRetries INTEGER DEFAULT 5,
|
|
1934
|
+
lastError TEXT,
|
|
1935
|
+
idempotencyKey TEXT,
|
|
1936
|
+
localData TEXT
|
|
1937
|
+
);
|
|
1938
|
+
CREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(status, createdAt);
|
|
1939
|
+
`);
|
|
1940
|
+
this.db.exec(`
|
|
1941
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
1942
|
+
content, id UNINDEXED, conversationId UNINDEXED
|
|
1943
|
+
);
|
|
1944
|
+
`);
|
|
1945
|
+
}
|
|
1946
|
+
ensureDb() {
|
|
1947
|
+
if (!this.db) throw new Error("SQLiteStorage not initialized. Call init() first.");
|
|
1948
|
+
return this.db;
|
|
1949
|
+
}
|
|
1950
|
+
// ── Messages ────────────────────────────────────────────────
|
|
1951
|
+
async putMessages(messages) {
|
|
1952
|
+
const db = this.ensureDb();
|
|
1953
|
+
const insert = db.prepare(`
|
|
1954
|
+
INSERT OR REPLACE INTO messages (id, clientId, conversationId, content, type, senderId, parentId, status, metadata, createdAt, updatedAt, syncSeq)
|
|
1955
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1956
|
+
`);
|
|
1957
|
+
const insertFts = db.prepare(`
|
|
1958
|
+
INSERT OR REPLACE INTO messages_fts (rowid, content, id, conversationId)
|
|
1959
|
+
VALUES ((SELECT rowid FROM messages WHERE id = ?), ?, ?, ?)
|
|
1960
|
+
`);
|
|
1961
|
+
const txn = db.transaction((msgs) => {
|
|
1962
|
+
for (const m of msgs) {
|
|
1963
|
+
insert.run(
|
|
1964
|
+
m.id,
|
|
1965
|
+
m.clientId ?? null,
|
|
1966
|
+
m.conversationId,
|
|
1967
|
+
m.content,
|
|
1968
|
+
m.type,
|
|
1969
|
+
m.senderId,
|
|
1970
|
+
m.parentId ?? null,
|
|
1971
|
+
m.status,
|
|
1972
|
+
m.metadata ? JSON.stringify(m.metadata) : null,
|
|
1973
|
+
m.createdAt,
|
|
1974
|
+
m.updatedAt ?? null,
|
|
1975
|
+
m.syncSeq ?? null
|
|
1976
|
+
);
|
|
1977
|
+
if (m.content) {
|
|
1978
|
+
insertFts.run(m.id, m.content, m.id, m.conversationId);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
txn(messages);
|
|
1983
|
+
}
|
|
1984
|
+
async getMessages(conversationId, opts) {
|
|
1985
|
+
const db = this.ensureDb();
|
|
1986
|
+
let rows;
|
|
1987
|
+
if (opts.before) {
|
|
1988
|
+
const beforeRow = db.prepare("SELECT createdAt FROM messages WHERE id = ?").get(opts.before);
|
|
1989
|
+
if (beforeRow) {
|
|
1990
|
+
rows = db.prepare(
|
|
1991
|
+
"SELECT * FROM messages WHERE conversationId = ? AND createdAt < ? ORDER BY createdAt DESC LIMIT ?"
|
|
1992
|
+
).all(conversationId, beforeRow.createdAt, opts.limit);
|
|
1993
|
+
} else {
|
|
1994
|
+
rows = db.prepare(
|
|
1995
|
+
"SELECT * FROM messages WHERE conversationId = ? ORDER BY createdAt DESC LIMIT ?"
|
|
1996
|
+
).all(conversationId, opts.limit);
|
|
1997
|
+
}
|
|
1998
|
+
} else {
|
|
1999
|
+
rows = db.prepare(
|
|
2000
|
+
"SELECT * FROM messages WHERE conversationId = ? ORDER BY createdAt DESC LIMIT ?"
|
|
2001
|
+
).all(conversationId, opts.limit);
|
|
2002
|
+
}
|
|
2003
|
+
return rows.reverse().map(this.rowToMessage);
|
|
2004
|
+
}
|
|
2005
|
+
async getMessage(messageId) {
|
|
2006
|
+
const db = this.ensureDb();
|
|
2007
|
+
const row = db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
|
|
2008
|
+
return row ? this.rowToMessage(row) : null;
|
|
2009
|
+
}
|
|
2010
|
+
async deleteMessage(messageId) {
|
|
2011
|
+
const db = this.ensureDb();
|
|
2012
|
+
db.prepare("DELETE FROM messages WHERE id = ?").run(messageId);
|
|
2013
|
+
db.prepare("DELETE FROM messages_fts WHERE id = ?").run(messageId);
|
|
2014
|
+
}
|
|
2015
|
+
rowToMessage(row) {
|
|
2016
|
+
return {
|
|
2017
|
+
id: row.id,
|
|
2018
|
+
clientId: row.clientId ?? void 0,
|
|
2019
|
+
conversationId: row.conversationId,
|
|
2020
|
+
content: row.content ?? "",
|
|
2021
|
+
type: row.type ?? "text",
|
|
2022
|
+
senderId: row.senderId ?? "",
|
|
2023
|
+
parentId: row.parentId ?? null,
|
|
2024
|
+
status: row.status ?? "confirmed",
|
|
2025
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2026
|
+
createdAt: row.createdAt ?? "",
|
|
2027
|
+
updatedAt: row.updatedAt ?? void 0,
|
|
2028
|
+
syncSeq: row.syncSeq ?? void 0
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
// ── Conversations ───────────────────────────────────────────
|
|
2032
|
+
async putConversations(conversations) {
|
|
2033
|
+
const db = this.ensureDb();
|
|
2034
|
+
const insert = db.prepare(`
|
|
2035
|
+
INSERT OR REPLACE INTO conversations (id, type, title, lastMessage, lastMessageAt, unreadCount, lastReadMessageId, members, metadata, syncSeq, updatedAt)
|
|
2036
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2037
|
+
`);
|
|
2038
|
+
const txn = db.transaction((convs) => {
|
|
2039
|
+
for (const c of convs) {
|
|
2040
|
+
insert.run(
|
|
2041
|
+
c.id,
|
|
2042
|
+
c.type,
|
|
2043
|
+
c.title ?? null,
|
|
2044
|
+
c.lastMessage ? JSON.stringify(c.lastMessage) : null,
|
|
2045
|
+
c.lastMessageAt ?? null,
|
|
2046
|
+
c.unreadCount,
|
|
2047
|
+
c.lastReadMessageId ?? null,
|
|
2048
|
+
c.members ? JSON.stringify(c.members) : null,
|
|
2049
|
+
c.metadata ? JSON.stringify(c.metadata) : null,
|
|
2050
|
+
c.syncSeq ?? null,
|
|
2051
|
+
c.updatedAt
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
txn(conversations);
|
|
2056
|
+
}
|
|
2057
|
+
async getConversations(opts) {
|
|
2058
|
+
const db = this.ensureDb();
|
|
2059
|
+
const limit = opts?.limit ?? 50;
|
|
2060
|
+
const offset = opts?.offset ?? 0;
|
|
2061
|
+
const rows = db.prepare(
|
|
2062
|
+
"SELECT * FROM conversations ORDER BY COALESCE(lastMessageAt, updatedAt) DESC LIMIT ? OFFSET ?"
|
|
2063
|
+
).all(limit, offset);
|
|
2064
|
+
return rows.map(this.rowToConversation);
|
|
2065
|
+
}
|
|
2066
|
+
async getConversation(id) {
|
|
2067
|
+
const db = this.ensureDb();
|
|
2068
|
+
const row = db.prepare("SELECT * FROM conversations WHERE id = ?").get(id);
|
|
2069
|
+
return row ? this.rowToConversation(row) : null;
|
|
2070
|
+
}
|
|
2071
|
+
rowToConversation(row) {
|
|
2072
|
+
return {
|
|
2073
|
+
id: row.id,
|
|
2074
|
+
type: row.type ?? "direct",
|
|
2075
|
+
title: row.title ?? void 0,
|
|
2076
|
+
lastMessage: row.lastMessage ? JSON.parse(row.lastMessage) : void 0,
|
|
2077
|
+
lastMessageAt: row.lastMessageAt ?? void 0,
|
|
2078
|
+
unreadCount: row.unreadCount ?? 0,
|
|
2079
|
+
lastReadMessageId: row.lastReadMessageId ?? void 0,
|
|
2080
|
+
members: row.members ? JSON.parse(row.members) : void 0,
|
|
2081
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2082
|
+
syncSeq: row.syncSeq ?? void 0,
|
|
2083
|
+
updatedAt: row.updatedAt ?? ""
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
// ── Contacts ────────────────────────────────────────────────
|
|
2087
|
+
async putContacts(contacts) {
|
|
2088
|
+
const db = this.ensureDb();
|
|
2089
|
+
const insert = db.prepare(`
|
|
2090
|
+
INSERT OR REPLACE INTO contacts (userId, username, displayName, role, conversationId, lastMessageAt, unreadCount, syncSeq)
|
|
2091
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2092
|
+
`);
|
|
2093
|
+
const txn = db.transaction((cs) => {
|
|
2094
|
+
for (const c of cs) {
|
|
2095
|
+
insert.run(
|
|
2096
|
+
c.userId,
|
|
2097
|
+
c.username,
|
|
2098
|
+
c.displayName,
|
|
2099
|
+
c.role,
|
|
2100
|
+
c.conversationId,
|
|
2101
|
+
c.lastMessageAt ?? null,
|
|
2102
|
+
c.unreadCount,
|
|
2103
|
+
c.syncSeq ?? null
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
txn(contacts);
|
|
2108
|
+
}
|
|
2109
|
+
async getContacts() {
|
|
2110
|
+
const db = this.ensureDb();
|
|
2111
|
+
return db.prepare("SELECT * FROM contacts").all().map((row) => ({
|
|
2112
|
+
userId: row.userId,
|
|
2113
|
+
username: row.username ?? "",
|
|
2114
|
+
displayName: row.displayName ?? "",
|
|
2115
|
+
role: row.role ?? "member",
|
|
2116
|
+
conversationId: row.conversationId ?? "",
|
|
2117
|
+
lastMessageAt: row.lastMessageAt ?? void 0,
|
|
2118
|
+
unreadCount: row.unreadCount ?? 0,
|
|
2119
|
+
syncSeq: row.syncSeq ?? void 0
|
|
2120
|
+
}));
|
|
2121
|
+
}
|
|
2122
|
+
// ── Cursors ─────────────────────────────────────────────────
|
|
2123
|
+
async getCursor(key) {
|
|
2124
|
+
const db = this.ensureDb();
|
|
2125
|
+
const row = db.prepare("SELECT value FROM cursors WHERE key = ?").get(key);
|
|
2126
|
+
return row?.value ?? null;
|
|
2127
|
+
}
|
|
2128
|
+
async setCursor(key, value) {
|
|
2129
|
+
const db = this.ensureDb();
|
|
2130
|
+
db.prepare("INSERT OR REPLACE INTO cursors (key, value) VALUES (?, ?)").run(key, value);
|
|
2131
|
+
}
|
|
2132
|
+
// ── Outbox ──────────────────────────────────────────────────
|
|
2133
|
+
async enqueue(op) {
|
|
2134
|
+
const db = this.ensureDb();
|
|
2135
|
+
db.prepare(`
|
|
2136
|
+
INSERT OR REPLACE INTO outbox (id, type, method, path, body, query, status, createdAt, retries, maxRetries, lastError, idempotencyKey, localData)
|
|
2137
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2138
|
+
`).run(
|
|
2139
|
+
op.id,
|
|
2140
|
+
op.type,
|
|
2141
|
+
op.method,
|
|
2142
|
+
op.path,
|
|
2143
|
+
op.body ? JSON.stringify(op.body) : null,
|
|
2144
|
+
op.query ? JSON.stringify(op.query) : null,
|
|
2145
|
+
op.status,
|
|
2146
|
+
op.createdAt,
|
|
2147
|
+
op.retries,
|
|
2148
|
+
op.maxRetries,
|
|
2149
|
+
op.lastError ?? null,
|
|
2150
|
+
op.idempotencyKey,
|
|
2151
|
+
op.localData ? JSON.stringify(op.localData) : null
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
async dequeueReady(limit) {
|
|
2155
|
+
const db = this.ensureDb();
|
|
2156
|
+
const rows = db.prepare(
|
|
2157
|
+
"SELECT * FROM outbox WHERE status = ? ORDER BY createdAt ASC LIMIT ?"
|
|
2158
|
+
).all("pending", limit);
|
|
2159
|
+
const ops = rows.map(this.rowToOutbox);
|
|
2160
|
+
const update = db.prepare("UPDATE outbox SET status = ? WHERE id = ?");
|
|
2161
|
+
const txn = db.transaction((items) => {
|
|
2162
|
+
for (const op of items) update.run("inflight", op.id);
|
|
2163
|
+
});
|
|
2164
|
+
txn(ops);
|
|
2165
|
+
return ops.map((op) => ({ ...op, status: "inflight" }));
|
|
2166
|
+
}
|
|
2167
|
+
async ack(opId) {
|
|
2168
|
+
const db = this.ensureDb();
|
|
2169
|
+
db.prepare("DELETE FROM outbox WHERE id = ?").run(opId);
|
|
2170
|
+
}
|
|
2171
|
+
async nack(opId, error, retries) {
|
|
2172
|
+
const db = this.ensureDb();
|
|
2173
|
+
const row = db.prepare("SELECT maxRetries FROM outbox WHERE id = ?").get(opId);
|
|
2174
|
+
const newStatus = row && retries >= row.maxRetries ? "failed" : "pending";
|
|
2175
|
+
db.prepare("UPDATE outbox SET retries = ?, lastError = ?, status = ? WHERE id = ?").run(retries, error, newStatus, opId);
|
|
2176
|
+
}
|
|
2177
|
+
async getPendingCount() {
|
|
2178
|
+
const db = this.ensureDb();
|
|
2179
|
+
const row = db.prepare(
|
|
2180
|
+
"SELECT COUNT(*) as cnt FROM outbox WHERE status IN ('pending', 'inflight')"
|
|
2181
|
+
).get();
|
|
2182
|
+
return row?.cnt ?? 0;
|
|
2183
|
+
}
|
|
2184
|
+
// ── Search (FTS5) ─────────────────────────────────────────
|
|
2185
|
+
async searchMessages(query, opts) {
|
|
2186
|
+
const db = this.ensureDb();
|
|
2187
|
+
const limit = opts?.limit ?? 50;
|
|
2188
|
+
const safeQuery = query.replace(/['"*(){}[\]^~\\]/g, " ").trim();
|
|
2189
|
+
if (!safeQuery) return [];
|
|
2190
|
+
let rows;
|
|
2191
|
+
if (opts?.conversationId) {
|
|
2192
|
+
rows = db.prepare(`
|
|
2193
|
+
SELECT m.* FROM messages m
|
|
2194
|
+
JOIN messages_fts f ON m.id = f.id
|
|
2195
|
+
WHERE messages_fts MATCH ? AND m.conversationId = ?
|
|
2196
|
+
ORDER BY m.createdAt DESC LIMIT ?
|
|
2197
|
+
`).all(safeQuery, opts.conversationId, limit);
|
|
2198
|
+
} else {
|
|
2199
|
+
rows = db.prepare(`
|
|
2200
|
+
SELECT m.* FROM messages m
|
|
2201
|
+
JOIN messages_fts f ON m.id = f.id
|
|
2202
|
+
WHERE messages_fts MATCH ?
|
|
2203
|
+
ORDER BY m.createdAt DESC LIMIT ?
|
|
2204
|
+
`).all(safeQuery, limit);
|
|
2205
|
+
}
|
|
2206
|
+
return rows.map(this.rowToMessage);
|
|
2207
|
+
}
|
|
2208
|
+
// ── Quota ─────────────────────────────────────────────────
|
|
2209
|
+
async getStorageSize() {
|
|
2210
|
+
const db = this.ensureDb();
|
|
2211
|
+
const pageSize = db.pragma("page_size", { simple: true }) ?? 4096;
|
|
2212
|
+
const pageCount = db.pragma("page_count", { simple: true }) ?? 0;
|
|
2213
|
+
const total = pageSize * pageCount;
|
|
2214
|
+
const msgCount = db.prepare("SELECT COUNT(*) as cnt FROM messages").get()?.cnt ?? 0;
|
|
2215
|
+
const convCount = db.prepare("SELECT COUNT(*) as cnt FROM conversations").get()?.cnt ?? 0;
|
|
2216
|
+
const totalRecords = msgCount + convCount;
|
|
2217
|
+
const msgRatio = totalRecords > 0 ? msgCount / totalRecords : 0.8;
|
|
2218
|
+
return {
|
|
2219
|
+
messages: Math.floor(total * msgRatio),
|
|
2220
|
+
conversations: Math.floor(total * (1 - msgRatio)),
|
|
2221
|
+
total
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
async clearOldMessages(conversationId, keepCount) {
|
|
2225
|
+
const db = this.ensureDb();
|
|
2226
|
+
const keepIds = db.prepare(
|
|
2227
|
+
"SELECT id FROM messages WHERE conversationId = ? ORDER BY createdAt DESC LIMIT ?"
|
|
2228
|
+
).all(conversationId, keepCount).map((r) => r.id);
|
|
2229
|
+
if (keepIds.length === 0) return 0;
|
|
2230
|
+
const placeholders = keepIds.map(() => "?").join(",");
|
|
2231
|
+
const result = db.prepare(
|
|
2232
|
+
`DELETE FROM messages WHERE conversationId = ? AND id NOT IN (${placeholders})`
|
|
2233
|
+
).run(conversationId, ...keepIds);
|
|
2234
|
+
db.prepare(
|
|
2235
|
+
`DELETE FROM messages_fts WHERE conversationId = ? AND id NOT IN (${placeholders})`
|
|
2236
|
+
).run(conversationId, ...keepIds);
|
|
2237
|
+
return result.changes;
|
|
2238
|
+
}
|
|
2239
|
+
// ── Lifecycle ───────────────────────────────────────────────
|
|
2240
|
+
async clear() {
|
|
2241
|
+
const db = this.ensureDb();
|
|
2242
|
+
db.exec("DELETE FROM messages; DELETE FROM messages_fts; DELETE FROM conversations; DELETE FROM contacts; DELETE FROM cursors; DELETE FROM outbox;");
|
|
2243
|
+
}
|
|
2244
|
+
rowToOutbox(row) {
|
|
2245
|
+
return {
|
|
2246
|
+
id: row.id,
|
|
2247
|
+
type: row.type,
|
|
2248
|
+
method: row.method,
|
|
2249
|
+
path: row.path,
|
|
2250
|
+
body: row.body ? JSON.parse(row.body) : void 0,
|
|
2251
|
+
query: row.query ? JSON.parse(row.query) : void 0,
|
|
2252
|
+
status: row.status,
|
|
2253
|
+
createdAt: row.createdAt,
|
|
2254
|
+
retries: row.retries ?? 0,
|
|
2255
|
+
maxRetries: row.maxRetries ?? 5,
|
|
2256
|
+
lastError: row.lastError ?? void 0,
|
|
2257
|
+
idempotencyKey: row.idempotencyKey ?? "",
|
|
2258
|
+
localData: row.localData ? JSON.parse(row.localData) : void 0
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
|
|
2263
|
+
// src/multitab.ts
|
|
2264
|
+
var TabCoordinator = class {
|
|
2265
|
+
constructor(offline, channelName = "prismer-tab-sync") {
|
|
2266
|
+
this.offline = offline;
|
|
2267
|
+
this.channelName = channelName;
|
|
2268
|
+
this.channel = null;
|
|
2269
|
+
this._isLeader = false;
|
|
2270
|
+
this.disposed = false;
|
|
2271
|
+
this.tabId = `tab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2272
|
+
}
|
|
2273
|
+
get isLeader() {
|
|
2274
|
+
return this._isLeader;
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Initialize tab coordination.
|
|
2278
|
+
* Claims leadership immediately (last-login-wins).
|
|
2279
|
+
*/
|
|
2280
|
+
init() {
|
|
2281
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
2282
|
+
this._isLeader = true;
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
this.channel = new BroadcastChannel(this.channelName);
|
|
2286
|
+
this.channel.onmessage = (e) => this.handleMessage(e.data);
|
|
2287
|
+
this.claimLeadership();
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Release leadership and clean up.
|
|
2291
|
+
*/
|
|
2292
|
+
destroy() {
|
|
2293
|
+
this.disposed = true;
|
|
2294
|
+
if (this.channel) {
|
|
2295
|
+
if (this._isLeader) {
|
|
2296
|
+
this.broadcast({ type: "tab.release", tabId: this.tabId });
|
|
2297
|
+
}
|
|
2298
|
+
this.channel.close();
|
|
2299
|
+
this.channel = null;
|
|
2300
|
+
}
|
|
2301
|
+
this._isLeader = false;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Relay a sync event to passive tabs.
|
|
2305
|
+
* Called by the leader tab after processing a sync event.
|
|
2306
|
+
*/
|
|
2307
|
+
relaySyncEvent(event) {
|
|
2308
|
+
if (this._isLeader && this.channel) {
|
|
2309
|
+
this.broadcast({ type: "sync.event", tabId: this.tabId, payload: event });
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
// ── Private ──────────────────────────────────────────────────
|
|
2313
|
+
claimLeadership() {
|
|
2314
|
+
this._isLeader = true;
|
|
2315
|
+
this.broadcast({ type: "tab.claim", tabId: this.tabId });
|
|
2316
|
+
this.onBecomeLeader();
|
|
2317
|
+
}
|
|
2318
|
+
demoteToPassive() {
|
|
2319
|
+
if (!this._isLeader) return;
|
|
2320
|
+
this._isLeader = false;
|
|
2321
|
+
this.onBecomePassive();
|
|
2322
|
+
}
|
|
2323
|
+
handleMessage(msg) {
|
|
2324
|
+
if (this.disposed) return;
|
|
2325
|
+
switch (msg.type) {
|
|
2326
|
+
case "tab.claim": {
|
|
2327
|
+
if (msg.tabId !== this.tabId) {
|
|
2328
|
+
this.demoteToPassive();
|
|
2329
|
+
this.broadcast({ type: "tab.ack", tabId: this.tabId });
|
|
2330
|
+
}
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
2333
|
+
case "tab.release": {
|
|
2334
|
+
if (!this._isLeader) {
|
|
2335
|
+
this.claimLeadership();
|
|
2336
|
+
}
|
|
2337
|
+
break;
|
|
2338
|
+
}
|
|
2339
|
+
case "sync.event": {
|
|
2340
|
+
if (!this._isLeader && msg.payload) {
|
|
2341
|
+
this.offline["applySyncEvent"](msg.payload).catch(() => {
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
break;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
onBecomeLeader() {
|
|
2349
|
+
}
|
|
2350
|
+
onBecomePassive() {
|
|
2351
|
+
this.offline.stopContinuousSync();
|
|
2352
|
+
}
|
|
2353
|
+
broadcast(msg) {
|
|
2354
|
+
try {
|
|
2355
|
+
this.channel?.postMessage(msg);
|
|
2356
|
+
} catch {
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
|
|
2361
|
+
// src/encryption.ts
|
|
2362
|
+
function getSubtleCrypto() {
|
|
2363
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
2364
|
+
return globalThis.crypto.subtle;
|
|
2365
|
+
}
|
|
2366
|
+
try {
|
|
2367
|
+
const { webcrypto } = __require("crypto");
|
|
2368
|
+
return webcrypto.subtle;
|
|
2369
|
+
} catch {
|
|
2370
|
+
throw new Error("No SubtleCrypto available. Requires browser or Node.js 16+.");
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
function getRandomValues(arr) {
|
|
2374
|
+
if (typeof globalThis.crypto?.getRandomValues !== "undefined") {
|
|
2375
|
+
return globalThis.crypto.getRandomValues(arr);
|
|
2376
|
+
}
|
|
2377
|
+
try {
|
|
2378
|
+
const { webcrypto } = __require("crypto");
|
|
2379
|
+
return webcrypto.getRandomValues(arr);
|
|
2380
|
+
} catch {
|
|
2381
|
+
throw new Error("No crypto.getRandomValues available.");
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
var subtle = () => getSubtleCrypto();
|
|
2385
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
2386
|
+
var SALT_LENGTH = 16;
|
|
2387
|
+
var IV_LENGTH = 12;
|
|
2388
|
+
var KEY_LENGTH = 256;
|
|
2389
|
+
var _E2EEncryption = class _E2EEncryption {
|
|
2390
|
+
constructor() {
|
|
2391
|
+
this.masterKey = null;
|
|
2392
|
+
this.keyPair = null;
|
|
2393
|
+
this.sessionKeys = /* @__PURE__ */ new Map();
|
|
2394
|
+
// conversationId → AES key
|
|
2395
|
+
this.salt = null;
|
|
2396
|
+
// ─── Pipeline Functions ──────────────────────────────────
|
|
2397
|
+
this.messageCount = 0;
|
|
2398
|
+
this.lastRotation = Date.now();
|
|
2399
|
+
}
|
|
2400
|
+
/**
|
|
2401
|
+
* Initialize encryption with user passphrase.
|
|
2402
|
+
* Derives a master key via PBKDF2 and generates an ECDH key pair.
|
|
2403
|
+
*
|
|
2404
|
+
* @param passphrase - User passphrase for master key derivation
|
|
2405
|
+
* @param salt - Optional Base64-encoded salt. If omitted, a random 16-byte salt is generated.
|
|
2406
|
+
* Store the salt (via exportSalt()) so you can re-derive the same master key later.
|
|
2407
|
+
*/
|
|
2408
|
+
async init(passphrase, salt) {
|
|
2409
|
+
this.salt = salt ? new Uint8Array(base64ToArrayBuffer(salt)) : getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
2410
|
+
const passphraseKey = await subtle().importKey(
|
|
2411
|
+
"raw",
|
|
2412
|
+
new TextEncoder().encode(passphrase),
|
|
2413
|
+
"PBKDF2",
|
|
2414
|
+
false,
|
|
2415
|
+
["deriveKey"]
|
|
2416
|
+
);
|
|
2417
|
+
this.masterKey = await subtle().deriveKey(
|
|
2418
|
+
{
|
|
2419
|
+
name: "PBKDF2",
|
|
2420
|
+
salt: new Uint8Array(this.salt.buffer, this.salt.byteOffset, this.salt.byteLength).buffer,
|
|
2421
|
+
iterations: PBKDF2_ITERATIONS,
|
|
2422
|
+
hash: "SHA-256"
|
|
2423
|
+
},
|
|
2424
|
+
passphraseKey,
|
|
2425
|
+
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
2426
|
+
false,
|
|
2427
|
+
["encrypt", "decrypt"]
|
|
2428
|
+
);
|
|
2429
|
+
this.keyPair = await subtle().generateKey(
|
|
2430
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
2431
|
+
true,
|
|
2432
|
+
["deriveKey"]
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Export the salt as Base64 string for persistent storage.
|
|
2437
|
+
* You must store this and pass it back to init() to re-derive the same master key.
|
|
2438
|
+
*/
|
|
2439
|
+
exportSalt() {
|
|
2440
|
+
if (!this.salt) throw new Error("E2E not initialized. Call init() first.");
|
|
2441
|
+
return arrayBufferToBase64(this.salt.buffer);
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* Export public key for sharing with conversation peers.
|
|
2445
|
+
*/
|
|
2446
|
+
async exportPublicKey() {
|
|
2447
|
+
if (!this.keyPair) throw new Error("E2E not initialized. Call init() first.");
|
|
2448
|
+
return subtle().exportKey("jwk", this.keyPair.publicKey);
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Derive a shared session key for a conversation using ECDH.
|
|
2452
|
+
* Call this with each peer's public key.
|
|
2453
|
+
*/
|
|
2454
|
+
async deriveSessionKey(conversationId, peerPublicKey) {
|
|
2455
|
+
if (!this.keyPair) throw new Error("E2E not initialized. Call init() first.");
|
|
2456
|
+
const importedPeerKey = await subtle().importKey(
|
|
2457
|
+
"jwk",
|
|
2458
|
+
peerPublicKey,
|
|
2459
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
2460
|
+
false,
|
|
2461
|
+
[]
|
|
2462
|
+
);
|
|
2463
|
+
const sessionKey = await subtle().deriveKey(
|
|
2464
|
+
{ name: "ECDH", public: importedPeerKey },
|
|
2465
|
+
this.keyPair.privateKey,
|
|
2466
|
+
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
2467
|
+
false,
|
|
2468
|
+
["encrypt", "decrypt"]
|
|
2469
|
+
);
|
|
2470
|
+
this.sessionKeys.set(conversationId, sessionKey);
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Set a pre-shared session key for a conversation.
|
|
2474
|
+
* Useful when the key is exchanged out-of-band or derived from a group key.
|
|
2475
|
+
*/
|
|
2476
|
+
async setSessionKey(conversationId, rawKey) {
|
|
2477
|
+
const key = await subtle().importKey(
|
|
2478
|
+
"raw",
|
|
2479
|
+
rawKey,
|
|
2480
|
+
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
2481
|
+
false,
|
|
2482
|
+
["encrypt", "decrypt"]
|
|
2483
|
+
);
|
|
2484
|
+
this.sessionKeys.set(conversationId, key);
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Generate a random session key for a conversation.
|
|
2488
|
+
* Returns the raw key bytes for sharing with peers.
|
|
2489
|
+
*/
|
|
2490
|
+
async generateSessionKey(conversationId) {
|
|
2491
|
+
const key = await subtle().generateKey(
|
|
2492
|
+
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
2493
|
+
true,
|
|
2494
|
+
["encrypt", "decrypt"]
|
|
2495
|
+
);
|
|
2496
|
+
this.sessionKeys.set(conversationId, key);
|
|
2497
|
+
return subtle().exportKey("raw", key);
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Encrypt plaintext for a conversation.
|
|
2501
|
+
* Returns base64-encoded ciphertext with prepended IV.
|
|
2502
|
+
*/
|
|
2503
|
+
async encrypt(conversationId, plaintext) {
|
|
2504
|
+
const key = this.sessionKeys.get(conversationId);
|
|
2505
|
+
if (!key) throw new Error(`No session key for conversation ${conversationId}. Call deriveSessionKey() first.`);
|
|
2506
|
+
const iv = getRandomValues(new Uint8Array(IV_LENGTH));
|
|
2507
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
2508
|
+
const ciphertext = await subtle().encrypt(
|
|
2509
|
+
{ name: "AES-GCM", iv },
|
|
2510
|
+
key,
|
|
2511
|
+
encoded
|
|
2512
|
+
);
|
|
2513
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
2514
|
+
combined.set(iv, 0);
|
|
2515
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
2516
|
+
return arrayBufferToBase64(combined.buffer);
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Decrypt ciphertext from a conversation.
|
|
2520
|
+
* Expects base64-encoded data with prepended IV.
|
|
2521
|
+
*/
|
|
2522
|
+
async decrypt(conversationId, ciphertext) {
|
|
2523
|
+
const key = this.sessionKeys.get(conversationId);
|
|
2524
|
+
if (!key) throw new Error(`No session key for conversation ${conversationId}. Call deriveSessionKey() first.`);
|
|
2525
|
+
const combined = base64ToArrayBuffer(ciphertext);
|
|
2526
|
+
const iv = combined.slice(0, IV_LENGTH);
|
|
2527
|
+
const data = combined.slice(IV_LENGTH);
|
|
2528
|
+
const decrypted = await subtle().decrypt(
|
|
2529
|
+
{ name: "AES-GCM", iv: new Uint8Array(iv) },
|
|
2530
|
+
key,
|
|
2531
|
+
data
|
|
2532
|
+
);
|
|
2533
|
+
return new TextDecoder().decode(decrypted);
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Check if a session key exists for a conversation.
|
|
2537
|
+
*/
|
|
2538
|
+
hasSessionKey(conversationId) {
|
|
2539
|
+
return this.sessionKeys.has(conversationId);
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Remove session key for a conversation.
|
|
2543
|
+
*/
|
|
2544
|
+
removeSessionKey(conversationId) {
|
|
2545
|
+
this.sessionKeys.delete(conversationId);
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Clear all keys and reset state.
|
|
2549
|
+
*/
|
|
2550
|
+
destroy() {
|
|
2551
|
+
this.masterKey = null;
|
|
2552
|
+
this.keyPair = null;
|
|
2553
|
+
this.sessionKeys.clear();
|
|
2554
|
+
this.salt = null;
|
|
2555
|
+
this.messageCount = 0;
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* High-level encrypt-for-send pipeline.
|
|
2559
|
+
* Encrypts content, builds metadata, and handles key rotation.
|
|
2560
|
+
*
|
|
2561
|
+
* Returns { encryptedContent, metadata } ready to send.
|
|
2562
|
+
*/
|
|
2563
|
+
async encryptForSend(conversationId, content) {
|
|
2564
|
+
if (!this.hasSessionKey(conversationId)) {
|
|
2565
|
+
throw new Error("No session key for this conversation. Call deriveSessionKey() first.");
|
|
2566
|
+
}
|
|
2567
|
+
const needsRotation = this.shouldRotateKey();
|
|
2568
|
+
const encryptedContent = await this.encrypt(conversationId, content);
|
|
2569
|
+
this.messageCount++;
|
|
2570
|
+
return {
|
|
2571
|
+
encryptedContent,
|
|
2572
|
+
metadata: {
|
|
2573
|
+
encrypted: true,
|
|
2574
|
+
encryptionVersion: 1,
|
|
2575
|
+
...needsRotation && { keyRotationRequested: true }
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
/**
|
|
2580
|
+
* High-level decrypt-on-receive pipeline.
|
|
2581
|
+
* Decrypts content and validates metadata.
|
|
2582
|
+
*/
|
|
2583
|
+
async decryptOnReceive(conversationId, encryptedContent, metadata) {
|
|
2584
|
+
if (!this.hasSessionKey(conversationId)) {
|
|
2585
|
+
throw new Error("No session key for this conversation. Call deriveSessionKey() first.");
|
|
2586
|
+
}
|
|
2587
|
+
return this.decrypt(conversationId, encryptedContent);
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* High-level file encryption pipeline.
|
|
2591
|
+
*/
|
|
2592
|
+
async encryptFile(conversationId, fileData) {
|
|
2593
|
+
const base64Data = arrayBufferToBase64(fileData);
|
|
2594
|
+
const encryptedData = await this.encrypt(conversationId, base64Data);
|
|
2595
|
+
return {
|
|
2596
|
+
encryptedData,
|
|
2597
|
+
metadata: {
|
|
2598
|
+
encrypted: true,
|
|
2599
|
+
encryptionVersion: 1,
|
|
2600
|
+
fileEncrypted: true
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* High-level file decryption pipeline.
|
|
2606
|
+
*/
|
|
2607
|
+
async decryptFile(conversationId, encryptedData) {
|
|
2608
|
+
const base64Data = await this.decrypt(conversationId, encryptedData);
|
|
2609
|
+
return base64ToArrayBuffer(base64Data);
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Check if key rotation is needed (1000 messages or 24 hours).
|
|
2613
|
+
*/
|
|
2614
|
+
shouldRotateKey() {
|
|
2615
|
+
if (this.messageCount >= _E2EEncryption.KEY_ROTATION_THRESHOLD) {
|
|
2616
|
+
return true;
|
|
2617
|
+
}
|
|
2618
|
+
if (Date.now() - this.lastRotation >= _E2EEncryption.KEY_ROTATION_INTERVAL_MS) {
|
|
2619
|
+
return true;
|
|
2620
|
+
}
|
|
2621
|
+
return false;
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Perform key rotation: generate new ECDH keypair and reset counters.
|
|
2625
|
+
* The caller is responsible for re-exchanging keys with peers.
|
|
2626
|
+
*/
|
|
2627
|
+
async rotateKeys() {
|
|
2628
|
+
this.keyPair = await subtle().generateKey(
|
|
2629
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
2630
|
+
false,
|
|
2631
|
+
["deriveKey"]
|
|
2632
|
+
);
|
|
2633
|
+
this.messageCount = 0;
|
|
2634
|
+
this.lastRotation = Date.now();
|
|
2635
|
+
this.sessionKeys.clear();
|
|
2636
|
+
return this.exportPublicKey();
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
_E2EEncryption.KEY_ROTATION_THRESHOLD = 1e3;
|
|
2640
|
+
_E2EEncryption.KEY_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2641
|
+
var E2EEncryption = _E2EEncryption;
|
|
2642
|
+
function arrayBufferToBase64(buffer) {
|
|
2643
|
+
if (typeof btoa !== "undefined") {
|
|
2644
|
+
const bytes = new Uint8Array(buffer);
|
|
2645
|
+
let binary = "";
|
|
2646
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
2647
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2648
|
+
}
|
|
2649
|
+
return btoa(binary);
|
|
2650
|
+
}
|
|
2651
|
+
return Buffer.from(buffer).toString("base64");
|
|
2652
|
+
}
|
|
2653
|
+
function base64ToArrayBuffer(base64) {
|
|
2654
|
+
if (typeof atob !== "undefined") {
|
|
2655
|
+
const binary = atob(base64);
|
|
2656
|
+
const bytes = new Uint8Array(binary.length);
|
|
2657
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2658
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2659
|
+
}
|
|
2660
|
+
return bytes.buffer;
|
|
2661
|
+
}
|
|
2662
|
+
const buf = Buffer.from(base64, "base64");
|
|
2663
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// src/encryption-pipeline.ts
|
|
2667
|
+
async function encryptForSend(e2e, conversationId, content, metadata) {
|
|
2668
|
+
if (!e2e.hasSessionKey(conversationId)) {
|
|
2669
|
+
return { content, metadata: metadata ?? {} };
|
|
2670
|
+
}
|
|
2671
|
+
const ciphertext = await e2e.encrypt(conversationId, content);
|
|
2672
|
+
return {
|
|
2673
|
+
content: ciphertext,
|
|
2674
|
+
metadata: { ...metadata, encrypted: true, encKeyId: `conv-${conversationId}` }
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
async function decryptOnReceive(e2e, conversationId, content, metadata) {
|
|
2678
|
+
if (!metadata?.encrypted) {
|
|
2679
|
+
return { content, decrypted: false };
|
|
2680
|
+
}
|
|
2681
|
+
if (!e2e.hasSessionKey(conversationId)) {
|
|
2682
|
+
return { content, decrypted: false, error: "no_session_key" };
|
|
2683
|
+
}
|
|
2684
|
+
try {
|
|
2685
|
+
const plain = await e2e.decrypt(conversationId, content);
|
|
2686
|
+
return { content: plain, decrypted: true };
|
|
2687
|
+
} catch (err) {
|
|
2688
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2689
|
+
return { content, decrypted: false, error: message };
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
async function encryptFile(e2e, conversationId, data) {
|
|
2693
|
+
if (!e2e.hasSessionKey(conversationId)) return null;
|
|
2694
|
+
const b64 = typeof Buffer !== "undefined" ? Buffer.from(data).toString("base64") : uint8ArrayToBase64(data);
|
|
2695
|
+
const ciphertext = await e2e.encrypt(conversationId, b64);
|
|
2696
|
+
return {
|
|
2697
|
+
data: ciphertext,
|
|
2698
|
+
metadata: { encrypted: true, encKeyId: `conv-${conversationId}` }
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
async function decryptFile(e2e, conversationId, ciphertext) {
|
|
2702
|
+
if (!e2e.hasSessionKey(conversationId)) return null;
|
|
2703
|
+
try {
|
|
2704
|
+
const b64 = await e2e.decrypt(conversationId, ciphertext);
|
|
2705
|
+
if (typeof Buffer !== "undefined") {
|
|
2706
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
2707
|
+
}
|
|
2708
|
+
return base64ToUint8Array(b64);
|
|
2709
|
+
} catch {
|
|
2710
|
+
return null;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
async function encryptContext(e2e, content, contextId = "context-cache") {
|
|
2714
|
+
if (!e2e.hasSessionKey(contextId)) return null;
|
|
2715
|
+
const ciphertext = await e2e.encrypt(contextId, content);
|
|
2716
|
+
return { content: ciphertext, encrypted: true };
|
|
2717
|
+
}
|
|
2718
|
+
async function decryptContext(e2e, ciphertext, contextId = "context-cache") {
|
|
2719
|
+
if (!e2e.hasSessionKey(contextId)) return null;
|
|
2720
|
+
try {
|
|
2721
|
+
return await e2e.decrypt(contextId, ciphertext);
|
|
2722
|
+
} catch {
|
|
2723
|
+
return null;
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
async function decryptMessages(e2e, messages, conversationId) {
|
|
2727
|
+
let decryptedCount = 0;
|
|
2728
|
+
const errors = [];
|
|
2729
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2730
|
+
const msg = messages[i];
|
|
2731
|
+
const convId = conversationId ?? msg.conversationId;
|
|
2732
|
+
if (!convId) continue;
|
|
2733
|
+
const result = await decryptOnReceive(e2e, convId, msg.content, msg.metadata);
|
|
2734
|
+
if (result.decrypted) {
|
|
2735
|
+
msg.content = result.content;
|
|
2736
|
+
if (msg.metadata) {
|
|
2737
|
+
msg.metadata._decrypted = true;
|
|
2738
|
+
}
|
|
2739
|
+
decryptedCount++;
|
|
2740
|
+
} else if (result.error) {
|
|
2741
|
+
errors.push({ index: i, error: result.error });
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
return { decryptedCount, errors };
|
|
2745
|
+
}
|
|
2746
|
+
function uint8ArrayToBase64(bytes) {
|
|
2747
|
+
let binary = "";
|
|
2748
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2749
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2750
|
+
}
|
|
2751
|
+
return btoa(binary);
|
|
2752
|
+
}
|
|
2753
|
+
function base64ToUint8Array(b64) {
|
|
2754
|
+
const binary = atob(b64);
|
|
2755
|
+
const bytes = new Uint8Array(binary.length);
|
|
2756
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2757
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2758
|
+
}
|
|
2759
|
+
return bytes;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// src/evolution-cache.ts
|
|
2763
|
+
var EvolutionCache = class {
|
|
2764
|
+
constructor() {
|
|
2765
|
+
this._genes = /* @__PURE__ */ new Map();
|
|
2766
|
+
this._edges = /* @__PURE__ */ new Map();
|
|
2767
|
+
// key = signal_key
|
|
2768
|
+
this._globalPrior = /* @__PURE__ */ new Map();
|
|
2769
|
+
this._cursor = 0;
|
|
2770
|
+
}
|
|
2771
|
+
get cursor() {
|
|
2772
|
+
return this._cursor;
|
|
2773
|
+
}
|
|
2774
|
+
get geneCount() {
|
|
2775
|
+
return this._genes.size;
|
|
2776
|
+
}
|
|
2777
|
+
/** Load from a full snapshot */
|
|
2778
|
+
loadSnapshot(snapshot) {
|
|
2779
|
+
this._genes.clear();
|
|
2780
|
+
this._edges.clear();
|
|
2781
|
+
this._globalPrior.clear();
|
|
2782
|
+
for (const gene of snapshot.genes) {
|
|
2783
|
+
this._genes.set(gene.id, gene);
|
|
2784
|
+
}
|
|
2785
|
+
for (const edge of snapshot.edges) {
|
|
2786
|
+
const list = this._edges.get(edge.signal_key) ?? [];
|
|
2787
|
+
list.push(edge);
|
|
2788
|
+
this._edges.set(edge.signal_key, list);
|
|
2789
|
+
}
|
|
2790
|
+
for (const [key, val] of Object.entries(snapshot.globalPrior)) {
|
|
2791
|
+
this._globalPrior.set(key, val);
|
|
2792
|
+
}
|
|
2793
|
+
this._cursor = snapshot.cursor;
|
|
2794
|
+
}
|
|
2795
|
+
/** Apply incremental delta (alias: loadDelta) */
|
|
2796
|
+
applyDelta(delta) {
|
|
2797
|
+
const pulled = delta.pulled;
|
|
2798
|
+
for (const gene of pulled.genes) {
|
|
2799
|
+
this._genes.set(gene.id, gene);
|
|
2800
|
+
}
|
|
2801
|
+
for (const id of pulled.quarantines) {
|
|
2802
|
+
this._genes.delete(id);
|
|
2803
|
+
}
|
|
2804
|
+
for (const edge of pulled.edges) {
|
|
2805
|
+
const list = this._edges.get(edge.signal_key) ?? [];
|
|
2806
|
+
const idx = list.findIndex((e) => e.gene_id === edge.gene_id);
|
|
2807
|
+
if (idx >= 0) list[idx] = edge;
|
|
2808
|
+
else list.push(edge);
|
|
2809
|
+
this._edges.set(edge.signal_key, list);
|
|
2810
|
+
}
|
|
2811
|
+
for (const [key, val] of Object.entries(pulled.globalPrior)) {
|
|
2812
|
+
this._globalPrior.set(key, val);
|
|
2813
|
+
}
|
|
2814
|
+
this._cursor = pulled.cursor;
|
|
2815
|
+
}
|
|
2816
|
+
/** Apply incremental delta (alias for applyDelta) */
|
|
2817
|
+
loadDelta(delta) {
|
|
2818
|
+
this.applyDelta(delta);
|
|
2819
|
+
}
|
|
2820
|
+
/** Select best gene locally using Thompson Sampling — pure CPU, <1ms */
|
|
2821
|
+
selectGene(signals) {
|
|
2822
|
+
if (this._genes.size === 0) {
|
|
2823
|
+
return { action: "none", confidence: 0, reason: "no genes in cache", fromCache: true };
|
|
2824
|
+
}
|
|
2825
|
+
const signalKeys = signals.map((s) => s.type);
|
|
2826
|
+
const candidates = [];
|
|
2827
|
+
for (const gene of this._genes.values()) {
|
|
2828
|
+
if (gene.visibility === "quarantined") continue;
|
|
2829
|
+
const geneSignalTypes = (gene.signals_match || []).map(
|
|
2830
|
+
(s) => typeof s === "string" ? s : s.type
|
|
2831
|
+
);
|
|
2832
|
+
if (geneSignalTypes.length === 0) continue;
|
|
2833
|
+
const matchCount = signalKeys.filter((k) => geneSignalTypes.includes(k)).length;
|
|
2834
|
+
const coverageScore = matchCount / geneSignalTypes.length;
|
|
2835
|
+
if (coverageScore === 0) continue;
|
|
2836
|
+
let alpha = gene.success_count + 1;
|
|
2837
|
+
let beta = gene.failure_count + 1;
|
|
2838
|
+
for (const key of signalKeys) {
|
|
2839
|
+
const prior = this._globalPrior.get(key);
|
|
2840
|
+
if (prior) {
|
|
2841
|
+
alpha += 0.3 * prior.alpha;
|
|
2842
|
+
beta += 0.3 * prior.beta;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
const sampledScore = alpha / (alpha + beta);
|
|
2846
|
+
const totalObs = gene.success_count + gene.failure_count;
|
|
2847
|
+
if (totalObs >= 10 && gene.success_count / totalObs < 0.18) continue;
|
|
2848
|
+
const rankScore = coverageScore * 0.4 + sampledScore * 0.6;
|
|
2849
|
+
candidates.push({ gene, rankScore, coverageScore, sampledScore });
|
|
2850
|
+
}
|
|
2851
|
+
if (candidates.length === 0) {
|
|
2852
|
+
return {
|
|
2853
|
+
action: "create_suggested",
|
|
2854
|
+
confidence: 0,
|
|
2855
|
+
reason: "no matching genes for signals",
|
|
2856
|
+
fromCache: true
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
candidates.sort((a, b) => b.rankScore - a.rankScore);
|
|
2860
|
+
const best = candidates[0];
|
|
2861
|
+
const alternatives = candidates.slice(1, 4).map((c) => ({
|
|
2862
|
+
gene_id: c.gene.id,
|
|
2863
|
+
confidence: Math.round(c.rankScore * 100) / 100,
|
|
2864
|
+
title: c.gene.title
|
|
2865
|
+
}));
|
|
2866
|
+
return {
|
|
2867
|
+
action: "apply_gene",
|
|
2868
|
+
gene_id: best.gene.id,
|
|
2869
|
+
gene: best.gene,
|
|
2870
|
+
strategy: best.gene.strategy,
|
|
2871
|
+
confidence: Math.round(best.rankScore * 100) / 100,
|
|
2872
|
+
coverageScore: Math.round(best.coverageScore * 100) / 100,
|
|
2873
|
+
alternatives,
|
|
2874
|
+
reason: `local cache selection (${this._genes.size} genes)`,
|
|
2875
|
+
fromCache: true
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
// src/signal-enrichment.ts
|
|
2881
|
+
var ERROR_PATTERNS = [
|
|
2882
|
+
{ pattern: /timeout|timed?\s*out|deadline\s*exceeded|context\s*deadline/i, type: "timeout" },
|
|
2883
|
+
{ pattern: /econnrefused|connection\s*refused/i, type: "connection_refused" },
|
|
2884
|
+
{ pattern: /enotfound|dns|getaddrinfo|resolve/i, type: "dns_error" },
|
|
2885
|
+
{ pattern: /rate\s*limit|too\s*many\s*requests|429/i, type: "rate_limit" },
|
|
2886
|
+
{ pattern: /401|unauthorized|unauthenticated|auth.*fail/i, type: "auth_error" },
|
|
2887
|
+
{ pattern: /403|forbidden|access\s*denied|permission/i, type: "permission_error" },
|
|
2888
|
+
{ pattern: /404|not\s*found/i, type: "not_found" },
|
|
2889
|
+
{ pattern: /5\d{2}|internal\s*server|server\s*error|502|503|504/i, type: "server_error" },
|
|
2890
|
+
{ pattern: /type\s*error|typeerror/i, type: "type_error" },
|
|
2891
|
+
{ pattern: /syntax\s*error|syntaxerror|unexpected\s*token/i, type: "syntax_error" },
|
|
2892
|
+
{ pattern: /reference\s*error|referenceerror|is\s*not\s*defined/i, type: "reference_error" },
|
|
2893
|
+
{ pattern: /out\s*of\s*memory|oom|heap|allocation\s*failed/i, type: "oom" },
|
|
2894
|
+
{ pattern: /crash|panic|segfault|sigsegv|sigabrt/i, type: "crash" },
|
|
2895
|
+
{ pattern: /quota|limit\s*exceeded|insufficient/i, type: "quota_exceeded" },
|
|
2896
|
+
{ pattern: /tls|ssl|certificate|cert\s*verify/i, type: "tls_error" },
|
|
2897
|
+
{ pattern: /deadlock|lock\s*timeout|lock\s*wait/i, type: "deadlock" }
|
|
2898
|
+
];
|
|
2899
|
+
function extractSignals(ctx) {
|
|
2900
|
+
const tags = [];
|
|
2901
|
+
if (ctx.error) {
|
|
2902
|
+
let matched = false;
|
|
2903
|
+
for (const { pattern, type } of ERROR_PATTERNS) {
|
|
2904
|
+
if (pattern.test(ctx.error)) {
|
|
2905
|
+
const tag = { type: `error:${type}` };
|
|
2906
|
+
if (ctx.provider) tag.provider = ctx.provider;
|
|
2907
|
+
if (ctx.stage) tag.stage = ctx.stage;
|
|
2908
|
+
if (ctx.severity) tag.severity = ctx.severity;
|
|
2909
|
+
tags.push(tag);
|
|
2910
|
+
matched = true;
|
|
2911
|
+
break;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
if (!matched) {
|
|
2915
|
+
const normalized = ctx.error.slice(0, 50).toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
2916
|
+
const tag = { type: `error:${normalized}` };
|
|
2917
|
+
if (ctx.provider) tag.provider = ctx.provider;
|
|
2918
|
+
if (ctx.stage) tag.stage = ctx.stage;
|
|
2919
|
+
tags.push(tag);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
if (ctx.taskStatus === "failed") tags.push({ type: "task.failed" });
|
|
2923
|
+
if (ctx.taskStatus === "completed") tags.push({ type: "task.completed" });
|
|
2924
|
+
if (ctx.taskCapability) {
|
|
2925
|
+
tags.push({ type: `capability:${ctx.taskCapability}` });
|
|
2926
|
+
}
|
|
2927
|
+
if (ctx.tags) {
|
|
2928
|
+
for (const tag of ctx.tags) {
|
|
2929
|
+
tags.push({ type: tag });
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
return tags;
|
|
2933
|
+
}
|
|
2934
|
+
function createEnrichedExtractor(config) {
|
|
2935
|
+
if (config.mode === "rules") {
|
|
2936
|
+
return async (ctx) => extractSignals(ctx);
|
|
2937
|
+
}
|
|
2938
|
+
const { llmExtract, timeoutMs = 3e3 } = config;
|
|
2939
|
+
if (!llmExtract) return async (ctx) => extractSignals(ctx);
|
|
2940
|
+
return async (ctx) => {
|
|
2941
|
+
try {
|
|
2942
|
+
const result = await Promise.race([
|
|
2943
|
+
llmExtract(ctx),
|
|
2944
|
+
new Promise(
|
|
2945
|
+
(_, reject) => setTimeout(() => reject(new Error("llm_timeout")), timeoutMs)
|
|
2946
|
+
)
|
|
2947
|
+
]);
|
|
2948
|
+
return result;
|
|
2949
|
+
} catch {
|
|
2950
|
+
return extractSignals(ctx);
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// src/evolution-runtime.ts
|
|
2956
|
+
var EvolutionRuntime = class {
|
|
2957
|
+
constructor(client, config) {
|
|
2958
|
+
this.client = client;
|
|
2959
|
+
this.outbox = [];
|
|
2960
|
+
this.started = false;
|
|
2961
|
+
// Session tracking
|
|
2962
|
+
this._sessions = [];
|
|
2963
|
+
this._sessionCounter = 0;
|
|
2964
|
+
this.config = {
|
|
2965
|
+
syncIntervalMs: config?.syncIntervalMs ?? 6e4,
|
|
2966
|
+
enrichment: config?.enrichment ?? { mode: "rules" },
|
|
2967
|
+
scope: config?.scope ?? "global",
|
|
2968
|
+
outboxMaxSize: config?.outboxMaxSize ?? 50,
|
|
2969
|
+
outboxFlushMs: config?.outboxFlushMs ?? 5e3
|
|
2970
|
+
};
|
|
2971
|
+
this.scope = this.config.scope;
|
|
2972
|
+
this.cache = new EvolutionCache();
|
|
2973
|
+
this.enricher = config?.enrichment ? createEnrichedExtractor(config.enrichment) : async (ctx) => extractSignals(ctx);
|
|
2974
|
+
}
|
|
2975
|
+
// ─── Lifecycle ──────────────────────────────────────
|
|
2976
|
+
/** Initialize: load snapshot + start sync + start outbox flush */
|
|
2977
|
+
async start() {
|
|
2978
|
+
if (this.started) return;
|
|
2979
|
+
this.started = true;
|
|
2980
|
+
try {
|
|
2981
|
+
const snapshot = await this.client.getSyncSnapshot(0);
|
|
2982
|
+
if (snapshot.data) {
|
|
2983
|
+
this.cache.loadSnapshot(snapshot.data);
|
|
2984
|
+
}
|
|
2985
|
+
} catch {
|
|
2986
|
+
}
|
|
2987
|
+
if (this.config.syncIntervalMs > 0) {
|
|
2988
|
+
this.syncTimer = setInterval(() => this.sync(), this.config.syncIntervalMs);
|
|
2989
|
+
}
|
|
2990
|
+
this.flushTimer = setInterval(() => this.flush(), this.config.outboxFlushMs);
|
|
2991
|
+
}
|
|
2992
|
+
/** Stop: clear timers + flush remaining outbox */
|
|
2993
|
+
async stop() {
|
|
2994
|
+
if (this.syncTimer) clearInterval(this.syncTimer);
|
|
2995
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
2996
|
+
await this.flush();
|
|
2997
|
+
this.started = false;
|
|
2998
|
+
}
|
|
2999
|
+
// ─── High-Level API ─────────────────────────────────
|
|
3000
|
+
/**
|
|
3001
|
+
* Get a strategy recommendation for an error/context.
|
|
3002
|
+
*
|
|
3003
|
+
* Flow: extract signals → try local cache (<1ms) → fallback to server (~30ms)
|
|
3004
|
+
*
|
|
3005
|
+
* @param error - Error message or Error object
|
|
3006
|
+
* @param context - Optional additional context (provider, stage, etc.)
|
|
3007
|
+
*/
|
|
3008
|
+
async suggest(error, context) {
|
|
3009
|
+
const errorStr = error instanceof Error ? error.message : error;
|
|
3010
|
+
const ctx = {
|
|
3011
|
+
error: errorStr,
|
|
3012
|
+
...context
|
|
3013
|
+
};
|
|
3014
|
+
const signals = await this.enricher(ctx);
|
|
3015
|
+
if (signals.length === 0) {
|
|
3016
|
+
return {
|
|
3017
|
+
action: "none",
|
|
3018
|
+
confidence: 0,
|
|
3019
|
+
signals: [],
|
|
3020
|
+
fromCache: false,
|
|
3021
|
+
reason: "no signals extracted from error"
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
const buildSuggestion = (action, geneId, gene, strategy, confidence, fromCache, reason, alternatives) => {
|
|
3025
|
+
this.lastSuggestedGeneId = geneId;
|
|
3026
|
+
this._activeSession = {
|
|
3027
|
+
id: `ses_${++this._sessionCounter}_${Date.now()}`,
|
|
3028
|
+
suggestedAt: Date.now(),
|
|
3029
|
+
suggestedGeneId: geneId,
|
|
3030
|
+
signals,
|
|
3031
|
+
adopted: false,
|
|
3032
|
+
confidence,
|
|
3033
|
+
fromCache
|
|
3034
|
+
};
|
|
3035
|
+
return {
|
|
3036
|
+
action,
|
|
3037
|
+
geneId,
|
|
3038
|
+
gene,
|
|
3039
|
+
strategy,
|
|
3040
|
+
confidence,
|
|
3041
|
+
signals,
|
|
3042
|
+
fromCache,
|
|
3043
|
+
reason,
|
|
3044
|
+
alternatives
|
|
3045
|
+
};
|
|
3046
|
+
};
|
|
3047
|
+
if (this.cache.geneCount > 0) {
|
|
3048
|
+
const local = this.cache.selectGene(signals);
|
|
3049
|
+
if (local.action === "apply_gene" && local.confidence > 0.3) {
|
|
3050
|
+
return buildSuggestion(
|
|
3051
|
+
local.action,
|
|
3052
|
+
local.gene_id,
|
|
3053
|
+
local.gene,
|
|
3054
|
+
local.strategy,
|
|
3055
|
+
local.confidence,
|
|
3056
|
+
true,
|
|
3057
|
+
local.reason,
|
|
3058
|
+
local.alternatives
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
try {
|
|
3063
|
+
const result = await this.client.analyze({
|
|
3064
|
+
signals,
|
|
3065
|
+
scope: this.scope
|
|
3066
|
+
});
|
|
3067
|
+
if (result.data) {
|
|
3068
|
+
return buildSuggestion(
|
|
3069
|
+
result.data.action,
|
|
3070
|
+
result.data.gene_id,
|
|
3071
|
+
result.data.gene,
|
|
3072
|
+
result.data.strategy,
|
|
3073
|
+
result.data.confidence ?? 0,
|
|
3074
|
+
false,
|
|
3075
|
+
result.data.reason,
|
|
3076
|
+
result.data.alternatives
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
} catch {
|
|
3080
|
+
const local = this.cache.selectGene(signals);
|
|
3081
|
+
return buildSuggestion(
|
|
3082
|
+
local.action,
|
|
3083
|
+
local.gene_id,
|
|
3084
|
+
local.gene,
|
|
3085
|
+
local.strategy,
|
|
3086
|
+
local.confidence,
|
|
3087
|
+
true,
|
|
3088
|
+
"server unreachable, using cache fallback",
|
|
3089
|
+
local.alternatives
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
return {
|
|
3093
|
+
action: "none",
|
|
3094
|
+
confidence: 0,
|
|
3095
|
+
signals,
|
|
3096
|
+
fromCache: false,
|
|
3097
|
+
reason: "no recommendation from server"
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Record an outcome. Fire-and-forget — never blocks, never throws.
|
|
3102
|
+
*
|
|
3103
|
+
* @param error - The error that was encountered
|
|
3104
|
+
* @param outcome - 'success' or 'failed'
|
|
3105
|
+
* @param summary - One-line summary of what happened
|
|
3106
|
+
* @param geneId - Gene that was used (auto-detected from last suggest() if omitted)
|
|
3107
|
+
*/
|
|
3108
|
+
learned(error, outcome, summary, geneId, metadata) {
|
|
3109
|
+
const errorStr = error instanceof Error ? error.message : error;
|
|
3110
|
+
const ctx = { error: errorStr };
|
|
3111
|
+
const signals = extractSignals(ctx);
|
|
3112
|
+
const resolvedGeneId = geneId || this.lastSuggestedGeneId;
|
|
3113
|
+
if (!resolvedGeneId) return;
|
|
3114
|
+
if (this._activeSession) {
|
|
3115
|
+
const session = this._activeSession;
|
|
3116
|
+
session.usedGeneId = resolvedGeneId;
|
|
3117
|
+
session.adopted = resolvedGeneId === session.suggestedGeneId;
|
|
3118
|
+
session.completedAt = Date.now();
|
|
3119
|
+
session.outcome = outcome;
|
|
3120
|
+
session.durationMs = session.completedAt - session.suggestedAt;
|
|
3121
|
+
this._sessions.push(session);
|
|
3122
|
+
this._activeSession = void 0;
|
|
3123
|
+
}
|
|
3124
|
+
this.outbox.push({
|
|
3125
|
+
geneId: resolvedGeneId,
|
|
3126
|
+
signals,
|
|
3127
|
+
outcome,
|
|
3128
|
+
summary,
|
|
3129
|
+
metadata,
|
|
3130
|
+
timestamp: Date.now(),
|
|
3131
|
+
sessionId: this._sessions[this._sessions.length - 1]?.id
|
|
3132
|
+
});
|
|
3133
|
+
if (this.outbox.length >= this.config.outboxMaxSize) {
|
|
3134
|
+
this.flush().catch(() => {
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
// ─── Session Metrics ────────────────────────────────
|
|
3139
|
+
/** Get all completed sessions. */
|
|
3140
|
+
get sessions() {
|
|
3141
|
+
return this._sessions;
|
|
3142
|
+
}
|
|
3143
|
+
/** Get aggregate metrics for benchmarking. */
|
|
3144
|
+
getMetrics() {
|
|
3145
|
+
const sessions = this._sessions;
|
|
3146
|
+
const totalSuggestions = sessions.length;
|
|
3147
|
+
const suggestionsWithGene = sessions.filter((s) => s.suggestedGeneId).length;
|
|
3148
|
+
const totalLearned = sessions.filter((s) => s.completedAt).length;
|
|
3149
|
+
const adoptedSessions = sessions.filter((s) => s.adopted && s.completedAt);
|
|
3150
|
+
const adoptedCount = adoptedSessions.length;
|
|
3151
|
+
const nonAdopted = sessions.filter((s) => !s.adopted && s.completedAt);
|
|
3152
|
+
const durations = sessions.filter((s) => s.durationMs != null).map((s) => s.durationMs);
|
|
3153
|
+
const avgDurationMs = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
|
|
3154
|
+
const adoptedSuccess = adoptedSessions.filter((s) => s.outcome === "success").length;
|
|
3155
|
+
const nonAdoptedSuccess = nonAdopted.filter((s) => s.outcome === "success").length;
|
|
3156
|
+
const cacheHits = sessions.filter((s) => s.fromCache).length;
|
|
3157
|
+
return {
|
|
3158
|
+
totalSuggestions,
|
|
3159
|
+
suggestionsWithGene,
|
|
3160
|
+
totalLearned,
|
|
3161
|
+
adoptedCount,
|
|
3162
|
+
geneUtilizationRate: suggestionsWithGene > 0 ? Math.round(adoptedCount / suggestionsWithGene * 100) / 100 : 0,
|
|
3163
|
+
avgDurationMs,
|
|
3164
|
+
adoptedSuccessRate: adoptedCount > 0 ? Math.round(adoptedSuccess / adoptedCount * 100) / 100 : 0,
|
|
3165
|
+
nonAdoptedSuccessRate: nonAdopted.length > 0 ? Math.round(nonAdoptedSuccess / nonAdopted.length * 100) / 100 : 0,
|
|
3166
|
+
cacheHitRate: totalSuggestions > 0 ? Math.round(cacheHits / totalSuggestions * 100) / 100 : 0
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
/** Reset session history. */
|
|
3170
|
+
resetMetrics() {
|
|
3171
|
+
this._sessions = [];
|
|
3172
|
+
}
|
|
3173
|
+
// ─── Internal ───────────────────────────────────────
|
|
3174
|
+
/** Sync cache with server */
|
|
3175
|
+
async sync() {
|
|
3176
|
+
try {
|
|
3177
|
+
const result = await this.client.sync({
|
|
3178
|
+
pull: { since: this.cache.cursor },
|
|
3179
|
+
scope: this.scope
|
|
3180
|
+
});
|
|
3181
|
+
if (result.data?.pulled) {
|
|
3182
|
+
this.cache.applyDelta({ pulled: result.data.pulled });
|
|
3183
|
+
}
|
|
3184
|
+
} catch {
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
/** Flush outbox to server */
|
|
3188
|
+
async flush() {
|
|
3189
|
+
if (this.outbox.length === 0) return;
|
|
3190
|
+
const batch = this.outbox.splice(0, this.config.outboxMaxSize);
|
|
3191
|
+
const promises = batch.map(
|
|
3192
|
+
(entry) => this.client.record({
|
|
3193
|
+
gene_id: entry.geneId,
|
|
3194
|
+
signals: entry.signals.map((s) => s.type),
|
|
3195
|
+
outcome: entry.outcome,
|
|
3196
|
+
summary: entry.summary,
|
|
3197
|
+
score: entry.score,
|
|
3198
|
+
metadata: entry.metadata,
|
|
3199
|
+
scope: this.scope
|
|
3200
|
+
}).catch(() => {
|
|
3201
|
+
this.outbox.push(entry);
|
|
3202
|
+
})
|
|
3203
|
+
);
|
|
3204
|
+
await Promise.allSettled(promises);
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
|
|
3208
|
+
// src/index.ts
|
|
3209
|
+
var _fs = null;
|
|
3210
|
+
var _os = null;
|
|
3211
|
+
var _path = null;
|
|
3212
|
+
try {
|
|
3213
|
+
_fs = __require("fs");
|
|
3214
|
+
_os = __require("os");
|
|
3215
|
+
_path = __require("path");
|
|
3216
|
+
} catch {
|
|
3217
|
+
}
|
|
3218
|
+
function resolveApiKey(explicit) {
|
|
3219
|
+
if (explicit) return explicit;
|
|
3220
|
+
try {
|
|
3221
|
+
if (typeof process !== "undefined" && process.env?.PRISMER_API_KEY) {
|
|
3222
|
+
return process.env.PRISMER_API_KEY;
|
|
3223
|
+
}
|
|
3224
|
+
} catch {
|
|
3225
|
+
}
|
|
3226
|
+
if (_fs && _os && _path) {
|
|
3227
|
+
try {
|
|
3228
|
+
const configPath = _path.join(_os.homedir(), ".prismer", "config.toml");
|
|
3229
|
+
const raw = _fs.readFileSync(configPath, "utf-8");
|
|
3230
|
+
const match = raw.match(/^api_key\s*=\s*'([^']+)'/m) || raw.match(/^api_key\s*=\s*"([^"]+)"/m);
|
|
3231
|
+
if (match?.[1]) return match[1];
|
|
3232
|
+
} catch {
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
return "";
|
|
3236
|
+
}
|
|
3237
|
+
function resolveBaseUrl(explicit) {
|
|
3238
|
+
if (explicit) return explicit;
|
|
3239
|
+
try {
|
|
3240
|
+
if (typeof process !== "undefined" && process.env?.PRISMER_BASE_URL) {
|
|
3241
|
+
return process.env.PRISMER_BASE_URL;
|
|
3242
|
+
}
|
|
3243
|
+
} catch {
|
|
3244
|
+
}
|
|
3245
|
+
if (_fs && _os && _path) {
|
|
3246
|
+
try {
|
|
3247
|
+
const configPath = _path.join(_os.homedir(), ".prismer", "config.toml");
|
|
3248
|
+
const raw = _fs.readFileSync(configPath, "utf-8");
|
|
3249
|
+
const match = raw.match(/^base_url\s*=\s*'([^']+)'/m) || raw.match(/^base_url\s*=\s*"([^"]+)"/m);
|
|
3250
|
+
if (match?.[1]) return match[1];
|
|
3251
|
+
} catch {
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
return void 0;
|
|
3255
|
+
}
|
|
3256
|
+
var AccountClient = class {
|
|
3257
|
+
constructor(_r) {
|
|
3258
|
+
this._r = _r;
|
|
3259
|
+
}
|
|
3260
|
+
/** Register an agent or human identity */
|
|
3261
|
+
async register(options) {
|
|
3262
|
+
return this._r("POST", "/api/im/register", options);
|
|
3263
|
+
}
|
|
3264
|
+
/** Get own identity, stats, bindings, credits */
|
|
3265
|
+
async me() {
|
|
3266
|
+
return this._r("GET", "/api/im/me");
|
|
3267
|
+
}
|
|
3268
|
+
/** Update own profile */
|
|
3269
|
+
async updateProfile(options) {
|
|
3270
|
+
return this._r("PATCH", "/api/im/me", options);
|
|
3271
|
+
}
|
|
3272
|
+
/** Refresh JWT token */
|
|
3273
|
+
async refreshToken() {
|
|
3274
|
+
return this._r("POST", "/api/im/token/refresh");
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
var DirectClient = class {
|
|
3278
|
+
constructor(_r) {
|
|
3279
|
+
this._r = _r;
|
|
3280
|
+
}
|
|
3281
|
+
/** Send a direct message to a user */
|
|
3282
|
+
async send(userId, content, options) {
|
|
3283
|
+
return this._r("POST", `/api/im/direct/${userId}/messages`, {
|
|
3284
|
+
content,
|
|
3285
|
+
type: options?.type ?? "text",
|
|
3286
|
+
metadata: options?.metadata,
|
|
3287
|
+
parentId: options?.parentId,
|
|
3288
|
+
quotedMessageId: options?.quotedMessageId
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
/** Get direct message history with a user */
|
|
3292
|
+
async getMessages(userId, options) {
|
|
3293
|
+
const query = {};
|
|
3294
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3295
|
+
if (options?.offset != null) query.offset = String(options.offset);
|
|
3296
|
+
return this._r("GET", `/api/im/direct/${userId}/messages`, void 0, query);
|
|
3297
|
+
}
|
|
3298
|
+
};
|
|
3299
|
+
var GroupsClient = class {
|
|
3300
|
+
constructor(_r) {
|
|
3301
|
+
this._r = _r;
|
|
3302
|
+
}
|
|
3303
|
+
/** Create a group chat */
|
|
3304
|
+
async create(options) {
|
|
3305
|
+
return this._r("POST", "/api/im/groups", options);
|
|
3306
|
+
}
|
|
3307
|
+
/** List groups you belong to */
|
|
3308
|
+
async list() {
|
|
3309
|
+
return this._r("GET", "/api/im/groups");
|
|
3310
|
+
}
|
|
3311
|
+
/** Get group details */
|
|
3312
|
+
async get(groupId) {
|
|
3313
|
+
return this._r("GET", `/api/im/groups/${groupId}`);
|
|
3314
|
+
}
|
|
3315
|
+
/** Send a message to a group */
|
|
3316
|
+
async send(groupId, content, options) {
|
|
3317
|
+
return this._r("POST", `/api/im/groups/${groupId}/messages`, {
|
|
3318
|
+
content,
|
|
3319
|
+
type: options?.type ?? "text",
|
|
3320
|
+
metadata: options?.metadata,
|
|
3321
|
+
parentId: options?.parentId,
|
|
3322
|
+
quotedMessageId: options?.quotedMessageId
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
/** Get group message history */
|
|
3326
|
+
async getMessages(groupId, options) {
|
|
3327
|
+
const query = {};
|
|
3328
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3329
|
+
if (options?.offset != null) query.offset = String(options.offset);
|
|
3330
|
+
return this._r("GET", `/api/im/groups/${groupId}/messages`, void 0, query);
|
|
3331
|
+
}
|
|
3332
|
+
/** Add a member to a group (owner/admin only) */
|
|
3333
|
+
async addMember(groupId, userId) {
|
|
3334
|
+
return this._r("POST", `/api/im/groups/${groupId}/members`, { userId });
|
|
3335
|
+
}
|
|
3336
|
+
/** Remove a member from a group (owner/admin only) */
|
|
3337
|
+
async removeMember(groupId, userId) {
|
|
3338
|
+
return this._r("DELETE", `/api/im/groups/${groupId}/members/${userId}`);
|
|
3339
|
+
}
|
|
3340
|
+
};
|
|
3341
|
+
var ConversationsClient = class {
|
|
3342
|
+
constructor(_r) {
|
|
3343
|
+
this._r = _r;
|
|
3344
|
+
}
|
|
3345
|
+
/** List conversations */
|
|
3346
|
+
async list(options) {
|
|
3347
|
+
const query = {};
|
|
3348
|
+
if (options?.withUnread) query.withUnread = "true";
|
|
3349
|
+
if (options?.unreadOnly) query.unreadOnly = "true";
|
|
3350
|
+
return this._r("GET", "/api/im/conversations", void 0, query);
|
|
3351
|
+
}
|
|
3352
|
+
/** Get conversation details */
|
|
3353
|
+
async get(conversationId) {
|
|
3354
|
+
return this._r("GET", `/api/im/conversations/${conversationId}`);
|
|
3355
|
+
}
|
|
3356
|
+
/** Create a direct conversation */
|
|
3357
|
+
async createDirect(userId) {
|
|
3358
|
+
return this._r("POST", "/api/im/conversations/direct", { userId });
|
|
3359
|
+
}
|
|
3360
|
+
/** Mark a conversation as read */
|
|
3361
|
+
async markAsRead(conversationId) {
|
|
3362
|
+
return this._r("POST", `/api/im/conversations/${conversationId}/read`);
|
|
3363
|
+
}
|
|
3364
|
+
/** Archive a conversation */
|
|
3365
|
+
async archive(conversationId) {
|
|
3366
|
+
return this._r("POST", `/api/im/conversations/${conversationId}/archive`);
|
|
3367
|
+
}
|
|
3368
|
+
/** Unarchive a conversation */
|
|
3369
|
+
async unarchive(conversationId) {
|
|
3370
|
+
return this._r("POST", `/api/im/conversations/${conversationId}/unarchive`);
|
|
3371
|
+
}
|
|
3372
|
+
/** Update conversation metadata */
|
|
3373
|
+
async update(conversationId, options) {
|
|
3374
|
+
return this._r("PATCH", `/api/im/conversations/${conversationId}`, options);
|
|
3375
|
+
}
|
|
3376
|
+
/** Pin or unpin a conversation */
|
|
3377
|
+
async pin(conversationId, pinned) {
|
|
3378
|
+
return this._r("PATCH", `/api/im/conversations/${conversationId}/pin`, { pinned });
|
|
3379
|
+
}
|
|
3380
|
+
/** Mute or unmute a conversation */
|
|
3381
|
+
async mute(conversationId, muted) {
|
|
3382
|
+
return this._r("PATCH", `/api/im/conversations/${conversationId}/mute`, { muted });
|
|
3383
|
+
}
|
|
3384
|
+
/** Delete a conversation */
|
|
3385
|
+
async delete(conversationId) {
|
|
3386
|
+
return this._r("DELETE", `/api/im/conversations/${conversationId}`);
|
|
3387
|
+
}
|
|
3388
|
+
};
|
|
3389
|
+
var MessagesClient = class {
|
|
3390
|
+
constructor(_r) {
|
|
3391
|
+
this._r = _r;
|
|
3392
|
+
}
|
|
3393
|
+
/** Send a message to a conversation */
|
|
3394
|
+
async send(conversationId, content, options) {
|
|
3395
|
+
return this._r("POST", `/api/im/messages/${conversationId}`, {
|
|
3396
|
+
content,
|
|
3397
|
+
type: options?.type ?? "text",
|
|
3398
|
+
metadata: options?.metadata,
|
|
3399
|
+
parentId: options?.parentId,
|
|
3400
|
+
quotedMessageId: options?.quotedMessageId
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
/** Get message history for a conversation */
|
|
3404
|
+
async getHistory(conversationId, options) {
|
|
3405
|
+
const query = {};
|
|
3406
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3407
|
+
if (options?.offset != null) query.offset = String(options.offset);
|
|
3408
|
+
return this._r("GET", `/api/im/messages/${conversationId}`, void 0, query);
|
|
3409
|
+
}
|
|
3410
|
+
/** Edit a message */
|
|
3411
|
+
async edit(conversationId, messageId, content, options) {
|
|
3412
|
+
return this._r("PATCH", `/api/im/messages/${conversationId}/${messageId}`, { content, ...options?.metadata ? { metadata: options.metadata } : {} });
|
|
3413
|
+
}
|
|
3414
|
+
/** Delete a message */
|
|
3415
|
+
async delete(conversationId, messageId) {
|
|
3416
|
+
return this._r("DELETE", `/api/im/messages/${conversationId}/${messageId}`);
|
|
3417
|
+
}
|
|
3418
|
+
/** Mark messages as delivered */
|
|
3419
|
+
async markDelivered(conversationId, messageIds) {
|
|
3420
|
+
return this._r("POST", "/api/im/messages/delivered", { conversationId, messageIds });
|
|
3421
|
+
}
|
|
3422
|
+
/**
|
|
3423
|
+
* Add or remove an emoji reaction on a message (v1.8.2).
|
|
3424
|
+
* Idempotent — adding an existing reaction or removing a non-existent one is a no-op.
|
|
3425
|
+
* Returns the full reactions snapshot: `{ "👍": ["userId-a", ...], ... }`.
|
|
3426
|
+
*/
|
|
3427
|
+
async react(conversationId, messageId, emoji, options) {
|
|
3428
|
+
return this._r("POST", `/api/im/messages/${conversationId}/${messageId}/reactions`, {
|
|
3429
|
+
emoji,
|
|
3430
|
+
...options?.remove ? { remove: true } : {}
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
3434
|
+
var ContactsClient = class {
|
|
3435
|
+
constructor(_r) {
|
|
3436
|
+
this._r = _r;
|
|
3437
|
+
}
|
|
3438
|
+
/** List contacts (users you've communicated with) */
|
|
3439
|
+
async list() {
|
|
3440
|
+
return this._r("GET", "/api/im/contacts");
|
|
3441
|
+
}
|
|
3442
|
+
/** Search users/agents by query */
|
|
3443
|
+
async search(query, options) {
|
|
3444
|
+
const params = { q: query };
|
|
3445
|
+
if (options?.type && options.type !== "all") params.type = options.type;
|
|
3446
|
+
if (options?.limit) params.limit = String(options.limit);
|
|
3447
|
+
if (options?.offset) params.offset = String(options.offset);
|
|
3448
|
+
return this._r("GET", "/api/im/discover", void 0, params);
|
|
3449
|
+
}
|
|
3450
|
+
/** Get a user's public profile */
|
|
3451
|
+
async getProfile(userId) {
|
|
3452
|
+
return this._r("GET", `/api/im/users/${userId}`);
|
|
3453
|
+
}
|
|
3454
|
+
/** Discover agents by capability or type */
|
|
3455
|
+
async discover(options) {
|
|
3456
|
+
const query = {};
|
|
3457
|
+
if (options?.type) query.type = options.type;
|
|
3458
|
+
if (options?.capability) query.capability = options.capability;
|
|
3459
|
+
return this._r("GET", "/api/im/discover", void 0, query);
|
|
3460
|
+
}
|
|
3461
|
+
// ─── Friend System (v1.8.0 P9) ─────────────────────────
|
|
3462
|
+
/** Send a friend request */
|
|
3463
|
+
async request(userId, opts) {
|
|
3464
|
+
return this._r("POST", "/api/im/contacts/request", { userId, ...opts });
|
|
3465
|
+
}
|
|
3466
|
+
/** List pending friend requests received */
|
|
3467
|
+
async pendingReceived(opts) {
|
|
3468
|
+
const params = {};
|
|
3469
|
+
if (opts?.limit) params.limit = String(opts.limit);
|
|
3470
|
+
if (opts?.offset) params.offset = String(opts.offset);
|
|
3471
|
+
return this._r("GET", "/api/im/contacts/requests/received", void 0, params);
|
|
3472
|
+
}
|
|
3473
|
+
/** List pending friend requests sent */
|
|
3474
|
+
async pendingSent(opts) {
|
|
3475
|
+
const params = {};
|
|
3476
|
+
if (opts?.limit) params.limit = String(opts.limit);
|
|
3477
|
+
if (opts?.offset) params.offset = String(opts.offset);
|
|
3478
|
+
return this._r("GET", "/api/im/contacts/requests/sent", void 0, params);
|
|
3479
|
+
}
|
|
3480
|
+
/** Accept a friend request */
|
|
3481
|
+
async accept(requestId) {
|
|
3482
|
+
return this._r("POST", `/api/im/contacts/requests/${requestId}/accept`);
|
|
3483
|
+
}
|
|
3484
|
+
/** Reject a friend request */
|
|
3485
|
+
async reject(requestId) {
|
|
3486
|
+
return this._r("POST", `/api/im/contacts/requests/${requestId}/reject`);
|
|
3487
|
+
}
|
|
3488
|
+
/** List friends */
|
|
3489
|
+
async friends(opts) {
|
|
3490
|
+
const params = {};
|
|
3491
|
+
if (opts?.limit) params.limit = String(opts.limit);
|
|
3492
|
+
if (opts?.offset) params.offset = String(opts.offset);
|
|
3493
|
+
return this._r("GET", "/api/im/contacts/friends", void 0, params);
|
|
3494
|
+
}
|
|
3495
|
+
/** Remove a friend */
|
|
3496
|
+
async remove(userId) {
|
|
3497
|
+
return this._r("DELETE", `/api/im/contacts/${userId}/remove`);
|
|
3498
|
+
}
|
|
3499
|
+
/** Set a remark/alias for a contact */
|
|
3500
|
+
async setRemark(userId, remark) {
|
|
3501
|
+
return this._r("PATCH", `/api/im/contacts/${userId}/remark`, { remark });
|
|
3502
|
+
}
|
|
3503
|
+
/** Block a user */
|
|
3504
|
+
async block(userId) {
|
|
3505
|
+
return this._r("POST", `/api/im/contacts/${userId}/block`, {});
|
|
3506
|
+
}
|
|
3507
|
+
/** Unblock a user */
|
|
3508
|
+
async unblock(userId) {
|
|
3509
|
+
return this._r("DELETE", `/api/im/contacts/${userId}/block`);
|
|
3510
|
+
}
|
|
3511
|
+
/** List blocked users */
|
|
3512
|
+
async blocklist(opts) {
|
|
3513
|
+
const params = {};
|
|
3514
|
+
if (opts?.limit) params.limit = String(opts.limit);
|
|
3515
|
+
if (opts?.offset) params.offset = String(opts.offset);
|
|
3516
|
+
return this._r("GET", "/api/im/contacts/blocked", void 0, params);
|
|
3517
|
+
}
|
|
3518
|
+
/** Get presence status for multiple users */
|
|
3519
|
+
async getPresence(userIds) {
|
|
3520
|
+
return this._r("POST", "/api/im/presence/batch", { userIds });
|
|
3521
|
+
}
|
|
3522
|
+
};
|
|
3523
|
+
var BindingsClient = class {
|
|
3524
|
+
constructor(_r) {
|
|
3525
|
+
this._r = _r;
|
|
3526
|
+
}
|
|
3527
|
+
/** Create a social binding */
|
|
3528
|
+
async create(options) {
|
|
3529
|
+
return this._r("POST", "/api/im/bindings", options);
|
|
3530
|
+
}
|
|
3531
|
+
/** Verify a binding with 6-digit code */
|
|
3532
|
+
async verify(bindingId, code) {
|
|
3533
|
+
return this._r("POST", `/api/im/bindings/${bindingId}/verify`, { code });
|
|
3534
|
+
}
|
|
3535
|
+
/** List bindings */
|
|
3536
|
+
async list() {
|
|
3537
|
+
return this._r("GET", "/api/im/bindings");
|
|
3538
|
+
}
|
|
3539
|
+
/** Delete a binding */
|
|
3540
|
+
async delete(bindingId) {
|
|
3541
|
+
return this._r("DELETE", `/api/im/bindings/${bindingId}`);
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
var CreditsClient = class {
|
|
3545
|
+
constructor(_r) {
|
|
3546
|
+
this._r = _r;
|
|
3547
|
+
}
|
|
3548
|
+
/** Get credits balance */
|
|
3549
|
+
async get() {
|
|
3550
|
+
return this._r("GET", "/api/im/credits");
|
|
3551
|
+
}
|
|
3552
|
+
/** Get credit transaction history */
|
|
3553
|
+
async transactions(options) {
|
|
3554
|
+
const query = {};
|
|
3555
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3556
|
+
if (options?.offset != null) query.offset = String(options.offset);
|
|
3557
|
+
return this._r("GET", "/api/im/credits/transactions", void 0, query);
|
|
3558
|
+
}
|
|
3559
|
+
};
|
|
3560
|
+
var WorkspaceClient = class {
|
|
3561
|
+
constructor(_r) {
|
|
3562
|
+
this._r = _r;
|
|
3563
|
+
}
|
|
3564
|
+
/** Initialize a 1:1 workspace (1 user + 1 agent) */
|
|
3565
|
+
async init(options) {
|
|
3566
|
+
return this._r("POST", "/api/im/workspace/init", options);
|
|
3567
|
+
}
|
|
3568
|
+
/** Initialize a group workspace (multi-user + multi-agent) */
|
|
3569
|
+
async initGroup(options) {
|
|
3570
|
+
return this._r("POST", "/api/im/workspace/init-group", options);
|
|
3571
|
+
}
|
|
3572
|
+
/** Add an agent to a workspace */
|
|
3573
|
+
async addAgent(workspaceId, agentId) {
|
|
3574
|
+
return this._r("POST", `/api/im/workspace/${workspaceId}/agents`, { agentId });
|
|
3575
|
+
}
|
|
3576
|
+
/** List agents in a workspace */
|
|
3577
|
+
async listAgents(workspaceId) {
|
|
3578
|
+
return this._r("GET", `/api/im/workspace/${workspaceId}/agents`);
|
|
3579
|
+
}
|
|
3580
|
+
/** @mention autocomplete */
|
|
3581
|
+
async mentionAutocomplete(conversationId, query) {
|
|
3582
|
+
const q = { conversationId };
|
|
3583
|
+
if (query) q.q = query;
|
|
3584
|
+
return this._r("GET", "/api/im/workspace/mentions/autocomplete", void 0, q);
|
|
3585
|
+
}
|
|
3586
|
+
};
|
|
3587
|
+
var TasksClient = class {
|
|
3588
|
+
constructor(_r) {
|
|
3589
|
+
this._r = _r;
|
|
3590
|
+
}
|
|
3591
|
+
/** Create a new task */
|
|
3592
|
+
async create(options) {
|
|
3593
|
+
return this._r("POST", "/api/im/tasks", options);
|
|
3594
|
+
}
|
|
3595
|
+
/** List tasks with optional filters */
|
|
3596
|
+
async list(options) {
|
|
3597
|
+
const query = {};
|
|
3598
|
+
if (options?.status) query.status = options.status;
|
|
3599
|
+
if (options?.capability) query.capability = options.capability;
|
|
3600
|
+
if (options?.assigneeId) query.assigneeId = options.assigneeId;
|
|
3601
|
+
if (options?.creatorId) query.creatorId = options.creatorId;
|
|
3602
|
+
if (options?.scheduleType) query.scheduleType = options.scheduleType;
|
|
3603
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3604
|
+
if (options?.cursor) query.cursor = options.cursor;
|
|
3605
|
+
return this._r("GET", "/api/im/tasks", void 0, query);
|
|
3606
|
+
}
|
|
3607
|
+
/** Get task details with logs */
|
|
3608
|
+
async get(taskId) {
|
|
3609
|
+
return this._r("GET", `/api/im/tasks/${taskId}`);
|
|
3610
|
+
}
|
|
3611
|
+
/** Update a task */
|
|
3612
|
+
async update(taskId, options) {
|
|
3613
|
+
return this._r("PATCH", `/api/im/tasks/${taskId}`, options);
|
|
3614
|
+
}
|
|
3615
|
+
/** Claim a pending task */
|
|
3616
|
+
async claim(taskId) {
|
|
3617
|
+
return this._r("POST", `/api/im/tasks/${taskId}/claim`);
|
|
3618
|
+
}
|
|
3619
|
+
/** Report progress on a task */
|
|
3620
|
+
async progress(taskId, options) {
|
|
3621
|
+
return this._r("POST", `/api/im/tasks/${taskId}/progress`, options);
|
|
3622
|
+
}
|
|
3623
|
+
/** Complete a task with result */
|
|
3624
|
+
async complete(taskId, options) {
|
|
3625
|
+
return this._r("POST", `/api/im/tasks/${taskId}/complete`, options);
|
|
3626
|
+
}
|
|
3627
|
+
/** Fail a task with error */
|
|
3628
|
+
async fail(taskId, error, metadata) {
|
|
3629
|
+
return this._r("POST", `/api/im/tasks/${taskId}/fail`, { error, metadata });
|
|
3630
|
+
}
|
|
3631
|
+
/** Approve a completed task */
|
|
3632
|
+
async approve(taskId) {
|
|
3633
|
+
return this._r("POST", `/api/im/tasks/${taskId}/approve`);
|
|
3634
|
+
}
|
|
3635
|
+
/** Reject a task with reason */
|
|
3636
|
+
async reject(taskId, reason) {
|
|
3637
|
+
return this._r("POST", `/api/im/tasks/${taskId}/reject`, { reason });
|
|
3638
|
+
}
|
|
3639
|
+
/** Cancel a task */
|
|
3640
|
+
async cancel(taskId) {
|
|
3641
|
+
return this._r("DELETE", `/api/im/tasks/${taskId}`);
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
var MemoryClient = class {
|
|
3645
|
+
constructor(_r) {
|
|
3646
|
+
this._r = _r;
|
|
3647
|
+
}
|
|
3648
|
+
/** Create a memory file */
|
|
3649
|
+
async createFile(options) {
|
|
3650
|
+
return this._r("POST", "/api/im/memory/files", options);
|
|
3651
|
+
}
|
|
3652
|
+
/** List memory files */
|
|
3653
|
+
async listFiles(options) {
|
|
3654
|
+
const query = {};
|
|
3655
|
+
if (options?.scope) query.scope = options.scope;
|
|
3656
|
+
if (options?.path) query.path = options.path;
|
|
3657
|
+
return this._r("GET", "/api/im/memory/files", void 0, query);
|
|
3658
|
+
}
|
|
3659
|
+
/** Get a memory file by ID */
|
|
3660
|
+
async getFile(fileId) {
|
|
3661
|
+
return this._r("GET", `/api/im/memory/files/${fileId}`);
|
|
3662
|
+
}
|
|
3663
|
+
/** Update a memory file (append, replace, or replace_section) */
|
|
3664
|
+
async updateFile(fileId, options) {
|
|
3665
|
+
return this._r("PATCH", `/api/im/memory/files/${fileId}`, options);
|
|
3666
|
+
}
|
|
3667
|
+
/** Delete a memory file */
|
|
3668
|
+
async deleteFile(fileId) {
|
|
3669
|
+
return this._r("DELETE", `/api/im/memory/files/${fileId}`);
|
|
3670
|
+
}
|
|
3671
|
+
/** Compact conversation messages into a summary */
|
|
3672
|
+
async compact(options) {
|
|
3673
|
+
return this._r("POST", "/api/im/memory/compact", options);
|
|
3674
|
+
}
|
|
3675
|
+
/** Get compaction summaries for a conversation */
|
|
3676
|
+
async getCompaction(conversationId) {
|
|
3677
|
+
return this._r("GET", `/api/im/memory/compact/${conversationId}`);
|
|
3678
|
+
}
|
|
3679
|
+
/** Load memory for session context */
|
|
3680
|
+
async load(scope) {
|
|
3681
|
+
const query = {};
|
|
3682
|
+
if (scope) query.scope = scope;
|
|
3683
|
+
return this._r("GET", "/api/im/memory/load", void 0, query);
|
|
3684
|
+
}
|
|
3685
|
+
/** Get memory-gene knowledge links for the authenticated user's memory files (v1.8.0) */
|
|
3686
|
+
async getKnowledgeLinks() {
|
|
3687
|
+
return this._r("GET", "/api/im/memory/links");
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
var KnowledgeLinkClient = class {
|
|
3691
|
+
constructor(_r) {
|
|
3692
|
+
this._r = _r;
|
|
3693
|
+
}
|
|
3694
|
+
/**
|
|
3695
|
+
* Get all knowledge links for a given entity.
|
|
3696
|
+
* @param entityType - One of: memory, gene, capsule, signal
|
|
3697
|
+
* @param entityId - The entity ID
|
|
3698
|
+
*/
|
|
3699
|
+
async getLinks(entityType, entityId) {
|
|
3700
|
+
return this._r("GET", "/api/im/knowledge/links", void 0, { entityType, entityId });
|
|
3701
|
+
}
|
|
3702
|
+
};
|
|
3703
|
+
var IdentityClient = class {
|
|
3704
|
+
constructor(_r) {
|
|
3705
|
+
this._r = _r;
|
|
3706
|
+
}
|
|
3707
|
+
/** Get server public key */
|
|
3708
|
+
async getServerKey() {
|
|
3709
|
+
return this._r("GET", "/api/im/keys/server");
|
|
3710
|
+
}
|
|
3711
|
+
/** Register or rotate an identity key */
|
|
3712
|
+
async registerKey(options) {
|
|
3713
|
+
return this._r("PUT", "/api/im/keys/identity", options);
|
|
3714
|
+
}
|
|
3715
|
+
/** Get a user's identity key */
|
|
3716
|
+
async getKey(userId) {
|
|
3717
|
+
return this._r("GET", `/api/im/keys/identity/${userId}`);
|
|
3718
|
+
}
|
|
3719
|
+
/** Revoke own identity key */
|
|
3720
|
+
async revokeKey() {
|
|
3721
|
+
return this._r("POST", "/api/im/keys/identity/revoke");
|
|
3722
|
+
}
|
|
3723
|
+
/** Get key audit log for a user */
|
|
3724
|
+
async getAuditLog(userId) {
|
|
3725
|
+
return this._r("GET", `/api/im/keys/audit/${userId}`);
|
|
3726
|
+
}
|
|
3727
|
+
/** Verify key audit log integrity */
|
|
3728
|
+
async verifyAuditLog(userId) {
|
|
3729
|
+
return this._r("GET", `/api/im/keys/audit/${userId}/verify`);
|
|
3730
|
+
}
|
|
3731
|
+
};
|
|
3732
|
+
var SecurityClient = class {
|
|
3733
|
+
constructor(_r) {
|
|
3734
|
+
this._r = _r;
|
|
3735
|
+
}
|
|
3736
|
+
/** Get conversation security settings */
|
|
3737
|
+
async getConversationSecurity(conversationId) {
|
|
3738
|
+
return this._r("GET", `/api/im/conversations/${conversationId}/security`);
|
|
3739
|
+
}
|
|
3740
|
+
/** Update conversation security settings */
|
|
3741
|
+
async setConversationSecurity(conversationId, options) {
|
|
3742
|
+
return this._r("PATCH", `/api/im/conversations/${conversationId}/security`, options);
|
|
3743
|
+
}
|
|
3744
|
+
/** Upload a public key for a conversation */
|
|
3745
|
+
async uploadKey(conversationId, publicKey, algorithm) {
|
|
3746
|
+
const body = { publicKey };
|
|
3747
|
+
if (algorithm) body.algorithm = algorithm;
|
|
3748
|
+
return this._r("POST", `/api/im/conversations/${conversationId}/keys`, body);
|
|
3749
|
+
}
|
|
3750
|
+
/** Get keys for a conversation */
|
|
3751
|
+
async getKeys(conversationId) {
|
|
3752
|
+
return this._r("GET", `/api/im/conversations/${conversationId}/keys`);
|
|
3753
|
+
}
|
|
3754
|
+
/** Revoke a key for a specific user in a conversation */
|
|
3755
|
+
async revokeKey(conversationId, keyUserId) {
|
|
3756
|
+
return this._r("DELETE", `/api/im/conversations/${conversationId}/keys/${keyUserId}`);
|
|
3757
|
+
}
|
|
3758
|
+
};
|
|
3759
|
+
var EvolutionClient = class {
|
|
3760
|
+
constructor(_r) {
|
|
3761
|
+
this._r = _r;
|
|
3762
|
+
}
|
|
3763
|
+
// ── Public endpoints (no auth required) ──
|
|
3764
|
+
/** Get evolution stats */
|
|
3765
|
+
async getStats() {
|
|
3766
|
+
return this._r("GET", "/api/im/evolution/public/stats");
|
|
3767
|
+
}
|
|
3768
|
+
/** Get hot/trending genes */
|
|
3769
|
+
async getHotGenes(limit) {
|
|
3770
|
+
const query = {};
|
|
3771
|
+
if (limit != null) query.limit = String(limit);
|
|
3772
|
+
return this._r("GET", "/api/im/evolution/public/hot", void 0, query);
|
|
3773
|
+
}
|
|
3774
|
+
/** Browse published genes */
|
|
3775
|
+
async browseGenes(options) {
|
|
3776
|
+
const query = {};
|
|
3777
|
+
if (options?.category) query.category = options.category;
|
|
3778
|
+
if (options?.search) query.search = options.search;
|
|
3779
|
+
if (options?.sort) query.sort = options.sort;
|
|
3780
|
+
if (options?.page != null) query.page = String(options.page);
|
|
3781
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3782
|
+
return this._r("GET", "/api/im/evolution/public/genes", void 0, query);
|
|
3783
|
+
}
|
|
3784
|
+
/** Get a public gene by ID */
|
|
3785
|
+
async getPublicGene(geneId) {
|
|
3786
|
+
return this._r("GET", `/api/im/evolution/public/genes/${geneId}`);
|
|
3787
|
+
}
|
|
3788
|
+
/** Get capsules for a public gene */
|
|
3789
|
+
async getGeneCapsules(geneId, limit) {
|
|
3790
|
+
const query = {};
|
|
3791
|
+
if (limit != null) query.limit = String(limit);
|
|
3792
|
+
return this._r("GET", `/api/im/evolution/public/genes/${geneId}/capsules`, void 0, query);
|
|
3793
|
+
}
|
|
3794
|
+
/** Get gene lineage (parent + children) */
|
|
3795
|
+
async getGeneLineage(geneId) {
|
|
3796
|
+
return this._r("GET", `/api/im/evolution/public/genes/${geneId}/lineage`);
|
|
3797
|
+
}
|
|
3798
|
+
/** Get public evolution feed */
|
|
3799
|
+
async getFeed(limit) {
|
|
3800
|
+
const query = {};
|
|
3801
|
+
if (limit != null) query.limit = String(limit);
|
|
3802
|
+
return this._r("GET", "/api/im/evolution/public/feed", void 0, query);
|
|
3803
|
+
}
|
|
3804
|
+
// ── Leaderboard V2 (public, no auth required) ──
|
|
3805
|
+
/** Get hero section global stats (total agents, genes, capsules, savings) */
|
|
3806
|
+
async getLeaderboardHero() {
|
|
3807
|
+
return this._r("GET", "/api/im/evolution/leaderboard/hero");
|
|
3808
|
+
}
|
|
3809
|
+
/** Get rising stars leaderboard */
|
|
3810
|
+
async getLeaderboardRising(period, limit) {
|
|
3811
|
+
const query = {};
|
|
3812
|
+
if (period) query.period = period;
|
|
3813
|
+
if (limit != null) query.limit = String(limit);
|
|
3814
|
+
return this._r("GET", "/api/im/evolution/leaderboard/rising", void 0, query);
|
|
3815
|
+
}
|
|
3816
|
+
/** Get leaderboard summary stats (totalAgentsEvolving, totalGenesCreated, etc.) */
|
|
3817
|
+
async getLeaderboardStats() {
|
|
3818
|
+
return this._r("GET", "/api/im/evolution/leaderboard/stats");
|
|
3819
|
+
}
|
|
3820
|
+
/** Get agent improvement board */
|
|
3821
|
+
async getLeaderboardAgents(period, domain) {
|
|
3822
|
+
const query = {};
|
|
3823
|
+
if (period) query.period = period;
|
|
3824
|
+
if (domain) query.domain = domain;
|
|
3825
|
+
return this._r("GET", "/api/im/evolution/leaderboard/agents", void 0, query);
|
|
3826
|
+
}
|
|
3827
|
+
/** Get gene impact board */
|
|
3828
|
+
async getLeaderboardGenes(period, sort) {
|
|
3829
|
+
const query = {};
|
|
3830
|
+
if (period) query.period = period;
|
|
3831
|
+
if (sort) query.sort = sort;
|
|
3832
|
+
return this._r("GET", "/api/im/evolution/leaderboard/genes", void 0, query);
|
|
3833
|
+
}
|
|
3834
|
+
/** Get contributor board */
|
|
3835
|
+
async getLeaderboardContributors(period) {
|
|
3836
|
+
const query = {};
|
|
3837
|
+
if (period) query.period = period;
|
|
3838
|
+
return this._r("GET", "/api/im/evolution/leaderboard/contributors", void 0, query);
|
|
3839
|
+
}
|
|
3840
|
+
/** Get cross-environment comparison data */
|
|
3841
|
+
async getLeaderboardComparison() {
|
|
3842
|
+
return this._r("GET", "/api/im/evolution/leaderboard/comparison");
|
|
3843
|
+
}
|
|
3844
|
+
/** Get public profile page data for an agent or owner */
|
|
3845
|
+
async getPublicProfile(entityId) {
|
|
3846
|
+
return this._r("GET", `/api/im/evolution/profile/${encodeURIComponent(entityId)}`);
|
|
3847
|
+
}
|
|
3848
|
+
/** Render agent/creator card as PNG */
|
|
3849
|
+
async renderCard(input) {
|
|
3850
|
+
return this._r("POST", "/api/im/evolution/card/render", input);
|
|
3851
|
+
}
|
|
3852
|
+
/** Get benchmark data for profile FOMO section */
|
|
3853
|
+
async getBenchmark() {
|
|
3854
|
+
return this._r("GET", "/api/im/evolution/benchmark");
|
|
3855
|
+
}
|
|
3856
|
+
/** Get gene highlight capsules for profile page */
|
|
3857
|
+
async getHighlights(geneId) {
|
|
3858
|
+
return this._r("GET", `/api/im/evolution/highlights/${encodeURIComponent(geneId)}`);
|
|
3859
|
+
}
|
|
3860
|
+
// ── Authenticated endpoints ──
|
|
3861
|
+
/** Analyze signals and get gene recommendation */
|
|
3862
|
+
async analyze(options) {
|
|
3863
|
+
const { scope, ...body } = options;
|
|
3864
|
+
const q = {};
|
|
3865
|
+
if (scope) q.scope = scope;
|
|
3866
|
+
return this._r("POST", "/api/im/evolution/analyze", body, q);
|
|
3867
|
+
}
|
|
3868
|
+
/** Record an outcome (success/failure) for a gene */
|
|
3869
|
+
async record(options) {
|
|
3870
|
+
const { scope, ...body } = options;
|
|
3871
|
+
const q = {};
|
|
3872
|
+
if (scope) q.scope = scope;
|
|
3873
|
+
return this._r("POST", "/api/im/evolution/record", body, q);
|
|
3874
|
+
}
|
|
3875
|
+
/**
|
|
3876
|
+
* One-step evolution: analyze context → get gene recommendation → auto-record outcome.
|
|
3877
|
+
* Combines analyze() + record() into a single call for the common case.
|
|
3878
|
+
*
|
|
3879
|
+
* Usage:
|
|
3880
|
+
* const result = await client.evolution.evolve({
|
|
3881
|
+
* error: 'Connection timeout after 10s',
|
|
3882
|
+
* outcome: 'success',
|
|
3883
|
+
* score: 0.85,
|
|
3884
|
+
* summary: 'Fixed with exponential backoff',
|
|
3885
|
+
* });
|
|
3886
|
+
*/
|
|
3887
|
+
async evolve(options) {
|
|
3888
|
+
const { outcome, score, summary, strategy_used, scope, ...analyzeOpts } = options;
|
|
3889
|
+
const analysis = await this.analyze({ ...analyzeOpts, ...scope ? { scope } : {} });
|
|
3890
|
+
if (!analysis.ok || !analysis.data) {
|
|
3891
|
+
return { ok: false, error: analysis.error };
|
|
3892
|
+
}
|
|
3893
|
+
const data = analysis.data;
|
|
3894
|
+
const geneId = data.gene_id;
|
|
3895
|
+
if (geneId && (data.action === "apply_gene" || data.action === "explore")) {
|
|
3896
|
+
const recordResult = await this.record({
|
|
3897
|
+
gene_id: geneId,
|
|
3898
|
+
signals: data.signals || analyzeOpts.signals || [],
|
|
3899
|
+
outcome,
|
|
3900
|
+
score: score ?? (outcome === "success" ? 0.8 : 0.2),
|
|
3901
|
+
summary: summary || `${outcome === "success" ? "Resolved" : "Failed to resolve"} using ${geneId}`,
|
|
3902
|
+
strategy_used,
|
|
3903
|
+
...scope ? { scope } : {}
|
|
3904
|
+
});
|
|
3905
|
+
return {
|
|
3906
|
+
ok: true,
|
|
3907
|
+
data: {
|
|
3908
|
+
analysis: data,
|
|
3909
|
+
recorded: true,
|
|
3910
|
+
edge_updated: recordResult.data?.edge_updated
|
|
3911
|
+
}
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
return {
|
|
3915
|
+
ok: true,
|
|
3916
|
+
data: { analysis: data, recorded: false }
|
|
3917
|
+
};
|
|
3918
|
+
}
|
|
3919
|
+
/** Trigger gene distillation */
|
|
3920
|
+
async distill(dryRun) {
|
|
3921
|
+
const query = {};
|
|
3922
|
+
if (dryRun) query.dry_run = "true";
|
|
3923
|
+
return this._r("POST", "/api/im/evolution/distill", void 0, query);
|
|
3924
|
+
}
|
|
3925
|
+
/** List own genes */
|
|
3926
|
+
async listGenes(signals, scope) {
|
|
3927
|
+
const query = {};
|
|
3928
|
+
if (signals) query.signals = signals;
|
|
3929
|
+
if (scope) query.scope = scope;
|
|
3930
|
+
return this._r("GET", "/api/im/evolution/genes", void 0, query);
|
|
3931
|
+
}
|
|
3932
|
+
/** Create a new gene */
|
|
3933
|
+
async createGene(options) {
|
|
3934
|
+
const { scope, ...body } = options;
|
|
3935
|
+
const q = {};
|
|
3936
|
+
if (scope) q.scope = scope;
|
|
3937
|
+
return this._r("POST", "/api/im/evolution/genes", body, q);
|
|
3938
|
+
}
|
|
3939
|
+
/** Delete a gene */
|
|
3940
|
+
async deleteGene(geneId) {
|
|
3941
|
+
return this._r("DELETE", `/api/im/evolution/genes/${encodeURIComponent(geneId)}`);
|
|
3942
|
+
}
|
|
3943
|
+
/** Publish a gene. Pass skipCanary=true to bypass canary validation (MVP/admin). */
|
|
3944
|
+
async publishGene(geneId, options) {
|
|
3945
|
+
return this._r("POST", `/api/im/evolution/genes/${encodeURIComponent(geneId)}/publish`, options?.skipCanary ? { skipCanary: true } : void 0);
|
|
3946
|
+
}
|
|
3947
|
+
/** Import a published gene */
|
|
3948
|
+
async importGene(geneId) {
|
|
3949
|
+
return this._r("POST", "/api/im/evolution/genes/import", { gene_id: geneId });
|
|
3950
|
+
}
|
|
3951
|
+
/** Fork a gene with modifications */
|
|
3952
|
+
async forkGene(options) {
|
|
3953
|
+
return this._r("POST", "/api/im/evolution/genes/fork", options);
|
|
3954
|
+
}
|
|
3955
|
+
/** Get signal-gene edges */
|
|
3956
|
+
async getEdges(options) {
|
|
3957
|
+
const query = {};
|
|
3958
|
+
if (options?.signalKey) query.signal_key = options.signalKey;
|
|
3959
|
+
if (options?.geneId) query.gene_id = options.geneId;
|
|
3960
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3961
|
+
if (options?.scope) query.scope = options.scope;
|
|
3962
|
+
return this._r("GET", "/api/im/evolution/edges", void 0, query);
|
|
3963
|
+
}
|
|
3964
|
+
/** Get agent personality profile */
|
|
3965
|
+
async getPersonality(agentId) {
|
|
3966
|
+
return this._r("GET", `/api/im/evolution/personality/${agentId}`);
|
|
3967
|
+
}
|
|
3968
|
+
/** Get own capsule history */
|
|
3969
|
+
async getCapsules(options) {
|
|
3970
|
+
const query = {};
|
|
3971
|
+
if (options?.page != null) query.page = String(options.page);
|
|
3972
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3973
|
+
if (options?.scope) query.scope = options.scope;
|
|
3974
|
+
return this._r("GET", "/api/im/evolution/capsules", void 0, query);
|
|
3975
|
+
}
|
|
3976
|
+
/** Get evolution report */
|
|
3977
|
+
async getReport(agentId, scope) {
|
|
3978
|
+
const query = {};
|
|
3979
|
+
if (agentId) query.agent_id = agentId;
|
|
3980
|
+
if (scope) query.scope = scope;
|
|
3981
|
+
return this._r("GET", "/api/im/evolution/report", void 0, query);
|
|
3982
|
+
}
|
|
3983
|
+
/** List available evolution scopes */
|
|
3984
|
+
async listScopes() {
|
|
3985
|
+
return this._r("GET", "/api/im/evolution/scopes");
|
|
3986
|
+
}
|
|
3987
|
+
// ─── v0.3.1: Stories, Metrics, Skills ──────────────
|
|
3988
|
+
/** Get recent evolution stories (for L1 narrative embedding) */
|
|
3989
|
+
async getStories(options) {
|
|
3990
|
+
const query = {};
|
|
3991
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
3992
|
+
if (options?.since != null) query.since = String(options.since);
|
|
3993
|
+
return this._r("GET", "/api/im/evolution/stories", void 0, query);
|
|
3994
|
+
}
|
|
3995
|
+
/** Get north-star metrics comparison (standard vs hypergraph) */
|
|
3996
|
+
async getMetrics() {
|
|
3997
|
+
return this._r("GET", "/api/im/evolution/metrics");
|
|
3998
|
+
}
|
|
3999
|
+
/** Trigger metrics collection snapshot */
|
|
4000
|
+
async collectMetrics(windowHours) {
|
|
4001
|
+
return this._r("POST", "/api/im/evolution/metrics/collect", { window_hours: windowHours ?? 1 });
|
|
4002
|
+
}
|
|
4003
|
+
/** Search skills catalog */
|
|
4004
|
+
async searchSkills(options) {
|
|
4005
|
+
const q = {};
|
|
4006
|
+
if (options?.query) q.query = options.query;
|
|
4007
|
+
if (options?.category) q.category = options.category;
|
|
4008
|
+
if (options?.limit != null) q.limit = String(options.limit);
|
|
4009
|
+
return this._r("GET", "/api/im/skills/search", void 0, q);
|
|
4010
|
+
}
|
|
4011
|
+
/** Get skill catalog stats */
|
|
4012
|
+
async getSkillStats() {
|
|
4013
|
+
return this._r("GET", "/api/im/skills/stats");
|
|
4014
|
+
}
|
|
4015
|
+
/** Install a skill — creates Gene + returns content + install guide */
|
|
4016
|
+
async installSkill(slugOrId, scope) {
|
|
4017
|
+
return this._r("POST", `/api/im/skills/${encodeURIComponent(slugOrId)}/install`, scope ? { scope } : void 0);
|
|
4018
|
+
}
|
|
4019
|
+
/** Uninstall a skill */
|
|
4020
|
+
async uninstallSkill(slugOrId) {
|
|
4021
|
+
return this._r("DELETE", `/api/im/skills/${encodeURIComponent(slugOrId)}/install`);
|
|
4022
|
+
}
|
|
4023
|
+
/** List installed skills for this agent */
|
|
4024
|
+
async installedSkills() {
|
|
4025
|
+
return this._r("GET", "/api/im/skills/installed");
|
|
4026
|
+
}
|
|
4027
|
+
/** Get full skill content (SKILL.md + package info) */
|
|
4028
|
+
async getSkillContent(slugOrId) {
|
|
4029
|
+
return this._r("GET", `/api/im/skills/${encodeURIComponent(slugOrId)}/content`);
|
|
4030
|
+
}
|
|
4031
|
+
/** Create/submit a community skill */
|
|
4032
|
+
async createSkill(input) {
|
|
4033
|
+
return this._r("POST", "/api/im/skills", input);
|
|
4034
|
+
}
|
|
4035
|
+
/** Star a skill (increment community rating) */
|
|
4036
|
+
async starSkill(skillId) {
|
|
4037
|
+
return this._r("POST", `/api/im/skills/${encodeURIComponent(skillId)}/star`);
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Install a skill and write SKILL.md to local filesystem.
|
|
4041
|
+
* Combines cloud install + local file sync for Claude Code / OpenClaw / OpenCode.
|
|
4042
|
+
* @param slugOrId - Skill slug or ID
|
|
4043
|
+
* @param options - Local install options
|
|
4044
|
+
*/
|
|
4045
|
+
async installSkillLocal(slugOrId, options) {
|
|
4046
|
+
const result = await this.installSkill(slugOrId);
|
|
4047
|
+
if (!result.ok || !result.data) return result;
|
|
4048
|
+
let content = result.data.skill?.content || "";
|
|
4049
|
+
if (!content) {
|
|
4050
|
+
const contentResult = await this.getSkillContent(slugOrId);
|
|
4051
|
+
content = contentResult.data?.content || "";
|
|
4052
|
+
}
|
|
4053
|
+
if (!content) {
|
|
4054
|
+
return { ...result, data: { ...result.data, localPaths: [] } };
|
|
4055
|
+
}
|
|
4056
|
+
const rawSlug = result.data.skill?.slug || slugOrId;
|
|
4057
|
+
const slug = rawSlug.replace(/[\/\\]/g, "").replace(/\.\./g, "");
|
|
4058
|
+
if (!slug) {
|
|
4059
|
+
return { ...result, data: { ...result.data, localPaths: [] } };
|
|
4060
|
+
}
|
|
4061
|
+
const localPaths = [];
|
|
4062
|
+
try {
|
|
4063
|
+
const fs = await import("fs");
|
|
4064
|
+
const path = await import("path");
|
|
4065
|
+
const os = await import("os");
|
|
4066
|
+
const home = os.homedir();
|
|
4067
|
+
const pluginBase = process.env.PRISMER_PLUGIN_DIR || path.join(home, ".claude", "plugins", "prismer");
|
|
4068
|
+
const platformPaths = options?.project ? {
|
|
4069
|
+
"claude-code": path.join(options.projectRoot || ".", ".claude", "skills", slug),
|
|
4070
|
+
"openclaw": path.join(options.projectRoot || ".", "skills", slug),
|
|
4071
|
+
"opencode": path.join(options.projectRoot || ".", ".opencode", "skills", slug),
|
|
4072
|
+
"plugin": path.join(options.projectRoot || ".", ".claude", "plugins", "prismer", "skills", slug)
|
|
4073
|
+
} : {
|
|
4074
|
+
"claude-code": path.join(home, ".claude", "skills", slug),
|
|
4075
|
+
"openclaw": path.join(home, ".openclaw", "skills", slug),
|
|
4076
|
+
"opencode": path.join(home, ".config", "opencode", "skills", slug),
|
|
4077
|
+
"plugin": path.join(pluginBase, "skills", slug)
|
|
4078
|
+
};
|
|
4079
|
+
const targets = options?.platforms || Object.keys(platformPaths);
|
|
4080
|
+
for (const platform of targets) {
|
|
4081
|
+
const dir = platformPaths[platform];
|
|
4082
|
+
if (!dir) continue;
|
|
4083
|
+
try {
|
|
4084
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4085
|
+
const filePath = path.join(dir, "SKILL.md");
|
|
4086
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
4087
|
+
localPaths.push(filePath);
|
|
4088
|
+
} catch {
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
} catch {
|
|
4092
|
+
}
|
|
4093
|
+
return { ...result, data: { ...result.data, localPaths } };
|
|
4094
|
+
}
|
|
4095
|
+
/**
|
|
4096
|
+
* Uninstall a skill and remove local SKILL.md files.
|
|
4097
|
+
*/
|
|
4098
|
+
async uninstallSkillLocal(slugOrId) {
|
|
4099
|
+
const result = await this.uninstallSkill(slugOrId);
|
|
4100
|
+
const removedPaths = [];
|
|
4101
|
+
const slug = safeSlug(slugOrId);
|
|
4102
|
+
if (!slug) return { ...result, data: { uninstalled: result.data?.uninstalled ?? false, removedPaths } };
|
|
4103
|
+
try {
|
|
4104
|
+
const fs = await import("fs");
|
|
4105
|
+
const path = await import("path");
|
|
4106
|
+
const os = await import("os");
|
|
4107
|
+
const home = os.homedir();
|
|
4108
|
+
const pluginBase = process.env.PRISMER_PLUGIN_DIR || path.join(home, ".claude", "plugins", "prismer");
|
|
4109
|
+
const dirs = [
|
|
4110
|
+
path.join(home, ".claude", "skills", slug),
|
|
4111
|
+
path.join(home, ".openclaw", "skills", slug),
|
|
4112
|
+
path.join(home, ".config", "opencode", "skills", slug),
|
|
4113
|
+
path.join(pluginBase, "skills", slug)
|
|
4114
|
+
];
|
|
4115
|
+
for (const dir of dirs) {
|
|
4116
|
+
try {
|
|
4117
|
+
if (fs.existsSync(dir)) {
|
|
4118
|
+
fs.rmSync(dir, { recursive: true });
|
|
4119
|
+
removedPaths.push(dir);
|
|
4120
|
+
}
|
|
4121
|
+
} catch {
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
} catch {
|
|
4125
|
+
}
|
|
4126
|
+
return { ...result, data: { uninstalled: result.data?.uninstalled ?? false, removedPaths } };
|
|
4127
|
+
}
|
|
4128
|
+
/**
|
|
4129
|
+
* Sync all installed skills to local filesystem.
|
|
4130
|
+
*/
|
|
4131
|
+
async syncSkillsLocal(options) {
|
|
4132
|
+
const installed = await this.installedSkills();
|
|
4133
|
+
if (!installed.ok || !installed.data) return { synced: 0, failed: 0, paths: [] };
|
|
4134
|
+
let synced = 0;
|
|
4135
|
+
let failed = 0;
|
|
4136
|
+
const paths = [];
|
|
4137
|
+
for (const record of installed.data) {
|
|
4138
|
+
const rawSlug = record.skill?.slug;
|
|
4139
|
+
if (!rawSlug) {
|
|
4140
|
+
failed++;
|
|
4141
|
+
continue;
|
|
4142
|
+
}
|
|
4143
|
+
const slug = rawSlug.replace(/[\/\\]/g, "").replace(/\.\./g, "");
|
|
4144
|
+
if (!slug) {
|
|
4145
|
+
failed++;
|
|
4146
|
+
continue;
|
|
4147
|
+
}
|
|
4148
|
+
try {
|
|
4149
|
+
const contentResult = await this.getSkillContent(slug);
|
|
4150
|
+
const content = contentResult.data?.content;
|
|
4151
|
+
if (!content) {
|
|
4152
|
+
failed++;
|
|
4153
|
+
continue;
|
|
4154
|
+
}
|
|
4155
|
+
const fs = await import("fs");
|
|
4156
|
+
const path = await import("path");
|
|
4157
|
+
const os = await import("os");
|
|
4158
|
+
const home = os.homedir();
|
|
4159
|
+
const pluginBase = process.env.PRISMER_PLUGIN_DIR || path.join(home, ".claude", "plugins", "prismer");
|
|
4160
|
+
const platformPaths = {
|
|
4161
|
+
"claude-code": path.join(home, ".claude", "skills", slug),
|
|
4162
|
+
"openclaw": path.join(home, ".openclaw", "skills", slug),
|
|
4163
|
+
"opencode": path.join(home, ".config", "opencode", "skills", slug),
|
|
4164
|
+
"plugin": path.join(pluginBase, "skills", slug)
|
|
4165
|
+
};
|
|
4166
|
+
const targets = options?.platforms || Object.keys(platformPaths);
|
|
4167
|
+
for (const platform of targets) {
|
|
4168
|
+
const dir = platformPaths[platform];
|
|
4169
|
+
if (!dir) continue;
|
|
4170
|
+
try {
|
|
4171
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4172
|
+
const filePath = path.join(dir, "SKILL.md");
|
|
4173
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
4174
|
+
paths.push(filePath);
|
|
4175
|
+
} catch {
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
synced++;
|
|
4179
|
+
} catch {
|
|
4180
|
+
failed++;
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
return { synced, failed, paths };
|
|
4184
|
+
}
|
|
4185
|
+
/** Export a Gene as a Skill */
|
|
4186
|
+
async exportAsSkill(geneId, options) {
|
|
4187
|
+
return this._r("POST", `/api/im/evolution/genes/${geneId}/export-skill`, options);
|
|
4188
|
+
}
|
|
4189
|
+
// ─── P0: Report, Achievements, Sync ──────────────
|
|
4190
|
+
/** Submit a raw-context evolution report (auto-creates signals + gene match) */
|
|
4191
|
+
async submitReport(options) {
|
|
4192
|
+
return this._r("POST", "/api/im/evolution/report", {
|
|
4193
|
+
raw_context: options.rawContext,
|
|
4194
|
+
outcome: options.outcome,
|
|
4195
|
+
task_context: options.taskContext,
|
|
4196
|
+
task_error: options.taskError,
|
|
4197
|
+
task_id: options.taskId,
|
|
4198
|
+
metadata: options.metadata
|
|
4199
|
+
});
|
|
4200
|
+
}
|
|
4201
|
+
/** Get status of a submitted report by traceId */
|
|
4202
|
+
async getReportStatus(traceId) {
|
|
4203
|
+
return this._r("GET", `/api/im/evolution/report/${traceId}`);
|
|
4204
|
+
}
|
|
4205
|
+
/** Get evolution achievements for the current agent */
|
|
4206
|
+
async getAchievements() {
|
|
4207
|
+
return this._r("GET", "/api/im/evolution/achievements");
|
|
4208
|
+
}
|
|
4209
|
+
/** Get a sync snapshot (global gene/edge state since a sequence number) */
|
|
4210
|
+
async getSyncSnapshot(since) {
|
|
4211
|
+
const query = { scope: "global" };
|
|
4212
|
+
if (since != null) query.since = String(since);
|
|
4213
|
+
return this._r("GET", "/api/im/evolution/sync/snapshot", void 0, query);
|
|
4214
|
+
}
|
|
4215
|
+
/** Bidirectional sync: push local outcomes and pull remote updates */
|
|
4216
|
+
async sync(options) {
|
|
4217
|
+
const body = {};
|
|
4218
|
+
if (options?.pushOutcomes) body.push = { outcomes: options.pushOutcomes };
|
|
4219
|
+
if (options?.pullSince != null) body.pull = { since: options.pullSince };
|
|
4220
|
+
return this._r("POST", "/api/im/evolution/sync", body);
|
|
4221
|
+
}
|
|
4222
|
+
};
|
|
4223
|
+
function safeSlug(input) {
|
|
4224
|
+
return input.replace(/[\/\\]/g, "").replace(/\.\./g, "").replace(/\0/g, "");
|
|
4225
|
+
}
|
|
4226
|
+
function guessMimeType(fileName) {
|
|
4227
|
+
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
4228
|
+
const map = {
|
|
4229
|
+
png: "image/png",
|
|
4230
|
+
jpg: "image/jpeg",
|
|
4231
|
+
jpeg: "image/jpeg",
|
|
4232
|
+
gif: "image/gif",
|
|
4233
|
+
webp: "image/webp",
|
|
4234
|
+
svg: "image/svg+xml",
|
|
4235
|
+
ico: "image/x-icon",
|
|
4236
|
+
bmp: "image/bmp",
|
|
4237
|
+
pdf: "application/pdf",
|
|
4238
|
+
doc: "application/msword",
|
|
4239
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
4240
|
+
xls: "application/vnd.ms-excel",
|
|
4241
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
4242
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
4243
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
4244
|
+
txt: "text/plain",
|
|
4245
|
+
csv: "text/csv",
|
|
4246
|
+
html: "text/html",
|
|
4247
|
+
css: "text/css",
|
|
4248
|
+
js: "text/javascript",
|
|
4249
|
+
json: "application/json",
|
|
4250
|
+
xml: "application/xml",
|
|
4251
|
+
md: "text/markdown",
|
|
4252
|
+
yaml: "text/yaml",
|
|
4253
|
+
yml: "text/yaml",
|
|
4254
|
+
zip: "application/zip",
|
|
4255
|
+
gz: "application/gzip",
|
|
4256
|
+
tar: "application/x-tar",
|
|
4257
|
+
mp3: "audio/mpeg",
|
|
4258
|
+
wav: "audio/wav",
|
|
4259
|
+
mp4: "video/mp4",
|
|
4260
|
+
webm: "video/webm"
|
|
4261
|
+
};
|
|
4262
|
+
return map[ext] || "application/octet-stream";
|
|
4263
|
+
}
|
|
4264
|
+
var FilesClient = class {
|
|
4265
|
+
constructor(_r, _baseUrl, _fetchFn, _getAuthHeaders) {
|
|
4266
|
+
this._r = _r;
|
|
4267
|
+
this._baseUrl = _baseUrl;
|
|
4268
|
+
this._fetchFn = _fetchFn;
|
|
4269
|
+
this._getAuthHeaders = _getAuthHeaders;
|
|
4270
|
+
}
|
|
4271
|
+
/** Get a presigned upload URL */
|
|
4272
|
+
async presign(options) {
|
|
4273
|
+
return this._r("POST", "/api/im/files/presign", options);
|
|
4274
|
+
}
|
|
4275
|
+
/** Confirm an uploaded file (triggers validation + CDN activation) */
|
|
4276
|
+
async confirm(uploadId) {
|
|
4277
|
+
return this._r("POST", "/api/im/files/confirm", { uploadId });
|
|
4278
|
+
}
|
|
4279
|
+
/** Get storage quota */
|
|
4280
|
+
async quota() {
|
|
4281
|
+
return this._r("GET", "/api/im/files/quota");
|
|
4282
|
+
}
|
|
4283
|
+
/** Delete a file */
|
|
4284
|
+
async delete(uploadId) {
|
|
4285
|
+
return this._r("DELETE", `/api/im/files/${uploadId}`);
|
|
4286
|
+
}
|
|
4287
|
+
/** List allowed MIME types */
|
|
4288
|
+
async types() {
|
|
4289
|
+
return this._r("GET", "/api/im/files/types");
|
|
4290
|
+
}
|
|
4291
|
+
/** Initialize a multipart upload (for files > 10 MB) */
|
|
4292
|
+
async initMultipart(opts) {
|
|
4293
|
+
return this._r("POST", "/api/im/files/upload/init", opts);
|
|
4294
|
+
}
|
|
4295
|
+
/** Complete a multipart upload */
|
|
4296
|
+
async completeMultipart(uploadId, parts) {
|
|
4297
|
+
return this._r("POST", "/api/im/files/upload/complete", { uploadId, parts });
|
|
4298
|
+
}
|
|
4299
|
+
// --------------------------------------------------------------------------
|
|
4300
|
+
// High-level convenience methods
|
|
4301
|
+
// --------------------------------------------------------------------------
|
|
4302
|
+
/**
|
|
4303
|
+
* Upload a file (full lifecycle: presign → upload → confirm).
|
|
4304
|
+
*
|
|
4305
|
+
* @param input - File, Blob, Buffer, Uint8Array, or file path (Node.js string)
|
|
4306
|
+
* @param opts - Optional fileName, mimeType, onProgress
|
|
4307
|
+
* @returns Confirmed upload result with CDN URL
|
|
4308
|
+
*/
|
|
4309
|
+
async upload(input, opts) {
|
|
4310
|
+
let bytes;
|
|
4311
|
+
let fileName;
|
|
4312
|
+
if (typeof input === "string") {
|
|
4313
|
+
const fs = await import("fs");
|
|
4314
|
+
const path = await import("path");
|
|
4315
|
+
const buf = await fs.promises.readFile(input);
|
|
4316
|
+
bytes = new Uint8Array(buf);
|
|
4317
|
+
fileName = opts?.fileName || path.basename(input);
|
|
4318
|
+
} else if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
4319
|
+
const ab = await input.arrayBuffer();
|
|
4320
|
+
bytes = new Uint8Array(ab);
|
|
4321
|
+
fileName = opts?.fileName || (input instanceof File ? input.name : "");
|
|
4322
|
+
if (!fileName) throw new Error("fileName is required when uploading Blob without name");
|
|
4323
|
+
} else if (input instanceof Uint8Array) {
|
|
4324
|
+
bytes = input;
|
|
4325
|
+
fileName = opts?.fileName || "";
|
|
4326
|
+
if (!fileName) throw new Error("fileName is required when uploading Buffer or Uint8Array");
|
|
4327
|
+
} else {
|
|
4328
|
+
throw new Error("Unsupported input type");
|
|
4329
|
+
}
|
|
4330
|
+
const fileSize = bytes.byteLength;
|
|
4331
|
+
const mimeType = opts?.mimeType || guessMimeType(fileName);
|
|
4332
|
+
if (fileSize > 50 * 1024 * 1024) {
|
|
4333
|
+
throw new Error("File exceeds maximum size of 50 MB");
|
|
4334
|
+
}
|
|
4335
|
+
if (fileSize <= 10 * 1024 * 1024) {
|
|
4336
|
+
return this._uploadSimple(bytes, fileName, fileSize, mimeType, opts?.onProgress);
|
|
4337
|
+
}
|
|
4338
|
+
return this._uploadMultipart(bytes, fileName, fileSize, mimeType, opts?.onProgress);
|
|
4339
|
+
}
|
|
4340
|
+
/**
|
|
4341
|
+
* Upload a file and send it as a message in one call.
|
|
4342
|
+
*
|
|
4343
|
+
* @param conversationId - Target conversation
|
|
4344
|
+
* @param input - File input (same as upload())
|
|
4345
|
+
* @param opts - Upload options + optional message content/parentId
|
|
4346
|
+
*/
|
|
4347
|
+
async sendFile(conversationId, input, opts) {
|
|
4348
|
+
const uploaded = await this.upload(input, opts);
|
|
4349
|
+
const msgRes = await this._r("POST", `/api/im/messages/${conversationId}`, {
|
|
4350
|
+
content: opts?.content || uploaded.fileName,
|
|
4351
|
+
type: "file",
|
|
4352
|
+
metadata: {
|
|
4353
|
+
uploadId: uploaded.uploadId,
|
|
4354
|
+
fileUrl: uploaded.cdnUrl,
|
|
4355
|
+
fileName: uploaded.fileName,
|
|
4356
|
+
fileSize: uploaded.fileSize,
|
|
4357
|
+
mimeType: uploaded.mimeType
|
|
4358
|
+
},
|
|
4359
|
+
parentId: opts?.parentId
|
|
4360
|
+
});
|
|
4361
|
+
if (!msgRes.ok) {
|
|
4362
|
+
throw new Error(msgRes.error?.message || "Failed to send file message");
|
|
4363
|
+
}
|
|
4364
|
+
return { upload: uploaded, message: msgRes.data };
|
|
4365
|
+
}
|
|
4366
|
+
// --------------------------------------------------------------------------
|
|
4367
|
+
// Private upload helpers
|
|
4368
|
+
// --------------------------------------------------------------------------
|
|
4369
|
+
async _uploadSimple(bytes, fileName, fileSize, mimeType, onProgress) {
|
|
4370
|
+
const presignRes = await this.presign({ fileName, fileSize, mimeType });
|
|
4371
|
+
if (!presignRes.ok || !presignRes.data) {
|
|
4372
|
+
throw new Error(presignRes.error?.message || "Presign failed");
|
|
4373
|
+
}
|
|
4374
|
+
const { uploadId, url, fields } = presignRes.data;
|
|
4375
|
+
const formData = new FormData();
|
|
4376
|
+
const isS3 = url.startsWith("http");
|
|
4377
|
+
const uploadUrl = isS3 ? url : `${this._baseUrl}${url}`;
|
|
4378
|
+
if (isS3) {
|
|
4379
|
+
for (const [k, v] of Object.entries(fields)) formData.append(k, v);
|
|
4380
|
+
}
|
|
4381
|
+
const ab = new ArrayBuffer(bytes.byteLength);
|
|
4382
|
+
new Uint8Array(ab).set(bytes);
|
|
4383
|
+
formData.append("file", new Blob([ab], { type: mimeType }), fileName);
|
|
4384
|
+
const headers = {};
|
|
4385
|
+
if (!isS3) Object.assign(headers, this._getAuthHeaders());
|
|
4386
|
+
const resp = await this._fetchFn(uploadUrl, { method: "POST", body: formData, headers });
|
|
4387
|
+
if (!resp.ok) {
|
|
4388
|
+
const text = await resp.text();
|
|
4389
|
+
throw new Error(`Upload failed (${resp.status}): ${text}`);
|
|
4390
|
+
}
|
|
4391
|
+
onProgress?.(fileSize, fileSize);
|
|
4392
|
+
const confirmRes = await this.confirm(uploadId);
|
|
4393
|
+
if (!confirmRes.ok || !confirmRes.data) {
|
|
4394
|
+
throw new Error(confirmRes.error?.message || "Confirm failed");
|
|
4395
|
+
}
|
|
4396
|
+
return confirmRes.data;
|
|
4397
|
+
}
|
|
4398
|
+
async _uploadMultipart(bytes, fileName, fileSize, mimeType, onProgress) {
|
|
4399
|
+
const initRes = await this.initMultipart({ fileName, fileSize, mimeType });
|
|
4400
|
+
if (!initRes.ok || !initRes.data) {
|
|
4401
|
+
throw new Error(initRes.error?.message || "Multipart init failed");
|
|
4402
|
+
}
|
|
4403
|
+
const { uploadId, parts: partUrls } = initRes.data;
|
|
4404
|
+
const CHUNK_SIZE = 5 * 1024 * 1024;
|
|
4405
|
+
const completedParts = [];
|
|
4406
|
+
let uploaded = 0;
|
|
4407
|
+
for (const part of partUrls) {
|
|
4408
|
+
const start = (part.partNumber - 1) * CHUNK_SIZE;
|
|
4409
|
+
const end = Math.min(start + CHUNK_SIZE, fileSize);
|
|
4410
|
+
const chunk = bytes.slice(start, end);
|
|
4411
|
+
const isS3 = part.url.startsWith("http");
|
|
4412
|
+
const partUrl = isS3 ? part.url : `${this._baseUrl}${part.url}`;
|
|
4413
|
+
const headers = { "Content-Type": mimeType };
|
|
4414
|
+
if (!isS3) Object.assign(headers, this._getAuthHeaders());
|
|
4415
|
+
const resp = await this._fetchFn(partUrl, { method: "PUT", body: chunk, headers });
|
|
4416
|
+
if (!resp.ok) {
|
|
4417
|
+
throw new Error(`Part ${part.partNumber} upload failed (${resp.status})`);
|
|
4418
|
+
}
|
|
4419
|
+
const etag = resp.headers.get("ETag") || `"part-${part.partNumber}"`;
|
|
4420
|
+
completedParts.push({ partNumber: part.partNumber, etag });
|
|
4421
|
+
uploaded += chunk.byteLength;
|
|
4422
|
+
onProgress?.(uploaded, fileSize);
|
|
4423
|
+
}
|
|
4424
|
+
const completeRes = await this.completeMultipart(uploadId, completedParts);
|
|
4425
|
+
if (!completeRes.ok || !completeRes.data) {
|
|
4426
|
+
throw new Error(completeRes.error?.message || "Multipart complete failed");
|
|
4427
|
+
}
|
|
4428
|
+
return completeRes.data;
|
|
4429
|
+
}
|
|
4430
|
+
};
|
|
4431
|
+
var IMRealtimeClient = class {
|
|
4432
|
+
constructor(_wsBase) {
|
|
4433
|
+
this._wsBase = _wsBase;
|
|
4434
|
+
}
|
|
4435
|
+
/** Get the WebSocket URL */
|
|
4436
|
+
wsUrl(token) {
|
|
4437
|
+
const base = this._wsBase.replace(/^http/, "ws");
|
|
4438
|
+
return token ? `${base}/ws?token=${token}` : `${base}/ws`;
|
|
4439
|
+
}
|
|
4440
|
+
/** Get the SSE URL */
|
|
4441
|
+
sseUrl(token) {
|
|
4442
|
+
return token ? `${this._wsBase}/sse?token=${token}` : `${this._wsBase}/sse`;
|
|
4443
|
+
}
|
|
4444
|
+
/** Create a WebSocket client. Call .connect() to establish connection. */
|
|
4445
|
+
connectWS(config) {
|
|
4446
|
+
return new RealtimeWSClient(this._wsBase, config);
|
|
4447
|
+
}
|
|
4448
|
+
/** Create an SSE client. Call .connect() to establish connection. */
|
|
4449
|
+
connectSSE(config) {
|
|
4450
|
+
return new RealtimeSSEClient(this._wsBase, config);
|
|
4451
|
+
}
|
|
4452
|
+
};
|
|
4453
|
+
var IMClient = class {
|
|
4454
|
+
constructor(request, wsBase, fetchFn, getAuthHeaders, offlineManager, communityHubConfig) {
|
|
4455
|
+
this.account = new AccountClient(request);
|
|
4456
|
+
this.direct = new DirectClient(request);
|
|
4457
|
+
this.groups = new GroupsClient(request);
|
|
4458
|
+
this.conversations = new ConversationsClient(request);
|
|
4459
|
+
this.messages = new MessagesClient(request);
|
|
4460
|
+
this.contacts = new ContactsClient(request);
|
|
4461
|
+
this.bindings = new BindingsClient(request);
|
|
4462
|
+
this.credits = new CreditsClient(request);
|
|
4463
|
+
this.workspace = new WorkspaceClient(request);
|
|
4464
|
+
this.tasks = new TasksClient(request);
|
|
4465
|
+
this.memory = new MemoryClient(request);
|
|
4466
|
+
this.knowledge = new KnowledgeLinkClient(request);
|
|
4467
|
+
this.identity = new IdentityClient(request);
|
|
4468
|
+
this.security = new SecurityClient(request);
|
|
4469
|
+
this.evolution = new EvolutionClient(request);
|
|
4470
|
+
this.community = new CommunityHub(request, communityHubConfig ?? void 0);
|
|
4471
|
+
this.files = new FilesClient(request, wsBase, fetchFn, getAuthHeaders);
|
|
4472
|
+
this.realtime = new IMRealtimeClient(wsBase);
|
|
4473
|
+
this.offline = offlineManager ?? null;
|
|
4474
|
+
}
|
|
4475
|
+
/** IM health check */
|
|
4476
|
+
async health() {
|
|
4477
|
+
return this.account["_r"]("GET", "/api/im/health");
|
|
4478
|
+
}
|
|
4479
|
+
/** Get workspace superset view with slot filtering */
|
|
4480
|
+
async getWorkspace(scope, slots, includeContent) {
|
|
4481
|
+
const params = new URLSearchParams();
|
|
4482
|
+
if (scope) params.set("scope", scope);
|
|
4483
|
+
if (slots?.length) params.set("slots", slots.join(","));
|
|
4484
|
+
if (includeContent) params.set("includeContent", "true");
|
|
4485
|
+
return this.workspace["_r"]("GET", `/api/im/workspace/view?${params}`);
|
|
4486
|
+
}
|
|
4487
|
+
};
|
|
4488
|
+
var PrismerClient = class {
|
|
4489
|
+
constructor(config = {}) {
|
|
4490
|
+
this._offlineManager = null;
|
|
4491
|
+
/** AIP identity for auto-signing (v1.8.0 S1) */
|
|
4492
|
+
this._identity = null;
|
|
4493
|
+
this._identityReady = null;
|
|
4494
|
+
const resolvedApiKey = resolveApiKey(config.apiKey);
|
|
4495
|
+
if (resolvedApiKey && !resolvedApiKey.startsWith("sk-prismer-") && !resolvedApiKey.startsWith("eyJ")) {
|
|
4496
|
+
console.warn('Warning: API key should start with "sk-prismer-" (or "eyJ" for IM JWT)');
|
|
4497
|
+
}
|
|
4498
|
+
this.apiKey = resolvedApiKey;
|
|
4499
|
+
const envUrl = ENVIRONMENTS[config.environment || "production"];
|
|
4500
|
+
this.baseUrl = (resolveBaseUrl(config.baseUrl) || envUrl).replace(/\/$/, "");
|
|
4501
|
+
this.timeout = config.timeout || 3e4;
|
|
4502
|
+
this.fetchFn = config.fetch || fetch;
|
|
4503
|
+
this.imAgent = config.imAgent;
|
|
4504
|
+
if (config.identity) {
|
|
4505
|
+
if (config.identity === "auto" && this.apiKey) {
|
|
4506
|
+
this._identityReady = AIPIdentity.fromApiKey(this.apiKey).then((id) => {
|
|
4507
|
+
this._identity = id;
|
|
4508
|
+
}).catch((err) => console.warn("[PrismerSDK] Identity init failed:", err));
|
|
4509
|
+
} else if (typeof config.identity === "object" && config.identity.privateKey) {
|
|
4510
|
+
const keyBytes = typeof Buffer !== "undefined" ? new Uint8Array(Buffer.from(config.identity.privateKey, "base64")) : new Uint8Array(atob(config.identity.privateKey).split("").map((c) => c.charCodeAt(0)));
|
|
4511
|
+
this._identityReady = AIPIdentity.fromPrivateKey(keyBytes).then((id) => {
|
|
4512
|
+
this._identity = id;
|
|
4513
|
+
}).catch((err) => console.warn("[PrismerSDK] Identity init failed:", err));
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
if (config.offline) {
|
|
4517
|
+
this._offlineManager = new OfflineManager(
|
|
4518
|
+
config.offline.storage,
|
|
4519
|
+
(m, p, b, q) => this._request(m, p, b, q),
|
|
4520
|
+
config.offline
|
|
4521
|
+
);
|
|
4522
|
+
this._offlineManager.init().catch(
|
|
4523
|
+
(err) => console.warn("[PrismerSDK] Offline storage init failed:", err)
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4526
|
+
let imRequest = this._offlineManager ? (m, p, b, q) => this._offlineManager.dispatch(m, p, b, q) : (m, p, b, q) => this._request(m, p, b, q);
|
|
4527
|
+
if (config.identity) {
|
|
4528
|
+
const baseRequest = imRequest;
|
|
4529
|
+
imRequest = (method, path, body, query) => {
|
|
4530
|
+
if (method === "POST" && path.includes("/messages") && body) {
|
|
4531
|
+
const b = body;
|
|
4532
|
+
if (!b.signature && !b.skipSigning) {
|
|
4533
|
+
const ready = this._identityReady || Promise.resolve();
|
|
4534
|
+
return ready.then(() => {
|
|
4535
|
+
if (this._identity) {
|
|
4536
|
+
return this._signAndSend(baseRequest, method, path, b, query);
|
|
4537
|
+
}
|
|
4538
|
+
return baseRequest(method, path, body, query);
|
|
4539
|
+
});
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
return baseRequest(method, path, body, query);
|
|
4543
|
+
};
|
|
4544
|
+
}
|
|
4545
|
+
this.im = new IMClient(
|
|
4546
|
+
imRequest,
|
|
4547
|
+
this.baseUrl,
|
|
4548
|
+
this.fetchFn,
|
|
4549
|
+
() => this._getAuthHeaders(),
|
|
4550
|
+
this._offlineManager,
|
|
4551
|
+
config.community ?? null
|
|
4552
|
+
);
|
|
4553
|
+
}
|
|
4554
|
+
/** Wait for identity to be ready (useful for tests or explicit await) */
|
|
4555
|
+
async ensureIdentity() {
|
|
4556
|
+
if (this._identityReady) await this._identityReady;
|
|
4557
|
+
return this._identity;
|
|
4558
|
+
}
|
|
4559
|
+
/** Auto-sign a message body and send (v1.8.0 S1) */
|
|
4560
|
+
async _signAndSend(baseRequest, method, path, body, query) {
|
|
4561
|
+
if (this._identityReady) await this._identityReady;
|
|
4562
|
+
if (!this._identity) return baseRequest(method, path, body, query);
|
|
4563
|
+
const content = body.content || "";
|
|
4564
|
+
const contentHashBytes = new Uint8Array(
|
|
4565
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content))
|
|
4566
|
+
);
|
|
4567
|
+
const contentHash = Array.from(contentHashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4568
|
+
const timestamp = Date.now();
|
|
4569
|
+
const payload = `1|${this._identity.did}|${body.type || "text"}|${timestamp}|${contentHash}`;
|
|
4570
|
+
const payloadBytes = new TextEncoder().encode(payload);
|
|
4571
|
+
const signature = await this._identity.sign(payloadBytes);
|
|
4572
|
+
return baseRequest(method, path, {
|
|
4573
|
+
...body,
|
|
4574
|
+
secVersion: 1,
|
|
4575
|
+
senderDid: this._identity.did,
|
|
4576
|
+
contentHash,
|
|
4577
|
+
signature,
|
|
4578
|
+
signedAt: timestamp
|
|
4579
|
+
}, query);
|
|
4580
|
+
}
|
|
4581
|
+
/** Build auth headers for raw HTTP requests (used by file upload) */
|
|
4582
|
+
_getAuthHeaders() {
|
|
4583
|
+
const headers = {};
|
|
4584
|
+
if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
4585
|
+
if (this.imAgent) headers["X-IM-Agent"] = this.imAgent;
|
|
4586
|
+
return headers;
|
|
4587
|
+
}
|
|
4588
|
+
/**
|
|
4589
|
+
* Set or update the auth token (API key or IM JWT).
|
|
4590
|
+
* Useful after anonymous registration to set the returned JWT.
|
|
4591
|
+
*/
|
|
4592
|
+
setToken(token) {
|
|
4593
|
+
this.apiKey = token;
|
|
4594
|
+
}
|
|
4595
|
+
/** Cleanup resources (offline manager, timers). Call when disposing the client. */
|
|
4596
|
+
async destroy() {
|
|
4597
|
+
if (this._offlineManager) {
|
|
4598
|
+
await this._offlineManager.destroy();
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
// --------------------------------------------------------------------------
|
|
4602
|
+
// Internal request helper
|
|
4603
|
+
// --------------------------------------------------------------------------
|
|
4604
|
+
async _request(method, path, body, query, _isRetry) {
|
|
4605
|
+
const controller = new AbortController();
|
|
4606
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
4607
|
+
try {
|
|
4608
|
+
let url = `${this.baseUrl}${path}`;
|
|
4609
|
+
if (query && Object.keys(query).length > 0) {
|
|
4610
|
+
url += "?" + new URLSearchParams(query).toString();
|
|
4611
|
+
}
|
|
4612
|
+
const headers = {};
|
|
4613
|
+
if (this.apiKey) {
|
|
4614
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
4615
|
+
}
|
|
4616
|
+
if (this.imAgent) {
|
|
4617
|
+
headers["X-IM-Agent"] = this.imAgent;
|
|
4618
|
+
}
|
|
4619
|
+
const init = { method, headers, signal: controller.signal };
|
|
4620
|
+
if (body !== void 0) {
|
|
4621
|
+
headers["Content-Type"] = "application/json";
|
|
4622
|
+
init.body = JSON.stringify(body);
|
|
4623
|
+
}
|
|
4624
|
+
const response = await this.fetchFn(url, init);
|
|
4625
|
+
const data = await response.json();
|
|
4626
|
+
if (response.status === 401 && this.apiKey.startsWith("eyJ") && !_isRetry && !path.includes("/token/refresh")) {
|
|
4627
|
+
try {
|
|
4628
|
+
const refreshRes = await this._request("POST", "/api/im/token/refresh", void 0, void 0, true);
|
|
4629
|
+
if (refreshRes?.ok && refreshRes?.data?.token) {
|
|
4630
|
+
this.apiKey = refreshRes.data.token;
|
|
4631
|
+
return this._request(method, path, body, query, true);
|
|
4632
|
+
}
|
|
4633
|
+
} catch {
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
if (!response.ok) {
|
|
4637
|
+
const err = data.error || { code: "HTTP_ERROR", message: `Request failed with status ${response.status}` };
|
|
4638
|
+
return { ...data, success: false, ok: false, error: err };
|
|
4639
|
+
}
|
|
4640
|
+
return data;
|
|
4641
|
+
} catch (error) {
|
|
4642
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
4643
|
+
return { success: false, ok: false, error: { code: "TIMEOUT", message: "Request timed out" } };
|
|
4644
|
+
}
|
|
4645
|
+
return {
|
|
4646
|
+
success: false,
|
|
4647
|
+
ok: false,
|
|
4648
|
+
error: { code: "NETWORK_ERROR", message: error instanceof Error ? error.message : "Unknown error" }
|
|
4649
|
+
};
|
|
4650
|
+
} finally {
|
|
4651
|
+
clearTimeout(timeoutId);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
// --------------------------------------------------------------------------
|
|
4655
|
+
// Context API
|
|
4656
|
+
// --------------------------------------------------------------------------
|
|
4657
|
+
/** Load content from URL(s) or search query */
|
|
4658
|
+
async load(input, options = {}) {
|
|
4659
|
+
return this._request("POST", "/api/context/load", {
|
|
4660
|
+
input,
|
|
4661
|
+
inputType: options.inputType,
|
|
4662
|
+
processUncached: options.processUncached,
|
|
4663
|
+
search: options.search,
|
|
4664
|
+
processing: options.processing,
|
|
4665
|
+
return: options.return,
|
|
4666
|
+
ranking: options.ranking
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
4669
|
+
/** Save content to Prismer cache */
|
|
4670
|
+
async save(options) {
|
|
4671
|
+
return this._request("POST", "/api/context/save", options);
|
|
4672
|
+
}
|
|
4673
|
+
/** Batch save multiple items (max 50) */
|
|
4674
|
+
async saveBatch(items) {
|
|
4675
|
+
return this.save({ items });
|
|
4676
|
+
}
|
|
4677
|
+
// --------------------------------------------------------------------------
|
|
4678
|
+
// Parse API
|
|
4679
|
+
// --------------------------------------------------------------------------
|
|
4680
|
+
/** Parse a document (PDF, image) into structured content */
|
|
4681
|
+
async parse(options) {
|
|
4682
|
+
return this._request("POST", "/api/parse", options);
|
|
4683
|
+
}
|
|
4684
|
+
/** Convenience: parse a PDF by URL */
|
|
4685
|
+
async parsePdf(url, mode = "fast") {
|
|
4686
|
+
return this.parse({ url, mode });
|
|
4687
|
+
}
|
|
4688
|
+
/** Check status of an async parse task */
|
|
4689
|
+
async parseStatus(taskId) {
|
|
4690
|
+
return this._request("GET", `/api/parse/status/${taskId}`);
|
|
4691
|
+
}
|
|
4692
|
+
/** Get result of a completed async parse task */
|
|
4693
|
+
async parseResult(taskId) {
|
|
4694
|
+
return this._request("GET", `/api/parse/result/${taskId}`);
|
|
4695
|
+
}
|
|
4696
|
+
// --------------------------------------------------------------------------
|
|
4697
|
+
// Convenience
|
|
4698
|
+
// --------------------------------------------------------------------------
|
|
4699
|
+
/** Search for content (convenience wrapper around load with query mode) */
|
|
4700
|
+
async search(query, options) {
|
|
4701
|
+
return this.load(query, {
|
|
4702
|
+
inputType: "query",
|
|
4703
|
+
search: options?.topK ? { topK: options.topK } : void 0,
|
|
4704
|
+
return: options?.returnTopK || options?.format ? { topK: options?.returnTopK, format: options?.format } : void 0,
|
|
4705
|
+
ranking: options?.ranking ? { preset: options.ranking } : void 0
|
|
4706
|
+
});
|
|
4707
|
+
}
|
|
4708
|
+
};
|
|
4709
|
+
var index_default = PrismerClient;
|
|
4710
|
+
function createClient(config) {
|
|
4711
|
+
return new PrismerClient(config);
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
export {
|
|
4715
|
+
__require,
|
|
4716
|
+
RealtimeWSClient,
|
|
4717
|
+
RealtimeSSEClient,
|
|
4718
|
+
OfflineManager,
|
|
4719
|
+
AttachmentQueue,
|
|
4720
|
+
CommunityHub,
|
|
4721
|
+
AIPIdentity,
|
|
4722
|
+
ENVIRONMENTS,
|
|
4723
|
+
MemoryStorage,
|
|
4724
|
+
IndexedDBStorage,
|
|
4725
|
+
SQLiteStorage,
|
|
4726
|
+
TabCoordinator,
|
|
4727
|
+
E2EEncryption,
|
|
4728
|
+
encryptForSend,
|
|
4729
|
+
decryptOnReceive,
|
|
4730
|
+
encryptFile,
|
|
4731
|
+
decryptFile,
|
|
4732
|
+
encryptContext,
|
|
4733
|
+
decryptContext,
|
|
4734
|
+
decryptMessages,
|
|
4735
|
+
EvolutionCache,
|
|
4736
|
+
extractSignals,
|
|
4737
|
+
createEnrichedExtractor,
|
|
4738
|
+
EvolutionRuntime,
|
|
4739
|
+
AccountClient,
|
|
4740
|
+
DirectClient,
|
|
4741
|
+
GroupsClient,
|
|
4742
|
+
ConversationsClient,
|
|
4743
|
+
MessagesClient,
|
|
4744
|
+
ContactsClient,
|
|
4745
|
+
BindingsClient,
|
|
4746
|
+
CreditsClient,
|
|
4747
|
+
WorkspaceClient,
|
|
4748
|
+
TasksClient,
|
|
4749
|
+
MemoryClient,
|
|
4750
|
+
KnowledgeLinkClient,
|
|
4751
|
+
IdentityClient,
|
|
4752
|
+
SecurityClient,
|
|
4753
|
+
EvolutionClient,
|
|
4754
|
+
safeSlug,
|
|
4755
|
+
guessMimeType,
|
|
4756
|
+
FilesClient,
|
|
4757
|
+
IMRealtimeClient,
|
|
4758
|
+
IMClient,
|
|
4759
|
+
PrismerClient,
|
|
4760
|
+
index_default,
|
|
4761
|
+
createClient
|
|
4762
|
+
};
|