@ekai/contexto 0.1.0 → 0.1.1-rc.0
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 +107 -0
- package/openclaw.plugin.json +4 -4
- package/package.json +4 -1
- package/src/index.ts +78 -17
- package/src/store.ts +0 -56
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @ekai/contexto
|
|
2
|
+
|
|
3
|
+
OpenClaw plugin that captures all 13 lifecycle events to structured JSONL storage. Built for context, memory, and analytics.
|
|
4
|
+
|
|
5
|
+
Uses [`@ekai/store`](../../store/) for event normalization, safe serialization, and per-session file organization.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install @ekai/contexto
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
In your OpenClaw config:
|
|
16
|
+
|
|
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
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`dataDir` defaults to `~/.openclaw/ekai/data` if not set.
|
|
32
|
+
|
|
33
|
+
## Verify
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
openclaw plugins list # should show ekai-contexto
|
|
37
|
+
openclaw hooks list # should show plugin:ekai-contexto:contexto:* hooks
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Storage Layout
|
|
41
|
+
|
|
42
|
+
Events are organized as one JSONL file per session, grouped by agent:
|
|
43
|
+
|
|
44
|
+
```
|
|
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
|
+
|
|
52
|
+
Each line is a JSON object with a versioned schema:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"id":"...","v":1,"eventTs":1709500000000,"ingestTs":1709500000050,"hook":"llm_output","sessionId":"abc-3f2a1b9c","agentId":"default-8e4c7d1a","event":{...},"ctx":{...}}
|
|
56
|
+
```
|
|
57
|
+
|
|
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
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Type-check (no build needed — OpenClaw loads .ts via jiti)
|
|
93
|
+
npm run type-check --workspace=@ekai/contexto
|
|
94
|
+
|
|
95
|
+
# Build the store dependency
|
|
96
|
+
npm run build --workspace=store
|
|
97
|
+
|
|
98
|
+
# Run store tests
|
|
99
|
+
npm run test --workspace=store
|
|
100
|
+
|
|
101
|
+
# Local dev install (symlink)
|
|
102
|
+
openclaw plugins install -l ./integrations/openclaw
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/openclaw.plugin.json
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"type": "object",
|
|
5
5
|
"additionalProperties": false,
|
|
6
6
|
"properties": {
|
|
7
|
-
"
|
|
7
|
+
"dataDir": { "type": "string" }
|
|
8
8
|
}
|
|
9
9
|
},
|
|
10
10
|
"uiHints": {
|
|
11
|
-
"
|
|
12
|
-
"label": "Event
|
|
13
|
-
"description": "
|
|
11
|
+
"dataDir": {
|
|
12
|
+
"label": "Event data directory",
|
|
13
|
+
"description": "Directory for structured JSONL event storage (one file per session)"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekai/contexto",
|
|
3
|
-
"version": "0.1.0",
|
|
3
|
+
"version": "0.1.1-rc.0",
|
|
4
4
|
"description": "OpenClaw plugin — captures all lifecycle events to JSONL for context, memory, and analytics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"type-check": "tsc --noEmit"
|
|
29
29
|
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@ekai/store": "*"
|
|
32
|
+
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"typescript": "^5.3.2"
|
|
32
35
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,47 +1,108 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EventWriter } from '@ekai/store';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Hooks that store events (10 of 13). */
|
|
4
|
+
const STORE_HOOKS = [
|
|
4
5
|
{ name: 'session_start', description: 'Log session start' },
|
|
5
6
|
{ name: 'session_end', description: 'Log session end' },
|
|
6
7
|
{ name: 'message_received', description: 'Log inbound message' },
|
|
7
8
|
{ name: 'message_sent', description: 'Log outbound message' },
|
|
8
|
-
{ name: 'before_prompt_build', description: 'Log pre-prompt state' },
|
|
9
9
|
{ name: 'llm_input', description: 'Log LLM request' },
|
|
10
10
|
{ name: 'llm_output', description: 'Log LLM response' },
|
|
11
11
|
{ name: 'before_tool_call', description: 'Log pre-tool invocation' },
|
|
12
12
|
{ name: 'after_tool_call', description: 'Log tool result' },
|
|
13
13
|
{ name: 'tool_result_persist', description: 'Log tool result persistence' },
|
|
14
14
|
{ name: 'agent_end', description: 'Log agent completion' },
|
|
15
|
-
|
|
16
|
-
|
|
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' },
|
|
17
22
|
] as const;
|
|
18
23
|
|
|
19
24
|
export default {
|
|
20
25
|
id: 'ekai-contexto',
|
|
21
26
|
name: 'Ekai Contexto',
|
|
22
27
|
description: 'Context engine for OpenClaw — captures lifecycle events, extensible to memory injection',
|
|
23
|
-
configSchema: {
|
|
28
|
+
configSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
properties: {
|
|
32
|
+
dataDir: { type: 'string' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
24
35
|
|
|
25
36
|
register(api: any) {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
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
|
+
}
|
|
28
70
|
|
|
29
|
-
for (const hook of HOOKS) {
|
|
30
71
|
api.registerHook({
|
|
31
72
|
name: `contexto:${hook.name}`,
|
|
32
73
|
description: hook.description,
|
|
33
74
|
hook: hook.name,
|
|
34
|
-
handler: (event:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
});
|
|
41
92
|
},
|
|
42
93
|
});
|
|
43
94
|
}
|
|
44
95
|
|
|
45
|
-
|
|
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
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
api.logger.info(`ekai-contexto: storing events to ${dataDir}`);
|
|
46
107
|
},
|
|
47
108
|
};
|
package/src/store.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Safe JSON replacer — handles circular refs, BigInt, undefined, errors.
|
|
6
|
-
* No external deps needed.
|
|
7
|
-
*/
|
|
8
|
-
function safeReplacer() {
|
|
9
|
-
const seen = new WeakSet();
|
|
10
|
-
return (_key: string, value: unknown): unknown => {
|
|
11
|
-
if (typeof value === 'bigint') return value.toString();
|
|
12
|
-
if (value instanceof Error) return { message: value.message, stack: value.stack };
|
|
13
|
-
if (value !== null && typeof value === 'object') {
|
|
14
|
-
if (seen.has(value)) return '[Circular]';
|
|
15
|
-
seen.add(value);
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function safeStringify(obj: unknown): string {
|
|
22
|
-
try {
|
|
23
|
-
return JSON.stringify(obj, safeReplacer());
|
|
24
|
-
} catch {
|
|
25
|
-
return JSON.stringify({ _error: 'serialization failed' });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Extract sessionId and agentId from event or ctx (whichever has them).
|
|
31
|
-
*/
|
|
32
|
-
function extractIds(event: any, ctx: any): { sessionId?: string; agentId?: string } {
|
|
33
|
-
return {
|
|
34
|
-
sessionId: event?.sessionId ?? ctx?.sessionId ?? ctx?.sessionKey ?? undefined,
|
|
35
|
-
agentId: event?.agentId ?? ctx?.agentId ?? undefined,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class EventLog {
|
|
40
|
-
constructor(private path: string) {
|
|
41
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Append a hook event to the JSONL log.
|
|
46
|
-
* Uses appendFileSync for tool_result_persist sync compatibility.
|
|
47
|
-
*
|
|
48
|
-
* NOTE (v0): appendFileSync blocks the event loop. Acceptable at low volume.
|
|
49
|
-
* Future: async write with buffering for high-throughput hooks.
|
|
50
|
-
*/
|
|
51
|
-
append(hook: string, event: unknown, ctx: unknown): void {
|
|
52
|
-
const { sessionId, agentId } = extractIds(event, ctx);
|
|
53
|
-
const line = safeStringify({ ts: Date.now(), hook, sessionId, agentId, event, ctx });
|
|
54
|
-
appendFileSync(this.path, line + '\n');
|
|
55
|
-
}
|
|
56
|
-
}
|