@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 +46 -78
- package/openclaw.plugin.json +6 -10
- package/package.json +10 -12
- package/src/index.ts +174 -95
package/README.md
CHANGED
|
@@ -1,107 +1,75 @@
|
|
|
1
|
-
# @ekai/contexto
|
|
1
|
+
# @ekai/contexto (v1)
|
|
2
2
|
|
|
3
|
-
OpenClaw plugin
|
|
3
|
+
OpenClaw plugin — Context graph engine that prevents context rot by visualizing and organizing conversation context.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Purpose
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
+
## Installation
|
|
16
15
|
|
|
17
|
-
```
|
|
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
|
-
|
|
20
|
+
## OpenClaw Setup
|
|
32
21
|
|
|
33
|
-
|
|
22
|
+
### 1. Install the plugin in OpenClaw
|
|
34
23
|
|
|
35
24
|
```bash
|
|
36
|
-
openclaw plugins
|
|
37
|
-
openclaw hooks list # should show plugin:ekai-contexto:contexto:* hooks
|
|
25
|
+
openclaw plugins install @ekai/contexto
|
|
38
26
|
```
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
### 2. Enable and configure the plugin
|
|
41
29
|
|
|
42
|
-
|
|
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
|
-
|
|
36
|
+
Or add to your OpenClaw config:
|
|
53
37
|
|
|
54
38
|
```json
|
|
55
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
60
|
+
openclaw gateway restart
|
|
61
|
+
```
|
|
94
62
|
|
|
95
|
-
|
|
96
|
-
npm run build --workspace=store
|
|
63
|
+
## Configuration
|
|
97
64
|
|
|
98
|
-
|
|
99
|
-
|
|
65
|
+
| Property | Type | Required | Description |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `apiKey` | string | Yes | Your Contexto API key |
|
|
100
68
|
|
|
101
|
-
|
|
102
|
-
|
|
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
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "ekai
|
|
2
|
+
"id": "@ekai/mindmap",
|
|
3
3
|
"configSchema": {
|
|
4
4
|
"type": "object",
|
|
5
|
-
"additionalProperties": false,
|
|
6
5
|
"properties": {
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
4
|
-
"description": "OpenClaw plugin —
|
|
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
|
-
"
|
|
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/
|
|
24
|
-
"directory": "
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
68
|
+
apiKey: { type: 'string' },
|
|
69
|
+
contextEnabled: { type: 'boolean', default: false },
|
|
33
70
|
},
|
|
34
71
|
},
|
|
35
72
|
|
|
36
73
|
register(api: any) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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;
|