@ekai/contexto 0.1.1-rc.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,107 +1,75 @@
1
- # @ekai/contexto
1
+ # @ekai/contexto (v1)
2
2
 
3
- OpenClaw plugin that captures all 13 lifecycle events to structured JSONL storage. Built for context, memory, and analytics.
3
+ OpenClaw plugin Context graph engine that prevents context rot by visualizing and organizing conversation context.
4
4
 
5
- Uses [`@ekai/store`](../../store/) for event normalization, safe serialization, and per-session file organization.
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
8
 
9
- ```bash
10
- openclaw plugins install @ekai/contexto
11
- ```
12
-
13
- ## Configure
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
14
13
 
15
- In your OpenClaw config:
14
+ ## Installation
16
15
 
17
- ```json5
18
- {
19
- plugins: {
20
- allow: ["ekai-contexto"],
21
- entries: {
22
- "ekai-contexto": {
23
- enabled: true,
24
- config: { "dataDir": "~/.openclaw/ekai/data" }
25
- }
26
- }
27
- }
28
- }
16
+ ```bash
17
+ npm install @ekai/contexto
29
18
  ```
30
19
 
31
- `dataDir` defaults to `~/.openclaw/ekai/data` if not set.
20
+ ## OpenClaw Setup
32
21
 
33
- ## Verify
22
+ ### 1. Install the plugin in OpenClaw
34
23
 
35
24
  ```bash
36
- openclaw plugins list # should show ekai-contexto
37
- openclaw hooks list # should show plugin:ekai-contexto:contexto:* hooks
25
+ openclaw plugins install @ekai/contexto
38
26
  ```
39
27
 
40
- ## Storage Layout
28
+ ### 2. Enable and configure the plugin
41
29
 
42
- Events are organized as one JSONL file per session, grouped by agent:
30
+ Set your API key via CLI:
43
31
 
32
+ ```bash
33
+ openclaw plugins config @ekai/contexto apiKey your-api-key-here
44
34
  ```
45
- {dataDir}/
46
- {agent_id}/
47
- {session_id}.jsonl
48
- ```
49
-
50
- IDs are sanitized for safe file paths (`[a-zA-Z0-9_-]` + 8-char SHA-256 hash suffix). Missing IDs fall back to `_unknown-agent` / `_unknown-session`.
51
35
 
52
- Each line is a JSON object with a versioned schema:
36
+ Or add to your OpenClaw config:
53
37
 
54
38
  ```json
55
- {"id":"...","v":1,"eventTs":1709500000000,"ingestTs":1709500000050,"hook":"llm_output","sessionId":"abc-3f2a1b9c","agentId":"default-8e4c7d1a","event":{...},"ctx":{...}}
39
+ {
40
+ "plugins": {
41
+ "slots": {
42
+ "contextEngine": "@ekai/contexto"
43
+ },
44
+ "allow": ["@ekai/contexto"],
45
+ "entries": {
46
+ "@ekai/contexto": {
47
+ "enabled": true,
48
+ "config": {
49
+ "apiKey": "your-api-key-here"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
56
55
  ```
57
56
 
58
- ## What It Captures
59
-
60
- All 13 OpenClaw lifecycle hooks:
61
-
62
- | Hook | Description |
63
- |------|-------------|
64
- | `session_start` | Session opened |
65
- | `session_end` | Session closed |
66
- | `message_received` | Inbound message |
67
- | `message_sent` | Outbound message |
68
- | `before_prompt_build` | Pre-prompt state |
69
- | `llm_input` | LLM request |
70
- | `llm_output` | LLM response |
71
- | `before_tool_call` | Pre-tool invocation |
72
- | `after_tool_call` | Tool result |
73
- | `tool_result_persist` | Tool result persistence |
74
- | `agent_end` | Agent completion |
75
- | `before_compaction` | Pre-compaction state |
76
- | `after_compaction` | Post-compaction state |
77
-
78
- Additional fields extracted per event: `sessionId`, `agentId`, `userId`, `conversationId`.
79
-
80
- ## Design
81
-
82
- - **Structured storage** — one JSONL file per session via `@ekai/store` EventWriter
83
- - **Safe serialization** — handles circular refs, BigInt, Error objects (never throws)
84
- - **Never crashes OpenClaw** — every handler wrapped in try/catch
85
- - **Sync writes** — `appendFileSync` for `tool_result_persist` compatibility
86
- - **ID sanitization** — safe file paths with collision-resistant hashing
87
- - **Schema versioned** — every event carries `v: 1` for future migration
88
-
89
- ## Development
57
+ ### 3. Restart OpenClaw
90
58
 
