@contextcompany/openclaw 1.0.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,86 @@
1
+ # @contextcompany/openclaw
2
+
3
+ The Context Company observability plugin for [OpenClaw](https://openclaw.ai).
4
+
5
+ Captures LLM calls, tool executions, and agent lifecycle events from OpenClaw's plugin hook system, then exports them to The Context Company for visualization and analysis.
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Install
10
+
11
+ ```bash
12
+ openclaw plugins install @contextcompany/openclaw
13
+ ```
14
+
15
+ ### 2. Configure
16
+
17
+ Add to your `openclaw.json`:
18
+
19
+ ```json
20
+ {
21
+ "plugins": {
22
+ "allow": ["@contextcompany/openclaw"],
23
+ "entries": {
24
+ "@contextcompany/openclaw": {
25
+ "enabled": true,
26
+ "config": {
27
+ "apiKey": "${TCC_API_KEY}"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ### 3. Restart
36
+
37
+ ```bash
38
+ openclaw gateway restart
39
+ ```
40
+
41
+ That's it. The plugin hooks into the agent runtime and starts sending traces to TCC.
42
+
43
+ ## Alternative: Manual Registration
44
+
45
+ If you prefer to register from a custom extension:
46
+
47
+ ```ts
48
+ // extensions/tcc-observability/index.ts
49
+ import { register } from "@contextcompany/openclaw";
50
+
51
+ export default async function (api) {
52
+ register(api);
53
+ }
54
+ ```
55
+
56
+ With explicit config:
57
+
58
+ ```ts
59
+ register(api, {
60
+ apiKey: "tcc_...",
61
+ endpoint: "https://api.thecontext.company/v1/openclaw",
62
+ debug: true,
63
+ });
64
+ ```
65
+
66
+ ## Configuration
67
+
68
+ | Option | Env Var | Default | Description |
69
+ |--------|---------|---------|-------------|
70
+ | `apiKey` | `TCC_API_KEY` | — | Your Context Company API key |
71
+ | `endpoint` | `TCC_URL` | Auto-detected from key | Ingestion endpoint URL |
72
+ | `debug` | `TCC_DEBUG` | `false` | Enable debug logging |
73
+
74
+ ## How It Works
75
+
76
+ The plugin hooks into OpenClaw's agent lifecycle events:
77
+
78
+ | Hook | What it captures |
79
+ |------|-----------------|
80
+ | `llm_input` | LLM call start — model, prompt, system prompt, history |
81
+ | `llm_output` | LLM call end — response, token usage, cost |
82
+ | `before_tool_call` | Tool execution start — tool name, arguments |
83
+ | `after_tool_call` | Tool execution end — result, errors, duration |
84
+ | `agent_end` | Run complete — success/failure, full message history |
85
+
86
+ All events are collected during the agent run and sent as a single batch when the run completes. Sessions that never receive an `agent_end` are flushed after 30 minutes.
package/dist/index.cjs ADDED
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ // src/transport.ts
6
+ var MAX_RETRIES = 2;
7
+ var INITIAL_BACKOFF_MS = 1e3;
8
+ async function sendToTcc(payload, apiKey, url, debug, log) {
9
+ const body = JSON.stringify(payload);
10
+ if (debug) log.info(`sending ${body.length} bytes to ${url}`);
11
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
12
+ if (attempt > 0)
13
+ await new Promise((r) => setTimeout(r, INITIAL_BACKOFF_MS * 2 ** (attempt - 1)));
14
+ try {
15
+ const res = await fetch(url, {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ Authorization: `Bearer ${apiKey}`
20
+ },
21
+ body
22
+ });
23
+ if (res.ok) {
24
+ if (debug) log.info("sent ok");
25
+ return;
26
+ }
27
+ const text = await res.text();
28
+ if (res.status !== 429 && res.status < 500) {
29
+ log.warn(`ingestion failed (${res.status}): ${text}`);
30
+ return;
31
+ }
32
+ } catch (err) {
33
+ if (attempt === MAX_RETRIES)
34
+ log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts: ${err}`);
35
+ }
36
+ }
37
+ log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts (server errors)`);
38
+ }
39
+ function safeClone(obj) {
40
+ try {
41
+ return JSON.parse(JSON.stringify(obj));
42
+ } catch {
43
+ return String(obj);
44
+ }
45
+ }
46
+
47
+ // src/plugin.ts
48
+ function registerHooks(api, configOverrides) {
49
+ const activeSessions = /* @__PURE__ */ new Map();
50
+ const pluginConfig = {
51
+ ...api.pluginConfig ?? {},
52
+ ...configOverrides
53
+ };
54
+ const debug = pluginConfig.debug === true || process.env.TCC_DEBUG === "true";
55
+ const log = {
56
+ info: (msg) => console.log(`[tcc] ${msg}`),
57
+ warn: (msg) => console.warn(`[tcc] ${msg}`)
58
+ };
59
+ const apiKey = (typeof pluginConfig.apiKey === "string" ? pluginConfig.apiKey : null) ?? process.env.TCC_API_KEY;
60
+ if (!apiKey) {
61
+ log.warn("No TCC_API_KEY found. Set env var or plugin config. Disabled.");
62
+ return;
63
+ }
64
+ const url = (typeof pluginConfig.endpoint === "string" ? pluginConfig.endpoint : null) ?? process.env.TCC_URL ?? (apiKey.startsWith("dev_") ? "https://dev.thecontext.company/v1/openclaw" : "https://api.thecontext.company/v1/openclaw");
65
+ log.info(`exporting runs to ${url}`);
66
+ const STALE_SESSION_MS = 30 * 60 * 1e3;
67
+ const cleanupInterval = setInterval(() => {
68
+ const now = Date.now();
69
+ for (const [key, session] of activeSessions) {
70
+ if (now - session.startedAt > STALE_SESSION_MS) {
71
+ if (debug) log.info(`flushing stale session: ${key}`);
72
+ sendToTcc(
73
+ { framework: "openclaw", events: session.events, stale: true },
74
+ apiKey,
75
+ url,
76
+ debug,
77
+ log
78
+ ).catch((err) => {
79
+ log.warn(`failed to send stale session: ${err}`);
80
+ });
81
+ activeSessions.delete(key);
82
+ }
83
+ }
84
+ }, 5 * 60 * 1e3);
85
+ if (cleanupInterval.unref) cleanupInterval.unref();
86
+ function pushEvent(hook, event, ctx) {
87
+ const sessionKey = ctx?.sessionKey;
88
+ if (!sessionKey) return;
89
+ let session = activeSessions.get(sessionKey);
90
+ if (!session) {
91
+ session = { events: [], startedAt: Date.now() };
92
+ activeSessions.set(sessionKey, session);
93
+ }
94
+ session.events.push({
95
+ hook,
96
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
97
+ event: safeClone(event),
98
+ context: safeClone(ctx)
99
+ });
100
+ }
101
+ api.on("llm_input", (event, ctx) => {
102
+ pushEvent("llm_input", event, ctx);
103
+ if (debug) log.info(`llm_input (model: ${event.model})`);
104
+ });
105
+ api.on("llm_output", (event, ctx) => {
106
+ pushEvent("llm_output", event, ctx);
107
+ if (debug) log.info(`llm_output (model: ${event.model})`);
108
+ });
109
+ api.on("before_tool_call", (event, ctx) => {
110
+ pushEvent("before_tool_call", event, ctx);
111
+ if (debug) log.info(`before_tool_call (tool: ${event.toolName})`);
112
+ });
113
+ api.on("after_tool_call", (event, ctx) => {
114
+ pushEvent("after_tool_call", event, ctx);
115
+ if (debug) log.info(`after_tool_call (tool: ${event.toolName})`);
116
+ });
117
+ api.on("agent_end", (event, ctx) => {
118
+ const sessionKey = ctx?.sessionKey;
119
+ if (!sessionKey) return;
120
+ pushEvent("agent_end", event, ctx);
121
+ queueMicrotask(() => {
122
+ const session = activeSessions.get(sessionKey);
123
+ if (!session) return;
124
+ if (debug)
125
+ log.info(`agent_end \u2014 sending ${session.events.length} events`);
126
+ const payload = {
127
+ framework: "openclaw",
128
+ events: session.events
129
+ };
130
+ sendToTcc(payload, apiKey, url, debug, log).catch((err) => {
131
+ log.warn(`failed to send events: ${err}`);
132
+ });
133
+ activeSessions.delete(sessionKey);
134
+ });
135
+ });
136
+ }
137
+ var plugin = {
138
+ id: "@contextcompany/openclaw",
139
+ name: "The Context Company",
140
+ description: "Agent observability \u2014 captures LLM calls, tool executions, and agent lifecycle events",
141
+ register(api) {
142
+ registerHooks(api);
143
+ }
144
+ };
145
+ var plugin_default = plugin;
146
+ function register(api, configOverrides) {
147
+ registerHooks(api, configOverrides);
148
+ }
149
+
150
+ exports.default = plugin_default;
151
+ exports.register = register;
@@ -0,0 +1,61 @@
1
+ /** Configuration for the TCC OpenClaw plugin. */
2
+ type OpenClawPluginConfig = {
3
+ /** TCC API key. Falls back to TCC_API_KEY env var. */
4
+ apiKey?: string;
5
+ /** TCC ingestion endpoint. Falls back to TCC_URL env var, then auto-detected from key prefix. */
6
+ endpoint?: string;
7
+ /** Enable debug logging. Falls back to TCC_DEBUG env var. */
8
+ debug?: boolean;
9
+ };
10
+
11
+ /**
12
+ * @contextcompany/openclaw — OpenClaw plugin for The Context Company
13
+ *
14
+ * Thin forwarder: collects raw hook events during an agent run,
15
+ * then sends them all as one batch on agent_end.
16
+ *
17
+ * All parsing/transformation happens server-side.
18
+ */
19
+
20
+ /**
21
+ * Full OpenClaw plugin object — install via `openclaw plugins install`
22
+ * and configure in `openclaw.json` under `plugins.entries`.
23
+ *
24
+ * @example
25
+ * ```json
26
+ * {
27
+ * "plugins": {
28
+ * "allow": ["@contextcompany/openclaw"],
29
+ * "entries": {
30
+ * "@contextcompany/openclaw": {
31
+ * "enabled": true,
32
+ * "config": {
33
+ * "apiKey": "${TCC_API_KEY}"
34
+ * }
35
+ * }
36
+ * }
37
+ * }
38
+ * }
39
+ * ```
40
+ */
41
+ declare const plugin: {
42
+ id: string;
43
+ name: string;
44
+ description: string;
45
+ register(api: any): void;
46
+ };
47
+
48
+ /**
49
+ * Named export for manual registration (e.g. from a custom extension).
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { register } from "@contextcompany/openclaw";
54
+ * export default async function (api) {
55
+ * register(api, { apiKey: "tcc_...", debug: true });
56
+ * }
57
+ * ```
58
+ */
59
+ declare function register(api: any, configOverrides?: OpenClawPluginConfig): void;
60
+
61
+ export { type OpenClawPluginConfig, plugin as default, register };
@@ -0,0 +1,61 @@
1
+ /** Configuration for the TCC OpenClaw plugin. */
2
+ type OpenClawPluginConfig = {
3
+ /** TCC API key. Falls back to TCC_API_KEY env var. */
4
+ apiKey?: string;
5
+ /** TCC ingestion endpoint. Falls back to TCC_URL env var, then auto-detected from key prefix. */
6
+ endpoint?: string;
7
+ /** Enable debug logging. Falls back to TCC_DEBUG env var. */
8
+ debug?: boolean;
9
+ };
10
+
11
+ /**
12
+ * @contextcompany/openclaw — OpenClaw plugin for The Context Company
13
+ *
14
+ * Thin forwarder: collects raw hook events during an agent run,
15
+ * then sends them all as one batch on agent_end.
16
+ *
17
+ * All parsing/transformation happens server-side.
18
+ */
19
+
20
+ /**
21
+ * Full OpenClaw plugin object — install via `openclaw plugins install`
22
+ * and configure in `openclaw.json` under `plugins.entries`.
23
+ *
24
+ * @example
25
+ * ```json
26
+ * {
27
+ * "plugins": {
28
+ * "allow": ["@contextcompany/openclaw"],
29
+ * "entries": {
30
+ * "@contextcompany/openclaw": {
31
+ * "enabled": true,
32
+ * "config": {
33
+ * "apiKey": "${TCC_API_KEY}"
34
+ * }
35
+ * }
36
+ * }
37
+ * }
38
+ * }
39
+ * ```
40
+ */
41
+ declare const plugin: {
42
+ id: string;
43
+ name: string;
44
+ description: string;
45
+ register(api: any): void;
46
+ };
47
+
48
+ /**
49
+ * Named export for manual registration (e.g. from a custom extension).
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { register } from "@contextcompany/openclaw";
54
+ * export default async function (api) {
55
+ * register(api, { apiKey: "tcc_...", debug: true });
56
+ * }
57
+ * ```
58
+ */
59
+ declare function register(api: any, configOverrides?: OpenClawPluginConfig): void;
60
+
61
+ export { type OpenClawPluginConfig, plugin as default, register };
package/dist/index.js ADDED
@@ -0,0 +1,146 @@
1
+ // src/transport.ts
2
+ var MAX_RETRIES = 2;
3
+ var INITIAL_BACKOFF_MS = 1e3;
4
+ async function sendToTcc(payload, apiKey, url, debug, log) {
5
+ const body = JSON.stringify(payload);
6
+ if (debug) log.info(`sending ${body.length} bytes to ${url}`);
7
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
8
+ if (attempt > 0)
9
+ await new Promise((r) => setTimeout(r, INITIAL_BACKOFF_MS * 2 ** (attempt - 1)));
10
+ try {
11
+ const res = await fetch(url, {
12
+ method: "POST",
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ Authorization: `Bearer ${apiKey}`
16
+ },
17
+ body
18
+ });
19
+ if (res.ok) {
20
+ if (debug) log.info("sent ok");
21
+ return;
22
+ }
23
+ const text = await res.text();
24
+ if (res.status !== 429 && res.status < 500) {
25
+ log.warn(`ingestion failed (${res.status}): ${text}`);
26
+ return;
27
+ }
28
+ } catch (err) {
29
+ if (attempt === MAX_RETRIES)
30
+ log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts: ${err}`);
31
+ }
32
+ }
33
+ log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts (server errors)`);
34
+ }
35
+ function safeClone(obj) {
36
+ try {
37
+ return JSON.parse(JSON.stringify(obj));
38
+ } catch {
39
+ return String(obj);
40
+ }
41
+ }
42
+
43
+ // src/plugin.ts
44
+ function registerHooks(api, configOverrides) {
45
+ const activeSessions = /* @__PURE__ */ new Map();
46
+ const pluginConfig = {
47
+ ...api.pluginConfig ?? {},
48
+ ...configOverrides
49
+ };
50
+ const debug = pluginConfig.debug === true || process.env.TCC_DEBUG === "true";
51
+ const log = {
52
+ info: (msg) => console.log(`[tcc] ${msg}`),
53
+ warn: (msg) => console.warn(`[tcc] ${msg}`)
54
+ };
55
+ const apiKey = (typeof pluginConfig.apiKey === "string" ? pluginConfig.apiKey : null) ?? process.env.TCC_API_KEY;
56
+ if (!apiKey) {
57
+ log.warn("No TCC_API_KEY found. Set env var or plugin config. Disabled.");
58
+ return;
59
+ }
60
+ const url = (typeof pluginConfig.endpoint === "string" ? pluginConfig.endpoint : null) ?? process.env.TCC_URL ?? (apiKey.startsWith("dev_") ? "https://dev.thecontext.company/v1/openclaw" : "https://api.thecontext.company/v1/openclaw");
61
+ log.info(`exporting runs to ${url}`);
62
+ const STALE_SESSION_MS = 30 * 60 * 1e3;
63
+ const cleanupInterval = setInterval(() => {
64
+ const now = Date.now();
65
+ for (const [key, session] of activeSessions) {
66
+ if (now - session.startedAt > STALE_SESSION_MS) {
67
+ if (debug) log.info(`flushing stale session: ${key}`);
68
+ sendToTcc(
69
+ { framework: "openclaw", events: session.events, stale: true },
70
+ apiKey,
71
+ url,
72
+ debug,
73
+ log
74
+ ).catch((err) => {
75
+ log.warn(`failed to send stale session: ${err}`);
76
+ });
77
+ activeSessions.delete(key);
78
+ }
79
+ }
80
+ }, 5 * 60 * 1e3);
81
+ if (cleanupInterval.unref) cleanupInterval.unref();
82
+ function pushEvent(hook, event, ctx) {
83
+ const sessionKey = ctx?.sessionKey;
84
+ if (!sessionKey) return;
85
+ let session = activeSessions.get(sessionKey);
86
+ if (!session) {
87
+ session = { events: [], startedAt: Date.now() };
88
+ activeSessions.set(sessionKey, session);
89
+ }
90
+ session.events.push({
91
+ hook,
92
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
93
+ event: safeClone(event),
94
+ context: safeClone(ctx)
95
+ });
96
+ }
97
+ api.on("llm_input", (event, ctx) => {
98
+ pushEvent("llm_input", event, ctx);
99
+ if (debug) log.info(`llm_input (model: ${event.model})`);
100
+ });
101
+ api.on("llm_output", (event, ctx) => {
102
+ pushEvent("llm_output", event, ctx);
103
+ if (debug) log.info(`llm_output (model: ${event.model})`);
104
+ });
105
+ api.on("before_tool_call", (event, ctx) => {
106
+ pushEvent("before_tool_call", event, ctx);
107
+ if (debug) log.info(`before_tool_call (tool: ${event.toolName})`);
108
+ });
109
+ api.on("after_tool_call", (event, ctx) => {
110
+ pushEvent("after_tool_call", event, ctx);
111
+ if (debug) log.info(`after_tool_call (tool: ${event.toolName})`);
112
+ });
113
+ api.on("agent_end", (event, ctx) => {
114
+ const sessionKey = ctx?.sessionKey;
115
+ if (!sessionKey) return;
116
+ pushEvent("agent_end", event, ctx);
117
+ queueMicrotask(() => {
118
+ const session = activeSessions.get(sessionKey);
119
+ if (!session) return;
120
+ if (debug)
121
+ log.info(`agent_end \u2014 sending ${session.events.length} events`);
122
+ const payload = {
123
+ framework: "openclaw",
124
+ events: session.events
125
+ };
126
+ sendToTcc(payload, apiKey, url, debug, log).catch((err) => {
127
+ log.warn(`failed to send events: ${err}`);
128
+ });
129
+ activeSessions.delete(sessionKey);
130
+ });
131
+ });
132
+ }
133
+ var plugin = {
134
+ id: "@contextcompany/openclaw",
135
+ name: "The Context Company",
136
+ description: "Agent observability \u2014 captures LLM calls, tool executions, and agent lifecycle events",
137
+ register(api) {
138
+ registerHooks(api);
139
+ }
140
+ };
141
+ var plugin_default = plugin;
142
+ function register(api, configOverrides) {
143
+ registerHooks(api, configOverrides);
144
+ }
145
+
146
+ export { plugin_default as default, register };
@@ -0,0 +1,36 @@
1
+ {
2
+ "id": "@contextcompany/openclaw",
3
+ "name": "The Context Company",
4
+ "description": "Agent observability — captures LLM calls, tool executions, and agent lifecycle events",
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "enabled": { "type": "boolean" },
10
+ "apiKey": { "type": "string" },
11
+ "endpoint": { "type": "string" },
12
+ "debug": { "type": "boolean" }
13
+ }
14
+ },
15
+ "uiHints": {
16
+ "enabled": {
17
+ "label": "Tracing Enabled",
18
+ "help": "Enable trace export to The Context Company."
19
+ },
20
+ "apiKey": {
21
+ "label": "TCC API Key",
22
+ "placeholder": "tcc_...",
23
+ "sensitive": true,
24
+ "help": "Falls back to TCC_API_KEY environment variable."
25
+ },
26
+ "endpoint": {
27
+ "label": "TCC Endpoint URL",
28
+ "placeholder": "https://api.thecontext.company/v1/openclaw",
29
+ "help": "Auto-detected from API key prefix. Override for custom deployments."
30
+ },
31
+ "debug": {
32
+ "label": "Debug Logging",
33
+ "help": "Log hook events and transport details to console."
34
+ }
35
+ }
36
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@contextcompany/openclaw",
3
+ "version": "1.0.0",
4
+ "description": "The Context Company integration for OpenClaw",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "openclaw.plugin.json",
9
+ "src",
10
+ "README.md"
11
+ ],
12
+ "openclaw": {
13
+ "extension": "src/index.ts"
14
+ },
15
+ "main": "dist/index.cjs",
16
+ "module": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "dev": "pnpm build --watch"
28
+ },
29
+ "sideEffects": false,
30
+ "keywords": [
31
+ "openclaw",
32
+ "observability",
33
+ "tracing",
34
+ "monitoring",
35
+ "context-company",
36
+ "ai-agents"
37
+ ],
38
+ "homepage": "https://github.com/The-Context-Company/observatory/tree/main/packages/ts/openclaw#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/The-Context-Company/observatory/issues"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/The-Context-Company/observatory",
45
+ "directory": "packages/ts/openclaw"
46
+ },
47
+ "author": "The Context Company",
48
+ "license": "MIT",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^20",
57
+ "tsup": "^8.5.0",
58
+ "typescript": "^5.9.3"
59
+ }
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default, register, type OpenClawPluginConfig } from "./plugin.js";
package/src/plugin.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @contextcompany/openclaw — OpenClaw plugin for The Context Company
3
+ *
4
+ * Thin forwarder: collects raw hook events during an agent run,
5
+ * then sends them all as one batch on agent_end.
6
+ *
7
+ * All parsing/transformation happens server-side.
8
+ */
9
+
10
+ import type { ActiveSession, OpenClawPluginConfig } from "./types.js";
11
+ export type { OpenClawPluginConfig } from "./types.js";
12
+ import { safeClone, sendToTcc } from "./transport.js";
13
+
14
+ function registerHooks(
15
+ api: any,
16
+ configOverrides?: OpenClawPluginConfig,
17
+ ): void {
18
+ const activeSessions = new Map<string, ActiveSession>();
19
+
20
+ const pluginConfig: Record<string, unknown> = {
21
+ ...(api.pluginConfig ?? {}),
22
+ ...configOverrides,
23
+ };
24
+ const debug =
25
+ pluginConfig.debug === true || process.env.TCC_DEBUG === "true";
26
+
27
+ const log = {
28
+ info: (msg: string) => console.log(`[tcc] ${msg}`),
29
+ warn: (msg: string) => console.warn(`[tcc] ${msg}`),
30
+ };
31
+
32
+ const apiKey =
33
+ (typeof pluginConfig.apiKey === "string" ? pluginConfig.apiKey : null) ??
34
+ process.env.TCC_API_KEY;
35
+
36
+ if (!apiKey) {
37
+ log.warn("No TCC_API_KEY found. Set env var or plugin config. Disabled.");
38
+ return;
39
+ }
40
+
41
+ const url =
42
+ (typeof pluginConfig.endpoint === "string"
43
+ ? pluginConfig.endpoint
44
+ : null) ??
45
+ process.env.TCC_URL ??
46
+ (apiKey.startsWith("dev_")
47
+ ? "https://dev.thecontext.company/v1/openclaw"
48
+ : "https://api.thecontext.company/v1/openclaw");
49
+
50
+ log.info(`exporting runs to ${url}`);
51
+
52
+ // -------------------------------------------------------------------
53
+ // Stale session cleanup — flush sessions that never got an agent_end
54
+ // -------------------------------------------------------------------
55
+ const STALE_SESSION_MS = 30 * 60 * 1000; // 30 minutes
56
+
57
+ const cleanupInterval = setInterval(() => {
58
+ const now = Date.now();
59
+ for (const [key, session] of activeSessions) {
60
+ if (now - session.startedAt > STALE_SESSION_MS) {
61
+ if (debug) log.info(`flushing stale session: ${key}`);
62
+
63
+ sendToTcc(
64
+ { framework: "openclaw", events: session.events, stale: true },
65
+ apiKey,
66
+ url,
67
+ debug,
68
+ log,
69
+ ).catch((err) => {
70
+ log.warn(`failed to send stale session: ${err}`);
71
+ });
72
+
73
+ activeSessions.delete(key);
74
+ }
75
+ }
76
+ }, 5 * 60 * 1000); // check every 5 minutes
77
+
78
+ if (cleanupInterval.unref) cleanupInterval.unref();
79
+
80
+ // -------------------------------------------------------------------
81
+ // Helper: push a raw event into the session's event list
82
+ // -------------------------------------------------------------------
83
+ function pushEvent(hook: string, event: unknown, ctx: unknown): void {
84
+ const sessionKey = (ctx as any)?.sessionKey;
85
+ if (!sessionKey) return;
86
+
87
+ let session = activeSessions.get(sessionKey);
88
+ if (!session) {
89
+ session = { events: [], startedAt: Date.now() };
90
+ activeSessions.set(sessionKey, session);
91
+ }
92
+
93
+ session.events.push({
94
+ hook,
95
+ timestamp: new Date().toISOString(),
96
+ event: safeClone(event) as Record<string, unknown>,
97
+ context: safeClone(ctx) as Record<string, unknown>,
98
+ });
99
+ }
100
+
101
+ // -------------------------------------------------------------------
102
+ // Hooks: collect raw events
103
+ // -------------------------------------------------------------------
104
+
105
+ api.on("llm_input", (event: any, ctx: any) => {
106
+ pushEvent("llm_input", event, ctx);
107
+ if (debug) log.info(`llm_input (model: ${event.model})`);
108
+ });
109
+
110
+ api.on("llm_output", (event: any, ctx: any) => {
111
+ pushEvent("llm_output", event, ctx);
112
+ if (debug) log.info(`llm_output (model: ${event.model})`);
113
+ });
114
+
115
+ api.on("before_tool_call", (event: any, ctx: any) => {
116
+ pushEvent("before_tool_call", event, ctx);
117
+ if (debug) log.info(`before_tool_call (tool: ${event.toolName})`);
118
+ });
119
+
120
+ api.on("after_tool_call", (event: any, ctx: any) => {
121
+ pushEvent("after_tool_call", event, ctx);
122
+ if (debug) log.info(`after_tool_call (tool: ${event.toolName})`);
123
+ });
124
+
125
+ api.on("agent_end", (event: any, ctx: any) => {
126
+ const sessionKey = ctx?.sessionKey;
127
+ if (!sessionKey) return;
128
+
129
+ pushEvent("agent_end", event, ctx);
130
+
131
+ // Defer sending to a microtask so llm_output (which fires on the
132
+ // same synchronous tick as agent_end) gets collected first.
133
+ queueMicrotask(() => {
134
+ const session = activeSessions.get(sessionKey);
135
+ if (!session) return;
136
+
137
+ if (debug)
138
+ log.info(`agent_end — sending ${session.events.length} events`);
139
+
140
+ const payload = {
141
+ framework: "openclaw",
142
+ events: session.events,
143
+ };
144
+
145
+ sendToTcc(payload, apiKey, url, debug, log).catch((err) => {
146
+ log.warn(`failed to send events: ${err}`);
147
+ });
148
+
149
+ activeSessions.delete(sessionKey);
150
+ });
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Full OpenClaw plugin object — install via `openclaw plugins install`
156
+ * and configure in `openclaw.json` under `plugins.entries`.
157
+ *
158
+ * @example
159
+ * ```json
160
+ * {
161
+ * "plugins": {
162
+ * "allow": ["@contextcompany/openclaw"],
163
+ * "entries": {
164
+ * "@contextcompany/openclaw": {
165
+ * "enabled": true,
166
+ * "config": {
167
+ * "apiKey": "${TCC_API_KEY}"
168
+ * }
169
+ * }
170
+ * }
171
+ * }
172
+ * }
173
+ * ```
174
+ */
175
+ const plugin = {
176
+ id: "@contextcompany/openclaw",
177
+ name: "The Context Company",
178
+ description: "Agent observability — captures LLM calls, tool executions, and agent lifecycle events",
179
+ register(api: any) {
180
+ registerHooks(api);
181
+ },
182
+ };
183
+
184
+ export default plugin;
185
+
186
+ /**
187
+ * Named export for manual registration (e.g. from a custom extension).
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * import { register } from "@contextcompany/openclaw";
192
+ * export default async function (api) {
193
+ * register(api, { apiKey: "tcc_...", debug: true });
194
+ * }
195
+ * ```
196
+ */
197
+ export function register(
198
+ api: any,
199
+ configOverrides?: OpenClawPluginConfig,
200
+ ): void {
201
+ registerHooks(api, configOverrides);
202
+ }
@@ -0,0 +1,49 @@
1
+ const MAX_RETRIES = 2;
2
+ const INITIAL_BACKOFF_MS = 1000;
3
+
4
+ export async function sendToTcc(
5
+ payload: Record<string, unknown>,
6
+ apiKey: string,
7
+ url: string,
8
+ debug: boolean,
9
+ log: { info: (m: string) => void; warn: (m: string) => void },
10
+ ): Promise<void> {
11
+ const body = JSON.stringify(payload);
12
+ if (debug) log.info(`sending ${body.length} bytes to ${url}`);
13
+
14
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
15
+ if (attempt > 0)
16
+ await new Promise((r) => setTimeout(r, INITIAL_BACKOFF_MS * 2 ** (attempt - 1)));
17
+ try {
18
+ const res = await fetch(url, {
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ Authorization: `Bearer ${apiKey}`,
23
+ },
24
+ body,
25
+ });
26
+ if (res.ok) {
27
+ if (debug) log.info("sent ok");
28
+ return;
29
+ }
30
+ const text = await res.text();
31
+ if (res.status !== 429 && res.status < 500) {
32
+ log.warn(`ingestion failed (${res.status}): ${text}`);
33
+ return;
34
+ }
35
+ } catch (err) {
36
+ if (attempt === MAX_RETRIES)
37
+ log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts: ${err}`);
38
+ }
39
+ }
40
+ log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts (server errors)`);
41
+ }
42
+
43
+ export function safeClone(obj: unknown): unknown {
44
+ try {
45
+ return JSON.parse(JSON.stringify(obj));
46
+ } catch {
47
+ return String(obj);
48
+ }
49
+ }
package/src/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ /** A single raw hook event captured from OpenClaw's plugin API. */
2
+ export type RawEvent = {
3
+ hook: string;
4
+ timestamp: string;
5
+ event: Record<string, unknown>;
6
+ context: Record<string, unknown>;
7
+ };
8
+
9
+ /** In-flight session accumulating events before send. */
10
+ export type ActiveSession = {
11
+ events: RawEvent[];
12
+ startedAt: number;
13
+ };
14
+
15
+ /** Configuration for the TCC OpenClaw plugin. */
16
+ export type OpenClawPluginConfig = {
17
+ /** TCC API key. Falls back to TCC_API_KEY env var. */
18
+ apiKey?: string;
19
+ /** TCC ingestion endpoint. Falls back to TCC_URL env var, then auto-detected from key prefix. */
20
+ endpoint?: string;
21
+ /** Enable debug logging. Falls back to TCC_DEBUG env var. */
22
+ debug?: boolean;
23
+ };