@engramind/client 0.0.0-stage → 0.1.1

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/server.js ADDED
@@ -0,0 +1,553 @@
1
+ import {
2
+ EngramindError,
3
+ clientStatus,
4
+ createConsumptionGrouper,
5
+ engramindCodeOf,
6
+ isConsumedEvent,
7
+ isMemoryChange,
8
+ mergeConsumption,
9
+ notConfiguredError,
10
+ proxyMaskedError,
11
+ toEngramindError,
12
+ toLiveEvent,
13
+ unreachableError
14
+ } from "./chunk-THVEWMKQ.js";
15
+
16
+ // src/server/client.ts
17
+ import { randomUUID } from "crypto";
18
+ var DEFAULT_TIMEOUT_MS = 3e4;
19
+ var USER_TEXT_QUERY = ["focus"];
20
+ var enc = encodeURIComponent;
21
+ function buildUrl(baseUrl, path, query = {}) {
22
+ const url = new URL(`${baseUrl.replace(/\/+$/, "")}${path}`);
23
+ for (const [key, value] of Object.entries(query)) if (value !== void 0 && value !== null && value !== "") url.searchParams.set(key, String(value));
24
+ return url.toString();
25
+ }
26
+ function debugUrl(url) {
27
+ const masked = new URL(url);
28
+ for (const key of USER_TEXT_QUERY) {
29
+ const value = masked.searchParams.get(key);
30
+ if (value !== null) masked.searchParams.set(key, `[omitido: ${value.length} caracteres]`);
31
+ }
32
+ return masked.toString();
33
+ }
34
+ function createEngramindClient(options) {
35
+ const doFetch = options.fetch ?? fetch;
36
+ const userAgent = options.userAgent ?? "engramind-client";
37
+ async function call(method, path, request = {}) {
38
+ const headers = { ...request.headers, Authorization: `Bearer ${options.token}`, Accept: "application/json", "User-Agent": userAgent };
39
+ if (method !== "GET") headers["Idempotency-Key"] = request.idempotencyKey ?? `engramind-client-${randomUUID()}`;
40
+ const hasBody = request.body !== void 0 && method !== "GET";
41
+ if (hasBody) headers["Content-Type"] = "application/json";
42
+ const timeout = AbortSignal.timeout(request.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
43
+ const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
44
+ const url = buildUrl(options.baseUrl, path, request.query);
45
+ const started = Date.now();
46
+ let res;
47
+ try {
48
+ res = await doFetch(url, { method, headers, body: hasBody ? JSON.stringify(request.body) : void 0, cache: "no-store", signal });
49
+ } catch (error) {
50
+ const reason = error instanceof Error ? error.name === "TimeoutError" || error.name === "AbortError" ? "tempo limite" : error.cause?.code ?? error.message : String(error);
51
+ options.onDebug?.({ method, url: debugUrl(url), status: null, durationMs: Date.now() - started, error: reason });
52
+ throw unreachableError(reason);
53
+ }
54
+ options.onDebug?.({ method, url: debugUrl(url), status: res.status, durationMs: Date.now() - started });
55
+ if (res.status === 304) return null;
56
+ const text = await res.text();
57
+ let json2 = null;
58
+ if (text) {
59
+ try {
60
+ json2 = JSON.parse(text);
61
+ } catch {
62
+ json2 = null;
63
+ }
64
+ }
65
+ if (!res.ok) {
66
+ const body = json2?.error ?? null;
67
+ throw toEngramindError(res.status, body && typeof body === "object" ? body : null, res.headers.get("retry-after"), body ? null : proxyMaskedError(text, json2));
68
+ }
69
+ return json2;
70
+ }
71
+ async function ready(signal) {
72
+ try {
73
+ const res = await doFetch(`${options.baseUrl.replace(/\/+$/, "")}/health/ready`, { headers: { Accept: "application/json", "User-Agent": userAgent }, cache: "no-store", signal: signal ?? AbortSignal.timeout(1e4) });
74
+ const body = await res.json().catch(() => null);
75
+ return { ready: res.ok && body?.status === "ready", detail: body?.status ?? `HTTP ${res.status}` };
76
+ } catch (error) {
77
+ return { ready: false, detail: error instanceof Error ? error.message : String(error) };
78
+ }
79
+ }
80
+ return {
81
+ call,
82
+ ready,
83
+ whoami: () => call("GET", "/v1/whoami"),
84
+ consumerSettings: () => call("GET", "/v1/consumer-settings"),
85
+ categories: {
86
+ list: () => call("GET", "/v1/categories"),
87
+ create: (input) => call("POST", "/v1/categories", { body: input })
88
+ },
89
+ agents: {
90
+ list: (query = {}) => call("GET", "/v1/agents", { query }),
91
+ get: (agent) => call("GET", `/v1/agents/${enc(agent)}`),
92
+ create: (input) => call("POST", "/v1/agents", { body: input }),
93
+ update: (agent, input) => call("PATCH", `/v1/agents/${enc(agent)}`, { body: input }),
94
+ delete: (agent) => call("DELETE", `/v1/agents/${enc(agent)}`)
95
+ },
96
+ subscriptions: {
97
+ list: (query = {}) => call("GET", "/v1/subscriptions", { query }),
98
+ create: (input) => call("POST", "/v1/subscriptions", { body: input }),
99
+ update: (subscription, input) => call("PATCH", `/v1/subscriptions/${enc(subscription)}`, { body: input }),
100
+ delete: (subscription) => call("DELETE", `/v1/subscriptions/${enc(subscription)}`)
101
+ },
102
+ channels: {
103
+ list: (query = {}) => call("GET", "/v1/channels", { query }),
104
+ get: (channel) => call("GET", `/v1/channels/${enc(channel)}`),
105
+ archive: (channel) => call("POST", `/v1/channels/${enc(channel)}/archive`, { body: {} }),
106
+ restore: (channel) => call("POST", `/v1/channels/${enc(channel)}/restore`, { body: {} })
107
+ },
108
+ memories: {
109
+ list: (query = {}) => call("GET", "/v1/memories", { query }),
110
+ get: (memory) => call("GET", `/v1/memories/${enc(memory)}`),
111
+ create: (input) => call("POST", "/v1/memories", { body: input }),
112
+ update: (memory, input) => call("PATCH", `/v1/memories/${enc(memory)}`, { body: input }),
113
+ delete: (memory) => call("DELETE", `/v1/memories/${enc(memory)}`),
114
+ archive: (memory) => call("POST", `/v1/memories/${enc(memory)}/archive`, { body: {} }),
115
+ restore: (memory) => call("POST", `/v1/memories/${enc(memory)}/restore`, { body: {} }),
116
+ pin: (memory) => call("POST", `/v1/memories/${enc(memory)}/pin`, { body: {} }),
117
+ unpin: (memory) => call("POST", `/v1/memories/${enc(memory)}/unpin`, { body: {} }),
118
+ move: (memory, channel) => call("POST", `/v1/memories/${enc(memory)}/move`, { body: { channel } }),
119
+ history: (memory, query = {}) => call("GET", `/v1/memories/${enc(memory)}/history`, { query }),
120
+ revert: (memory, version) => call("POST", `/v1/memories/${enc(memory)}/revert`, { body: { version } }),
121
+ import: (input, idempotencyKey) => call("POST", "/v1/memories/import", { body: input, idempotencyKey }),
122
+ search: (input) => call("POST", "/v1/memories/search", { body: input })
123
+ },
124
+ hints: (input) => call("POST", "/v1/hints", { body: input }),
125
+ context: (input, request = {}) => call("GET", "/v1/context", { query: { channels: input.channels?.join(","), format: input.format, focus: input.focus }, headers: input.ifNoneMatch ? { "If-None-Match": `"${input.ifNoneMatch}"` } : void 0, ...request }),
126
+ tokens: {
127
+ list: (query = {}) => call("GET", "/v1/tokens", { query }),
128
+ issue: (input) => call("POST", "/v1/tokens", { body: input }),
129
+ update: (token, input) => call("PATCH", `/v1/tokens/${enc(token)}`, { body: input }),
130
+ revoke: (token) => call("POST", `/v1/tokens/${enc(token)}/revoke`, { body: {} }),
131
+ rotate: (token) => call("POST", `/v1/tokens/${enc(token)}/rotate`, { body: {} })
132
+ },
133
+ apps: {
134
+ get: (app) => call("GET", `/v1/apps/${enc(app)}`),
135
+ /** `update_app_webhook`: grava ou remove (com `null`) a URL e o segredo do webhook do app. */
136
+ updateWebhook: (app, input) => call("PUT", `/v1/apps/${enc(app)}/webhook`, { body: input })
137
+ }
138
+ };
139
+ }
140
+
141
+ // src/server/routes.ts
142
+ var BOTH = ["app", "manager"];
143
+ var MANAGER = ["manager"];
144
+ var r = (method, pattern, operation, modes = BOTH) => ({ method, pattern, operation, modes });
145
+ var PROXY_ROUTES = [
146
+ r("GET", "whoami", "whoami"),
147
+ r("GET", "consumer-settings", "get_consumer_settings"),
148
+ r("GET", "categories", "list_categories"),
149
+ r("POST", "categories", "create_category"),
150
+ r("GET", "agents", "list_agents"),
151
+ r("POST", "agents", "create_agent"),
152
+ r("GET", "agents/:agent", "get_agent"),
153
+ r("PATCH", "agents/:agent", "update_agent"),
154
+ r("DELETE", "agents/:agent", "delete_agent"),
155
+ r("GET", "subscriptions", "list_subscriptions"),
156
+ r("POST", "subscriptions", "subscribe"),
157
+ r("PATCH", "subscriptions/:subscription", "update_subscription"),
158
+ r("DELETE", "subscriptions/:subscription", "unsubscribe"),
159
+ r("GET", "channels", "list_channels"),
160
+ r("GET", "channels/:channel", "get_channel"),
161
+ r("GET", "channels/:channel/stats", "channel_stats"),
162
+ r("POST", "channels/:channel/archive", "archive_channel"),
163
+ r("POST", "channels/:channel/restore", "restore_channel"),
164
+ r("GET", "memories", "list_memories"),
165
+ r("POST", "memories", "create_memory"),
166
+ r("POST", "memories/import", "import_memories"),
167
+ r("POST", "memories/search", "search_memories"),
168
+ r("POST", "hints", "get_hints"),
169
+ r("GET", "memories/:memory", "get_memory"),
170
+ r("PATCH", "memories/:memory", "update_memory"),
171
+ r("DELETE", "memories/:memory", "delete_memory"),
172
+ r("GET", "memories/:memory/history", "memory_history"),
173
+ r("POST", "memories/:memory/archive", "archive_memory"),
174
+ r("POST", "memories/:memory/restore", "restore_memory"),
175
+ r("POST", "memories/:memory/pin", "pin_memory"),
176
+ r("POST", "memories/:memory/unpin", "unpin_memory"),
177
+ r("POST", "memories/:memory/move", "move_memory"),
178
+ r("POST", "memories/:memory/revert", "revert_memory"),
179
+ r("POST", "memories/:memory/verify", "mark_verified"),
180
+ r("GET", "tokens", "list_tokens"),
181
+ r("POST", "tokens", "issue_token"),
182
+ r("PATCH", "tokens/:token", "update_token"),
183
+ r("POST", "tokens/:token/revoke", "revoke_token"),
184
+ r("POST", "tokens/:token/rotate", "rotate_token"),
185
+ r("GET", "apps/:app", "get_app"),
186
+ r("PUT", "apps/:app/webhook", "update_app_webhook"),
187
+ r("GET", "context", "get_context"),
188
+ // Só o gestor (token admin).
189
+ r("GET", "apps", "list_apps", MANAGER),
190
+ r("POST", "channels", "create_channel", MANAGER),
191
+ r("PATCH", "channels/:channel", "update_channel", MANAGER),
192
+ r("DELETE", "channels/:channel", "delete_channel", MANAGER),
193
+ r("GET", "channels/:channel/settings", "get_channel_settings", MANAGER),
194
+ r("PUT", "channels/:channel/settings", "update_channel_settings", MANAGER),
195
+ r("GET", "settings", "get_settings", MANAGER)
196
+ ];
197
+ function matchProxyRoute(method, segments, mode) {
198
+ const upper = method.toUpperCase();
199
+ for (const route of PROXY_ROUTES) {
200
+ if (route.method !== upper || !route.modes.includes(mode)) continue;
201
+ const parts = route.pattern.split("/");
202
+ if (parts.length !== segments.length) continue;
203
+ const params = {};
204
+ let ok = true;
205
+ for (let index = 0; index < parts.length; index += 1) {
206
+ const part = parts[index];
207
+ const segment = segments[index];
208
+ if (part.startsWith(":")) {
209
+ if (!segment) ok = false;
210
+ else params[part.slice(1)] = segment;
211
+ } else if (part !== segment) ok = false;
212
+ if (!ok) break;
213
+ }
214
+ if (ok) return { route, params, path: `/v1/${segments.map((segment) => encodeURIComponent(segment)).join("/")}` };
215
+ }
216
+ return null;
217
+ }
218
+
219
+ // src/server/proxy.ts
220
+ var json = (status, body, headers = {}) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store", ...headers } });
221
+ function errorResponse(error) {
222
+ if (error instanceof EngramindError) {
223
+ const headers = error.retryAfter !== null ? { "Retry-After": String(error.retryAfter) } : {};
224
+ return json(error.status || 500, { success: false, error: error.message, code: error.code, ...error.details !== void 0 ? { details: error.details } : {} }, headers);
225
+ }
226
+ return json(500, { success: false, error: "Falha inesperada ao falar com o Engramind.", code: "ENGRAMIND_PROXY_ERROR" });
227
+ }
228
+ function proxySegments(pathname, basePath) {
229
+ const base = basePath.replace(/\/+$/, "");
230
+ if (!pathname.startsWith(`${base}/`) && pathname !== base) return null;
231
+ const rest = pathname.slice(base.length).replace(/^\/+/, "");
232
+ if (!rest) return [];
233
+ try {
234
+ return rest.split("/").map((segment) => decodeURIComponent(segment));
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+ async function readBody(request) {
240
+ const text = await request.text();
241
+ if (!text) return void 0;
242
+ try {
243
+ return JSON.parse(text);
244
+ } catch {
245
+ throw new EngramindError(400, "Corpo da requisi\xE7\xE3o inv\xE1lido (JSON).", "ENGRAMIND_VALIDATION_ERROR");
246
+ }
247
+ }
248
+ function createEngramindProxy(options) {
249
+ const mode = options.mode ?? "app";
250
+ return async function handle(request) {
251
+ const url = new URL(request.url);
252
+ const segments = proxySegments(url.pathname, options.basePath);
253
+ const match = segments ? matchProxyRoute(request.method, segments, mode) : null;
254
+ if (!match) return json(404, { success: false, error: "Opera\xE7\xE3o do Engramind n\xE3o dispon\xEDvel neste app.", code: "ENGRAMIND_ROUTE_NOT_ALLOWED" });
255
+ try {
256
+ const client = typeof options.client === "function" ? await options.client() : options.client;
257
+ if (!client) throw notConfiguredError();
258
+ const method = match.route.method;
259
+ let body = method === "GET" || method === "DELETE" ? void 0 : await readBody(request);
260
+ if (match.route.operation === "issue_token" && mode === "app") {
261
+ const input = body && typeof body === "object" ? body : {};
262
+ if (input.kind !== void 0 && input.kind !== "agent") throw new EngramindError(403, "O app s\xF3 emite tokens de agente.", "ENGRAMIND_SCOPE_REQUIRED");
263
+ body = { ...input, kind: "agent" };
264
+ }
265
+ const query = Object.fromEntries(url.searchParams.entries());
266
+ const result = await client.call(method, match.path, { query, body: body ?? (method === "GET" || method === "DELETE" ? void 0 : {}), headers: method === "GET" && request.headers.get("if-none-match") ? { "If-None-Match": request.headers.get("if-none-match") } : void 0 });
267
+ if (method !== "GET") {
268
+ const issued = (match.route.operation === "issue_token" || match.route.operation === "rotate_token") && result && typeof result === "object" ? result : null;
269
+ if (issued && typeof issued.secret === "string" && issued.token) {
270
+ const token = issued.token;
271
+ await options.onTokenIssued?.(issued.secret, { id: token.id, kind: token.kind, app: token.app?.slug ?? null, agent: token.agent && token.app ? `${token.app.slug}/${token.agent.slug}` : token.agent?.slug ?? null, operation: match.route.operation });
272
+ }
273
+ try {
274
+ await options.onMutation?.({ operation: match.route.operation, method, params: match.params, body: redactBody(match.route.operation, body), result: issued ? { ...issued, secret: void 0 } : result });
275
+ } catch (error) {
276
+ options.onError?.(error, match.route);
277
+ }
278
+ }
279
+ return json(200, { success: true, data: result });
280
+ } catch (error) {
281
+ options.onError?.(error, match.route);
282
+ return errorResponse(error);
283
+ }
284
+ };
285
+ }
286
+ function redactBody(operation, body) {
287
+ if (operation !== "update_app_webhook" || !body || typeof body !== "object") return body;
288
+ const copy = { ...body };
289
+ if (copy.secret) copy.secret = "[segredo omitido]";
290
+ return copy;
291
+ }
292
+
293
+ // src/server/webhook.ts
294
+ import { createHmac, timingSafeEqual } from "crypto";
295
+ var WEBHOOK_TOLERANCE_SECONDS = 300;
296
+ function parseSignatureHeader(header) {
297
+ if (!header) return null;
298
+ let t = null;
299
+ const v1 = [];
300
+ for (const part of header.split(",")) {
301
+ const [key, ...rest] = part.trim().split("=");
302
+ const value = rest.join("=");
303
+ if (key === "t") t = Number(value);
304
+ if (key === "v1" && value) v1.push(value);
305
+ }
306
+ if (t === null || !Number.isFinite(t) || !v1.length) return null;
307
+ return { t, v1 };
308
+ }
309
+ function signWebhook(secret, timestamp, body) {
310
+ return createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
311
+ }
312
+ function verifyWebhookSignature(header, body, secret, options = {}) {
313
+ const parsed = parseSignatureHeader(header);
314
+ if (!parsed || !secret) return false;
315
+ const now = Math.floor((options.now ?? Date.now()) / 1e3);
316
+ if (Math.abs(now - parsed.t) > (options.toleranceSeconds ?? WEBHOOK_TOLERANCE_SECONDS)) return false;
317
+ const expected = Buffer.from(signWebhook(secret, parsed.t, body), "hex");
318
+ return parsed.v1.some((candidate) => {
319
+ const given = Buffer.from(candidate, "hex");
320
+ return given.length === expected.length && timingSafeEqual(given, expected);
321
+ });
322
+ }
323
+ function createMemoryDedupe(options = {}) {
324
+ const ttl = options.ttlMs ?? 24 * 60 * 6e4;
325
+ const max = options.max ?? 1e4;
326
+ const ids = /* @__PURE__ */ new Map();
327
+ return {
328
+ seen(id) {
329
+ const now = Date.now();
330
+ for (const [key, at] of ids) {
331
+ if (now - at <= ttl && ids.size <= max) break;
332
+ ids.delete(key);
333
+ }
334
+ if (ids.has(id)) return true;
335
+ ids.set(id, now);
336
+ return false;
337
+ }
338
+ };
339
+ }
340
+ function createWebhookHandler(options) {
341
+ const dedupe = options.dedupe ?? createMemoryDedupe();
342
+ return async function handle(request) {
343
+ const body = await request.text();
344
+ const secret = typeof options.secret === "function" ? await options.secret() : options.secret;
345
+ if (!secret || !verifyWebhookSignature(request.headers.get("engramind-signature"), body, secret, { toleranceSeconds: options.toleranceSeconds })) return new Response(null, { status: 401 });
346
+ let event;
347
+ try {
348
+ event = JSON.parse(body);
349
+ } catch {
350
+ return new Response(null, { status: 400 });
351
+ }
352
+ if (!event || typeof event.id !== "string" || typeof event.type !== "string") return new Response(null, { status: 400 });
353
+ if (await dedupe.seen(event.id)) return Response.json({ ok: true, duplicate: true });
354
+ try {
355
+ options.context?.handleEvent(event);
356
+ await options.bus?.publish(toLiveEvent(event));
357
+ await options.onEvent?.(event);
358
+ } catch (error) {
359
+ options.onError?.(error);
360
+ }
361
+ return Response.json({ ok: true });
362
+ };
363
+ }
364
+
365
+ // src/server/bus.ts
366
+ function createEventBus(options = {}) {
367
+ const listeners = /* @__PURE__ */ new Set();
368
+ const deliver = (event) => {
369
+ for (const listener of listeners) {
370
+ try {
371
+ listener(event);
372
+ } catch (error) {
373
+ options.onError?.(error);
374
+ }
375
+ }
376
+ };
377
+ const remote = options.broadcaster?.subscribe(deliver);
378
+ void remote;
379
+ return {
380
+ async publish(event) {
381
+ if (options.broadcaster) await options.broadcaster.publish(event);
382
+ else deliver(event);
383
+ },
384
+ subscribe(listener) {
385
+ listeners.add(listener);
386
+ return () => listeners.delete(listener);
387
+ },
388
+ get size() {
389
+ return listeners.size;
390
+ }
391
+ };
392
+ }
393
+
394
+ // src/server/sse.ts
395
+ var encoder = new TextEncoder();
396
+ var formatSse = (event, data) => `event: ${event}
397
+ data: ${JSON.stringify(data)}
398
+
399
+ `;
400
+ function createSseHandler(options) {
401
+ const heartbeatMs = options.heartbeatMs ?? 25e3;
402
+ return function handle(request) {
403
+ let cleanup = null;
404
+ const stream = new ReadableStream({
405
+ start(controller) {
406
+ const send = (chunk) => {
407
+ try {
408
+ controller.enqueue(encoder.encode(chunk));
409
+ } catch {
410
+ cleanup?.();
411
+ }
412
+ };
413
+ send(`retry: 3000
414
+ ${formatSse("ready", { at: (/* @__PURE__ */ new Date()).toISOString() })}`);
415
+ const unsubscribe = options.bus.subscribe((event) => {
416
+ if (options.filter && !options.filter(event, request)) return;
417
+ send(formatSse("engramind", event));
418
+ });
419
+ const timer = setInterval(() => send(": ping\n\n"), heartbeatMs);
420
+ cleanup = () => {
421
+ if (!cleanup) return;
422
+ cleanup = null;
423
+ clearInterval(timer);
424
+ unsubscribe();
425
+ try {
426
+ controller.close();
427
+ } catch {
428
+ }
429
+ };
430
+ request.signal.addEventListener("abort", () => cleanup?.(), { once: true });
431
+ },
432
+ cancel() {
433
+ cleanup?.();
434
+ }
435
+ });
436
+ return new Response(stream, { headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", "X-Accel-Buffering": "no" } });
437
+ };
438
+ }
439
+
440
+ // src/server/context.ts
441
+ function createMemoryContextStore() {
442
+ const map = /* @__PURE__ */ new Map();
443
+ return { get: (key) => map.get(key) ?? null, set: (key, entry) => void map.set(key, entry), keys: () => [...map.keys()] };
444
+ }
445
+ var SEP = "\0";
446
+ var contextKey = (agent, channels, format) => [agent, [...channels].sort().join(","), format].join(SEP);
447
+ var channelsOfKey = (key) => (key.split(SEP)[1] ?? "").split(",").filter(Boolean);
448
+ function invalidatesContext(event) {
449
+ if (isConsumedEvent(event.type)) return false;
450
+ return event.type.startsWith("memory.") || event.type === "channel.updated" || event.type === "subscription.updated" || event.type === "settings.updated";
451
+ }
452
+ function createContextCache(options) {
453
+ const store = options.store ?? createMemoryContextStore();
454
+ const format = options.format ?? "tagged";
455
+ const maxAge = options.maxAgeMs ?? 5 * 6e4;
456
+ let allDirty = false;
457
+ async function markChannels(channels) {
458
+ const wanted = channels.filter((channel) => Boolean(channel));
459
+ for (const key of await store.keys()) {
460
+ const keyChannels = channelsOfKey(key);
461
+ const hit = !wanted.length || !keyChannels.length || wanted.some((channel) => keyChannels.includes(channel));
462
+ if (!hit) continue;
463
+ const entry = await store.get(key);
464
+ if (entry && !entry.dirty) await store.set(key, { ...entry, dirty: true });
465
+ }
466
+ }
467
+ return {
468
+ /** Webhook recebido: suja o contexto dos canais afetados (nunca por `memory.consumed`). */
469
+ handleEvent(event) {
470
+ if (!invalidatesContext(event)) return;
471
+ if (!event.channel) allDirty = true;
472
+ void markChannels(event.channel ? [event.channel] : []);
473
+ },
474
+ async getContext(agent, input = {}) {
475
+ const channels = input.channels ?? [];
476
+ const key = contextKey(agent, channels, format);
477
+ const cached = await store.get(key);
478
+ const usable = cached && !input.focus;
479
+ if (usable && options.revalidate === "on-dirty" && !cached.dirty && !allDirty && Date.now() - cached.fetchedAt < maxAge) {
480
+ return { block: cached.content, version: cached.version, source: "live", stale: false, counts: cached.counts, error: null };
481
+ }
482
+ try {
483
+ const client = await options.client(agent);
484
+ if (!client) throw new Error(`token do agente ${agent} indispon\xEDvel`);
485
+ const view = await client.context({ channels: channels.length ? [...channels] : void 0, format, focus: input.focus, ifNoneMatch: usable ? cached.version : null });
486
+ allDirty = false;
487
+ if (view === null && cached) {
488
+ await store.set(key, { ...cached, fetchedAt: Date.now(), dirty: false });
489
+ return { block: cached.content, version: cached.version, source: "live", stale: false, counts: cached.counts, error: null };
490
+ }
491
+ if (!view) throw new Error("o Engramind respondeu sem o contexto");
492
+ const entry = { content: typeof view.content === "string" ? view.content : "", version: view.version ?? "", counts: view.counts, format, fetchedAt: Date.now(), dirty: false };
493
+ if (!input.focus) await store.set(key, entry);
494
+ return { block: entry.content, version: entry.version, source: "live", stale: false, counts: entry.counts, error: null };
495
+ } catch (error) {
496
+ options.onError?.(error, agent);
497
+ const reason = error instanceof Error ? error.message : String(error);
498
+ if (cached) return { block: cached.content, version: cached.version, source: "cache", stale: true, counts: cached.counts, error: reason };
499
+ return { block: "", version: null, source: "none", stale: true, counts: null, error: reason };
500
+ }
501
+ },
502
+ /** Bloco guardado (sem chamar o Engramind). */
503
+ peek: async (agent, channels = []) => store.get(contextKey(agent, channels, format))
504
+ };
505
+ }
506
+
507
+ // src/server/mcp.ts
508
+ function engramindMcpConfig(input) {
509
+ const headers = { Authorization: `Bearer ${input.agentToken}` };
510
+ if (input.channels?.length) headers["Engramind-Channels"] = input.channels.join(",");
511
+ return { type: "http", url: `${input.baseUrl.replace(/\/+$/, "")}/mcp`, headers };
512
+ }
513
+ var MCP_READ_TOOLS = ["get_context", "search_memories", "get_memory", "get_hints", "list_memories", "list_categories"];
514
+ var MCP_WRITE_TOOLS = ["create_memory", "update_memory"];
515
+ export {
516
+ EngramindError,
517
+ MCP_READ_TOOLS,
518
+ MCP_WRITE_TOOLS,
519
+ PROXY_ROUTES,
520
+ WEBHOOK_TOLERANCE_SECONDS,
521
+ buildUrl,
522
+ clientStatus,
523
+ contextKey,
524
+ createConsumptionGrouper,
525
+ createContextCache,
526
+ createEngramindClient,
527
+ createEngramindProxy,
528
+ createEventBus,
529
+ createMemoryContextStore,
530
+ createMemoryDedupe,
531
+ createSseHandler,
532
+ createWebhookHandler,
533
+ debugUrl,
534
+ engramindCodeOf,
535
+ engramindMcpConfig,
536
+ errorResponse,
537
+ formatSse,
538
+ invalidatesContext,
539
+ isConsumedEvent,
540
+ isMemoryChange,
541
+ matchProxyRoute,
542
+ mergeConsumption,
543
+ notConfiguredError,
544
+ parseSignatureHeader,
545
+ proxyMaskedError,
546
+ proxySegments,
547
+ signWebhook,
548
+ toEngramindError,
549
+ toLiveEvent,
550
+ unreachableError,
551
+ verifyWebhookSignature
552
+ };
553
+ //# sourceMappingURL=server.js.map