91
59
  ```bash
92
- # Type-check (no build needed — OpenClaw loads .ts via jiti)
93
- npm run type-check --workspace=@ekai/contexto
60
+ openclaw gateway restart
61
+ ```
94
62
 
95
- # Build the store dependency
96
- npm run build --workspace=store
63
+ ## Configuration
97
64
 
98
- # Run store tests
99
- npm run test --workspace=store
65
+ | Property | Type | Required | Description |
66
+ | --- | --- | --- | --- |
67
+ | `apiKey` | string | Yes | Your Contexto API key |
100
68
 
101
- # Local dev install (symlink)
102
- openclaw plugins install -l ./integrations/openclaw
103
- ```
69
+ ## Version
70
+
71
+ This is **v1** of @ekai/contexto. For the legacy version (v0), see [`../v0`](../v0).
104
72
 
105
73
  ## License
106
74
 
107
- MIT
75
+ MIT
@@ -1,16 +1,12 @@
1
1
  {
2
- "id": "ekai-contexto",
2
+ "id": "@ekai/mindmap",
3
3
  "configSchema": {
4
4
  "type": "object",
5
- "additionalProperties": false,
6
5
  "properties": {
7
- "dataDir": { "type": "string" }
8
- }
9
- },
10
- "uiHints": {
11
- "dataDir": {
12
- "label": "Event data directory",
13
- "description": "Directory for structured JSONL event storage (one file per session)"
6
+ "apiKey": {
7
+ "type": "string",
8
+ "description": "API key for Bearer authentication"
9
+ }
14
10
  }
15
11
  }
16
- }
12
+ }
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@ekai/contexto",
3
- "version": "0.1.1-rc.0",
4
- "description": "OpenClaw plugin — captures all lifecycle events to JSONL for context, memory, and analytics",
3
+ "version": "0.1.2",
4
+ "description": "OpenClaw plugin — sends all events to a webhook API",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
7
10
  "main": "src/index.ts",
8
11
  "files": [
9
12
  "src/",
@@ -12,28 +15,23 @@
12
15
  "keywords": [
13
16
  "openclaw",
14
17
  "openclaw-plugin",
15
- "ai",
16
- "context",
17
- "memory",
18
- "lifecycle",
18
+ "webhook",
19
19
  "events"
20
20
  ],
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/ekai-labs/ekai-gateway",
24
- "directory": "integrations/openclaw"
23
+ "url": "https://github.com/ekailabs/contexto",
24
+ "directory": "packages/contexto/v1"
25
25
  },
26
26
  "author": "Ekai Labs",
27
27
  "scripts": {
28
28
  "type-check": "tsc --noEmit"
29
29
  },
30
- "dependencies": {
31
- "@ekai/store": "*"
32
- },
33
30
  "devDependencies": {
31
+ "@types/node": "^20.10.0",
34
32
  "typescript": "^5.3.2"
35
33
  },
36
34
  "openclaw": {
37
35
  "extensions": ["./src/index.ts"]
38
36
  }
39
- }
37
+ }
package/src/index.ts CHANGED
@@ -1,108 +1,187 @@
1
- import { EventWriter } from '@ekai/store';
2
-
3
- /** Hooks that store events (10 of 13). */
4
- const STORE_HOOKS = [
5
- { name: 'session_start', description: 'Log session start' },
6
- { name: 'session_end', description: 'Log session end' },
7
- { name: 'message_received', description: 'Log inbound message' },
8
- { name: 'message_sent', description: 'Log outbound message' },
9
- { name: 'llm_input', description: 'Log LLM request' },
10
- { name: 'llm_output', description: 'Log LLM response' },
11
- { name: 'before_tool_call', description: 'Log pre-tool invocation' },
12
- { name: 'after_tool_call', description: 'Log tool result' },
13
- { name: 'tool_result_persist', description: 'Log tool result persistence' },
14
- { name: 'agent_end', description: 'Log agent completion' },
15
- ] as const;
16
-
17
- /** Hooks registered as no-ops — keeps OpenClaw aware we're listening. */
18
- const NOOP_HOOKS = [
19
- { name: 'before_prompt_build', description: 'Stub for future memory injection' },
20
- { name: 'before_compaction', description: 'Monitor compaction start' },
21
- { name: 'after_compaction', description: 'Monitor compaction end' },
22
- ] as const;
23
-
24
- export default {
25
- id: 'ekai-contexto',
26
- name: 'Ekai Contexto',
27
- description: 'Context engine for OpenClaw — captures lifecycle events, extensible to memory injection',
1
+ const WEBHOOK_URL_BASE = 'https://api.getcontexto.com';
2
+
3
+ interface WebhookConfig {
4
+ apiKey: string;
5
+ contextEnabled: boolean;
6
+ }
7
+
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>;
18
+ }
19
+
20
+ async function sendWebhook(config: WebhookConfig, payload: WebhookPayload, logger: any): Promise<void> {
21
+ try {
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
+ });
30
+
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}`);
36
+ }
37
+ } catch (err) {
38
+ logger.warn(`[webhook] Failed to send: ${err instanceof Error ? err.message : String(err)}`);
39
+ }
40
+ }
41
+
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
+ }
59
+
60
+ const webhookPlugin = {
61
+ id: '@ekai/mindmap',
62
+ name: 'Mind Map',
63
+ description: 'Sends OpenClaw conversation events to a webhook API',
64
+
28
65
  configSchema: {
29
66
  type: 'object',
30
- additionalProperties: false,
31
67
  properties: {
32
- dataDir: { type: 'string' },
68
+ apiKey: { type: 'string' },
69
+ contextEnabled: { type: 'boolean', default: false },
33
70
  },
34
71
  },
35
72
 
36
73
  register(api: any) {
37
- const dataDir = api.resolvePath(api.pluginConfig?.dataDir ?? '~/.openclaw/ekai/data');
38
- const store = new EventWriter(dataDir);
39
-
40
- // Store hooks — fire-and-forget with .catch() logging
41
- for (const hook of STORE_HOOKS) {
42
- if (hook.name === 'agent_end') {
43
- // agent_end: store event then flush for clean shutdown
44
- api.registerHook({
45
- name: `contexto:${hook.name}`,
46
- description: hook.description,
47
- hook: hook.name,
48
- handler: (event: any, ctx: any) => {
49
- const sessionId = event?.sessionId ?? ctx?.sessionId ?? ctx?.sessionKey;
50
- const agentId = event?.agentId ?? ctx?.agentId;
51
- const userId = event?.userId ?? ctx?.userId ?? ctx?.user;
52
- const conversationId = event?.conversationId ?? ctx?.conversationId;
53
-
54
- store.append({
55
- hook: hook.name,
56
- sessionId,
57
- agentId,
58
- userId,
59
- conversationId,
60
- event: event ?? {},
61
- ctx,
62
- })
63
- .catch(err => api.logger.warn(`ekai-contexto: append failed: ${String(err)}`))
64
- .finally(() => store.flush()
65
- .catch(err => api.logger.warn(`ekai-contexto: flush failed: ${String(err)}`)));
66
- },
67
- });
68
- continue;
69
- }
74
+ const config: WebhookConfig = {
75
+ apiKey: api.pluginConfig?.apiKey,
76
+ contextEnabled: api.pluginConfig?.contextEnabled ?? false,
77
+ };
70
78
 
71
- api.registerHook({
72
- name: `contexto:${hook.name}`,
73
- description: hook.description,
74
- hook: hook.name,
75
- handler: (event: any, ctx: any) => {
76
- const sessionId = event?.sessionId ?? ctx?.sessionId ?? ctx?.sessionKey;
77
- const agentId = event?.agentId ?? ctx?.agentId;
78
- const userId = event?.userId ?? ctx?.userId ?? ctx?.user;
79
- const conversationId = event?.conversationId ?? ctx?.conversationId;
80
-
81
- store.append({
82
- hook: hook.name,
83
- sessionId,
84
- agentId,
85
- userId,
86
- conversationId,
87
- event: event ?? {},
88
- ctx,
89
- }).catch(err => {
90
- api.logger.warn(`ekai-contexto: store.append failed: ${String(err)}`);
91
- });
92
- },
93
- });
94
- }
79
+ const logger = api.logger;
95
80
 
96
- // No-op hooks — registered so OpenClaw knows we're listening
97
- for (const hook of NOOP_HOOKS) {
98
- api.registerHook({
99
- name: `contexto:${hook.name}`,
100
- description: hook.description,
101
- hook: hook.name,
102
- handler: () => {},
103
- });
81
+ if (!config.apiKey) {
82
+ logger.warn('[webhook] Missing apiKey - events will not be sent');
104
83
  }
105
84
 
106
- api.logger.info(`ekai-contexto: storing events to ${dataDir}`);
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,
113
+ }
114
+ );
115
+
116
+ sendWebhook(config, payload, logger);
117
+ });
118
+
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
136
+ }
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,
159
+ }
160
+ );
161
+
162
+ sendWebhook(config, payload, logger);
163
+ });
164
+
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
+ );
179
+
180
+ sendWebhook(config, payload, logger);
181
+ });
182
+
183
+ logger.info('[webhook] All hooks registered via api.on()');
107
184
  },
108
185
  };
186
+
187
+ export default webhookPlugin;