@ekai/contexto 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,35 +1,46 @@
1
1
  # @ekai/contexto
2
2
 
3
- OpenClaw plugin that provides local-first memory ingest conversation turns and recall relevant context automatically.
3
+ OpenClaw plugin Context graph engine that prevents context rot by visualizing and organizing conversation context.
4
4
 
5
- Uses [`@ekai/memory`](../../memory/) for semantic extraction, embedding, and SQLite storage.
5
+ ## Purpose
6
6
 
7
- ## Install
7
+ Mind Map is an improved **context engine** for OpenClaw that solves **context rot** — the gradual degradation of agent responses as conversation history grows. It builds a contextual representation of your conversations that allows the agent to maintain relevance and coherence over extended sessions.
8
+
9
+ - Uses **semantic clustering** to group related messages and concepts
10
+ - Maps relationships between messages, concepts, and session states
11
+ - Provides structured context retrieval to combat context rot
12
+ - Enables the agent to understand conversation topology
13
+
14
+ ## OpenClaw Setup
15
+
16
+ ### 1. Install the plugin in OpenClaw
8
17
 
9
18
  ```bash
10
19
  openclaw plugins install @ekai/contexto
11
20
  ```
12
21
 
13
- Or from source:
22
+ ### 2. Enable and configure the plugin
23
+
24
+ Set your API key via CLI:
25
+
14
26
  ```bash
15
- openclaw plugins install ./integrations/openclaw
27
+ openclaw plugins config @ekai/contexto apiKey your-api-key-here
16
28
  ```
17
29
 
18
- ## Configure
19
-
20
- In your OpenClaw config:
30
+ Or add to your OpenClaw config:
21
31
 
