@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 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
@@ -4,13 +4,13 @@
4
4
  "type": "object",
5
5
  "additionalProperties": false,
6
6
  "properties": {
7
- "logPath": { "type": "string" }
7
+ "dataDir": { "type": "string" }
8
8
  }
9
9
  },
10
10
  "uiHints": {
11
- "logPath": {
12
- "label": "Event log path",
13
- "description": "Path to the JSONL event log file"
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 { EventLog } from './store.js';
1
+ import { EventWriter } from '@ekai/store';
2
2
 
3
- const HOOKS = [
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
- { name: 'before_compaction', description: 'Log pre-compaction state' },
16
- { name: 'after_compaction', description: 'Log post-compaction state' },
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 logPath = api.resolvePath(api.pluginConfig?.logPath ?? '~/.openclaw/ekai/events.jsonl');
27
- const log = new EventLog(logPath);
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: unknown, ctx: unknown) => {
35
- try {
36
- log.append(hook.name, event, ctx);
37
- } catch (err) {
38
- // Never crash OpenClaw log the failure and move on
39
- api.logger.warn(`ekai-contexto: failed to log ${hook.name}: ${String(err)}`);
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
- api.logger.info(`ekai-contexto: logging to ${logPath}`);
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
- }