@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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ Todas as mudanças relevantes deste pacote. O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e o projeto usa [SemVer](https://semver.org/lang/pt-BR/).
4
+
5
+ ## [0.1.1]
6
+
7
+ ### Alterado
8
+
9
+ - Publicação pelo GitHub Actions com **trusted publishing** (OIDC, sem token nem secret) e attestation de
10
+ provenance no npm. Sem mudança de código em relação à `0.1.0`.
11
+
12
+ ## [0.1.0]
13
+
14
+ ### Adicionado
15
+
16
+ - `@engramind/client/server`: cliente tipado da API (Idempotency-Key, `If-None-Match`, erros pt-BR, 5xx mascarado pelo Cloudflare), proxy dos componentes nos modos `app` e `manager`, webhook com HMAC e descarte de repetidos, barramento + SSE (com interface `Broadcaster`), contexto com cache por `version` e configuração de MCP.
17
+ - `@engramind/client/ai`: ferramentas `search_memories`, `get_memory` e `create_memory` para o Vercel AI SDK.
18
+ - `@engramind/client/react`: rede (Agentes x canais, Universo do canal e Universo do agente; Apps x canais no modo gestor), consumo ao vivo pelo `memory.consumed` (um pulso por `consumption`), configuração do app (Agentes e assinaturas, Canais, Tokens de agente e Webhook), textos pt-BR sobrescrevíveis e tema só por tokens.
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nandobfer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,232 @@
1
- # Temporary Holding Version
1
+ # @engramind/client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Cliente do [Engramind](https://github.com/nandobfer/engramind) para **apps consumidores**: os apps cujos agentes usam memórias. O pacote tem três partes:
4
+
5
+ - **Servidor** (`@engramind/client/server`): cliente da API, proxy dos componentes, webhook + SSE, contexto com cache e configuração de MCP.
6
+ - **Ferramentas de IA** (`@engramind/client/ai`): buscar, ler e criar memória no Vercel AI SDK.
7
+ - **React** (`@engramind/client/react`): componentes de configuração do app e de visualização das redes de memória. As cores vêm do tema do seu app.
8
+
9
+ A lib é só para consumidores. A gestão da rede (outros apps, tokens admin ou de app, configurações globais, provedores, operação) fica com o gestor. A versão atual está no [`package.json`](./package.json) e as mudanças no [CHANGELOG](./CHANGELOG.md).
10
+
11
+ ## Requisitos
12
+
13
+ - Engramind com o evento de webhook `memory.consumed` (consumo ao vivo; versão indicada no CHANGELOG do Engramind).
14
+ - React 19, Tailwind CSS 4 e os tokens de tema do shadcn/ui.
15
+ - Peer dependencies: `react`, `react-dom`, `@xyflow/react`, `framer-motion`, `@tanstack/react-query`, `lucide-react`, `zod`. `ai` só é necessário para `@engramind/client/ai`.
16
+ - Um **token de app** (`kind: app`) emitido pelo gestor da rede. Ele fica só no servidor.
17
+
18
+ ## Instalação
19
+
20
+ ```bash
21
+ pnpm add @engramind/client @xyflow/react framer-motion @tanstack/react-query lucide-react zod
22
+ ```
23
+
24
+ Os componentes usam classes do Tailwind. Inclua o pacote na varredura do Tailwind e importe as folhas do React Flow e do `react-day-picker` (o pacote não importa CSS, para funcionar em qualquer bundler e nos testes):
25
+
26
+ ```css
27
+ @import "tailwindcss";
28
+ @import "@xyflow/react/dist/style.css";
29
+ @import "react-day-picker/style.css";
30
+ @source "../node_modules/@engramind/client/dist";
31
+ ```
32
+
33
+ Ajuste o caminho do `@source` a partir do arquivo CSS.
34
+
35
+ ### Tokens de tema usados
36
+
37
+ A lib não tem nenhuma cor fixa (um teste garante isso). Ela usa as variáveis do Tailwind 4 e do shadcn/ui:
38
+
39
+ - `--color-primary`, `--color-primary-foreground`, `--color-background`, `--color-foreground`, `--color-card`, `--color-muted-foreground`, `--color-border`, `--color-input`, `--color-ring`, `--color-accent`, `--color-destructive` e `--radius`. São as classes `bg-primary`, `text-foreground` e as demais.
40
+ - `--chart-1` a `--chart-6`, para a cor de cada canal. Sem `--chart-6`, a lib usa uma variação do `--chart-3`.
41
+ - Opcionais: `--color-success` (partícula de memória criada e selos de sucesso) e `--color-warning` (selos de aviso). Sem eles, a lib usa `--color-primary` e `--color-accent`.
42
+
43
+ Com uma primária azul, a rede, as partículas, os pulsos e as telas ficam azuis.
44
+
45
+ ## Servidor (exemplo com Next.js App Router)
46
+
47
+ Todas as rotas ficam **atrás do login do app**: a lib não verifica usuário. O token do app fica só no servidor (variável de ambiente ou cofre do app) e nunca vai ao navegador nem às respostas do proxy.
48
+
49
+ ```ts
50
+ // lib/engramind.ts (só servidor)
51
+ import { createContextCache, createEngramindClient, createEventBus } from "@engramind/client/server"
52
+
53
+ export const engramind = createEngramindClient({ baseUrl: process.env.ENGRAMIND_URL!, token: process.env.ENGRAMIND_APP_TOKEN!, userAgent: "meu-app" })
54
+ export const engramindBus = createEventBus()
55
+ export const engramindContext = createContextCache({
56
+ // O contexto é o do agente: use o token de cada agente.
57
+ client: (agent) => createEngramindClient({ baseUrl: process.env.ENGRAMIND_URL!, token: agentToken(agent) }),
58
+ })
59
+ ```
60
+
61
+ ```ts
62
+ // app/api/engramind/[...path]/route.ts: proxy dos componentes
63
+ import { createEngramindProxy } from "@engramind/client/server"
64
+ import { engramind } from "@/lib/engramind"
65
+ import { requireUser } from "@/lib/auth"
66
+
67
+ const proxy = createEngramindProxy({
68
+ client: engramind,
69
+ mode: "app",
70
+ basePath: "/api/engramind",
71
+ onMutation: (mutation) => audit(mutation), // opcional: auditoria do app
72
+ onTokenIssued: (secret, token) => saveAgentToken(token.agent, secret), // opcional: guardar o token emitido
73
+ })
74
+ const handler = async (request: Request) => (await requireUser(request)) ?? proxy(request)
75
+ export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
76
+ ```
77
+
78
+ ```ts
79
+ // app/api/engramind/events/route.ts: SSE dos componentes
80
+ import { createSseHandler } from "@engramind/client/server"
81
+ import { engramindBus } from "@/lib/engramind"
82
+
83
+ export const dynamic = "force-dynamic"
84
+ const sse = createSseHandler({ bus: engramindBus })
85
+ export async function GET(request: Request) {
86
+ return (await requireUser(request)) ?? sse(request)
87
+ }
88
+ ```
89
+
90
+ ```ts
91
+ // app/api/engramind/webhook/route.ts: webhook do Engramind (público, autenticado pela assinatura)
92
+ import { createWebhookHandler } from "@engramind/client/server"
93
+ import { engramindBus, engramindContext } from "@/lib/engramind"
94
+
95
+ export const POST = createWebhookHandler({ secret: process.env.ENGRAMIND_WEBHOOK_SECRET!, bus: engramindBus, context: engramindContext })
96
+ ```
97
+
98
+ As pastas `events` e `webhook` têm prioridade sobre o catch-all `[...path]`.
99
+
100
+ ### O que cada parte faz
101
+
102
+ - **Proxy** (`createEngramindProxy`): repassa só as rotas permitidas no modo.
103
+ - O caminho é o do REST do Engramind sem o `/v1`.
104
+ - No modo `app`, rotas de gestor (como `apps`, `settings` e criar canal) recebem `404` sem chamar o Engramind, e `issue_token` só emite `kind: agent`.
105
+ - A resposta é `{ success: true, data }` ou `{ success: false, error, code, details }`. Os códigos levam o prefixo `ENGRAMIND_` (ex.: `ENGRAMIND_CHANNEL_SHARED`), a mensagem é pt-BR e o `Retry-After` do `429` é repassado.
106
+ - **Webhook** (`createWebhookHandler`): valida `Engramind-Signature`, que é o HMAC-SHA256 de `t + "." + corpo`.
107
+ - A janela é de 300 s e a comparação é em tempo constante. Sem assinatura válida, a resposta é `401` sem corpo.
108
+ - Um `id` repetido é descartado (padrão em memória por 24 h; troque com `dedupe`).
109
+ - Marca o contexto do canal como sujo, exceto em `memory.consumed`, e republica o evento no SSE só com ids, slugs e tipos.
110
+ - **SSE** (`createSseHandler`): manda `event: engramind` e um sinal de vida a cada 25 s. Quando a conexão cai, o navegador reconecta com espera crescente e relê as telas.
111
+ - **Contexto** (`createContextCache`): faz o `get_context` por agente com `If-None-Match` (o `304` reaproveita o bloco).
112
+ - Se o Engramind cair, devolve o último bloco (`stale: true`).
113
+ - `revalidate: "on-dirty"` só chama o Engramind quando um evento sujou o canal.
114
+ - Coloque o bloco na parte estável do prompt.
115
+
116
+ ### Limite: um processo
117
+
118
+ O barramento padrão vive no processo. Com várias instâncias, só os navegadores ligados à instância que recebeu o webhook são avisados. Nesse caso, passe um `Broadcaster` (Redis pub/sub, Postgres `NOTIFY`, Pusher…):
119
+
120
+ ```ts
121
+ const engramindBus = createEventBus({ broadcaster: { publish: (event) => redis.publish("engramind", JSON.stringify(event)), subscribe: (listener) => subscribeRedis("engramind", listener) } })
122
+ ```
123
+
124
+ ### Registrar o webhook
125
+
126
+ Use a aba **Webhook** da configuração (`EngramindAppSettings`) ou `engramind.apps.updateWebhook(app, { url, secret })`. O segredo (de 32 a 256 caracteres) aparece uma única vez: grave-o no servidor do app (`ENGRAMIND_WEBHOOK_SECRET`). Sem webhook, ou com `webhooks.enabled = false` no Engramind, não há tempo real nem pulso de consumo, e as telas atualizam pelas próprias ações e ao reconsultar.
127
+
128
+ ## React
129
+
130
+ ```tsx
131
+ "use client"
132
+ import Link from "next/link"
133
+ import { usePathname, useRouter, useSearchParams } from "next/navigation"
134
+ import { toast } from "sonner"
135
+ import { createHttpDataSource, EngramindAppSettings, EngramindNetwork, EngramindProvider } from "@engramind/client/react"
136
+
137
+ const dataSource = createHttpDataSource({ basePath: "/api/engramind" })
138
+ const navigation = { useRouter, usePathname, useSearchParams }
139
+
140
+ export function MemoriasPage() {
141
+ return (
142
+ <EngramindProvider
143
+ dataSource={dataSource}
144
+ events="/api/engramind/events"
145
+ app="meu-app"
146
+ appName="Meu app"
147
+ navigation={navigation}
148
+ renderLink={({ href, children, ...props }) => <Link href={href} {...props}>{children}</Link>}
149
+ notify={(kind, message) => toast[kind](message)}
150
+ hrefs={{ network: "/memorias", channel: (slug) => `/memorias/canais/${encodeURIComponent(slug)}` }}
151
+ onConfigure={() => router.push("/memorias/configuracao")}
152
+ >
153
+ <EngramindNetwork />
154
+ </EngramindProvider>
155
+ )
156
+ }
157
+ ```
158
+
159
+ Monte dentro do `QueryClientProvider` do app (TanStack Query). Use um `EngramindProvider` por página: dois providers com fontes de dados diferentes na mesma tela usam a fonte do último montado.
160
+
161
+ ### Componentes
162
+
163
+ | Componente | O que mostra |
164
+ |---|---|
165
+ | `EngramindNetwork` | A rede. No modo app, a visão inicial é **Agentes x canais** do app (distribuições Órbita e Radial), com o **Universo do canal** e o **Universo do agente** (`?universo=`). Traz estatísticas, card da memória, filtros, busca, painel do canal, lista "Sem memórias (N)", Centralizar e layout de celular. |
166
+ | `EngramindAppSettings` | A configuração do app em quatro abas: **Agentes** (criar, editar, desativar e reativar, excluir e as assinaturas exata/prefixo com o modo inject, search ou manage), **Canais** (criar pela assinatura exata, arquivar e restaurar os próprios), **Tokens de agente** (o segredo aparece uma única vez, com Copiar; editar, rotacionar e revogar) e **Webhook**. |
167
+ | `ChannelDetail` | Página do canal: assinantes e o conteúdo extra do app (`slots.channelDetail`). |
168
+ | `MemoryUniverse` | O universo por props (memórias que o app já tem), com consumo ao vivo. |
169
+
170
+ Memórias: tudo o que o token permite (criar, editar, ativar e desativar, fixar, mover, excluir e importar), com confirmação nas ações críticas. Quando o Engramind recusa, aparece uma mensagem em pt-BR:
171
+
172
+ - `403 FOREIGN_APP` e `403 CHANNEL_NOT_SUBSCRIBED` mostram "Sem acesso".
173
+ - `401` mostra o estado de reconfigurar.
174
+ - Fora do ar, proxy mascarado e `429` mostram "Tentar novamente".
175
+
176
+ ### Consumo ao vivo
177
+
178
+ O webhook `memory.consumed` chega um por canal. A lib junta os eventos da mesma leitura pelo `consumption` e anima **uma vez**: a partícula vai do agente aos canais e volta dos que entregaram, com brilho cheio até a releitura. Um consumo é ignorado quando:
179
+
180
+ - não tem agente (leitura por token de app ou admin);
181
+ - só traz memórias que não estão carregadas na tela.
182
+
183
+ O agente de outro app que lê um canal visível pulsa só o canal. O consumo não invalida o contexto.
184
+
185
+ O brilho das memórias usa `last_retrieved_at` e o prazo `consolidation.stale_after_days`. No modo app, o prazo vem de `get_consumer_settings` quando o Engramind o expõe ([pedido ao Engramind](./docs/engramind-consumer-settings.md)). Sem ele, a lib usa 90 dias ou o `staleAfterDays` do provider.
186
+
187
+ ### Textos
188
+
189
+ Os textos estão em pt-BR. Para sobrescrever qualquer chave de `DEFAULT_MESSAGES`, passe `messages`. Chave ausente usa o padrão.
190
+
191
+ ```tsx
192
+ <EngramindProvider messages={{ "network.noAgents.title": "Nenhum robô ativo" }} …>
193
+ ```
194
+
195
+ ### Modo gestor
196
+
197
+ `mode="manager"` é para o gestor da rede, como o Opex, com o proxy em modo `manager` e o token admin. Ele libera a visão **Apps x canais**, os apps, criar, editar e excluir canais e as configurações globais da rede. O modo gestor não aparece no modo app.
198
+
199
+ ## Ferramentas de IA e MCP
200
+
201
+ ```ts
202
+ import { engramindTools } from "@engramind/client/ai"
203
+ import { createEngramindClient, engramindMcpConfig } from "@engramind/client/server"
204
+
205
+ const agentClient = createEngramindClient({ baseUrl, token: agentToken })
206
+ const tools = engramindTools({ client: agentClient, write: true }) // search_memories, get_memory, create_memory
207
+
208
+ const mcp = engramindMcpConfig({ baseUrl, agentToken, channels: ["meu-app/global"] }) // POST /mcp + Engramind-Channels
209
+ ```
210
+
211
+ Use o token **do agente**. Assim, o consumo aparece na rede como dele.
212
+
213
+ ## Desenvolvimento e publicação
214
+
215
+ ```bash
216
+ pnpm install
217
+ pnpm typecheck
218
+ pnpm test
219
+ pnpm build
220
+ ```
221
+
222
+ A GitHub Action roda TypeScript, Vitest e build em todo PR e push. A tag `vX.Y.Z` (igual à versão do `package.json`) publica no npm público por **trusted publishing**: o pacote aponta para este repositório e este arquivo de workflow em npmjs.com, e o GitHub assina com OIDC (sem token nem secret).
223
+
224
+ ## Pendências no Engramind
225
+
226
+ Algumas leituras que o modo app precisa ainda são só do admin no contrato atual:
227
+
228
+ - o prazo de desuso e os pesos da busca;
229
+ - o estado do webhook do próprio app;
230
+ - a edição dos canais que o app criou.
231
+
232
+ A tarefa está descrita em [`docs/engramind-consumer-settings.md`](./docs/engramind-consumer-settings.md).
package/dist/ai.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { Tool } from 'ai';
2
+ import { E as EngramindClient } from './client-Q0QFU2A6.js';
3
+ import './types-BnU5iHwB.js';
4
+
5
+ /**
6
+ * `@engramind/client/ai`: ferramentas prontas para o Vercel AI SDK (`ai` ≥ 5), chamadas com o token **do
7
+ * agente** (o consumo aparece na rede como dele). Buscar e ler sempre; criar só com `write: true`.
8
+ * Falhas viram `{ error }` em pt-BR para o modelo, sem derrubar o turno.
9
+ */
10
+ interface EngramindToolsOptions {
11
+ client: EngramindClient;
12
+ /** Canais citados por padrão na busca (o Engramind busca nas assinaturas do agente). */
13
+ channels?: readonly string[];
14
+ /** Inclui `create_memory`. */
15
+ write?: boolean;
16
+ /** Exclui da busca o que o agente já tem no contexto (padrão `true`). */
17
+ excludeContext?: boolean;
18
+ searchLimit?: number;
19
+ }
20
+ declare function engramindTools(options: EngramindToolsOptions): Record<string, Tool>;
21
+
22
+ export { type EngramindToolsOptions, engramindTools };
package/dist/ai.js ADDED
@@ -0,0 +1,61 @@
1
+ // src/ai/index.ts
2
+ import { tool } from "ai";
3
+ import { z } from "zod";
4
+ var message = (error) => error instanceof Error ? error.message : String(error);
5
+ function engramindTools(options) {
6
+ const { client } = options;
7
+ const tools = {
8
+ search_memories: tool({
9
+ description: "Busca mem\xF3rias do Engramind por sentido e por texto nos canais do agente. Devolve id, t\xEDtulo, canal, categoria e um trecho.",
10
+ inputSchema: z.object({
11
+ query: z.string().min(1).max(500).describe("O que procurar"),
12
+ channels: z.array(z.string()).max(50).optional().describe("Canais (slugs) para restringir a busca"),
13
+ limit: z.number().int().min(1).max(20).optional()
14
+ }),
15
+ execute: async ({ query, channels, limit }) => {
16
+ try {
17
+ const result = await client.memories.search({ query, channels: channels ?? (options.channels ? [...options.channels] : void 0), limit: limit ?? options.searchLimit ?? 8, exclude_context: options.excludeContext ?? true });
18
+ return { mode: result.mode, results: result.results.map(({ memory, score }) => ({ id: memory.id, title: memory.title ?? null, channel: memory.channel, category: memory.category, score, excerpt: memory.text.slice(0, 280) })) };
19
+ } catch (error) {
20
+ return { error: message(error) };
21
+ }
22
+ }
23
+ }),
24
+ get_memory: tool({
25
+ description: "L\xEA uma mem\xF3ria do Engramind pelo id (texto completo, t\xEDtulo, canal e categoria).",
26
+ inputSchema: z.object({ id: z.string().min(1).max(60).describe("Id da mem\xF3ria (m_\u2026)") }),
27
+ execute: async ({ id }) => {
28
+ try {
29
+ const memory = await client.memories.get(id);
30
+ return { id: memory.id, title: memory.title ?? null, text: memory.text, channel: memory.channel, category: memory.category, status: memory.status };
31
+ } catch (error) {
32
+ return { error: message(error) };
33
+ }
34
+ }
35
+ })
36
+ };
37
+ if (!options.write) return tools;
38
+ return {
39
+ ...tools,
40
+ create_memory: tool({
41
+ description: "Grava uma mem\xF3ria dur\xE1vel no Engramind (um fato autossuficiente, na categoria certa).",
42
+ inputSchema: z.object({
43
+ channel: z.string().min(1).max(160).describe("Canal (slug) em que o agente escreve"),
44
+ text: z.string().min(1).describe("O fato, escrito para quem n\xE3o tem o contexto da conversa"),
45
+ category: z.string().min(1).max(64).optional()
46
+ }),
47
+ execute: async ({ channel, text, category }) => {
48
+ try {
49
+ const result = await client.memories.create({ channel, text, category });
50
+ return { id: result.memory.id, duplicate: result.duplicate, similar: result.similar.map((item) => item.id) };
51
+ } catch (error) {
52
+ return { error: message(error) };
53
+ }
54
+ }
55
+ })
56
+ };
57
+ }
58
+ export {
59
+ engramindTools
60
+ };
61
+ //# sourceMappingURL=ai.js.map
package/dist/ai.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ai/index.ts"],"sourcesContent":["import { tool, type Tool } from \"ai\"\nimport { z } from \"zod\"\nimport type { EngramindClient } from \"../server/client\"\n\n/**\n * `@engramind/client/ai`: ferramentas prontas para o Vercel AI SDK (`ai` ≥ 5), chamadas com o token **do\n * agente** (o consumo aparece na rede como dele). Buscar e ler sempre; criar só com `write: true`.\n * Falhas viram `{ error }` em pt-BR para o modelo, sem derrubar o turno.\n */\nexport interface EngramindToolsOptions {\n client: EngramindClient\n /** Canais citados por padrão na busca (o Engramind busca nas assinaturas do agente). */\n channels?: readonly string[]\n /** Inclui `create_memory`. */\n write?: boolean\n /** Exclui da busca o que o agente já tem no contexto (padrão `true`). */\n excludeContext?: boolean\n searchLimit?: number\n}\n\nconst message = (error: unknown) => (error instanceof Error ? error.message : String(error))\n\nexport function engramindTools(options: EngramindToolsOptions): Record<string, Tool> {\n const { client } = options\n const tools = {\n search_memories: tool({\n description: \"Busca memórias do Engramind por sentido e por texto nos canais do agente. Devolve id, título, canal, categoria e um trecho.\",\n inputSchema: z.object({\n query: z.string().min(1).max(500).describe(\"O que procurar\"),\n channels: z.array(z.string()).max(50).optional().describe(\"Canais (slugs) para restringir a busca\"),\n limit: z.number().int().min(1).max(20).optional(),\n }),\n execute: async ({ query, channels, limit }) => {\n try {\n const result = await client.memories.search({ query, channels: channels ?? (options.channels ? [...options.channels] : undefined), limit: limit ?? options.searchLimit ?? 8, exclude_context: options.excludeContext ?? true })\n return { mode: result.mode, results: result.results.map(({ memory, score }) => ({ id: memory.id, title: memory.title ?? null, channel: memory.channel, category: memory.category, score, excerpt: memory.text.slice(0, 280) })) }\n } catch (error) {\n return { error: message(error) }\n }\n },\n }),\n get_memory: tool({\n description: \"Lê uma memória do Engramind pelo id (texto completo, título, canal e categoria).\",\n inputSchema: z.object({ id: z.string().min(1).max(60).describe(\"Id da memória (m_…)\") }),\n execute: async ({ id }) => {\n try {\n const memory = await client.memories.get(id)\n return { id: memory.id, title: memory.title ?? null, text: memory.text, channel: memory.channel, category: memory.category, status: memory.status }\n } catch (error) {\n return { error: message(error) }\n }\n },\n }),\n }\n if (!options.write) return tools\n return {\n ...tools,\n create_memory: tool({\n description: \"Grava uma memória durável no Engramind (um fato autossuficiente, na categoria certa).\",\n inputSchema: z.object({\n channel: z.string().min(1).max(160).describe(\"Canal (slug) em que o agente escreve\"),\n text: z.string().min(1).describe(\"O fato, escrito para quem não tem o contexto da conversa\"),\n category: z.string().min(1).max(64).optional(),\n }),\n execute: async ({ channel, text, category }) => {\n try {\n const result = await client.memories.create({ channel, text, category })\n return { id: result.memory.id, duplicate: result.duplicate, similar: result.similar.map((item) => item.id) }\n } catch (error) {\n return { error: message(error) }\n }\n },\n }),\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,193 @@
1
+ // src/core/errors.ts
2
+ var EngramindError = class extends Error {
3
+ status;
4
+ code;
5
+ details;
6
+ /** Segundos de `Retry-After` (429). */
7
+ retryAfter;
8
+ constructor(status, message, code, details, retryAfter = null) {
9
+ super(message);
10
+ this.name = "EngramindError";
11
+ this.status = status;
12
+ this.code = code;
13
+ this.details = details;
14
+ this.retryAfter = retryAfter;
15
+ }
16
+ };
17
+ var asDetails = (details) => details && typeof details === "object" ? details : {};
18
+ var list = (value) => Array.isArray(value) ? value.map(String).join(", ") : "";
19
+ var MESSAGES = {
20
+ AUTH_INVALID: () => "O Engramind recusou o token do app (ausente, inv\xE1lido, expirado, revogado ou de app desativado). Pe\xE7a um novo token ao gestor e reconfigure o app.",
21
+ SCOPE_REQUIRED: (d) => `O token do app n\xE3o tem permiss\xE3o para esta opera\xE7\xE3o${d.required_kind ? ` (exige ${String(d.required_kind)}` : ""}${d.required_scope ? ` com ${String(d.required_scope)})` : d.required_kind ? ")" : ""}.`,
22
+ FOREIGN_APP: () => "Sem acesso: o registro pertence a outro app.",
23
+ CHANNEL_NOT_SUBSCRIBED: (d) => `Sem acesso: o token n\xE3o alcan\xE7a o canal${d.channel ? ` ${String(d.channel)}` : ""}.`,
24
+ CHANNEL_SHARED: () => "O canal \xE9 assinado por outro app e n\xE3o pode ser arquivado por este app.",
25
+ LAST_ADMIN_TOKEN: () => "A rede ficaria sem nenhum token admin ativo com write. Emita outro token admin antes.",
26
+ SLUG_ALREADY_EXISTS: () => "J\xE1 existe um registro com esse slug. Escolha outro.",
27
+ SUBSCRIPTION_EXISTS: () => "O agente j\xE1 assina esse alvo.",
28
+ CATEGORY_NOT_FOUND: (d) => `Categoria fora da lista${d.valid ? ` (aceitas: ${list(d.valid)})` : ""}${d.index !== void 0 ? `; item ${Number(d.index) + 1}` : ""}.`,
29
+ BATCH_TOO_LARGE: (d) => `O lote passa do limite${d.max !== void 0 ? ` (${String(d.received)} de no m\xE1ximo ${String(d.max)})` : ""}. Divida o arquivo.`,
30
+ IDEMPOTENCY_KEY_REQUIRED: () => "A opera\xE7\xE3o exige uma chave de idempot\xEAncia.",
31
+ IDEMPOTENCY_KEY_CONFLICT: () => "A chave de idempot\xEAncia j\xE1 foi usada com outro conte\xFAdo.",
32
+ RATE_LIMITED: (_d, retryAfter) => `O Engramind limitou as requisi\xE7\xF5es do app${retryAfter ? `; tente de novo em ${retryAfter} s` : ""}.`,
33
+ VALIDATION_ERROR: (d) => `O Engramind recusou os dados${d.index !== void 0 ? ` (item ${Number(d.index) + 1})` : ""}${d.field ? ` no campo ${String(d.field)}` : ""}.`,
34
+ CHANNEL_NOT_ARCHIVED: () => "S\xF3 um canal arquivado pode ser exclu\xEDdo: arquive antes.",
35
+ CHANNEL_ARCHIVED: () => "O canal est\xE1 arquivado e n\xE3o recebe escrita: restaure-o antes.",
36
+ CHANNEL_NOT_FOUND: () => "Canal n\xE3o encontrado no Engramind.",
37
+ APP_NOT_FOUND: () => "App n\xE3o encontrado no Engramind.",
38
+ AGENT_NOT_FOUND: () => "Agente n\xE3o encontrado no Engramind.",
39
+ TOKEN_NOT_FOUND: () => "Token n\xE3o encontrado no Engramind.",
40
+ SUBSCRIPTION_NOT_FOUND: () => "Assinatura n\xE3o encontrada no Engramind.",
41
+ MEMORY_NOT_FOUND: () => "Mem\xF3ria n\xE3o encontrada no Engramind.",
42
+ REVISION_NOT_FOUND: () => "A mem\xF3ria n\xE3o tem essa vers\xE3o.",
43
+ MEMORY_VERSION_CONFLICT: () => "Esta mem\xF3ria mudou enquanto voc\xEA editava. Confira a vers\xE3o atual.",
44
+ MEMORY_DUPLICATE: (d) => `J\xE1 existe uma mem\xF3ria com esse texto no canal${d.existing_id ? ` (${String(d.existing_id)})` : ""}.`,
45
+ MEMORY_KEY_EXISTS: () => "J\xE1 existe uma mem\xF3ria com essa chave no canal.",
46
+ PAYLOAD_TOO_LARGE: () => "O corpo passou do limite do Engramind.",
47
+ DATABASE_UNAVAILABLE: () => "O banco do Engramind est\xE1 indispon\xEDvel. Tente novamente.",
48
+ MIGRATIONS_PENDING: () => "O Engramind tem migra\xE7\xF5es pendentes e ainda n\xE3o recebe tr\xE1fego. Tente novamente em instantes.",
49
+ REQUEST_TIMEOUT: () => "O Engramind excedeu o tempo limite da requisi\xE7\xE3o. Tente novamente.",
50
+ INTERNAL_ERROR: () => "Falha inesperada no Engramind. Tente novamente.",
51
+ ROUTE_NOT_FOUND: () => "Opera\xE7\xE3o n\xE3o dispon\xEDvel neste Engramind."
52
+ };
53
+ function clientStatus(status) {
54
+ if (status === 401) return 409;
55
+ if (status >= 500) return 502;
56
+ return status;
57
+ }
58
+ function proxyMaskedError(text, json) {
59
+ if (json && typeof json === "object" && ("ray_id" in json || "error_name" in json || "cloudflare_error" in json)) {
60
+ const name = json.error_name ?? json.title;
61
+ return typeof name === "string" && name ? name : "cloudflare";
62
+ }
63
+ if (/cloudflare/i.test(text) && /ray id|cf-ray|cloudflare/i.test(text)) return "cloudflare";
64
+ return null;
65
+ }
66
+ var parseRetryAfter = (value) => {
67
+ if (!value) return null;
68
+ const seconds = Number(value);
69
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds;
70
+ const date = Date.parse(value);
71
+ return Number.isNaN(date) ? null : Math.max(0, Math.round((date - Date.now()) / 1e3));
72
+ };
73
+ function toEngramindError(status, body, retryAfter = null, masked = null) {
74
+ if (!body && status >= 500 && masked) {
75
+ return new EngramindError(502, `Um proxy (Cloudflare) escondeu a resposta HTTP ${status} do Engramind (${masked}). Chame a URL de origem do Engramind para ver o motivo.`, "ENGRAMIND_PROXY_MASKED", { status, proxyError: masked });
76
+ }
77
+ const code = body?.code ?? (status === 404 ? "ROUTE_NOT_FOUND" : `HTTP_${status}`);
78
+ const message = MESSAGES[code]?.(asDetails(body?.details), retryAfter) ?? body?.message ?? `O Engramind respondeu HTTP ${status}.`;
79
+ return new EngramindError(clientStatus(status), message, `ENGRAMIND_${code}`, body?.details ?? void 0, parseRetryAfter(retryAfter));
80
+ }
81
+ var unreachableError = (reason) => new EngramindError(503, `N\xE3o foi poss\xEDvel falar com o Engramind (${reason}). Confira a URL base e se o servi\xE7o est\xE1 no ar.`, "ENGRAMIND_UNREACHABLE");
82
+ var notConfiguredError = () => new EngramindError(409, "Configure a conex\xE3o com o Engramind (URL base e token) no servidor do app.", "ENGRAMIND_NOT_CONFIGURED");
83
+ var engramindCodeOf = (error) => error instanceof EngramindError && error.code.startsWith("ENGRAMIND_") ? error.code.slice("ENGRAMIND_".length) : null;
84
+
85
+ // src/core/events.ts
86
+ var ids = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : void 0;
87
+ var str = (value) => typeof value === "string" && value ? value : void 0;
88
+ function toLiveEvent(event) {
89
+ const live = { id: event.id, type: event.type, channel: event.channel ?? null };
90
+ const memoryIds = ids(event.memory_ids);
91
+ if (memoryIds) live.memoryIds = memoryIds;
92
+ const keys = ids(event.keys);
93
+ if (keys) live.keys = keys;
94
+ if (event.agent !== void 0) live.agent = event.agent ?? null;
95
+ if (event.app !== void 0) live.app = event.app ?? null;
96
+ const consumption = str(event.consumption);
97
+ if (consumption) live.consumption = consumption;
98
+ const operation = str(event.operation);
99
+ if (operation) live.operation = operation;
100
+ const titleIds = ids(event.title_ids);
101
+ if (titleIds) live.titleIds = titleIds;
102
+ const contextVersion = str(event.context_version);
103
+ if (contextVersion) live.contextVersion = contextVersion;
104
+ const at = str(event.at);
105
+ if (at) live.at = at;
106
+ return live;
107
+ }
108
+ var isConsumedEvent = (type) => type === "memory.consumed";
109
+ var isMemoryChange = (type) => type.startsWith("memory.") && !isConsumedEvent(type);
110
+ function mergeConsumption(events) {
111
+ const first = events.find((event) => event.agent);
112
+ if (!first?.agent) return null;
113
+ const channels = /* @__PURE__ */ new Set();
114
+ const memoryIds = /* @__PURE__ */ new Set();
115
+ const titleIds = /* @__PURE__ */ new Set();
116
+ let version;
117
+ for (const event of events) {
118
+ if (event.channel && (event.memoryIds?.length ?? 0) > 0) channels.add(event.channel);
119
+ for (const id of event.memoryIds ?? []) memoryIds.add(id);
120
+ for (const id of event.titleIds ?? []) titleIds.add(id);
121
+ version = version ?? event.contextVersion;
122
+ }
123
+ if (!memoryIds.size) return null;
124
+ const merged = {
125
+ agent: first.agent,
126
+ kind: first.operation === "get_context" ? "context" : "search",
127
+ channels: [...channels],
128
+ delivered: [...channels],
129
+ memoryIds: [...memoryIds],
130
+ ...titleIds.size ? { titleIds: [...titleIds] } : {},
131
+ ...version ? { version } : {},
132
+ ...first.consumption ? { consumption: first.consumption } : {},
133
+ ...first.operation ? { operation: first.operation } : {}
134
+ };
135
+ return merged;
136
+ }
137
+ function createConsumptionGrouper(options) {
138
+ const windowMs = options.windowMs ?? 400;
139
+ const limit = options.memory ?? 500;
140
+ const pending = /* @__PURE__ */ new Map();
141
+ const done = [];
142
+ const doneSet = /* @__PURE__ */ new Set();
143
+ const flush = (key) => {
144
+ const entry = pending.get(key);
145
+ if (!entry) return;
146
+ pending.delete(key);
147
+ doneSet.add(key);
148
+ done.push(key);
149
+ while (done.length > limit) doneSet.delete(done.shift());
150
+ const merged = mergeConsumption(entry.events);
151
+ if (merged) options.onGroup(merged);
152
+ };
153
+ return {
154
+ push(event) {
155
+ if (!isConsumedEvent(event.type) || !event.agent) return;
156
+ const key = event.consumption ?? event.id ?? `${event.agent}:${event.at ?? ""}`;
157
+ if (doneSet.has(key)) return;
158
+ const entry = pending.get(key);
159
+ if (entry) {
160
+ clearTimeout(entry.timer);
161
+ entry.events.push(event);
162
+ entry.timer = setTimeout(() => flush(key), windowMs);
163
+ } else pending.set(key, { events: [event], timer: setTimeout(() => flush(key), windowMs) });
164
+ },
165
+ /** Emite os grupos pendentes na hora (desmontagem, testes). */
166
+ flushAll() {
167
+ for (const key of [...pending.keys()]) {
168
+ clearTimeout(pending.get(key).timer);
169
+ flush(key);
170
+ }
171
+ },
172
+ dispose() {
173
+ for (const entry of pending.values()) clearTimeout(entry.timer);
174
+ pending.clear();
175
+ }
176
+ };
177
+ }
178
+
179
+ export {
180
+ EngramindError,
181
+ clientStatus,
182
+ proxyMaskedError,
183
+ toEngramindError,
184
+ unreachableError,
185
+ notConfiguredError,
186
+ engramindCodeOf,
187
+ toLiveEvent,
188
+ isConsumedEvent,
189
+ isMemoryChange,
190
+ mergeConsumption,
191
+ createConsumptionGrouper
192
+ };
193
+ //# sourceMappingURL=chunk-THVEWMKQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/errors.ts","../src/core/events.ts"],"sourcesContent":["/**\n * Erros do Engramind traduzidos para pt-BR. Decide pelo `code` estável do envelope\n * `{ error: { code, message, details } }`, nunca pela `message`. Os códigos expostos pela lib levam o\n * prefixo `ENGRAMIND_` (ex.: `ENGRAMIND_CHANNEL_SHARED`), e os erros da própria lib também\n * (`ENGRAMIND_UNREACHABLE`, `ENGRAMIND_PROXY_MASKED`, `ENGRAMIND_NOT_CONFIGURED`).\n */\n\nexport interface EngramindErrorBody {\n code: string\n message?: string\n details?: unknown\n}\n\nexport class EngramindError extends Error {\n readonly status: number\n readonly code: string\n readonly details?: unknown\n /** Segundos de `Retry-After` (429). */\n readonly retryAfter: number | null\n\n constructor(status: number, message: string, code: string, details?: unknown, retryAfter: number | null = null) {\n super(message)\n this.name = \"EngramindError\"\n this.status = status\n this.code = code\n this.details = details\n this.retryAfter = retryAfter\n }\n}\n\ntype Details = Record<string, unknown>\nconst asDetails = (details: unknown): Details => (details && typeof details === \"object\" ? (details as Details) : {})\nconst list = (value: unknown) => (Array.isArray(value) ? value.map(String).join(\", \") : \"\")\n\nconst MESSAGES: Record<string, (details: Details, retryAfter: string | null) => string> = {\n AUTH_INVALID: () => \"O Engramind recusou o token do app (ausente, inválido, expirado, revogado ou de app desativado). Peça um novo token ao gestor e reconfigure o app.\",\n SCOPE_REQUIRED: (d) =>\n `O token do app não tem permissão para esta operação${d.required_kind ? ` (exige ${String(d.required_kind)}` : \"\"}${d.required_scope ? ` com ${String(d.required_scope)})` : d.required_kind ? \")\" : \"\"}.`,\n FOREIGN_APP: () => \"Sem acesso: o registro pertence a outro app.\",\n CHANNEL_NOT_SUBSCRIBED: (d) => `Sem acesso: o token não alcança o canal${d.channel ? ` ${String(d.channel)}` : \"\"}.`,\n CHANNEL_SHARED: () => \"O canal é assinado por outro app e não pode ser arquivado por este app.\",\n LAST_ADMIN_TOKEN: () => \"A rede ficaria sem nenhum token admin ativo com write. Emita outro token admin antes.\",\n SLUG_ALREADY_EXISTS: () => \"Já existe um registro com esse slug. Escolha outro.\",\n SUBSCRIPTION_EXISTS: () => \"O agente já assina esse alvo.\",\n CATEGORY_NOT_FOUND: (d) => `Categoria fora da lista${d.valid ? ` (aceitas: ${list(d.valid)})` : \"\"}${d.index !== undefined ? `; item ${Number(d.index) + 1}` : \"\"}.`,\n BATCH_TOO_LARGE: (d) => `O lote passa do limite${d.max !== undefined ? ` (${String(d.received)} de no máximo ${String(d.max)})` : \"\"}. Divida o arquivo.`,\n IDEMPOTENCY_KEY_REQUIRED: () => \"A operação exige uma chave de idempotência.\",\n IDEMPOTENCY_KEY_CONFLICT: () => \"A chave de idempotência já foi usada com outro conteúdo.\",\n RATE_LIMITED: (_d, retryAfter) => `O Engramind limitou as requisições do app${retryAfter ? `; tente de novo em ${retryAfter} s` : \"\"}.`,\n VALIDATION_ERROR: (d) => `O Engramind recusou os dados${d.index !== undefined ? ` (item ${Number(d.index) + 1})` : \"\"}${d.field ? ` no campo ${String(d.field)}` : \"\"}.`,\n CHANNEL_NOT_ARCHIVED: () => \"Só um canal arquivado pode ser excluído: arquive antes.\",\n CHANNEL_ARCHIVED: () => \"O canal está arquivado e não recebe escrita: restaure-o antes.\",\n CHANNEL_NOT_FOUND: () => \"Canal não encontrado no Engramind.\",\n APP_NOT_FOUND: () => \"App não encontrado no Engramind.\",\n AGENT_NOT_FOUND: () => \"Agente não encontrado no Engramind.\",\n TOKEN_NOT_FOUND: () => \"Token não encontrado no Engramind.\",\n SUBSCRIPTION_NOT_FOUND: () => \"Assinatura não encontrada no Engramind.\",\n MEMORY_NOT_FOUND: () => \"Memória não encontrada no Engramind.\",\n REVISION_NOT_FOUND: () => \"A memória não tem essa versão.\",\n MEMORY_VERSION_CONFLICT: () => \"Esta memória mudou enquanto você editava. Confira a versão atual.\",\n MEMORY_DUPLICATE: (d) => `Já existe uma memória com esse texto no canal${d.existing_id ? ` (${String(d.existing_id)})` : \"\"}.`,\n MEMORY_KEY_EXISTS: () => \"Já existe uma memória com essa chave no canal.\",\n PAYLOAD_TOO_LARGE: () => \"O corpo passou do limite do Engramind.\",\n DATABASE_UNAVAILABLE: () => \"O banco do Engramind está indisponível. Tente novamente.\",\n MIGRATIONS_PENDING: () => \"O Engramind tem migrações pendentes e ainda não recebe tráfego. Tente novamente em instantes.\",\n REQUEST_TIMEOUT: () => \"O Engramind excedeu o tempo limite da requisição. Tente novamente.\",\n INTERNAL_ERROR: () => \"Falha inesperada no Engramind. Tente novamente.\",\n ROUTE_NOT_FOUND: () => \"Operação não disponível neste Engramind.\",\n}\n\n/** Status devolvido ao navegador: falha do próprio serviço vira 502 e token recusado vira 409 (reconfigurar). */\nexport function clientStatus(status: number): number {\n if (status === 401) return 409\n if (status >= 500) return 502\n return status\n}\n\n/**\n * Corpo de um 5xx trocado pelo proxy na frente do Engramind (o Cloudflare substitui o corpo das respostas\n * 5xx pela própria página/JSON): devolve o nome do erro do proxy, quando reconhecido.\n */\nexport function proxyMaskedError(text: string, json: unknown): string | null {\n if (json && typeof json === \"object\" && (\"ray_id\" in json || \"error_name\" in json || \"cloudflare_error\" in json)) {\n const name = (json as { error_name?: unknown; title?: unknown }).error_name ?? (json as { title?: unknown }).title\n return typeof name === \"string\" && name ? name : \"cloudflare\"\n }\n if (/cloudflare/i.test(text) && /ray id|cf-ray|cloudflare/i.test(text)) return \"cloudflare\"\n return null\n}\n\nconst parseRetryAfter = (value: string | null): number | null => {\n if (!value) return null\n const seconds = Number(value)\n if (Number.isFinite(seconds) && seconds >= 0) return seconds\n const date = Date.parse(value)\n return Number.isNaN(date) ? null : Math.max(0, Math.round((date - Date.now()) / 1000))\n}\n\nexport function toEngramindError(status: number, body: EngramindErrorBody | null, retryAfter: string | null = null, masked: string | null = null): EngramindError {\n if (!body && status >= 500 && masked) {\n return new EngramindError(502, `Um proxy (Cloudflare) escondeu a resposta HTTP ${status} do Engramind (${masked}). Chame a URL de origem do Engramind para ver o motivo.`, \"ENGRAMIND_PROXY_MASKED\", { status, proxyError: masked })\n }\n const code = body?.code ?? (status === 404 ? \"ROUTE_NOT_FOUND\" : `HTTP_${status}`)\n const message = MESSAGES[code]?.(asDetails(body?.details), retryAfter) ?? body?.message ?? `O Engramind respondeu HTTP ${status}.`\n return new EngramindError(clientStatus(status), message, `ENGRAMIND_${code}`, body?.details ?? undefined, parseRetryAfter(retryAfter))\n}\n\n/** Engramind fora do ar ou inalcançável. */\nexport const unreachableError = (reason: string) =>\n new EngramindError(503, `Não foi possível falar com o Engramind (${reason}). Confira a URL base e se o serviço está no ar.`, \"ENGRAMIND_UNREACHABLE\")\n\nexport const notConfiguredError = () => new EngramindError(409, \"Configure a conexão com o Engramind (URL base e token) no servidor do app.\", \"ENGRAMIND_NOT_CONFIGURED\")\n\n/** Código do Engramind (sem o prefixo) de um erro traduzido. */\nexport const engramindCodeOf = (error: unknown): string | null => (error instanceof EngramindError && error.code.startsWith(\"ENGRAMIND_\") ? error.code.slice(\"ENGRAMIND_\".length) : null)\n","/**\n * Eventos do Engramind (webhook) e o formato que a lib repassa ao navegador pelo SSE: só ids, slugs e\n * tipos — nunca texto, título, `query` ou segredo.\n */\n\nexport type WebhookEventType =\n | \"memory.created\"\n | \"memory.updated\"\n | \"memory.archived\"\n | \"memory.deleted\"\n | \"memory.consumed\"\n | \"channel.updated\"\n | \"subscription.updated\"\n | \"settings.updated\"\n | \"identities.updated\"\n\n/** Corpo do webhook (`POST <webhook_url>`), contrato do Engramind ≥ 0.12.0. */\nexport interface WebhookEvent {\n id: string\n type: WebhookEventType | (string & {})\n channel: string | null\n channel_version?: number | null\n memory_ids?: string[]\n at?: string\n keys?: string[]\n subscription?: string\n agent?: string | null\n target?: string\n app?: string | null\n consumption?: string\n operation?: \"get_context\" | \"search_memories\" | \"get_hints\" | \"get_memory\" | (string & {})\n title_ids?: string[]\n context_version?: string\n}\n\n/** Evento ao vivo entregue pelo SSE da lib (`event: engramind`). */\nexport interface LiveEvent {\n /** `id` do webhook (ausente nos eventos gerados pelo próprio app). */\n id?: string\n type: string\n channel?: string | null\n memoryIds?: string[]\n keys?: string[]\n agent?: string | null\n app?: string | null\n consumption?: string\n operation?: string\n titleIds?: string[]\n contextVersion?: string\n at?: string\n}\n\nconst ids = (value: unknown): string[] | undefined => (Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : undefined)\nconst str = (value: unknown): string | undefined => (typeof value === \"string\" && value ? value : undefined)\n\n/** Webhook → evento ao vivo (só os campos permitidos). */\nexport function toLiveEvent(event: WebhookEvent): LiveEvent {\n const live: LiveEvent = { id: event.id, type: event.type, channel: event.channel ?? null }\n const memoryIds = ids(event.memory_ids)\n if (memoryIds) live.memoryIds = memoryIds\n const keys = ids(event.keys)\n if (keys) live.keys = keys\n if (event.agent !== undefined) live.agent = event.agent ?? null\n if (event.app !== undefined) live.app = event.app ?? null\n const consumption = str(event.consumption)\n if (consumption) live.consumption = consumption\n const operation = str(event.operation)\n if (operation) live.operation = operation\n const titleIds = ids(event.title_ids)\n if (titleIds) live.titleIds = titleIds\n const contextVersion = str(event.context_version)\n if (contextVersion) live.contextVersion = contextVersion\n const at = str(event.at)\n if (at) live.at = at\n return live\n}\n\n/** `memory.consumed` não muda nada: nunca marca o contexto como sujo nem invalida as listas. */\nexport const isConsumedEvent = (type: string) => type === \"memory.consumed\"\n\n/** Eventos `memory.*` que mudam o contexto (todos menos `memory.consumed`). */\nexport const isMemoryChange = (type: string) => type.startsWith(\"memory.\") && !isConsumedEvent(type)\n\n/**\n * Consumo agrupado (um por `consumption`): o formato que a rede anima. `channels` são os canais que\n * entregaram memórias (um evento por canal), `delivered` idem.\n */\nexport interface ConsumptionEvent {\n /** Agente que leu (`app/agente`). */\n agent: string\n kind: \"context\" | \"search\"\n channels: string[]\n delivered: string[]\n memoryIds: string[]\n titleIds?: string[]\n version?: string\n /** Id do consumo no Engramind (`cns_…`). */\n consumption?: string\n operation?: string\n}\n\n/** Junta os eventos `memory.consumed` de uma mesma leitura (`consumption`). */\nexport function mergeConsumption(events: readonly LiveEvent[]): ConsumptionEvent | null {\n const first = events.find((event) => event.agent)\n if (!first?.agent) return null\n const channels = new Set<string>()\n const memoryIds = new Set<string>()\n const titleIds = new Set<string>()\n let version: string | undefined\n for (const event of events) {\n if (event.channel && (event.memoryIds?.length ?? 0) > 0) channels.add(event.channel)\n for (const id of event.memoryIds ?? []) memoryIds.add(id)\n for (const id of event.titleIds ?? []) titleIds.add(id)\n version = version ?? event.contextVersion\n }\n if (!memoryIds.size) return null\n const merged: ConsumptionEvent = {\n agent: first.agent,\n kind: first.operation === \"get_context\" ? \"context\" : \"search\",\n channels: [...channels],\n delivered: [...channels],\n memoryIds: [...memoryIds],\n ...(titleIds.size ? { titleIds: [...titleIds] } : {}),\n ...(version ? { version } : {}),\n ...(first.consumption ? { consumption: first.consumption } : {}),\n ...(first.operation ? { operation: first.operation } : {}),\n }\n return merged\n}\n\nexport interface ConsumptionGrouperOptions {\n /** Espera para juntar os eventos de canais da mesma leitura (padrão 400 ms). */\n windowMs?: number\n /** Quantos `consumption` já emitidos são lembrados para descartar repetições (padrão 500). */\n memory?: number\n onGroup: (event: ConsumptionEvent) => void\n}\n\n/**\n * Agrupa `memory.consumed` por `consumption`: emite **um** consumo por leitura, depois de `windowMs` sem\n * novos canais. Evento sem `agent` (leitura por token de app ou admin) é ignorado, e um `consumption` já\n * emitido não é emitido de novo (retentativa do webhook). Isomórfico (servidor e navegador).\n */\nexport function createConsumptionGrouper(options: ConsumptionGrouperOptions) {\n const windowMs = options.windowMs ?? 400\n const limit = options.memory ?? 500\n const pending = new Map<string, { events: LiveEvent[]; timer: ReturnType<typeof setTimeout> }>()\n const done: string[] = []\n const doneSet = new Set<string>()\n const flush = (key: string) => {\n const entry = pending.get(key)\n if (!entry) return\n pending.delete(key)\n doneSet.add(key)\n done.push(key)\n while (done.length > limit) doneSet.delete(done.shift()!)\n const merged = mergeConsumption(entry.events)\n if (merged) options.onGroup(merged)\n }\n return {\n push(event: LiveEvent) {\n if (!isConsumedEvent(event.type) || !event.agent) return\n const key = event.consumption ?? event.id ?? `${event.agent}:${event.at ?? \"\"}`\n if (doneSet.has(key)) return\n const entry = pending.get(key)\n if (entry) {\n clearTimeout(entry.timer)\n entry.events.push(event)\n entry.timer = setTimeout(() => flush(key), windowMs)\n } else pending.set(key, { events: [event], timer: setTimeout(() => flush(key), windowMs) })\n },\n /** Emite os grupos pendentes na hora (desmontagem, testes). */\n flushAll() {\n for (const key of [...pending.keys()]) {\n clearTimeout(pending.get(key)!.timer)\n flush(key)\n }\n },\n dispose() {\n for (const entry of pending.values()) clearTimeout(entry.timer)\n pending.clear()\n },\n }\n}\n"],"mappings":"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","names":[]}