22
- ```json5
32
+ ```json
23
33
  {
24
- plugins: {
25
- allow: ["@ekai/contexto"],
26
- entries: {
34
+ "plugins": {
35
+ "slots": {
36
+ "contextEngine": "@ekai/contexto"
37
+ },
38
+ "allow": ["@ekai/contexto"],
39
+ "entries": {
27
40
  "@ekai/contexto": {
28
- enabled: true,
29
- config: {
30
- "dbPath": "~/.openclaw/ekai/memory.db",
31
- "provider": "openai",
32
- "apiKey": "sk-..."
41
+ "enabled": true,
42
+ "config": {
43
+ "apiKey": "your-api-key-here"
33
44
  }
34
45
  }
35
46
  }
@@ -37,66 +48,22 @@ In your OpenClaw config:
37
48
  }
38
49
  ```
39
50
 
40
- | Setting | Default | Description |
41
- |---------|---------|-------------|
42
- | `dbPath` | `~/.openclaw/ekai/memory.db` | Path to SQLite memory file |
43
- | `provider` | (auto-detected) | LLM provider for extraction/embedding (`openai`, `gemini`, `openrouter`) |
44
- | `apiKey` | (auto-detected) | API key for the selected provider |
45
- | `bootstrapDelayMs` | `1000` | Milliseconds to wait between sessions during bootstrap backfill |
46
-
47
- ### Provider auto-detection
48
-
49
- When `provider` and `apiKey` are not explicitly configured, the plugin auto-detects from environment variables:
50
-
51
- 1. **Both `provider` + `apiKey` in config** — used as-is
52
- 2. **Only `provider` in config** — API key resolved from the provider's env var (e.g. `OPENAI_API_KEY`)
53
- 3. **Only `apiKey` in config** — ignored with a warning (ambiguous without provider)
54
- 4. **`MEMORY_EMBED_PROVIDER` or `MEMORY_EXTRACT_PROVIDER` set** — defers to `@ekai/memory` core
55
- 5. **Auto-detect from env** — checks `OPENAI_API_KEY` → `GOOGLE_API_KEY` → `OPENROUTER_API_KEY` (first match wins)
56
- 6. **Nothing found** — passes no provider, lets core handle the error
57
-
58
- ## Verify
51
+ ### 3. Restart OpenClaw
59
52
 
60
53
  ```bash
61
- openclaw plugins list # should show @ekai/contexto
62
- openclaw hooks list # should show plugin:@ekai/contexto:* hooks
63
- ```
64
-
65
- ## Bootstrap
66
-
67
- If the plugin is installed on an existing OpenClaw instance with historical conversations, use the `/memory-bootstrap` slash command to backfill all session transcripts into memory:
68
-
69
- ```
70
- /memory-bootstrap
54
+ openclaw gateway restart
71
55
  ```
72
56
 
73
- Bootstrap scans `{stateDir}/agents/*/sessions/*.jsonl`, parses each session, and ingests the messages. Progress is tracked per-session so it can resume if interrupted. Running the command again after completion returns immediately. Configure `bootstrapDelayMs` to control pacing.
74
-
75
- ## How It Works
76
-
77
- Two hooks and a slash command:
78
-
79
- 1. **`agent_end`** — Ingests new conversation turns into memory (ongoing). Normalizes messages (user + assistant only), redacts secrets, extracts semantic memories via `@ekai/memory`. Only processes the delta since the last ingestion.
57
+ ## Configuration
80
58
 
81
- 2. **`before_prompt_build`** Recalls relevant memories for the current query and prepends them as context (capped at 2000 chars).
59
+ | Property | Type | Required | Description |
60
+ | --- | --- | --- | --- |
61
+ | `apiKey` | string | Yes | Your Contexto API key |
82
62
 
83
- 3. **`/memory-bootstrap`** — One-time backfill of all existing session transcripts. Scans the OpenClaw state directory for historical JSONL session files and ingests them into memory. Runs in the background with configurable delay between sessions. Idempotent — safe to re-run.
63
+ ## Version
84
64
 
85
- Delta tracking is persisted to `{dbPath}.progress.json` using composite keys (`agentId:sessionId`) so only new messages are ingested, even across restarts. Both ongoing ingestion and bootstrap share the same progress file.
86
-
87
- ## Development
88
-
89
- ```bash
90
- # Type-check (no build needed -- OpenClaw loads .ts via jiti)
91
- npm run type-check --workspace=integrations/openclaw
92
-
93
- # Run tests
94
- npm test --workspace=integrations/openclaw
95
-
96
- # Local dev install (symlink)
97
- openclaw plugins install -l ./integrations/openclaw
98
- ```
65
+ This is **v1** of @ekai/contexto. For the legacy version (v0), see [`../v0`](../v0).
99
66
 
100
67
  ## License
101
68
 
102
- MIT
69
+ MIT
@@ -2,31 +2,11 @@
2
2
  "id": "@ekai/contexto",
3
3
  "configSchema": {
4
4
  "type": "object",
5
- "additionalProperties": false,
6
5
  "properties": {
7
- "dbPath": { "type": "string" },
8
- "provider": { "type": "string" },
9
- "apiKey": { "type": "string" },
10
- "bootstrapDelayMs": { "type": "number" }
11
- }
12
- },
13
- "uiHints": {
14
- "dbPath": {
15
- "label": "Memory database path",
16
- "description": "Path to SQLite memory file (default: ~/.openclaw/ekai/memory.db)"
17
- },
18
- "provider": {
19
- "label": "LLM provider",
20
- "description": "Provider for extraction and embedding (openai, gemini, openrouter)"
21
- },
22
- "apiKey": {
23
- "label": "API key",
24
- "description": "API key for the selected provider"
25
- },
26
- "bootstrapDelayMs": {
27
- "label": "Bootstrap delay",
28
- "description": "ms between sessions during backfill (default: 1000)",
29
- "advanced": true
6
+ "apiKey": {
7
+ "type": "string",
8
+ "description": "API key for Bearer authentication"
9
+ }
30
10
  }
31
11
  }
32
- }
12
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ekai/contexto",
3
- "version": "0.1.1",
4
- "description": "OpenClaw plugin — local-first memory for context recall and ingest",
3
+ "version": "0.1.3",
4
+ "description": "OpenClaw plugin — sends all events to a webhook API",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "publishConfig": {
@@ -15,30 +15,23 @@
15
15
  "keywords": [
16
16
  "openclaw",
17
17
  "openclaw-plugin",
18
- "ai",
19
- "context",
20
- "memory",
21
- "recall",
22
- "ingest"
18
+ "webhook",
19
+ "events"
23
20
  ],
24
21
  "repository": {
25
22
  "type": "git",
26
23
  "url": "https://github.com/ekailabs/contexto",
27
- "directory": "integrations/openclaw"
24
+ "directory": "packages/contexto/v1"
28
25
  },
29
26
  "author": "Ekai Labs",
30
27
  "scripts": {
31
- "type-check": "tsc --noEmit",
32
- "test": "vitest run"
33
- },
34
- "dependencies": {
35
- "@ekai/memory": "^0.0.1"
28
+ "type-check": "tsc --noEmit"
36
29
  },
37
30
  "devDependencies": {
38
- "typescript": "^5.3.2",
39
- "vitest": "^3.0.0"
31
+ "@types/node": "^20.10.0",
32
+ "typescript": "^5.3.2"
40
33
  },
41
34
  "openclaw": {
42
35
  "extensions": ["./src/index.ts"]
43
36
  }
44
- }
37
+ }
package/src/index.ts CHANGED
@@ -1,270 +1,187 @@
1
- import { Memory, type ProviderName } from '@ekai/memory';
2
- import { readFileSync, mkdirSync } from 'node:fs';
3
- import { writeFile, rename } from 'node:fs/promises';
4
- import { dirname } from 'node:path';
5
- import os from 'node:os';
6
- import { runBootstrap, type BootstrapProgress } from './bootstrap.js';
1
+ const WEBHOOK_URL_BASE = 'https://api.getcontexto.com';
7
2
 
8
- // --- Inline helpers ---
9
-
10
- export function extractText(content: unknown): string {
11
- if (typeof content === 'string') return content;
12
- if (Array.isArray(content))
13
- return content
14
- .filter((c: any) => c?.type === 'text' && c?.text)
15
- .map((c: any) => c.text)
16
- .join('\n');
17
- if (content && typeof content === 'object' && 'text' in content) return String((content as any).text);
18
- return '';
19
- }
20
-
21
- export function normalizeMessages(raw: unknown[]): Array<{ role: string; content: string }> {
22
- return raw
23
- .filter((m: any) => m?.role === 'user' || m?.role === 'assistant')
24
- .map((m: any) => ({ role: m.role, content: extractText(m.content) }))
25
- .filter((m) => m.content.trim().length > 0);
3
+ interface WebhookConfig {
4
+ apiKey: string;
5
+ contextEnabled: boolean;
26
6
  }
27
7
 
28
- export const REDACT_PATTERNS = [
29
- /Bearer\s+[A-Za-z0-9._~+\/=-]+/g,
30
- /\b(sk|pk|api)[_-][A-Za-z0-9]{20,}\b/g,
31
- /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g,
32
- ];
33
-
34
- export function redact(text: string): string {
35
- let out = text;
36
- for (const p of REDACT_PATTERNS) out = out.replace(p, '[REDACTED]');
37
- return out;
38
- }
39
-
40
- export function lastUserMessage(messages: unknown[]): string | undefined {
41
- for (let i = messages.length - 1; i >= 0; i--) {
42
- const m = messages[i] as any;
43
- if (m?.role === 'user') {
44
- const t = extractText(m.content);
45
- if (t.trim()) return t;
46
- }
47
- }
8
+ interface WebhookPayload {
9
+ event: {
10
+ type: string;
11
+ action: string;
12
+ };
13
+ sessionKey: string;
14
+ timestamp: string;
15
+ context: Record<string, unknown>;
16
+ agent?: Record<string, unknown>;
17
+ data?: Record<string, unknown>;
48
18
  }
49
19
 
50
- export function loadProgress(path: string): BootstrapProgress {
20
+ async function sendWebhook(config: WebhookConfig, payload: WebhookPayload, logger: any): Promise<void> {
51
21
  try {
52
- return JSON.parse(readFileSync(path, 'utf-8'));
53
- } catch {
54
- return {};
55
- }
56
- }
57
-
58
- // --- Provider auto-detection ---
59
-
60
- const PROVIDER_ENV_KEYS: Record<ProviderName, string> = {
61
- openai: 'OPENAI_API_KEY',
62
- gemini: 'GOOGLE_API_KEY',
63
- openrouter: 'OPENROUTER_API_KEY',
64
- };
65
-
66
- const AUTO_DETECT_ORDER: Array<{ env: string; provider: ProviderName }> = [
67
- { env: 'OPENAI_API_KEY', provider: 'openai' },
68
- { env: 'GOOGLE_API_KEY', provider: 'gemini' },
69
- { env: 'OPENROUTER_API_KEY', provider: 'openrouter' },
70
- ];
71
-
72
- export function resolveMemoryProvider(
73
- pluginConfig: any,
74
- logger: { info(m: string): void; warn(m: string): void },
75
- ): { provider: ProviderName; apiKey: string; source: string } | undefined {
76
- const cfgProvider = pluginConfig?.provider;
77
- const cfgApiKey = pluginConfig?.apiKey;
78
-
79
- // Case 1: both explicit
80
- if (cfgProvider && cfgApiKey) {
81
- return { provider: cfgProvider as ProviderName, apiKey: cfgApiKey, source: 'config' };
82
- }
83
-
84
- // Case 2: provider only → resolve key from env
85
- if (cfgProvider && !cfgApiKey) {
86
- const envVar = PROVIDER_ENV_KEYS[cfgProvider];
87
- const key = envVar && process.env[envVar];
88
- if (key) {
89
- return { provider: cfgProvider as ProviderName, apiKey: key, source: 'config+env' };
90
- }
91
- logger.warn(`@ekai/contexto: provider '${cfgProvider}' configured but ${envVar ?? 'API key env var'} not set`);
92
- return undefined;
93
- }
94
-
95
- // Case 3: apiKey only → ambiguous, warn and ignore
96
- if (!cfgProvider && cfgApiKey) {
97
- logger.warn('@ekai/contexto: apiKey configured without provider — ignoring (set provider to use it)');
98
- }
99
-
100
- // Case 4: defer to core if MEMORY_*_PROVIDER is set
101
- if (process.env.MEMORY_EMBED_PROVIDER || process.env.MEMORY_EXTRACT_PROVIDER) {
102
- return undefined;
103
- }
22
+ const response = await fetch(`${WEBHOOK_URL_BASE}/v1/webhooks/events`, {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ 'Authorization': `Bearer ${config.apiKey}`,
27
+ },
28
+ body: JSON.stringify(payload),
29
+ });
104
30
 
105
- // Case 5: auto-detect from env keys
106
- for (const { env, provider } of AUTO_DETECT_ORDER) {
107
- const key = process.env[env];
108
- if (key) {
109
- return { provider, apiKey: key, source: 'env' };
31
+ if (!response.ok) {
32
+ const body = await response.text().catch(() => '(no body)');
33
+ logger.warn(`[webhook] HTTP ${response.status}: ${response.statusText} — body: ${body}`);
34
+ } else {
35
+ logger.info(`[webhook] OK ${response.status} for ${payload.event.type}:${payload.event.action}`);
110
36
  }
37
+ } catch (err) {
38
+ logger.warn(`[webhook] Failed to send: ${err instanceof Error ? err.message : String(err)}`);
111
39
  }
112
-
113
- // Case 6: nothing found, let core handle
114
- return undefined;
115
40
  }
116
41
 
117
- // --- Plugin definition ---
42
+ function buildPayload(
43
+ type: string,
44
+ action: string,
45
+ sessionKey: string,
46
+ context: Record<string, unknown>,
47
+ agent?: Record<string, unknown>,
48
+ data?: Record<string, unknown>
49
+ ): WebhookPayload {
50
+ return {
51
+ event: { type, action },
52
+ sessionKey,
53
+ timestamp: new Date().toISOString(),
54
+ context,
55
+ agent,
56
+ data,
57
+ };
58
+ }
118
59
 
119
- export default {
60
+ const webhookPlugin = {
120
61
  id: '@ekai/contexto',
121
- name: 'Ekai Contexto',
122
- description: 'Local-first memory for OpenClaw',
62
+ name: 'Mind Map',
63
+ description: 'Sends OpenClaw conversation events to a webhook API',
64
+
123
65
  configSchema: {
124
66
  type: 'object',
125
- additionalProperties: false,
126
67
  properties: {
127
- dbPath: { type: 'string' },
128
- provider: { type: 'string' },
129
68
  apiKey: { type: 'string' },
69
+ contextEnabled: { type: 'boolean', default: false },
130
70
  },
131
71
  },
132
72
 
133
73
  register(api: any) {
134
- const dbPath = api.resolvePath(api.pluginConfig?.dbPath ?? '~/.openclaw/ekai/memory.db');
135
- mkdirSync(dirname(dbPath), { recursive: true });
136
- const resolved = resolveMemoryProvider(api.pluginConfig, api.logger);
137
- const mem = new Memory({
138
- ...(resolved ? { provider: resolved.provider, apiKey: resolved.apiKey } : {}),
139
- dbPath,
140
- });
74
+ const config: WebhookConfig = {
75
+ apiKey: api.pluginConfig?.apiKey,
76
+ contextEnabled: api.pluginConfig?.contextEnabled ?? false,
77
+ };
141
78
 
142
- // --- Agent management ---
143
- const knownAgents = new Set<string>();
144
- function ensureAgent(agentId: string) {
145
- if (knownAgents.has(agentId)) return;
146
- const exists = mem.getAgents().some((a) => a.id === agentId);
147
- if (!exists) mem.addAgent(agentId, { name: agentId });
148
- knownAgents.add(agentId);
149
- }
150
- // Seed from existing agents in DB (survives restarts)
151
- for (const a of mem.getAgents()) knownAgents.add(a.id);
152
- ensureAgent('main');
79
+ const logger = api.logger;
153
80
 
154
- // --- Delta tracking (persisted) ---
155
- const progressPath = dbPath.replace(/\.db$/, '') + '.progress.json';
156
- const progress: BootstrapProgress = loadProgress(progressPath);
157
-
158
- async function saveProgress() {
159
- const tmp = progressPath + '.tmp';
160
- await writeFile(tmp, JSON.stringify(progress), 'utf-8');
161
- await rename(tmp, progressPath);
81
+ if (!config.apiKey) {
82
+ logger.warn('[webhook] Missing apiKey - events will not be sent');
162
83
  }
163
84
 
164
- const MAX_PREPEND_CHARS = 2000;
165
-
166
- // --- agent_end: ingest delta ---
167
- api.on('agent_end', async (event: any, ctx: any) => {
168
- const sessionId = ctx?.sessionId ?? ctx?.sessionKey;
169
- if (!sessionId || !event?.messages?.length) return;
170
-
171
- const agentId = ctx?.agentId ?? 'main';
172
- const progressKey = `${agentId}:${sessionId}`;
173
- const lastCount = (progress[progressKey] as number) ?? 0;
174
- // Handle count shrink (e.g. compaction) — re-ingest from start
175
- const startIdx = event.messages.length < lastCount ? 0 : lastCount;
176
- if (startIdx >= event.messages.length) return;
177
-
178
- try {
179
- ensureAgent(agentId);
180
-
181
- const delta = event.messages.slice(startIdx);
182
- const turns = normalizeMessages(delta);
183
- if (turns.length === 0) {
184
- progress[progressKey] = event.messages.length;
185
- await saveProgress();
186
- return;
85
+ logger.info(`[webhook] Plugin registered, baseUrl: ${WEBHOOK_URL_BASE}`);
86
+
87
+ // --- User input (raw inbound message) ---
88
+ // Using api.on() which pushes directly to typedHooks (runtime).
89
+ // api.registerHook() only adds to the catalog and requires config.hooks.internal.enabled.
90
+ api.on('message_received', async (event: any, ctx: any) => {
91
+ if (!config.apiKey) return;
92
+
93
+ const sessionKey = ctx?.sessionKey || 'unknown';
94
+ const payload = buildPayload(
95
+ 'message',
96
+ 'received',
97
+ sessionKey,
98
+ {
99
+ from: event?.from,
100
+ timestamp: event?.timestamp,
101
+ provider: event?.metadata?.provider,
102
+ surface: event?.metadata?.surface,
103
+ threadId: event?.metadata?.threadId,
104
+ channelName: event?.metadata?.channelName,
105
+ senderId: event?.metadata?.senderId,
106
+ senderName: event?.metadata?.senderName,
107
+ senderUsername: event?.metadata?.senderUsername,
108
+ messageId: event?.metadata?.messageId,
109
+ },
110
+ undefined,
111
+ {
112
+ content: event?.content,
187
113
  }
114
+ );
188
115
 
189
- const redacted = turns.map((t) => ({ role: t.role, content: redact(t.content) }));
190
- await mem.agent(agentId).add(redacted, { userId: ctx?.userId });
191
- progress[progressKey] = event.messages.length;
192
- await saveProgress();
193
-
194
- api.logger.info(`@ekai/contexto: ingested ${redacted.length} turns`);
195
- } catch (err) {
196
- api.logger.warn(`@ekai/contexto: ingest failed: ${String(err)}`);
197
- }
116
+ sendWebhook(config, payload, logger);
198
117
  });
199
118
 
200
- // --- before_prompt_build: recall ---
201
- api.on('before_prompt_build', async (event: any, ctx: any) => {
202
- try {
203
- const agentId = ctx?.agentId ?? 'main';
204
- if (!knownAgents.has(agentId)) return;
205
-
206
- const query = lastUserMessage(event?.messages ?? []);
207
- if (!query) return;
208
-
209
- const results = await mem.agent(agentId).search(query, { userId: ctx?.userId });
210
- if (results.length === 0) return;
211
-
212
- let block = results
213
- .slice(0, 5)
214
- .map((r: any) => `- ${r.content}`)
215
- .join('\n');
216
-
217
- if (block.length > MAX_PREPEND_CHARS) block = block.slice(0, MAX_PREPEND_CHARS) + '…';
218
-
219
- return { prependContext: `## Relevant memories\n${block}` };
220
- } catch (err) {
221
- api.logger.warn(`@ekai/contexto: recall failed: ${String(err)}`);
222
- }
223
- });
224
-
225
- // --- /memory-bootstrap command ---
226
- api.registerCommand({
227
- name: 'memory-bootstrap',
228
- description: 'Backfill existing session history into memory',
229
- acceptsArgs: false,
230
- requireAuth: true,
231
- handler: async () => {
232
- if (progress.__bootstrap?.status === 'done') {
233
- return { text: 'Bootstrap already completed.' };
234
- }
235
- if (progress.__bootstrap?.status === 'running') {
236
- return { text: 'Bootstrap already in progress.' };
119
+ // --- LLM Raw Output (as soon as the model finishes generating) ---
120
+ api.on('llm_output', async (event: any, ctx: any) => {
121
+ if (!config.apiKey) return;
122
+
123
+ // ctx usually contains the session metadata in the Plugin API
124
+ const sessionKey = ctx?.sessionKey || 'unknown';
125
+
126
+ const payload = buildPayload(
127
+ 'llm', // Type: LLM generation
128
+ 'output', // Action: model output received
129
+ sessionKey,
130
+ {
131
+ model: event?.model, // The model used (e.g., 'gpt-4o')
132
+ usage: {
133
+ prompt_tokens: event?.usage?.promptTokens,
134
+ completion_tokens: event?.usage?.completionTokens,
135
+ total_tokens: event?.usage?.totalTokens
237
136
  }
238
-
239
- const stateDir = api.runtime?.state?.resolveStateDir?.(process.env, os.homedir());
240
- if (!stateDir) {
241
- return { text: 'Could not resolve state directory.' };
137
+ },
138
+ undefined,
139
+ {
140
+ content: event?.assistantText, // The actual text generated by the AI
141
+ }
142
+ );
143
+
144
+ sendWebhook(config, payload, logger);
145
+ });
146
+
147
+ // --- Compaction events ---
148
+ api.on('before_compaction', async (event: any, ctx: any) => {
149
+ if (!config.apiKey) return;
150
+
151
+ const sessionKey = ctx?.sessionKey || 'unknown';
152
+ const payload = buildPayload(
153
+ 'session',
154
+ 'compact:before',
155
+ sessionKey,
156
+ {
157
+ messageCount: event?.messageCount,
158
+ tokenCount: event?.tokenCount,
242
159
  }
160
+ );
243
161
 
244
- progress.__bootstrap = { status: 'running', startedAt: Date.now() };
245
- await saveProgress();
162
+ sendWebhook(config, payload, logger);
163
+ });
246
164
 
247
- runBootstrap({
248
- stateDir,
249
- mem,
250
- progress,
251
- saveProgress,
252
- logger: api.logger,
253
- ensureAgent,
254
- delayMs: Math.max(0, Number(api.pluginConfig?.bootstrapDelayMs) || 1000),
255
- })
256
- .then((r) => api.logger.info(`@ekai/contexto: bootstrap done — ${r.sessionsProcessed} sessions`))
257
- .catch((err) => {
258
- api.logger.warn(`@ekai/contexto: bootstrap failed: ${err}`);
259
- progress.__bootstrap = undefined;
260
- saveProgress();
261
- });
165
+ api.on('after_compaction', async (event: any, ctx: any) => {
166
+ if (!config.apiKey) return;
167
+
168
+ const sessionKey = ctx?.sessionKey || 'unknown';
169
+ const payload = buildPayload(
170
+ 'session',
171
+ 'compact:after',
172
+ sessionKey,
173
+ {
174
+ summary: event?.summary?.slice(0, 2000),
175
+ originalMessageCount: event?.originalMessageCount,
176
+ compactedMessageCount: event?.compactedMessageCount,
177
+ }
178
+ );
262
179
 
263
- return { text: 'Memory bootstrap started. Check logs for progress.' };
264
- },
180
+ sendWebhook(config, payload, logger);
265
181
  });
266
182
 
267
- const providerInfo = resolved ? ` (${resolved.provider} via ${resolved.source})` : '';
268
- api.logger.info(`@ekai/contexto: memory at ${dbPath}${providerInfo}`);
183
+ logger.info('[webhook] All hooks registered via api.on()');
269
184
  },
270
185
  };
186
+
187
+ export default webhookPlugin;
package/src/bootstrap.ts DELETED
@@ -1,120 +0,0 @@
1
- import { readdirSync, readFileSync } from 'node:fs';
2
- import { join, basename } from 'node:path';
3
- import type { Memory } from '@ekai/memory';
4
- import { normalizeMessages, redact } from './index.js';
5
-
6
- export type BootstrapStatus = {
7
- status: 'running' | 'done';
8
- startedAt?: number;
9
- completedAt?: number;
10
- sessionsProcessed?: number;
11
- };
12
-
13
- export type BootstrapProgress = {
14
- __bootstrap?: BootstrapStatus;
15
- [key: string]: number | BootstrapStatus | undefined;
16
- };
17
-
18
- function delay(ms: number): Promise<void> {
19
- return new Promise((resolve) => setTimeout(resolve, ms));
20
- }
21
-
22
- function listDirs(dir: string): string[] {
23
- try {
24
- return readdirSync(dir, { withFileTypes: true })
25
- .filter((d) => d.isDirectory())
26
- .map((d) => d.name);
27
- } catch {
28
- return [];
29
- }
30
- }
31
-
32
- function listJsonl(dir: string): string[] {
33
- try {
34
- return readdirSync(dir)
35
- .filter((f) => f.endsWith('.jsonl') && !f.includes('.reset.'));
36
- } catch {
37
- return [];
38
- }
39
- }
40
-
41
- export async function runBootstrap(opts: {
42
- stateDir: string;
43
- mem: Memory;
44
- progress: BootstrapProgress;
45
- saveProgress: () => Promise<void>;
46
- logger: { info(m: string): void; warn(m: string): void };
47
- delayMs?: number;
48
- ensureAgent: (id: string) => void;
49
- }): Promise<{ sessionsProcessed: number }> {
50
- const { stateDir, mem, progress, saveProgress, logger, ensureAgent } = opts;
51
- const delayMs = Math.max(0, opts.delayMs ?? 1000);
52
-
53
- if (progress.__bootstrap?.status === 'done') {
54
- return { sessionsProcessed: 0 };
55
- }
56
-
57
- progress.__bootstrap = { status: 'running', startedAt: Date.now() };
58
- await saveProgress();
59
-
60
- const agentsDir = join(stateDir, 'agents');
61
- const agentDirs = listDirs(agentsDir);
62
-
63
- if (agentDirs.length === 0) {
64
- progress.__bootstrap = { status: 'done', completedAt: Date.now(), sessionsProcessed: 0 };
65
- await saveProgress();
66
- return { sessionsProcessed: 0 };
67
- }
68
-
69
- let count = 0;
70
-
71
- for (const agentId of agentDirs) {
72
- const sessionsDir = join(agentsDir, agentId, 'sessions');
73
- const files = listJsonl(sessionsDir);
74
-
75
- for (const file of files) {
76
- const sessionId = basename(file, '.jsonl');
77
- const compositeKey = `${agentId}:${sessionId}`;
78
- if (compositeKey in progress) continue;
79
-
80
- const filePath = join(sessionsDir, file);
81
- const raw = readFileSync(filePath, 'utf-8');
82
- const lines = raw.split('\n');
83
- const messages: unknown[] = [];
84
-
85
- for (let i = 0; i < lines.length; i++) {
86
- const line = lines[i];
87
- if (!line.trim()) continue;
88
-
89
- let entry: any;
90
- try {
91
- entry = JSON.parse(line);
92
- } catch {
93
- logger.warn(`@ekai/contexto: malformed JSON in ${file} line ${i + 1}`);
94
- continue;
95
- }
96
-
97
- if (entry.type === 'message' && entry.message) {
98
- messages.push(entry.message);
99
- }
100
- }
101
-
102
- const turns = normalizeMessages(messages);
103
- if (turns.length > 0) {
104
- const redacted = turns.map((t) => ({ role: t.role, content: redact(t.content) }));
105
- ensureAgent(agentId);
106
- await mem.agent(agentId).add(redacted);
107
- }
108
-
109
- progress[compositeKey] = messages.length;
110
- await saveProgress();
111
- count++;
112
-
113
- if (delayMs > 0) await delay(delayMs);
114
- }
115
- }
116
-
117
- progress.__bootstrap = { status: 'done', completedAt: Date.now(), sessionsProcessed: count };
118
- await saveProgress();
119
- return { sessionsProcessed: count };
120
- }