@ekai/contexto 0.1.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.
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "ekai-contexto",
3
+ "configSchema": {
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "logPath": { "type": "string" }
8
+ }
9
+ },
10
+ "uiHints": {
11
+ "logPath": {
12
+ "label": "Event log path",
13
+ "description": "Path to the JSONL event log file"
14
+ }
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@ekai/contexto",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw plugin — captures all lifecycle events to JSONL for context, memory, and analytics",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "src/index.ts",
8
+ "files": [
9
+ "src/",
10
+ "openclaw.plugin.json"
11
+ ],
12
+ "keywords": [
13
+ "openclaw",
14
+ "openclaw-plugin",
15
+ "ai",
16
+ "context",
17
+ "memory",
18
+ "lifecycle",
19
+ "events"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/ekai-labs/ekai-gateway",
24
+ "directory": "integrations/openclaw"
25
+ },
26
+ "author": "Ekai Labs",
27
+ "scripts": {
28
+ "type-check": "tsc --noEmit"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.3.2"
32
+ },
33
+ "openclaw": {
34
+ "extensions": ["./src/index.ts"]
35
+ }
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { EventLog } from './store.js';
2
+
3
+ const HOOKS = [
4
+ { name: 'session_start', description: 'Log session start' },
5
+ { name: 'session_end', description: 'Log session end' },
6
+ { name: 'message_received', description: 'Log inbound message' },
7
+ { name: 'message_sent', description: 'Log outbound message' },
8
+ { name: 'before_prompt_build', description: 'Log pre-prompt state' },
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
+ { name: 'before_compaction', description: 'Log pre-compaction state' },
16
+ { name: 'after_compaction', description: 'Log post-compaction state' },
17
+ ] as const;
18
+
19
+ export default {
20
+ id: 'ekai-contexto',
21
+ name: 'Ekai Contexto',
22
+ description: 'Context engine for OpenClaw — captures lifecycle events, extensible to memory injection',
23
+ configSchema: {},
24
+
25
+ register(api: any) {
26
+ const logPath = api.resolvePath(api.pluginConfig?.logPath ?? '~/.openclaw/ekai/events.jsonl');
27
+ const log = new EventLog(logPath);
28
+
29
+ for (const hook of HOOKS) {
30
+ api.registerHook({
31
+ name: `contexto:${hook.name}`,
32
+ description: hook.description,
33
+ 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
+ }
41
+ },
42
+ });
43
+ }
44
+
45
+ api.logger.info(`ekai-contexto: logging to ${logPath}`);
46
+ },
47
+ };
package/src/store.ts ADDED
@@ -0,0 +1,56 @@
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
+ }