@latitude-data/openclaw-telemetry 0.0.3 → 0.0.4

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 CHANGED
@@ -4,22 +4,23 @@ OpenClaw plugin that streams every agent run to [Latitude](https://latitude.so)
4
4
 
5
5
  ## Install
6
6
 
7
+ Requires OpenClaw **2026.4.25 or newer** on PATH. Older versions are detected up-front and the installer aborts with an upgrade message.
8
+
7
9
  ```bash
8
10
  npx -y @latitude-data/openclaw-telemetry install
11
+ openclaw gateway restart
9
12
  ```
10
13
 
11
14
  The installer prompts for your Latitude API key and project slug, then:
12
15
 
13
- 1. Materializes the plugin's runtime files into `~/.openclaw/extensions/latitude-telemetry/` (where OpenClaw's plugin discovery scans).
14
- 2. Writes the plugin entry to `~/.openclaw/openclaw.json` under `plugins.entries["@latitude-data/openclaw-telemetry"].config` credentials, base URL, and the `allowConversationAccess` flag all live here.
15
-
16
- Restart the OpenClaw gateway after install:
17
-
18
- ```bash
19
- openclaw gateway restart
20
- ```
16
+ 1. Verifies your OpenClaw version (aborts on `< 2026.4.25`).
17
+ 2. Hands plugin placement to OpenClaw via `openclaw plugins install <package-path> --force`. OpenClaw copies files into `~/.openclaw/extensions/<id>/`, writes the install record to `~/.openclaw/plugins/installs.json`, and creates the `plugins.entries[id]` block.
18
+ 3. Layers our config on top in `~/.openclaw/openclaw.json`:
19
+ - **`config.*`** — credentials, baseUrl, and `allowConversationAccess` (the payload-content gate the plugin's runtime reads).
20
+ - **`hooks.allowConversationAccess`** — the dispatch gate OpenClaw's runtime checks before forwarding LLM/tool/agent events to non-bundled plugins. Always mirrored to the same value as `config.allowConversationAccess`.
21
+ 4. Adds the plugin id to `plugins.allow` (running `npx install` is the trust signal). Pass `--no-trust` to opt out.
21
22
 
22
- That's it. Traces show up at `https://console.latitude.so/projects/<your-slug>`.
23
+ Traces show up at `https://console.latitude.so/projects/<your-slug>` once the gateway restarts.
23
24
 
24
25
  ### Install flags
25
26
 
@@ -30,9 +31,10 @@ That's it. Traces show up at `https://console.latitude.so/projects/<your-slug>`.
30
31
  | `--staging` | Target `https://staging.latitude.so` / `https://staging-ingest.latitude.so`. |
31
32
  | `--dev` | Target `http://localhost:3000` / `http://localhost:3002`. |
32
33
  | `--yes` / `--no-prompt` | Skip all prompts. Required for non-TTY / CI invocations. |
33
- | `--no-content` | Skip raw prompt/response/tool I/O capture. Spans still emit with timing, token usage, model name, and ids. |
34
+ | `--no-content` | Skip raw prompt/response/tool I/O capture. Spans still emit with timing, token usage, model name, and ids. Mirrored into both `config.allowConversationAccess` and `hooks.allowConversationAccess`. |
35
+ | `--no-trust` | Skip auto-adding the plugin id to `plugins.allow`. OpenClaw will keep printing `plugins.allow is empty` warnings until you add it manually. |
34
36
 
35
- Re-running `install` is idempotent — existing values are preserved.
37
+ Re-running `install` is idempotent — credentials/baseUrl are overwritten from prompts, but hand-edited `enabled`, `debug`, and `allowConversationAccess` values in `openclaw.json` are preserved unless you pass the corresponding flag.
36
38
 
37
39
  ## Uninstall
38
40
 
@@ -40,7 +42,7 @@ Re-running `install` is idempotent — existing values are preserved.
40
42
  npx -y @latitude-data/openclaw-telemetry uninstall
41
43
  ```
42
44
 
43
- Shows a plan, asks for confirmation, then removes the plugin entry from `~/.openclaw/openclaw.json` and the materialized files at `~/.openclaw/extensions/latitude-telemetry/`. A backup of the settings file is saved at `openclaw.json.latitude-bak`.
45
+ Shows a plan, asks for confirmation, then runs `openclaw plugins uninstall @latitude-data/openclaw-telemetry --force` (which removes files, the install record, the plugin entry, and the `plugins.allow` entry). Defensive cleanup follows for any leftover keys, with a backup at `openclaw.json.latitude-bak`.
44
46
 
45
47
  ## What gets sent
46
48
 
@@ -70,36 +72,59 @@ OpenClaw runs LLM hooks **fire-and-forget** (see [`src/plugins/hooks.ts`](https:
70
72
 
71
73
  ## Configuration reference
72
74
 
73
- The installer writes the plugin entry under `plugins.entries[id].config`. Every key is optional except `apiKey` and `project`. You can hand-edit `~/.openclaw/openclaw.json` to tweak:
75
+ The installer writes two blocks to `plugins.entries["@latitude-data/openclaw-telemetry"]`:
74
76
 
75
- ### `plugins.entries["@latitude-data/openclaw-telemetry"].config`
77
+ ### `.config` — read by the plugin's runtime
76
78
 
77
79
  | Key | Required | Default | Description |
78
80
  | --- | --- | --- | --- |
79
81
  | `apiKey` | yes | — | Bearer token for Latitude ingestion. |
80
82
  | `project` | yes | — | Slug of the project to route traces into. |
81
83
  | `baseUrl` | no | `https://ingest.latitude.so` | Override OTLP ingest origin. Installer sets this only when you pass `--staging` or `--dev`. |
82
- | `allowConversationAccess` | no | `false` | When `true`, attach raw prompts, assistant responses, system instructions, and tool I/O to spans. When `false`, emit only timing, token usage, model name, agent id, and structural ids — same span tree, scrubbed payloads. |
84
+ | `allowConversationAccess` | no | `false` | When `true`, attach raw prompts, assistant responses, system instructions, and tool I/O to spans. When `false`, emit only timing, token usage, model name, agent id, and structural ids — same span tree, scrubbed payloads. **Must match `hooks.allowConversationAccess` below — see "the two flags".** |
83
85
  | `enabled` | no | `true` | Set to `false` to pause emission without uninstalling. |
84
86
  | `debug` | no | `false` | Log diagnostic lines to stderr (visible in the gateway log). |
85
87
 
88
+ ### `.hooks` — read by OpenClaw's runtime
89
+
90
+ | Key | Required | Default | Description |
91
+ | --- | --- | --- | --- |
92
+ | `allowConversationAccess` | yes (on 2026.4.25+) | — | OpenClaw's hook dispatcher gates `llm_input` / `llm_output` / `before_tool_call` / `after_tool_call` / `agent_end` events on this. When `false` or absent, every typed hook is blocked and the plugin never sees an event — which means no traces, with the gateway log showing `[plugins] typed hook "..." blocked because non-bundled plugins must set plugins.entries.<id>.hooks.allowConversationAccess=true`. |
93
+
94
+ ### The two flags
95
+
96
+ `hooks.allowConversationAccess` and `config.allowConversationAccess` mean different things:
97
+
98
+ - **`hooks.*`** is the **dispatch gate**. `false` → OpenClaw never forwards events to us. No traces.
99
+ - **`config.*`** is the **payload-content gate**. `false` → we emit spans normally but scrub message content from them. Structural-only telemetry.
100
+
101
+ For *this* plugin, we always couple them — the installer writes both from the same source. If you hand-edit, set them to the same value:
102
+
103
+ - **Both `true`**: full content capture (the default).
104
+ - **Both `false`**: structural-only telemetry (set via `--no-content`).
105
+ - Anything else is incoherent: `hooks: false + config: true` means dispatch is off and content gating is moot; `hooks: true + config: false` works but is what you'd get with both `false` plus extra ceremony.
106
+
86
107
  ### Environment variable fallbacks
87
108
 
88
- If a key isn't set in `config`, the runtime falls back to environment variables on the gateway process. `LATITUDE_API_KEY`, `LATITUDE_PROJECT`, `LATITUDE_BASE_URL`, `LATITUDE_DEBUG`, and `LATITUDE_OPENCLAW_ENABLED` are all read this way. The installer doesn't set them — pluginConfig is the canonical surface — but they're useful for kicking debug on/off without editing `openclaw.json`.
109
+ If a `config.*` key isn't set, the runtime falls back to env vars on the gateway process: `LATITUDE_API_KEY`, `LATITUDE_PROJECT`, `LATITUDE_BASE_URL`, `LATITUDE_DEBUG`, `LATITUDE_OPENCLAW_ENABLED`. The installer doesn't set them — pluginConfig is the canonical surface — but they're useful for flipping `debug` without editing `openclaw.json`.
89
110
 
90
111
  ### Manual installation
91
112
 
92
- If the installer doesn't fit your setup, you need two things:
113
+ If you can't run the installer, do exactly what it does:
93
114
 
94
- 1. **The plugin files** under a directory OpenClaw discovers (`~/.openclaw/extensions/<name>/` or any path listed in `plugins.load.paths`). The directory must contain at minimum `openclaw.plugin.json` and the compiled `dist/plugin.js`. Easiest: copy them out of the installed `node_modules/@latitude-data/openclaw-telemetry/`.
95
- 2. **The plugin entry** in `~/.openclaw/openclaw.json`:
115
+ 1. **Hand placement to OpenClaw** with `openclaw plugins install <path-to-extracted-package> --force`. This is what populates `~/.openclaw/extensions/`, writes the install record, and creates the (initially empty) `plugins.entries[id]` block. Don't hand-place files into the extensions directory the persisted plugin index won't see them.
116
+ 2. **Add the config + hooks block** to `~/.openclaw/openclaw.json`:
96
117
 
97
118
  ```jsonc
98
119
  {
99
120
  "plugins": {
121
+ "allow": ["@latitude-data/openclaw-telemetry"],
100
122
  "entries": {
101
123
  "@latitude-data/openclaw-telemetry": {
102
124
  "enabled": true,
125
+ "hooks": {
126
+ "allowConversationAccess": true
127
+ },
103
128
  "config": {
104
129
  "apiKey": "lat_xxx",
105
130
  "project": "my-openclaw-project",
@@ -111,21 +136,24 @@ If the installer doesn't fit your setup, you need two things:
111
136
  }
112
137
  ```
113
138
 
114
- Don't put `LATITUDE_*` keys at top-level `env`OpenClaw's strict zod schema rejects them. Don't put `allowConversationAccess` under `hooks` either — that field is OpenClaw's strict reserved namespace and only accepts `allowPromptInjection` (older versions) or `allowPromptInjection` + `allowConversationAccess` (2026.4.22+). Our config bucket is `plugins.entries[id].config`, which is `record(string, unknown)` and accepted across all versions.
115
-
116
- After editing, run `openclaw config validate` — it should print `valid: true`. Then `openclaw gateway restart`.
139
+ Run `openclaw config validate` — should print `valid: true`. Then `openclaw gateway restart`.
117
140
 
118
141
  ## Privacy
119
142
 
120
- By default we emit **structural telemetry only** span tree, timings, token usage, model name, agent name, run/session ids — but **no prompt or response content**. You opt in to content capture by setting `allowConversationAccess: true` (the default the interactive installer writes).
143
+ There are two defaults at playkeep them straight:
144
+
145
+ - **Installer default**: when you run `npx -y @latitude-data/openclaw-telemetry install` without `--no-content`, the installer writes `allowConversationAccess: true` to both the `hooks` and `config` blocks. **You get full content capture** — prompts, assistant responses, system instructions, tool I/O — shipped to Latitude alongside structural telemetry.
146
+ - **Runtime default for absent keys**: if you hand-write `openclaw.json` and leave `allowConversationAccess` out entirely, `config.allowConversationAccess` falls back to `false` at the plugin's runtime (privacy-preserving) and `hooks.allowConversationAccess` falls back to OpenClaw's default (also `false`, which means dispatch is blocked and you get nothing). **Manual installs without those keys produce no traces, not "structural-only traces".**
147
+
148
+ The installer always writes both keys to the same value, so the installer-driven path is unambiguous. The runtime-default trap only matters for hand-rolled configs.
121
149
 
122
- When `allowConversationAccess` is on, every LLM call's full input messages, assistant output, system instructions, and tool I/O are attached to spans. Pass `--no-content` to the installer (or set the flag to `false` in `openclaw.json`) if you want telemetry without payloads.
150
+ When you pass `--no-content` (or hand-edit both flags to `false`), we emit the same span tree but scrub the content attributes (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`, `gen_ai.tool.call.arguments`/`result`, the interaction's `user_prompt`). Timings, token usage, model name, agent name, ids, and the `latitude.captured.content: false` boolean still flow.
123
151
 
124
- To pause emission entirely without uninstalling, set `LATITUDE_OPENCLAW_ENABLED=0` in the gateway environment.
152
+ To pause emission entirely without uninstalling, set `LATITUDE_OPENCLAW_ENABLED=0` in the gateway environment, or set `enabled: false` on the plugin entry in `openclaw.json`.
125
153
 
126
154
  ## Supported OpenClaw versions
127
155
 
128
- Requires OpenClaw **2026.3.0+** for the `llm_input` / `llm_output` hooks. Older versions lack these hooks and would only surface metadata via `model.usage` diagnostics not enough for full trace fidelity.
156
+ Requires OpenClaw **2026.4.25 or newer**. The installer detects the version up-front via `openclaw --version` and aborts with an upgrade message on older versions supporting the full range with portability shims would mean shipping known-broken behaviour (≤ 2026.4.21 reject `hooks.allowConversationAccess` outright; 2026.4.22 2026.4.24 have unverified dispatch gating). Run `npm install -g openclaw@latest` to upgrade.
129
157
 
130
158
  ## How it fails
131
159
 
package/dist/cli.js CHANGED
@@ -1,15 +1,141 @@
1
1
  #!/usr/bin/env node
2
- import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { cancel, confirm, intro, isCancel, log, note, outro, password, spinner, text } from "@clack/prompts";
6
6
  import pc from "picocolors";
7
+ import { spawnSync } from "node:child_process";
7
8
  import { homedir } from "node:os";
9
+ //#region src/openclaw-cli.ts
10
+ /**
11
+ * Lowest OpenClaw version we support. The reporter verified hook-dispatch
12
+ * gating works correctly here; older versions either reject
13
+ * `hooks.allowConversationAccess` outright (≤ 2026.4.21) or have unverified
14
+ * gating behaviour (2026.4.22 – 2026.4.24). Refusing to install on older
15
+ * versions is intentional — we'd rather fail loudly than ship a
16
+ * config the gateway will quarantine or hooks the dispatcher will block.
17
+ */
18
+ const MIN_OPENCLAW_VERSION = "2026.4.25";
19
+ const DEFAULT_TIMEOUT_MS = 1e4;
20
+ /**
21
+ * Spawn `openclaw <args>` synchronously. Reports failure modes structurally so
22
+ * callers can decide how to degrade (missing binary vs. timed out vs. exited
23
+ * non-zero). Never throws; ENOENT becomes `{ reason: "enoent" }`.
24
+ */
25
+ function runOpenclaw(args, opts = {}) {
26
+ const result = spawnSync("openclaw", args, {
27
+ encoding: "utf-8",
28
+ timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
29
+ input: opts.stdin,
30
+ stdio: [
31
+ "pipe",
32
+ "pipe",
33
+ "pipe"
34
+ ]
35
+ });
36
+ const err = result.error;
37
+ if (err?.code === "ENOENT") return {
38
+ ok: false,
39
+ reason: "enoent",
40
+ stdout: "",
41
+ stderr: "",
42
+ code: null
43
+ };
44
+ if (err?.code === "ETIMEDOUT" || result.signal === "SIGTERM" || result.signal === "SIGKILL") return {
45
+ ok: false,
46
+ reason: "timeout",
47
+ stdout: result.stdout ?? "",
48
+ stderr: result.stderr ?? "",
49
+ code: null
50
+ };
51
+ if (err) return {
52
+ ok: false,
53
+ reason: "exit",
54
+ stdout: result.stdout ?? "",
55
+ stderr: result.stderr ?? String(err),
56
+ code: typeof result.status === "number" ? result.status : 1
57
+ };
58
+ if (result.status === 0) return {
59
+ ok: true,
60
+ stdout: result.stdout ?? "",
61
+ stderr: result.stderr ?? "",
62
+ code: 0
63
+ };
64
+ return {
65
+ ok: false,
66
+ reason: "exit",
67
+ stdout: result.stdout ?? "",
68
+ stderr: result.stderr ?? "",
69
+ code: typeof result.status === "number" ? result.status : 1
70
+ };
71
+ }
72
+ /**
73
+ * Run `openclaw --version` and parse out the version string.
74
+ *
75
+ * Banner format (per OpenClaw `src/cli/banner.ts` `formatCliBannerLine`):
76
+ * `🦞 OpenClaw <version> (<commit-sha>)`
77
+ *
78
+ * The banner is normally suppressed when `--version` is the flag, but the
79
+ * version itself still goes to stdout. We accept either layout (with or
80
+ * without the lobster + commit sha) so we don't break if OpenClaw later
81
+ * prints just the bare version string.
82
+ */
83
+ function getOpenclawVersion() {
84
+ const result = runOpenclaw(["--version"], { timeoutMs: 5e3 });
85
+ if (!result.ok) {
86
+ if (result.reason === "enoent") return {
87
+ ok: false,
88
+ error: "missing"
89
+ };
90
+ return {
91
+ ok: false,
92
+ error: "unparseable",
93
+ raw: result.stdout || result.stderr
94
+ };
95
+ }
96
+ const raw = result.stdout.trim();
97
+ const match = raw.match(/(\d{4}\.\d+\.\d+)/);
98
+ if (!match) return {
99
+ ok: false,
100
+ error: "unparseable",
101
+ raw
102
+ };
103
+ return {
104
+ ok: true,
105
+ version: match[1],
106
+ raw
107
+ };
108
+ }
109
+ /**
110
+ * Compare two CalVer strings (`YYYY.M.PATCH`). Returns -1 if `a` < `b`,
111
+ * 0 if equal, 1 if `a` > `b`. Strings with extra components or non-numeric
112
+ * pieces fall back to a per-component string comparison so unexpected
113
+ * formats don't crash the installer.
114
+ */
115
+ function compareCalver(a, b) {
116
+ const ap = a.split(".");
117
+ const bp = b.split(".");
118
+ const len = Math.max(ap.length, bp.length);
119
+ for (let i = 0; i < len; i++) {
120
+ const ai = ap[i] ?? "0";
121
+ const bi = bp[i] ?? "0";
122
+ const an = Number(ai);
123
+ const bn = Number(bi);
124
+ if (Number.isFinite(an) && Number.isFinite(bn)) {
125
+ if (an < bn) return -1;
126
+ if (an > bn) return 1;
127
+ continue;
128
+ }
129
+ if (ai < bi) return -1;
130
+ if (ai > bi) return 1;
131
+ }
132
+ return 0;
133
+ }
134
+ //#endregion
8
135
  //#region src/settings-file.ts
9
136
  const CONFIG_DIR = join(homedir(), ".openclaw");
10
137
  const SETTINGS_PATH = join(CONFIG_DIR, "openclaw.json");
11
138
  const SETTINGS_BACKUP_PATH = join(CONFIG_DIR, "openclaw.json.latitude-bak");
12
- const PLUGIN_INSTALL_DIR = join(join(CONFIG_DIR, "extensions"), "latitude-telemetry");
13
139
  /** Plugin id used both as the npm package name and as the OpenClaw plugin id. */
14
140
  const PLUGIN_ID = "@latitude-data/openclaw-telemetry";
15
141
  function readSettings() {
@@ -29,22 +155,38 @@ function backupSettings() {
29
155
  if (existsSync(SETTINGS_PATH)) copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH);
30
156
  }
31
157
  /**
32
- * Set the `plugins.entries[id]` block for our plugin. Writes credentials and
33
- * options into the `.config` bucket — never under `hooks` (strict zod) and
34
- * never as top-level `env` keys (root schema rejects them).
158
+ * Set the `plugins.entries[id]` block for our plugin.
159
+ *
160
+ * Two places in the entry get written:
161
+ *
162
+ * - `.config` (free-form `record(string, unknown)`): credentials, baseUrl,
163
+ * and our copy of `allowConversationAccess`. This is what the plugin
164
+ * runtime reads via `api.pluginConfig`.
165
+ * - `.hooks.allowConversationAccess`: controls whether OpenClaw's hook
166
+ * dispatcher actually forwards `llm_input` / `llm_output` / tool /
167
+ * `agent_end` events to our handlers. Without it set to `true` on
168
+ * OpenClaw 2026.4.25+, every typed hook is blocked at the dispatcher
169
+ * and the plugin's handlers never fire.
170
+ *
171
+ * The two flags mean different things — `hooks.*` is the dispatch gate,
172
+ * `config.*` is the payload-content gate — but for THIS plugin we always
173
+ * couple them: dispatch off + payload on is useless (no payloads to gate),
174
+ * and dispatch on + payload off is a legitimate "structural-only telemetry"
175
+ * mode (timing, tokens, ids, agent name; no message bodies). Always writing
176
+ * both from the same source keeps the operator's mental model simple.
35
177
  *
36
178
  * Re-install idempotency: only `apiKey` / `project` / `baseUrl` always
37
- * overwrite (these come from the install prompts). `enabled`, `debug`, and
38
- * `allowConversationAccess` are preserved when not provided in the patch,
39
- * so a user who hand-edited `enabled: false` or `debug: true` doesn't lose
40
- * their choice on a re-install.
179
+ * overwrite (these come from install prompts). `enabled`, `debug`, and
180
+ * `allowConversationAccess` are preserved when not provided in the patch.
41
181
  */
42
182
  function setPluginEntry(settings, patch) {
43
183
  const plugins = settings.plugins ?? {};
44
184
  const entries = plugins.entries ?? {};
45
185
  const existing = entries["@latitude-data/openclaw-telemetry"] ?? {};
186
+ const existingConfig = existing.config ?? {};
187
+ const existingHooks = existing.hooks ?? {};
46
188
  const nextConfig = {
47
- ...existing.config ?? {},
189
+ ...existingConfig,
48
190
  apiKey: patch.apiKey,
49
191
  project: patch.project
50
192
  };
@@ -52,16 +194,22 @@ function setPluginEntry(settings, patch) {
52
194
  else delete nextConfig.baseUrl;
53
195
  if (patch.allowConversationAccess !== void 0) nextConfig.allowConversationAccess = patch.allowConversationAccess;
54
196
  if (patch.debug !== void 0) nextConfig.debug = patch.debug;
197
+ const effectiveAccess = typeof nextConfig.allowConversationAccess === "boolean" ? nextConfig.allowConversationAccess : typeof existingHooks.allowConversationAccess === "boolean" ? existingHooks.allowConversationAccess : true;
198
+ const nextHooks = {
199
+ ...existingHooks,
200
+ allowConversationAccess: effectiveAccess
201
+ };
55
202
  const nextEnabled = patch.enabled ?? existing.enabled ?? true;
56
203
  entries[PLUGIN_ID] = {
57
204
  ...existing,
58
205
  enabled: nextEnabled,
206
+ hooks: nextHooks,
59
207
  config: nextConfig
60
208
  };
61
209
  plugins.entries = entries;
62
210
  settings.plugins = plugins;
63
211
  }
64
- /** Remove the plugin entry entirely. */
212
+ /** Remove the plugin entry entirely. Used by uninstall as defense-in-depth. */
65
213
  function removePluginEntry(settings) {
66
214
  const plugins = settings.plugins;
67
215
  if (!plugins?.entries) return false;
@@ -69,26 +217,58 @@ function removePluginEntry(settings) {
69
217
  delete plugins.entries[PLUGIN_ID];
70
218
  return true;
71
219
  }
220
+ /**
221
+ * Add the plugin id to `plugins.allow`. Idempotent — returns `true` only when
222
+ * the array changed. OpenClaw warns at every gateway start when a non-bundled
223
+ * plugin auto-loads without provenance via `plugins.allow` or an install
224
+ * record. We get one warning cleared by going through `openclaw plugins
225
+ * install` (provenance) and the other by adding ourselves to allow.
226
+ *
227
+ * Defensive against hand-edited non-array values: if `plugins.allow` is
228
+ * present but not an array (e.g. someone wrote a string), we replace it
229
+ * with a single-element array rather than spreading the bad value.
230
+ */
231
+ function addToPluginsAllow(settings) {
232
+ const plugins = settings.plugins ?? {};
233
+ const existing = plugins.allow;
234
+ const allow = Array.isArray(existing) ? existing : [];
235
+ if (allow.includes("@latitude-data/openclaw-telemetry")) return false;
236
+ plugins.allow = [...allow, PLUGIN_ID];
237
+ settings.plugins = plugins;
238
+ return true;
239
+ }
240
+ /**
241
+ * Inverse of `addToPluginsAllow`. Defense-in-depth — `openclaw plugins
242
+ * uninstall` already strips the entry, but the install path can be skipped
243
+ * (e.g. the user removed the plugin manually) and we want re-install/uninstall
244
+ * round-trips to be tidy regardless.
245
+ */
246
+ function removeFromPluginsAllow(settings) {
247
+ const allow = settings.plugins?.allow;
248
+ if (!Array.isArray(allow) || !allow.includes("@latitude-data/openclaw-telemetry")) return false;
249
+ if (settings.plugins) settings.plugins.allow = allow.filter((id) => id !== PLUGIN_ID);
250
+ return true;
251
+ }
72
252
  function hasLatitudePlugin(settings) {
73
253
  return Boolean(settings.plugins?.entries && "@latitude-data/openclaw-telemetry" in settings.plugins.entries);
74
254
  }
75
255
  /**
76
- * Strip any leftover top-level keys our older installer (<= 0.0.1) wrote. The
77
- * 0.0.1 installer wrote `hooks.allowConversationAccess` (rejected by strict
78
- * zod) and `LATITUDE_*` keys at root-level `env` (also rejected). Both are
79
- * cleaned up here so re-running install lands a config OpenClaw will validate.
256
+ * Strip leftover keys from older installers that the strict zod schema
257
+ * rejects on current OpenClaw versions.
258
+ *
259
+ * 0.0.1 wrote `LATITUDE_*` keys directly under `settings.env`. OpenClaw's
260
+ * root schema is strict; the `env` block accepts only `{shellEnv, vars}`,
261
+ * so those keys cause the gateway to quarantine the config as
262
+ * `clobbered.<ts>` and roll back. We sweep them on every install.
263
+ *
264
+ * Note: 0.0.1 also wrote `hooks.allowConversationAccess` (when that key was
265
+ * not yet in the schema). We deliberately do NOT strip it anymore — on
266
+ * OpenClaw 2026.4.25+ the key IS in the schema and IS load-bearing for
267
+ * dispatch. `setPluginEntry` overwrites it on every install with the right
268
+ * value, so any 0.0.1 leftover is reconciled there.
80
269
  */
81
270
  function migrateLegacyEntries(settings) {
82
271
  let changed = false;
83
- const entry = settings.plugins?.entries?.[PLUGIN_ID];
84
- if (entry && typeof entry === "object") {
85
- const hooks = entry.hooks;
86
- if (hooks && typeof hooks === "object" && "allowConversationAccess" in hooks) {
87
- delete hooks.allowConversationAccess;
88
- if (Object.keys(hooks).length === 0) delete entry.hooks;
89
- changed = true;
90
- }
91
- }
92
272
  const env = settings.env;
93
273
  if (env && typeof env === "object" && !Array.isArray(env)) {
94
274
  const envObj = env;
@@ -105,63 +285,6 @@ function migrateLegacyEntries(settings) {
105
285
  return { changed };
106
286
  }
107
287
  //#endregion
108
- //#region src/install-files.ts
109
- /**
110
- * Copies the plugin's runtime files into `~/.openclaw/extensions/latitude-telemetry/`
111
- * so OpenClaw's discovery (`<configDir>/extensions/<plugin>`) picks it up.
112
- *
113
- * `npx -y @latitude-data/openclaw-telemetry` runs the CLI from a temporary npm
114
- * location — relying on that path persisting across runs (or even across the
115
- * gateway restart that follows install) is unsafe. So we materialize a stable
116
- * copy under the user's OpenClaw config dir, the same path OpenClaw scans on
117
- * startup. Layout we produce:
118
- *
119
- * ~/.openclaw/extensions/latitude-telemetry/
120
- * openclaw.plugin.json <- the manifest, required by discovery
121
- * package.json <- minimal — keeps node module-resolution happy
122
- * dist/ <- compiled plugin entrypoint(s)
123
- */
124
- function installPluginFiles() {
125
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
126
- const manifestSrc = join(packageRoot, "openclaw.plugin.json");
127
- const distSrc = join(packageRoot, "dist");
128
- const pkgSrc = join(packageRoot, "package.json");
129
- if (!existsSync(manifestSrc)) throw new Error(`Cannot install: missing openclaw.plugin.json at ${manifestSrc}. This is a packaging bug — please file an issue.`);
130
- if (!existsSync(distSrc)) throw new Error(`Cannot install: missing compiled dist at ${distSrc}.`);
131
- if (!existsSync(pkgSrc)) throw new Error(`Cannot install: missing package.json at ${pkgSrc}. This is a packaging bug — please file an issue.`);
132
- if (existsSync(PLUGIN_INSTALL_DIR)) rmSync(PLUGIN_INSTALL_DIR, {
133
- recursive: true,
134
- force: true
135
- });
136
- mkdirSync(PLUGIN_INSTALL_DIR, { recursive: true });
137
- copyFileSync(manifestSrc, join(PLUGIN_INSTALL_DIR, "openclaw.plugin.json"));
138
- cpSync(distSrc, join(PLUGIN_INSTALL_DIR, "dist"), { recursive: true });
139
- const sourcePkg = JSON.parse(readFileSync(pkgSrc, "utf-8"));
140
- const extensions = sourcePkg.openclaw?.extensions ?? ["./dist/plugin.js"];
141
- const minimalPkg = {
142
- name: sourcePkg.name,
143
- version: sourcePkg.version,
144
- type: sourcePkg.type ?? "module",
145
- main: sourcePkg.main ?? "./dist/plugin.js",
146
- private: true,
147
- openclaw: { extensions }
148
- };
149
- writeFileSync(join(PLUGIN_INSTALL_DIR, "package.json"), `${JSON.stringify(minimalPkg, null, 2)}\n`, "utf-8");
150
- return {
151
- destination: PLUGIN_INSTALL_DIR,
152
- entryPath: resolve(PLUGIN_INSTALL_DIR, extensions[0] ?? "./dist/plugin.js")
153
- };
154
- }
155
- /** Remove the materialized plugin directory under `~/.openclaw/extensions/`. */
156
- function removePluginFiles() {
157
- if (!existsSync(PLUGIN_INSTALL_DIR)) return false;
158
- rmSync(PLUGIN_INSTALL_DIR, {
159
- recursive: true,
160
- force: true
161
- });
162
- return true;
163
- }
164
- //#endregion
165
288
  //#region src/setup.ts
166
289
  const DOCS_URL = "https://docs.latitude.so/openclaw-telemetry";
167
290
  const PRODUCTION_ENV = {
@@ -218,6 +341,7 @@ function normalizeInstallFlags(flags) {
218
341
  project: typeof flags.project === "string" ? flags.project : void 0,
219
342
  environment,
220
343
  allowConversationAccess,
344
+ noTrust: flags["no-trust"] === true,
221
345
  noPrompt: flags["no-prompt"] === true || flags.yes === true,
222
346
  yes: flags.yes === true
223
347
  };
@@ -228,6 +352,7 @@ async function runInstall(flags = {}) {
228
352
  }
229
353
  async function runInteractiveInstall(flags) {
230
354
  intro(pc.bgCyan(pc.black(" Latitude · OpenClaw telemetry ")));
355
+ ensureOpenclawIsCompatible();
231
356
  const existingConfig = readSettings().plugins?.entries?.["@latitude-data/openclaw-telemetry"]?.config ?? void 0;
232
357
  const envConfig = flags.environment ?? PRODUCTION_ENV;
233
358
  const urls = urlsFor(envConfig);
@@ -248,7 +373,8 @@ async function runInteractiveInstall(flags) {
248
373
  apiKey,
249
374
  project,
250
375
  envConfig,
251
- allowConversationAccess: flags.allowConversationAccess
376
+ allowConversationAccess: flags.allowConversationAccess,
377
+ noTrust: flags.noTrust === true
252
378
  });
253
379
  note([
254
380
  "Restart the OpenClaw gateway for the plugin to load:",
@@ -259,6 +385,7 @@ async function runInteractiveInstall(flags) {
259
385
  outro(pc.green("✓ Installed"));
260
386
  }
261
387
  async function runFlagDrivenInstall(flags) {
388
+ ensureOpenclawIsCompatible();
262
389
  const apiKey = flags.apiKey;
263
390
  const project = flags.project;
264
391
  if (!apiKey || !project) throw new Error("Non-interactive install requires --api-key=... and --project=... (or run in a TTY).");
@@ -266,10 +393,10 @@ async function runFlagDrivenInstall(flags) {
266
393
  apiKey,
267
394
  project,
268
395
  envConfig: flags.environment ?? PRODUCTION_ENV,
269
- allowConversationAccess: flags.allowConversationAccess
396
+ allowConversationAccess: flags.allowConversationAccess,
397
+ noTrust: flags.noTrust === true
270
398
  });
271
399
  process.stdout.write(`Installed Latitude plugin in ${SETTINGS_PATH}\n`);
272
- process.stdout.write(`Plugin files at ${PLUGIN_INSTALL_DIR}\n`);
273
400
  }
274
401
  async function promptApiKey(_existing, flag) {
275
402
  if (flag) return flag;
@@ -296,48 +423,92 @@ function onCancel() {
296
423
  cancel("Cancelled — nothing was changed");
297
424
  process.exit(1);
298
425
  }
299
- async function applyChanges({ apiKey, project, envConfig, allowConversationAccess }) {
300
- const filesSpinner = spinner();
301
- filesSpinner.start("Installing plugin files");
302
- const { destination } = installPluginFiles();
303
- filesSpinner.stop(`Plugin files installed at ${destination}`);
304
- const settingsSpinner = spinner();
305
- settingsSpinner.start("Updating openclaw.json");
426
+ async function applyChanges({ apiKey, project, envConfig, allowConversationAccess, noTrust }) {
306
427
  ensureSettingsDir();
307
428
  backupSettings();
429
+ const packageRoot = resolvePackageRoot();
430
+ const installSpinner = spinner();
431
+ installSpinner.start(`Installing plugin via openclaw plugins install ${packageRoot}`);
432
+ const installResult = runOpenclaw([
433
+ "plugins",
434
+ "install",
435
+ packageRoot,
436
+ "--force"
437
+ ], { timeoutMs: 6e4 });
438
+ if (!installResult.ok) {
439
+ installSpinner.stop("openclaw plugins install failed");
440
+ if (installResult.reason === "enoent") throw new Error("`openclaw` not found on PATH. Install OpenClaw first (https://openclaw.ai/install) and re-run.");
441
+ if (installResult.reason === "timeout") throw new Error("openclaw plugins install timed out after 60s. Try running it manually to see what's stuck.");
442
+ const detail = installResult.stderr.trim() || installResult.stdout.trim() || `exit code ${installResult.code}`;
443
+ throw new Error(`openclaw plugins install failed: ${detail}`);
444
+ }
445
+ installSpinner.stop("Plugin registered with OpenClaw");
446
+ const settingsSpinner = spinner();
447
+ settingsSpinner.start("Updating openclaw.json");
308
448
  const settings = readSettings();
309
449
  migrateLegacyEntries(settings);
310
- const existingConfig = settings.plugins?.entries?.["@latitude-data/openclaw-telemetry"]?.config ?? {};
311
- const finalAllowConversationAccess = allowConversationAccess ?? existingConfig.allowConversationAccess ?? true;
312
450
  setPluginEntry(settings, {
313
451
  apiKey,
314
452
  project,
315
453
  baseUrl: envConfig.name === "production" ? void 0 : envConfig.ingest,
316
- allowConversationAccess: finalAllowConversationAccess
454
+ allowConversationAccess
317
455
  });
456
+ if (!noTrust) addToPluginsAllow(settings);
318
457
  writeSettings(settings);
319
458
  settingsSpinner.stop(`Updated ${SETTINGS_PATH}`);
320
459
  if (existsSync(SETTINGS_BACKUP_PATH)) log.info(`Backup saved at ${pc.dim(SETTINGS_BACKUP_PATH)}`);
460
+ if (noTrust) log.warning(`--no-trust set; OpenClaw will warn at every gateway start that ${PLUGIN_ID} is untrusted. Add it to plugins.allow yourself when you're ready.`);
321
461
  }
322
462
  function ensureSettingsDir() {
323
463
  const dir = dirname(SETTINGS_PATH);
324
464
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
325
465
  }
466
+ /**
467
+ * Verify `openclaw` is on PATH AND its version is >= MIN_OPENCLAW_VERSION.
468
+ * Aborts with a clear upgrade message otherwise. Called before any user
469
+ * prompts so we don't waste their time collecting credentials we can't
470
+ * use.
471
+ */
472
+ function ensureOpenclawIsCompatible() {
473
+ const v = getOpenclawVersion();
474
+ if (!v.ok) {
475
+ if (v.error === "missing") {
476
+ cancel("OpenClaw CLI not found on PATH. Install or update via `npm install -g openclaw@latest` and re-run.");
477
+ process.exit(1);
478
+ }
479
+ cancel(`Couldn't parse OpenClaw version output${v.raw ? ` (got: ${pc.dim(v.raw)})` : ""}. Run \`openclaw --version\` and report the output.`);
480
+ process.exit(1);
481
+ }
482
+ if (compareCalver(v.version, "2026.4.25") < 0) {
483
+ cancel(`OpenClaw ${v.version} is older than the minimum supported version (${MIN_OPENCLAW_VERSION}). Run \`npm install -g openclaw@latest\` and re-run install.`);
484
+ process.exit(1);
485
+ }
486
+ log.info(`OpenClaw ${pc.dim(v.version)} (>= ${MIN_OPENCLAW_VERSION})`);
487
+ }
488
+ /**
489
+ * Resolve the absolute path to our package's root (the directory that
490
+ * contains `package.json` + `openclaw.plugin.json` + `dist/`). The compiled
491
+ * CLI lives at `<package-root>/dist/cli.js`, so `import.meta.url`'s parent
492
+ * directory's parent is our root.
493
+ *
494
+ * `openclaw plugins install <path>` copies files synchronously, so we
495
+ * don't have to keep this directory alive past the spawn return.
496
+ */
497
+ function resolvePackageRoot() {
498
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..");
499
+ }
326
500
  async function runUninstall(flags = {}) {
327
501
  intro(pc.bgYellow(pc.black(" Latitude · OpenClaw telemetry — uninstall ")));
328
- const settings = readSettings();
329
- const hasEntry = hasLatitudePlugin(settings);
330
- const hasFiles = existsSync(PLUGIN_INSTALL_DIR);
331
- if (!hasEntry && !hasFiles) {
332
- note("No Latitude plugin entry or files found — nothing to remove.", "Status");
502
+ if (!hasLatitudePlugin(readSettings())) {
503
+ note("No Latitude plugin entry found — nothing to remove.", "Status");
333
504
  outro(pc.dim("Nothing changed"));
334
505
  return;
335
506
  }
336
507
  note([
337
- hasEntry ? `Remove "${PLUGIN_ID}" plugin entry from ${SETTINGS_PATH}` : null,
338
- hasFiles ? `Delete plugin files at ${PLUGIN_INSTALL_DIR}` : null,
508
+ `Run \`openclaw plugins uninstall ${PLUGIN_ID} --force\` (removes files, install record, and plugin entry)`,
509
+ `Sweep any leftover LATITUDE_* keys from settings.env`,
339
510
  `Backup of openclaw.json saved at ${SETTINGS_BACKUP_PATH}`
340
- ].filter(Boolean).join("\n"), "Plan");
511
+ ].join("\n"), "Plan");
341
512
  if (!flags.noPrompt && process.stdin.isTTY === true) {
342
513
  const ok = await confirm({
343
514
  message: "Proceed?",
@@ -345,16 +516,31 @@ async function runUninstall(flags = {}) {
345
516
  });
346
517
  if (isCancel(ok) || ok !== true) return onCancel();
347
518
  }
519
+ backupSettings();
348
520
  const s = spinner();
349
- s.start("Reverting settings");
350
- if (hasEntry) {
351
- backupSettings();
352
- removePluginEntry(settings);
353
- migrateLegacyEntries(settings);
354
- writeSettings(settings);
355
- }
356
- if (hasFiles) removePluginFiles();
357
- s.stop("Done");
521
+ s.start("Reverting via openclaw plugins uninstall");
522
+ const uninstallResult = runOpenclaw([
523
+ "plugins",
524
+ "uninstall",
525
+ PLUGIN_ID,
526
+ "--force"
527
+ ], { timeoutMs: 6e4 });
528
+ if (!uninstallResult.ok) {
529
+ s.stop("openclaw plugins uninstall failed");
530
+ if (uninstallResult.reason === "enoent") log.warning("`openclaw` not found on PATH. Falling back to local cleanup — files at ~/.openclaw/extensions/ may remain.");
531
+ else {
532
+ const detail = uninstallResult.stderr.trim() || uninstallResult.stdout.trim() || `exit code ${uninstallResult.code}`;
533
+ log.warning(`openclaw plugins uninstall reported: ${detail}. Continuing with local cleanup.`);
534
+ }
535
+ } else s.stop("Plugin removed by OpenClaw");
536
+ const cleanupSpinner = spinner();
537
+ cleanupSpinner.start("Reverting openclaw.json");
538
+ const post = readSettings();
539
+ removePluginEntry(post);
540
+ removeFromPluginsAllow(post);
541
+ migrateLegacyEntries(post);
542
+ writeSettings(post);
543
+ cleanupSpinner.stop("Done");
358
544
  outro(pc.green("✓ Uninstalled"));
359
545
  }
360
546
  //#endregion
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/settings-file.ts","../src/install-files.ts","../src/setup.ts","../src/cli.ts"],"sourcesContent":["import { copyFileSync, existsSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\nconst CONFIG_DIR = join(homedir(), \".openclaw\")\nexport const SETTINGS_PATH = join(CONFIG_DIR, \"openclaw.json\")\nexport const SETTINGS_BACKUP_PATH = join(CONFIG_DIR, \"openclaw.json.latitude-bak\")\nconst EXTENSIONS_DIR = join(CONFIG_DIR, \"extensions\")\nexport const PLUGIN_INSTALL_DIR = join(EXTENSIONS_DIR, \"latitude-telemetry\")\n\n/** Plugin id used both as the npm package name and as the OpenClaw plugin id. */\nexport const PLUGIN_ID = \"@latitude-data/openclaw-telemetry\"\n\n/**\n * The shape OpenClaw's strict zod schema accepts for a single\n * `plugins.entries[id]` block. We deliberately keep this minimal — only the\n * fields we actually write — so we don't drop anything when round-tripping\n * an existing entry that uses fields we don't know about.\n */\ninterface OpenClawPluginEntry {\n enabled?: boolean\n /**\n * The free-form bucket OpenClaw passes to the plugin at activation. This is\n * where we store all our credentials and feature flags — `hooks` is strict,\n * `env` at root is not a free-form key/value passthrough, and using\n * `.config` keeps everything namespaced to our plugin and survives any\n * version of OpenClaw because the field is a `record(string, unknown)`.\n */\n config?: Record<string, unknown>\n // Pass through anything else that may already be there — `hooks`, `subagent`, etc.\n [key: string]: unknown\n}\n\nexport interface OpenClawSettings {\n plugins?: {\n enabled?: boolean\n entries?: Record<string, OpenClawPluginEntry>\n load?: { paths?: string[] }\n [key: string]: unknown\n }\n [key: string]: unknown\n}\n\nexport interface LatitudePluginConfig {\n apiKey: string\n project: string\n baseUrl?: string | undefined\n allowConversationAccess?: boolean | undefined\n debug?: boolean | undefined\n}\n\nexport function readSettings(): OpenClawSettings {\n if (!existsSync(SETTINGS_PATH)) return {}\n try {\n const raw = readFileSync(SETTINGS_PATH, \"utf-8\")\n const parsed = JSON.parse(raw) as OpenClawSettings\n return parsed && typeof parsed === \"object\" ? parsed : {}\n } catch {\n return {}\n }\n}\n\nexport function writeSettings(settings: OpenClawSettings): void {\n writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\\n`, \"utf-8\")\n}\n\nexport function backupSettings(): void {\n if (existsSync(SETTINGS_PATH)) copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH)\n}\n\ninterface SetPluginEntryPatch {\n /** New API key. Always overwrites — comes from the install prompt. */\n apiKey: string\n /** New project slug. Always overwrites — comes from the install prompt. */\n project: string\n /**\n * New `baseUrl`. `undefined` clears any existing override (used when\n * installing back to production). Anything else overwrites.\n */\n baseUrl: string | undefined\n /**\n * `true`/`false` overwrites; `undefined` preserves the existing value (or\n * leaves the key absent if there is none).\n */\n allowConversationAccess?: boolean | undefined\n /** Same semantics as `allowConversationAccess`. */\n debug?: boolean | undefined\n /**\n * `true`/`false` overwrites; `undefined` preserves the existing value, or\n * defaults to `true` for a fresh install. This keeps a paused plugin\n * (`enabled: false` in openclaw.json) paused across re-installs.\n */\n enabled?: boolean | undefined\n}\n\n/**\n * Set the `plugins.entries[id]` block for our plugin. Writes credentials and\n * options into the `.config` bucket — never under `hooks` (strict zod) and\n * never as top-level `env` keys (root schema rejects them).\n *\n * Re-install idempotency: only `apiKey` / `project` / `baseUrl` always\n * overwrite (these come from the install prompts). `enabled`, `debug`, and\n * `allowConversationAccess` are preserved when not provided in the patch,\n * so a user who hand-edited `enabled: false` or `debug: true` doesn't lose\n * their choice on a re-install.\n */\nexport function setPluginEntry(settings: OpenClawSettings, patch: SetPluginEntryPatch): void {\n const plugins = settings.plugins ?? {}\n const entries = plugins.entries ?? {}\n const existing = entries[PLUGIN_ID] ?? {}\n const existingConfig = (existing.config ?? {}) as Record<string, unknown>\n\n const nextConfig: Record<string, unknown> = {\n ...existingConfig,\n apiKey: patch.apiKey,\n project: patch.project,\n }\n if (patch.baseUrl !== undefined) {\n nextConfig.baseUrl = patch.baseUrl\n } else {\n delete nextConfig.baseUrl\n }\n if (patch.allowConversationAccess !== undefined) {\n nextConfig.allowConversationAccess = patch.allowConversationAccess\n }\n if (patch.debug !== undefined) {\n nextConfig.debug = patch.debug\n }\n\n // Preserve user-edited `enabled: false` across re-installs. Fresh install\n // (no existing entry, no explicit patch) defaults to true.\n const nextEnabled = patch.enabled ?? existing.enabled ?? true\n\n entries[PLUGIN_ID] = {\n ...existing,\n enabled: nextEnabled,\n config: nextConfig,\n }\n plugins.entries = entries\n settings.plugins = plugins\n}\n\n/** Remove the plugin entry entirely. */\nexport function removePluginEntry(settings: OpenClawSettings): boolean {\n const plugins = settings.plugins\n if (!plugins?.entries) return false\n if (!(PLUGIN_ID in plugins.entries)) return false\n delete plugins.entries[PLUGIN_ID]\n return true\n}\n\nexport function hasLatitudePlugin(settings: OpenClawSettings): boolean {\n return Boolean(settings.plugins?.entries && PLUGIN_ID in settings.plugins.entries)\n}\n\n/**\n * Strip any leftover top-level keys our older installer (<= 0.0.1) wrote. The\n * 0.0.1 installer wrote `hooks.allowConversationAccess` (rejected by strict\n * zod) and `LATITUDE_*` keys at root-level `env` (also rejected). Both are\n * cleaned up here so re-running install lands a config OpenClaw will validate.\n */\nexport function migrateLegacyEntries(settings: OpenClawSettings): { changed: boolean } {\n let changed = false\n\n // Wipe `hooks.allowConversationAccess` from our entry — OpenClaw < 2026.4.22\n // rejects unknown keys under the strict `hooks` shape.\n const entry = settings.plugins?.entries?.[PLUGIN_ID]\n if (entry && typeof entry === \"object\") {\n const hooks = (entry as { hooks?: Record<string, unknown> }).hooks\n if (hooks && typeof hooks === \"object\" && \"allowConversationAccess\" in hooks) {\n delete hooks.allowConversationAccess\n // If the hooks object is now empty, drop it entirely so we don't leave\n // a `\"hooks\": {}` carcass behind.\n if (Object.keys(hooks).length === 0) {\n delete (entry as { hooks?: unknown }).hooks\n }\n changed = true\n }\n }\n\n // Strip `LATITUDE_*` keys our 0.0.1 installer mistakenly wrote under\n // `settings.env`. OpenClaw's root schema is strict; the `env` block accepts\n // only `{shellEnv, vars}`, so any `LATITUDE_*` key sitting directly under\n // `env` causes the gateway to quarantine the file. (0.0.1 only ever wrote\n // to `settings.env.LATITUDE_*`, never to top-level `settings.LATITUDE_*`,\n // so we don't bother sweeping the root.)\n const env = settings.env\n if (env && typeof env === \"object\" && !Array.isArray(env)) {\n const envObj = env as Record<string, unknown>\n for (const key of [\"LATITUDE_API_KEY\", \"LATITUDE_PROJECT\", \"LATITUDE_BASE_URL\"]) {\n if (key in envObj) {\n delete envObj[key]\n changed = true\n }\n }\n // Drop the env object if there's nothing left in it AND it's not OpenClaw's\n // canonical {shellEnv, vars} shape — only do this when we know it was ours.\n if (Object.keys(envObj).length === 0) {\n delete settings.env\n }\n }\n\n return { changed }\n}\n","import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { PLUGIN_INSTALL_DIR } from \"./settings-file.ts\"\n\n/**\n * Copies the plugin's runtime files into `~/.openclaw/extensions/latitude-telemetry/`\n * so OpenClaw's discovery (`<configDir>/extensions/<plugin>`) picks it up.\n *\n * `npx -y @latitude-data/openclaw-telemetry` runs the CLI from a temporary npm\n * location — relying on that path persisting across runs (or even across the\n * gateway restart that follows install) is unsafe. So we materialize a stable\n * copy under the user's OpenClaw config dir, the same path OpenClaw scans on\n * startup. Layout we produce:\n *\n * ~/.openclaw/extensions/latitude-telemetry/\n * openclaw.plugin.json <- the manifest, required by discovery\n * package.json <- minimal — keeps node module-resolution happy\n * dist/ <- compiled plugin entrypoint(s)\n */\nexport function installPluginFiles(): { destination: string; entryPath: string } {\n // The CLI runs from `dist/cli.js`. Resolve the package root from there.\n const here = dirname(fileURLToPath(import.meta.url))\n const packageRoot = resolve(here, \"..\") // dist/cli.js -> dist/ -> package root\n\n const manifestSrc = join(packageRoot, \"openclaw.plugin.json\")\n const distSrc = join(packageRoot, \"dist\")\n const pkgSrc = join(packageRoot, \"package.json\")\n\n if (!existsSync(manifestSrc)) {\n throw new Error(\n `Cannot install: missing openclaw.plugin.json at ${manifestSrc}. This is a packaging bug — please file an issue.`,\n )\n }\n if (!existsSync(distSrc)) {\n throw new Error(`Cannot install: missing compiled dist at ${distSrc}.`)\n }\n if (!existsSync(pkgSrc)) {\n // package.json is now load-bearing for OpenClaw discovery (it carries\n // `openclaw.extensions`), so missing it would silently regress to the\n // \"plugin not found\" failure mode 0.0.3 was meant to fix. Match the\n // other packaging-bug checks above and fail loudly instead.\n throw new Error(\n `Cannot install: missing package.json at ${pkgSrc}. This is a packaging bug — please file an issue.`,\n )\n }\n\n // Wipe and recreate so a re-install isn't polluted by a previous version's\n // files. Idempotent.\n if (existsSync(PLUGIN_INSTALL_DIR)) rmSync(PLUGIN_INSTALL_DIR, { recursive: true, force: true })\n mkdirSync(PLUGIN_INSTALL_DIR, { recursive: true })\n\n copyFileSync(manifestSrc, join(PLUGIN_INSTALL_DIR, \"openclaw.plugin.json\"))\n cpSync(distSrc, join(PLUGIN_INSTALL_DIR, \"dist\"), { recursive: true })\n\n // Write a minimal package.json. Three fields matter for OpenClaw:\n // - `name`/`version` for diagnostics.\n // - `type: \"module\"` so node treats `dist/plugin.js` as ESM.\n // - `openclaw.extensions: [...]` — THIS is what OpenClaw's plugin\n // discovery uses to find the plugin entry file. The\n // `openclaw.plugin.json` manifest provides id + configSchema, but\n // discovery's `resolvePackageExtensionEntries` reads\n // `package.json[\"openclaw\"].extensions`. Without this, discovery\n // falls through to looking for `index.{ts,js,mjs,cjs}` at the dir\n // root, doesn't find one, and skips us — which manifests as\n // \"plugin not found\" at gateway start.\n const sourcePkg = JSON.parse(readFileSync(pkgSrc, \"utf-8\")) as {\n name?: string\n version?: string\n type?: string\n main?: string\n openclaw?: { extensions?: string[] }\n }\n const extensions = sourcePkg.openclaw?.extensions ?? [\"./dist/plugin.js\"]\n const minimalPkg = {\n name: sourcePkg.name,\n version: sourcePkg.version,\n type: sourcePkg.type ?? \"module\",\n main: sourcePkg.main ?? \"./dist/plugin.js\",\n private: true,\n openclaw: { extensions },\n }\n writeFileSync(join(PLUGIN_INSTALL_DIR, \"package.json\"), `${JSON.stringify(minimalPkg, null, 2)}\\n`, \"utf-8\")\n\n // Derive the entry path from the resolved extensions rather than\n // hard-coding it, so callers reading `entryPath` stay consistent if the\n // package's openclaw.extensions ever points somewhere other than\n // `./dist/plugin.js` (e.g. multi-entry plugins).\n const primaryEntry = extensions[0] ?? \"./dist/plugin.js\"\n return {\n destination: PLUGIN_INSTALL_DIR,\n entryPath: resolve(PLUGIN_INSTALL_DIR, primaryEntry),\n }\n}\n\n/** Remove the materialized plugin directory under `~/.openclaw/extensions/`. */\nexport function removePluginFiles(): boolean {\n if (!existsSync(PLUGIN_INSTALL_DIR)) return false\n rmSync(PLUGIN_INSTALL_DIR, { recursive: true, force: true })\n return true\n}\n","import { existsSync, mkdirSync } from \"node:fs\"\nimport { dirname } from \"node:path\"\nimport { cancel, confirm, intro, isCancel, log, note, outro, password, spinner, text } from \"@clack/prompts\"\nimport pc from \"picocolors\"\nimport { installPluginFiles, removePluginFiles } from \"./install-files.ts\"\nimport {\n backupSettings,\n hasLatitudePlugin,\n type LatitudePluginConfig,\n migrateLegacyEntries,\n PLUGIN_ID,\n PLUGIN_INSTALL_DIR,\n readSettings,\n removePluginEntry,\n SETTINGS_BACKUP_PATH,\n SETTINGS_PATH,\n setPluginEntry,\n writeSettings,\n} from \"./settings-file.ts\"\n\nconst DOCS_URL = \"https://docs.latitude.so/openclaw-telemetry\"\n\ninterface EnvironmentConfig {\n name: \"production\" | \"staging\" | \"dev\"\n label: string\n app: string\n ingest: string\n}\n\nconst PRODUCTION_ENV: EnvironmentConfig = {\n name: \"production\",\n label: \"production\",\n app: \"https://console.latitude.so\",\n ingest: \"https://ingest.latitude.so\",\n}\nconst STAGING_ENV: EnvironmentConfig = {\n name: \"staging\",\n label: \"staging\",\n app: \"https://staging.latitude.so\",\n ingest: \"https://staging-ingest.latitude.so\",\n}\nconst DEV_ENV: EnvironmentConfig = {\n name: \"dev\",\n label: \"local dev\",\n app: \"http://localhost:3000\",\n ingest: \"http://localhost:3002\",\n}\n\nfunction urlsFor(env: EnvironmentConfig): {\n apiKeys: string\n projects: string\n projectView: (slug: string) => string\n} {\n return {\n apiKeys: `${env.app}/settings/api-keys`,\n projects: env.app,\n projectView: (slug: string) => `${env.app}/projects/${slug}`,\n }\n}\n\ninterface InstallFlags {\n apiKey?: string | undefined\n project?: string | undefined\n environment?: EnvironmentConfig | undefined\n /**\n * Tristate: `true` = capture (the user passed `--allow-conversation`),\n * `false` = scrub (the user passed `--no-content`), `undefined` = preserve\n * existing or fall through to the first-install default. Keeping this\n * tristate is what makes re-installs idempotent for hand-edited values.\n */\n allowConversationAccess?: boolean | undefined\n noPrompt?: boolean\n yes?: boolean\n}\n\nexport function parseFlags(argv: string[]): {\n subcommand: string | undefined\n flags: Record<string, string | boolean>\n} {\n const [subcommand, ...rest] = argv\n const flags: Record<string, string | boolean> = {}\n for (const arg of rest) {\n if (!arg.startsWith(\"--\")) continue\n const eq = arg.indexOf(\"=\")\n if (eq >= 0) {\n flags[arg.slice(2, eq)] = arg.slice(eq + 1)\n } else {\n flags[arg.slice(2)] = true\n }\n }\n return { subcommand, flags }\n}\n\nexport function normalizeInstallFlags(flags: Record<string, string | boolean>): InstallFlags {\n let environment: EnvironmentConfig | undefined\n if (flags.staging === true) environment = STAGING_ENV\n if (flags.dev === true) {\n if (environment) throw new Error(\"--staging and --dev are mutually exclusive\")\n environment = DEV_ENV\n }\n // Tristate: leave undefined unless the user explicitly asked one way or\n // the other. Re-install then preserves whatever's in openclaw.json.\n let allowConversationAccess: boolean | undefined\n if (flags[\"no-content\"] === true || flags[\"no-conversation\"] === true) allowConversationAccess = false\n if (flags[\"allow-conversation\"] === true) allowConversationAccess = true\n\n return {\n apiKey: typeof flags[\"api-key\"] === \"string\" ? flags[\"api-key\"] : undefined,\n project: typeof flags.project === \"string\" ? flags.project : undefined,\n environment,\n allowConversationAccess,\n noPrompt: flags[\"no-prompt\"] === true || flags.yes === true,\n yes: flags.yes === true,\n }\n}\n\n// ─── Install ────────────────────────────────────────────────────────────────\n\nexport async function runInstall(flags: InstallFlags = {}): Promise<void> {\n const canPrompt = !flags.noPrompt && process.stdin.isTTY === true\n if (!canPrompt) return runFlagDrivenInstall(flags)\n await runInteractiveInstall(flags)\n}\n\nasync function runInteractiveInstall(flags: InstallFlags): Promise<void> {\n intro(pc.bgCyan(pc.black(\" Latitude · OpenClaw telemetry \")))\n\n const existing = readSettings()\n const existingConfig =\n (existing.plugins?.entries?.[PLUGIN_ID]?.config as LatitudePluginConfig | undefined) ?? undefined\n const envConfig = flags.environment ?? PRODUCTION_ENV\n const urls = urlsFor(envConfig)\n\n const aboutLines = [\n \"Captures every OpenClaw agent run and ships it to Latitude as\",\n \"OpenTelemetry traces — full system prompt, tool I/O, messages,\",\n \"token usage, and agent name on every span.\",\n \"\",\n `${pc.dim(\"Docs\")} ${pc.cyan(DOCS_URL)}`,\n ]\n if (envConfig.name !== \"production\") {\n aboutLines.push(\"\", pc.yellow(`Using ${envConfig.label} environment (${envConfig.ingest})`))\n }\n note(aboutLines.join(\"\\n\"), \"About\")\n\n log.info(`Get an API key at ${pc.cyan(urls.apiKeys)}`)\n log.info(`Create a project at ${pc.cyan(urls.projects)}`)\n\n const apiKey = await promptApiKey(existingConfig?.apiKey, flags.apiKey)\n const project = await promptProject(existingConfig?.project, flags.project)\n\n await applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess: flags.allowConversationAccess,\n })\n\n note(\n [\n \"Restart the OpenClaw gateway for the plugin to load:\",\n pc.dim(\" openclaw gateway restart\"),\n \"\",\n `View your traces at ${pc.cyan(urls.projectView(project))}`,\n ].join(\"\\n\"),\n \"Next step\",\n )\n outro(pc.green(\"✓ Installed\"))\n}\n\nasync function runFlagDrivenInstall(flags: InstallFlags): Promise<void> {\n const apiKey = flags.apiKey\n const project = flags.project\n if (!apiKey || !project) {\n throw new Error(\"Non-interactive install requires --api-key=... and --project=... (or run in a TTY).\")\n }\n const envConfig = flags.environment ?? PRODUCTION_ENV\n await applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess: flags.allowConversationAccess,\n })\n process.stdout.write(`Installed Latitude plugin in ${SETTINGS_PATH}\\n`)\n process.stdout.write(`Plugin files at ${PLUGIN_INSTALL_DIR}\\n`)\n}\n\nasync function promptApiKey(_existing: string | undefined, flag: string | undefined): Promise<string> {\n if (flag) return flag\n const result = await password({\n message: \"Latitude API key\",\n mask: \"•\",\n validate: (v) => (v && v.length > 0 ? undefined : \"Required\"),\n })\n if (isCancel(result)) return onCancel()\n return result\n}\n\nasync function promptProject(existing: string | undefined, flag: string | undefined): Promise<string> {\n if (flag) return flag\n const result = await text({\n message: \"Latitude project slug\",\n placeholder: existing ?? \"my-openclaw-project\",\n ...(existing ? { initialValue: existing } : {}),\n validate: (v) => (v && v.length > 0 ? undefined : \"Required\"),\n })\n if (isCancel(result)) return onCancel()\n return result\n}\n\nfunction onCancel(): never {\n cancel(\"Cancelled — nothing was changed\")\n process.exit(1)\n}\n\ninterface ApplyParams {\n apiKey: string\n project: string\n envConfig: EnvironmentConfig\n /** Tristate — see `InstallFlags.allowConversationAccess`. */\n allowConversationAccess: boolean | undefined\n}\n\nasync function applyChanges({ apiKey, project, envConfig, allowConversationAccess }: ApplyParams): Promise<void> {\n // 1. Materialize plugin runtime files into ~/.openclaw/extensions/latitude-telemetry/\n // so OpenClaw's plugin discovery picks them up. This MUST happen before\n // we write the openclaw.json entry, otherwise the gateway file-watcher\n // will see the entry, fail to find the plugin, and emit a warning.\n const filesSpinner = spinner()\n filesSpinner.start(\"Installing plugin files\")\n const { destination } = installPluginFiles()\n filesSpinner.stop(`Plugin files installed at ${destination}`)\n\n // 2. Update openclaw.json with the plugin entry.\n const settingsSpinner = spinner()\n settingsSpinner.start(\"Updating openclaw.json\")\n ensureSettingsDir()\n backupSettings()\n const settings = readSettings()\n // Migrate any leftover keys our 0.0.1 installer wrote that the strict zod\n // schema rejects. Without this, re-installing on top of a 0.0.1 install\n // would leave the gateway quarantining the file as `clobbered`.\n migrateLegacyEntries(settings)\n\n // Decide allowConversationAccess for this install:\n // - explicit flag (true|false) wins\n // - else preserve whatever's already in openclaw.json\n // - else first-install default is `true` (matches the README's promise)\n const existingConfig = (settings.plugins?.entries?.[PLUGIN_ID]?.config ?? {}) as Partial<LatitudePluginConfig>\n const finalAllowConversationAccess = allowConversationAccess ?? existingConfig.allowConversationAccess ?? true\n\n setPluginEntry(settings, {\n apiKey,\n project,\n baseUrl: envConfig.name === \"production\" ? undefined : envConfig.ingest,\n allowConversationAccess: finalAllowConversationAccess,\n // `debug` is intentionally not passed — `setPluginEntry` preserves the\n // user's hand-edited value. Fresh installs leave the key absent (the\n // runtime default is `false`).\n })\n writeSettings(settings)\n settingsSpinner.stop(`Updated ${SETTINGS_PATH}`)\n if (existsSync(SETTINGS_BACKUP_PATH)) log.info(`Backup saved at ${pc.dim(SETTINGS_BACKUP_PATH)}`)\n}\n\nfunction ensureSettingsDir(): void {\n const dir = dirname(SETTINGS_PATH)\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true })\n}\n\n// ─── Uninstall ──────────────────────────────────────────────────────────────\n\ninterface UninstallFlags {\n noPrompt?: boolean\n}\n\nexport async function runUninstall(flags: UninstallFlags = {}): Promise<void> {\n intro(pc.bgYellow(pc.black(\" Latitude · OpenClaw telemetry — uninstall \")))\n const settings = readSettings()\n\n const hasEntry = hasLatitudePlugin(settings)\n const hasFiles = existsSync(PLUGIN_INSTALL_DIR)\n\n if (!hasEntry && !hasFiles) {\n note(\"No Latitude plugin entry or files found — nothing to remove.\", \"Status\")\n outro(pc.dim(\"Nothing changed\"))\n return\n }\n\n const plan = [\n hasEntry ? `Remove \"${PLUGIN_ID}\" plugin entry from ${SETTINGS_PATH}` : null,\n hasFiles ? `Delete plugin files at ${PLUGIN_INSTALL_DIR}` : null,\n `Backup of openclaw.json saved at ${SETTINGS_BACKUP_PATH}`,\n ].filter(Boolean) as string[]\n note(plan.join(\"\\n\"), \"Plan\")\n\n if (!flags.noPrompt && process.stdin.isTTY === true) {\n const ok = await confirm({ message: \"Proceed?\", initialValue: true })\n if (isCancel(ok) || ok !== true) return onCancel()\n }\n\n const s = spinner()\n s.start(\"Reverting settings\")\n if (hasEntry) {\n backupSettings()\n removePluginEntry(settings)\n // Also clean any legacy keys our 0.0.1 installer left behind so the file\n // is fully back to a clean state.\n migrateLegacyEntries(settings)\n writeSettings(settings)\n }\n if (hasFiles) removePluginFiles()\n s.stop(\"Done\")\n outro(pc.green(\"✓ Uninstalled\"))\n}\n","import { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { normalizeInstallFlags, parseFlags, runInstall, runUninstall } from \"./setup.ts\"\n\nconst USAGE = `usage: latitude-openclaw <command> [options]\n\ncommands:\n install Install the plugin (interactive when stdin is a TTY)\n uninstall Remove the plugin entry and files\n --version, -v Print the package version\n --help, -h Print this message\n\ninstall options:\n --api-key=<key> Pass the API key non-interactively\n --project=<slug> Pass the project slug non-interactively\n --staging Target https://staging.latitude.so / staging-ingest\n --dev Target http://localhost:3000 / 3002\n --no-content Skip raw prompt/response/tool I/O capture\n --allow-conversation Force conversation capture on (overrides existing config)\n --yes / --no-prompt Skip all prompts (required for non-TTY / CI)\n`\n\nfunction readVersion(): string {\n // dist/cli.js → ../package.json\n const here = dirname(fileURLToPath(import.meta.url))\n const pkgPath = join(here, \"..\", \"package.json\")\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version?: string }\n return pkg.version ?? \"unknown\"\n } catch {\n return \"unknown\"\n }\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2)\n // Top-level --version / --help are handled before parseFlags so users can\n // type them as the first arg without hitting the \"unknown subcommand\" path.\n if (argv[0] === \"--version\" || argv[0] === \"-v\") {\n process.stdout.write(`${readVersion()}\\n`)\n return\n }\n if (argv[0] === \"--help\" || argv[0] === \"-h\") {\n process.stdout.write(USAGE)\n return\n }\n\n const { subcommand, flags } = parseFlags(argv)\n if (subcommand === \"install\" || subcommand === undefined) {\n await runInstall(normalizeInstallFlags(flags))\n return\n }\n if (subcommand === \"uninstall\") {\n await runUninstall({ noPrompt: flags[\"no-prompt\"] === true || flags.yes === true })\n return\n }\n process.stderr.write(`unknown subcommand: ${subcommand}\\n`)\n process.stderr.write(USAGE)\n process.exit(1)\n}\n\nmain().catch((err) => {\n process.stderr.write(`${String(err)}\\n`)\n process.exit(1)\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/openclaw-cli.ts","../src/settings-file.ts","../src/setup.ts","../src/cli.ts"],"sourcesContent":["import { type SpawnSyncReturns, spawnSync } from \"node:child_process\"\n\n/**\n * Lowest OpenClaw version we support. The reporter verified hook-dispatch\n * gating works correctly here; older versions either reject\n * `hooks.allowConversationAccess` outright (≤ 2026.4.21) or have unverified\n * gating behaviour (2026.4.22 – 2026.4.24). Refusing to install on older\n * versions is intentional — we'd rather fail loudly than ship a\n * config the gateway will quarantine or hooks the dispatcher will block.\n */\nexport const MIN_OPENCLAW_VERSION = \"2026.4.25\"\n\nconst DEFAULT_TIMEOUT_MS = 10_000\n\ntype RunResult =\n | { ok: true; stdout: string; stderr: string; code: 0 }\n | { ok: false; reason: \"enoent\"; stdout: \"\"; stderr: \"\"; code: null }\n | { ok: false; reason: \"timeout\"; stdout: string; stderr: string; code: null }\n | { ok: false; reason: \"exit\"; stdout: string; stderr: string; code: number }\n\n/**\n * Spawn `openclaw <args>` synchronously. Reports failure modes structurally so\n * callers can decide how to degrade (missing binary vs. timed out vs. exited\n * non-zero). Never throws; ENOENT becomes `{ reason: \"enoent\" }`.\n */\nexport function runOpenclaw(args: string[], opts: { timeoutMs?: number; stdin?: string } = {}): RunResult {\n const result: SpawnSyncReturns<string> = spawnSync(\"openclaw\", args, {\n encoding: \"utf-8\",\n timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n input: opts.stdin,\n // Inherit env so the user's PATH and any LATITUDE_* / OPENCLAW_* vars\n // reach the child process. We don't need a TTY — install is non-interactive.\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n })\n\n // spawnSync surfaces I/O errors through `result.error` rather than a thrown\n // exception when called this way. Order matters: a timeout shows up as\n // `error.code === \"ETIMEDOUT\"` AND/OR `signal === \"SIGTERM\"|\"SIGKILL\"`\n // depending on platform — classify it before the generic `if (err)`\n // catch-all so callers reliably get `reason: \"timeout\"` (and the\n // timeout-specific error messages in setup.ts aren't dead code).\n const err = result.error as (NodeJS.ErrnoException & { code?: string }) | undefined\n if (err?.code === \"ENOENT\") {\n return { ok: false, reason: \"enoent\", stdout: \"\", stderr: \"\", code: null }\n }\n if (err?.code === \"ETIMEDOUT\" || result.signal === \"SIGTERM\" || result.signal === \"SIGKILL\") {\n return {\n ok: false,\n reason: \"timeout\",\n stdout: result.stdout ?? \"\",\n stderr: result.stderr ?? \"\",\n code: null,\n }\n }\n if (err) {\n // Treat any other spawn error as an unparseable exit failure.\n return {\n ok: false,\n reason: \"exit\",\n stdout: result.stdout ?? \"\",\n stderr: result.stderr ?? String(err),\n code: typeof result.status === \"number\" ? result.status : 1,\n }\n }\n\n if (result.status === 0) {\n return { ok: true, stdout: result.stdout ?? \"\", stderr: result.stderr ?? \"\", code: 0 }\n }\n\n return {\n ok: false,\n reason: \"exit\",\n stdout: result.stdout ?? \"\",\n stderr: result.stderr ?? \"\",\n code: typeof result.status === \"number\" ? result.status : 1,\n }\n}\n\ntype VersionLookup =\n | { ok: true; version: string; raw: string }\n | { ok: false; error: \"missing\" | \"unparseable\"; raw?: string | undefined }\n\n/**\n * Run `openclaw --version` and parse out the version string.\n *\n * Banner format (per OpenClaw `src/cli/banner.ts` `formatCliBannerLine`):\n * `🦞 OpenClaw <version> (<commit-sha>)`\n *\n * The banner is normally suppressed when `--version` is the flag, but the\n * version itself still goes to stdout. We accept either layout (with or\n * without the lobster + commit sha) so we don't break if OpenClaw later\n * prints just the bare version string.\n */\nexport function getOpenclawVersion(): VersionLookup {\n const result = runOpenclaw([\"--version\"], { timeoutMs: 5_000 })\n if (!result.ok) {\n if (result.reason === \"enoent\") return { ok: false, error: \"missing\" }\n return { ok: false, error: \"unparseable\", raw: result.stdout || result.stderr }\n }\n\n const raw = result.stdout.trim()\n // Match a CalVer triple anywhere in the output. Be liberal — banner\n // decorations (emoji, \"OpenClaw\" prefix, trailing \"(sha)\") all just\n // surround a single `YYYY.M.PATCH` somewhere.\n const match = raw.match(/(\\d{4}\\.\\d+\\.\\d+)/)\n if (!match) return { ok: false, error: \"unparseable\", raw }\n return { ok: true, version: match[1] as string, raw }\n}\n\n/**\n * Compare two CalVer strings (`YYYY.M.PATCH`). Returns -1 if `a` < `b`,\n * 0 if equal, 1 if `a` > `b`. Strings with extra components or non-numeric\n * pieces fall back to a per-component string comparison so unexpected\n * formats don't crash the installer.\n */\nexport function compareCalver(a: string, b: string): -1 | 0 | 1 {\n const ap = a.split(\".\")\n const bp = b.split(\".\")\n const len = Math.max(ap.length, bp.length)\n for (let i = 0; i < len; i++) {\n const ai = ap[i] ?? \"0\"\n const bi = bp[i] ?? \"0\"\n const an = Number(ai)\n const bn = Number(bi)\n if (Number.isFinite(an) && Number.isFinite(bn)) {\n if (an < bn) return -1\n if (an > bn) return 1\n continue\n }\n if (ai < bi) return -1\n if (ai > bi) return 1\n }\n return 0\n}\n","import { copyFileSync, existsSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\nconst CONFIG_DIR = join(homedir(), \".openclaw\")\nexport const SETTINGS_PATH = join(CONFIG_DIR, \"openclaw.json\")\nexport const SETTINGS_BACKUP_PATH = join(CONFIG_DIR, \"openclaw.json.latitude-bak\")\n\n/** Plugin id used both as the npm package name and as the OpenClaw plugin id. */\nexport const PLUGIN_ID = \"@latitude-data/openclaw-telemetry\"\n\n/**\n * The shape OpenClaw's strict zod schema accepts for a single\n * `plugins.entries[id]` block. We keep this minimal — only the fields we\n * actually write — so we don't drop anything when round-tripping an entry\n * with fields we don't know about.\n */\ninterface OpenClawPluginEntry {\n enabled?: boolean\n /**\n * Strict zod object on OpenClaw 2026.4.25+: only `allowPromptInjection` and\n * `allowConversationAccess` are accepted. We populate the latter — it's the\n * dispatch gate the OpenClaw runtime checks before forwarding LLM/tool/agent\n * events to non-bundled plugins. Without it, the gateway logs\n * `[plugins] typed hook \"...\" blocked because non-bundled plugins must set\n * plugins.entries.<id>.hooks.allowConversationAccess=true` and the plugin's\n * handlers never fire.\n */\n hooks?: {\n allowPromptInjection?: boolean\n allowConversationAccess?: boolean\n [key: string]: unknown\n }\n /**\n * Free-form bucket OpenClaw passes to the plugin at activation\n * (`api.pluginConfig`). Our credentials and feature flags live here.\n */\n config?: Record<string, unknown>\n // Pass through anything else that may already be there — `subagent`, etc.\n [key: string]: unknown\n}\n\nexport interface OpenClawSettings {\n plugins?: {\n enabled?: boolean\n entries?: Record<string, OpenClawPluginEntry>\n /**\n * Operator-managed allowlist of plugin ids that may auto-load. OpenClaw\n * warns at every gateway start when a non-bundled plugin loads without\n * provenance via `plugins.allow` or an install record.\n */\n allow?: string[]\n load?: { paths?: string[] }\n [key: string]: unknown\n }\n [key: string]: unknown\n}\n\nexport interface LatitudePluginConfig {\n apiKey: string\n project: string\n baseUrl?: string | undefined\n allowConversationAccess?: boolean | undefined\n debug?: boolean | undefined\n}\n\nexport function readSettings(): OpenClawSettings {\n if (!existsSync(SETTINGS_PATH)) return {}\n try {\n const raw = readFileSync(SETTINGS_PATH, \"utf-8\")\n const parsed = JSON.parse(raw) as OpenClawSettings\n return parsed && typeof parsed === \"object\" ? parsed : {}\n } catch {\n return {}\n }\n}\n\nexport function writeSettings(settings: OpenClawSettings): void {\n writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\\n`, \"utf-8\")\n}\n\nexport function backupSettings(): void {\n if (existsSync(SETTINGS_PATH)) copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH)\n}\n\ninterface SetPluginEntryPatch {\n /** New API key. Always overwrites — comes from the install prompt. */\n apiKey: string\n /** New project slug. Always overwrites — comes from the install prompt. */\n project: string\n /**\n * New `baseUrl`. `undefined` clears any existing override (used when\n * installing back to production). Anything else overwrites.\n */\n baseUrl: string | undefined\n /**\n * `true`/`false` overwrites both `config.allowConversationAccess` AND\n * `hooks.allowConversationAccess` (always coupled — see `setPluginEntry`).\n * `undefined` preserves the existing value, or defaults to `true` for a\n * fresh install.\n */\n allowConversationAccess?: boolean | undefined\n /**\n * `true`/`false` overwrites; `undefined` preserves the existing value (the\n * installer doesn't pass `debug` — it's a hand-edit affordance).\n */\n debug?: boolean | undefined\n /**\n * `true`/`false` overwrites; `undefined` preserves existing, defaulting to\n * `true` for a fresh install. This keeps a paused plugin (`enabled: false`\n * hand-edited in openclaw.json) paused across re-installs.\n */\n enabled?: boolean | undefined\n}\n\n/**\n * Set the `plugins.entries[id]` block for our plugin.\n *\n * Two places in the entry get written:\n *\n * - `.config` (free-form `record(string, unknown)`): credentials, baseUrl,\n * and our copy of `allowConversationAccess`. This is what the plugin\n * runtime reads via `api.pluginConfig`.\n * - `.hooks.allowConversationAccess`: controls whether OpenClaw's hook\n * dispatcher actually forwards `llm_input` / `llm_output` / tool /\n * `agent_end` events to our handlers. Without it set to `true` on\n * OpenClaw 2026.4.25+, every typed hook is blocked at the dispatcher\n * and the plugin's handlers never fire.\n *\n * The two flags mean different things — `hooks.*` is the dispatch gate,\n * `config.*` is the payload-content gate — but for THIS plugin we always\n * couple them: dispatch off + payload on is useless (no payloads to gate),\n * and dispatch on + payload off is a legitimate \"structural-only telemetry\"\n * mode (timing, tokens, ids, agent name; no message bodies). Always writing\n * both from the same source keeps the operator's mental model simple.\n *\n * Re-install idempotency: only `apiKey` / `project` / `baseUrl` always\n * overwrite (these come from install prompts). `enabled`, `debug`, and\n * `allowConversationAccess` are preserved when not provided in the patch.\n */\nexport function setPluginEntry(settings: OpenClawSettings, patch: SetPluginEntryPatch): void {\n const plugins = settings.plugins ?? {}\n const entries = plugins.entries ?? {}\n const existing = entries[PLUGIN_ID] ?? {}\n const existingConfig = (existing.config ?? {}) as Record<string, unknown>\n const existingHooks = existing.hooks ?? {}\n\n const nextConfig: Record<string, unknown> = {\n ...existingConfig,\n apiKey: patch.apiKey,\n project: patch.project,\n }\n if (patch.baseUrl !== undefined) {\n nextConfig.baseUrl = patch.baseUrl\n } else {\n delete nextConfig.baseUrl\n }\n if (patch.allowConversationAccess !== undefined) {\n nextConfig.allowConversationAccess = patch.allowConversationAccess\n }\n if (patch.debug !== undefined) {\n nextConfig.debug = patch.debug\n }\n\n // Resolve the effective allowConversationAccess for the hooks block.\n // We mirror whatever ended up in nextConfig.allowConversationAccess (just\n // computed above) so the two flags always agree. If neither the patch nor\n // the existing config sets it, fall back to the existing hooks value, then\n // to `true` (matches the README's first-install promise).\n const effectiveAccess =\n typeof nextConfig.allowConversationAccess === \"boolean\"\n ? nextConfig.allowConversationAccess\n : typeof existingHooks.allowConversationAccess === \"boolean\"\n ? existingHooks.allowConversationAccess\n : true\n\n const nextHooks: OpenClawPluginEntry[\"hooks\"] = {\n ...existingHooks,\n allowConversationAccess: effectiveAccess,\n }\n\n // Preserve a hand-edited `enabled: false` across re-installs. Fresh install\n // (no existing entry, no explicit patch) defaults to true.\n const nextEnabled = patch.enabled ?? existing.enabled ?? true\n\n entries[PLUGIN_ID] = {\n ...existing,\n enabled: nextEnabled,\n hooks: nextHooks,\n config: nextConfig,\n }\n plugins.entries = entries\n settings.plugins = plugins\n}\n\n/** Remove the plugin entry entirely. Used by uninstall as defense-in-depth. */\nexport function removePluginEntry(settings: OpenClawSettings): boolean {\n const plugins = settings.plugins\n if (!plugins?.entries) return false\n if (!(PLUGIN_ID in plugins.entries)) return false\n delete plugins.entries[PLUGIN_ID]\n return true\n}\n\n/**\n * Add the plugin id to `plugins.allow`. Idempotent — returns `true` only when\n * the array changed. OpenClaw warns at every gateway start when a non-bundled\n * plugin auto-loads without provenance via `plugins.allow` or an install\n * record. We get one warning cleared by going through `openclaw plugins\n * install` (provenance) and the other by adding ourselves to allow.\n *\n * Defensive against hand-edited non-array values: if `plugins.allow` is\n * present but not an array (e.g. someone wrote a string), we replace it\n * with a single-element array rather than spreading the bad value.\n */\nexport function addToPluginsAllow(settings: OpenClawSettings): boolean {\n const plugins = settings.plugins ?? {}\n const existing = plugins.allow\n const allow = Array.isArray(existing) ? existing : []\n if (allow.includes(PLUGIN_ID)) return false\n plugins.allow = [...allow, PLUGIN_ID]\n settings.plugins = plugins\n return true\n}\n\n/**\n * Inverse of `addToPluginsAllow`. Defense-in-depth — `openclaw plugins\n * uninstall` already strips the entry, but the install path can be skipped\n * (e.g. the user removed the plugin manually) and we want re-install/uninstall\n * round-trips to be tidy regardless.\n */\nexport function removeFromPluginsAllow(settings: OpenClawSettings): boolean {\n const allow = settings.plugins?.allow\n if (!Array.isArray(allow) || !allow.includes(PLUGIN_ID)) return false\n if (settings.plugins) {\n settings.plugins.allow = allow.filter((id) => id !== PLUGIN_ID)\n }\n return true\n}\n\nexport function hasLatitudePlugin(settings: OpenClawSettings): boolean {\n return Boolean(settings.plugins?.entries && PLUGIN_ID in settings.plugins.entries)\n}\n\n/**\n * Strip leftover keys from older installers that the strict zod schema\n * rejects on current OpenClaw versions.\n *\n * 0.0.1 wrote `LATITUDE_*` keys directly under `settings.env`. OpenClaw's\n * root schema is strict; the `env` block accepts only `{shellEnv, vars}`,\n * so those keys cause the gateway to quarantine the config as\n * `clobbered.<ts>` and roll back. We sweep them on every install.\n *\n * Note: 0.0.1 also wrote `hooks.allowConversationAccess` (when that key was\n * not yet in the schema). We deliberately do NOT strip it anymore — on\n * OpenClaw 2026.4.25+ the key IS in the schema and IS load-bearing for\n * dispatch. `setPluginEntry` overwrites it on every install with the right\n * value, so any 0.0.1 leftover is reconciled there.\n */\nexport function migrateLegacyEntries(settings: OpenClawSettings): { changed: boolean } {\n let changed = false\n\n const env = settings.env\n if (env && typeof env === \"object\" && !Array.isArray(env)) {\n const envObj = env as Record<string, unknown>\n for (const key of [\"LATITUDE_API_KEY\", \"LATITUDE_PROJECT\", \"LATITUDE_BASE_URL\"]) {\n if (key in envObj) {\n delete envObj[key]\n changed = true\n }\n }\n // Drop the env object only if it's now empty; don't touch a real\n // OpenClaw env block (`{shellEnv, vars}`) that already had those keys.\n if (Object.keys(envObj).length === 0) {\n delete settings.env\n }\n }\n\n return { changed }\n}\n","import { existsSync, mkdirSync } from \"node:fs\"\nimport { dirname, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { cancel, confirm, intro, isCancel, log, note, outro, password, spinner, text } from \"@clack/prompts\"\nimport pc from \"picocolors\"\nimport { compareCalver, getOpenclawVersion, MIN_OPENCLAW_VERSION, runOpenclaw } from \"./openclaw-cli.ts\"\nimport {\n addToPluginsAllow,\n backupSettings,\n hasLatitudePlugin,\n type LatitudePluginConfig,\n migrateLegacyEntries,\n PLUGIN_ID,\n readSettings,\n removeFromPluginsAllow,\n removePluginEntry,\n SETTINGS_BACKUP_PATH,\n SETTINGS_PATH,\n setPluginEntry,\n writeSettings,\n} from \"./settings-file.ts\"\n\nconst DOCS_URL = \"https://docs.latitude.so/openclaw-telemetry\"\n\ninterface EnvironmentConfig {\n name: \"production\" | \"staging\" | \"dev\"\n label: string\n app: string\n ingest: string\n}\n\nconst PRODUCTION_ENV: EnvironmentConfig = {\n name: \"production\",\n label: \"production\",\n app: \"https://console.latitude.so\",\n ingest: \"https://ingest.latitude.so\",\n}\nconst STAGING_ENV: EnvironmentConfig = {\n name: \"staging\",\n label: \"staging\",\n app: \"https://staging.latitude.so\",\n ingest: \"https://staging-ingest.latitude.so\",\n}\nconst DEV_ENV: EnvironmentConfig = {\n name: \"dev\",\n label: \"local dev\",\n app: \"http://localhost:3000\",\n ingest: \"http://localhost:3002\",\n}\n\nfunction urlsFor(env: EnvironmentConfig): {\n apiKeys: string\n projects: string\n projectView: (slug: string) => string\n} {\n return {\n apiKeys: `${env.app}/settings/api-keys`,\n projects: env.app,\n projectView: (slug: string) => `${env.app}/projects/${slug}`,\n }\n}\n\ninterface InstallFlags {\n apiKey?: string | undefined\n project?: string | undefined\n environment?: EnvironmentConfig | undefined\n /**\n * Tristate: `true` = capture (`--allow-conversation`), `false` = scrub\n * (`--no-content`), `undefined` = preserve existing or first-install\n * default. Tristate keeps re-install idempotent for hand-edited values.\n */\n allowConversationAccess?: boolean | undefined\n /** When true, skip adding the plugin id to `plugins.allow`. */\n noTrust?: boolean\n noPrompt?: boolean\n yes?: boolean\n}\n\nexport function parseFlags(argv: string[]): {\n subcommand: string | undefined\n flags: Record<string, string | boolean>\n} {\n const [subcommand, ...rest] = argv\n const flags: Record<string, string | boolean> = {}\n for (const arg of rest) {\n if (!arg.startsWith(\"--\")) continue\n const eq = arg.indexOf(\"=\")\n if (eq >= 0) {\n flags[arg.slice(2, eq)] = arg.slice(eq + 1)\n } else {\n flags[arg.slice(2)] = true\n }\n }\n return { subcommand, flags }\n}\n\nexport function normalizeInstallFlags(flags: Record<string, string | boolean>): InstallFlags {\n let environment: EnvironmentConfig | undefined\n if (flags.staging === true) environment = STAGING_ENV\n if (flags.dev === true) {\n if (environment) throw new Error(\"--staging and --dev are mutually exclusive\")\n environment = DEV_ENV\n }\n // Tristate: leave undefined unless the user explicitly asked one way or the\n // other. Re-install then preserves whatever's already in openclaw.json.\n let allowConversationAccess: boolean | undefined\n if (flags[\"no-content\"] === true || flags[\"no-conversation\"] === true) allowConversationAccess = false\n if (flags[\"allow-conversation\"] === true) allowConversationAccess = true\n\n return {\n apiKey: typeof flags[\"api-key\"] === \"string\" ? flags[\"api-key\"] : undefined,\n project: typeof flags.project === \"string\" ? flags.project : undefined,\n environment,\n allowConversationAccess,\n noTrust: flags[\"no-trust\"] === true,\n noPrompt: flags[\"no-prompt\"] === true || flags.yes === true,\n yes: flags.yes === true,\n }\n}\n\n// ─── Install ────────────────────────────────────────────────────────────────\n\nexport async function runInstall(flags: InstallFlags = {}): Promise<void> {\n const canPrompt = !flags.noPrompt && process.stdin.isTTY === true\n if (!canPrompt) return runFlagDrivenInstall(flags)\n await runInteractiveInstall(flags)\n}\n\nasync function runInteractiveInstall(flags: InstallFlags): Promise<void> {\n intro(pc.bgCyan(pc.black(\" Latitude · OpenClaw telemetry \")))\n\n // Bail before any prompts if the host OpenClaw is too old. We'd rather\n // tell the user to upgrade than walk them through a config we can't make\n // work on their version.\n ensureOpenclawIsCompatible()\n\n const existing = readSettings()\n const existingConfig =\n (existing.plugins?.entries?.[PLUGIN_ID]?.config as LatitudePluginConfig | undefined) ?? undefined\n const envConfig = flags.environment ?? PRODUCTION_ENV\n const urls = urlsFor(envConfig)\n\n const aboutLines = [\n \"Captures every OpenClaw agent run and ships it to Latitude as\",\n \"OpenTelemetry traces — full system prompt, tool I/O, messages,\",\n \"token usage, and agent name on every span.\",\n \"\",\n `${pc.dim(\"Docs\")} ${pc.cyan(DOCS_URL)}`,\n ]\n if (envConfig.name !== \"production\") {\n aboutLines.push(\"\", pc.yellow(`Using ${envConfig.label} environment (${envConfig.ingest})`))\n }\n note(aboutLines.join(\"\\n\"), \"About\")\n\n log.info(`Get an API key at ${pc.cyan(urls.apiKeys)}`)\n log.info(`Create a project at ${pc.cyan(urls.projects)}`)\n\n const apiKey = await promptApiKey(existingConfig?.apiKey, flags.apiKey)\n const project = await promptProject(existingConfig?.project, flags.project)\n\n await applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess: flags.allowConversationAccess,\n noTrust: flags.noTrust === true,\n })\n\n note(\n [\n \"Restart the OpenClaw gateway for the plugin to load:\",\n pc.dim(\" openclaw gateway restart\"),\n \"\",\n `View your traces at ${pc.cyan(urls.projectView(project))}`,\n ].join(\"\\n\"),\n \"Next step\",\n )\n outro(pc.green(\"✓ Installed\"))\n}\n\nasync function runFlagDrivenInstall(flags: InstallFlags): Promise<void> {\n ensureOpenclawIsCompatible()\n const apiKey = flags.apiKey\n const project = flags.project\n if (!apiKey || !project) {\n throw new Error(\"Non-interactive install requires --api-key=... and --project=... (or run in a TTY).\")\n }\n const envConfig = flags.environment ?? PRODUCTION_ENV\n await applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess: flags.allowConversationAccess,\n noTrust: flags.noTrust === true,\n })\n process.stdout.write(`Installed Latitude plugin in ${SETTINGS_PATH}\\n`)\n}\n\nasync function promptApiKey(_existing: string | undefined, flag: string | undefined): Promise<string> {\n if (flag) return flag\n const result = await password({\n message: \"Latitude API key\",\n mask: \"•\",\n validate: (v) => (v && v.length > 0 ? undefined : \"Required\"),\n })\n if (isCancel(result)) return onCancel()\n return result\n}\n\nasync function promptProject(existing: string | undefined, flag: string | undefined): Promise<string> {\n if (flag) return flag\n const result = await text({\n message: \"Latitude project slug\",\n placeholder: existing ?? \"my-openclaw-project\",\n ...(existing ? { initialValue: existing } : {}),\n validate: (v) => (v && v.length > 0 ? undefined : \"Required\"),\n })\n if (isCancel(result)) return onCancel()\n return result\n}\n\nfunction onCancel(): never {\n cancel(\"Cancelled — nothing was changed\")\n process.exit(1)\n}\n\ninterface ApplyParams {\n apiKey: string\n project: string\n envConfig: EnvironmentConfig\n /** Tristate — see `InstallFlags.allowConversationAccess`. */\n allowConversationAccess: boolean | undefined\n noTrust: boolean\n}\n\nasync function applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess,\n noTrust,\n}: ApplyParams): Promise<void> {\n // 1. Take the backup BEFORE openclaw plugins install touches openclaw.json\n // so it represents the user's true pre-install state. `openclaw plugins\n // install` creates plugins.entries[id], so backing up after would lose\n // the original \"no entry\" state and make recovery harder if any later\n // step fails.\n ensureSettingsDir()\n backupSettings()\n\n // 2. Hand placement off to OpenClaw. `openclaw plugins install <path>`\n // copies our package into ~/.openclaw/extensions/<encoded-id>/, writes\n // the install record into ~/.openclaw/plugins/installs.json, and\n // creates a (disabled, configless) plugins.entries[id] in\n // openclaw.json. We layer config + hooks + allow on top in step 3.\n // --force lets us overwrite an existing install (e.g. when re-running\n // on top of a previous version).\n const packageRoot = resolvePackageRoot()\n const installSpinner = spinner()\n installSpinner.start(`Installing plugin via openclaw plugins install ${packageRoot}`)\n const installResult = runOpenclaw([\"plugins\", \"install\", packageRoot, \"--force\"], { timeoutMs: 60_000 })\n if (!installResult.ok) {\n installSpinner.stop(\"openclaw plugins install failed\")\n if (installResult.reason === \"enoent\") {\n throw new Error(\"`openclaw` not found on PATH. Install OpenClaw first (https://openclaw.ai/install) and re-run.\")\n }\n if (installResult.reason === \"timeout\") {\n throw new Error(\"openclaw plugins install timed out after 60s. Try running it manually to see what's stuck.\")\n }\n const detail = installResult.stderr.trim() || installResult.stdout.trim() || `exit code ${installResult.code}`\n throw new Error(`openclaw plugins install failed: ${detail}`)\n }\n installSpinner.stop(\"Plugin registered with OpenClaw\")\n\n // 3. Layer our config, hooks, and (optionally) plugins.allow on top of the\n // entry OpenClaw just created. We don't touch placement — that's\n // OpenClaw's job — only the policy fields.\n const settingsSpinner = spinner()\n settingsSpinner.start(\"Updating openclaw.json\")\n const settings = readSettings()\n // Sweep `LATITUDE_*` keys our 0.0.1 leaked under settings.env. Idempotent\n // when the keys aren't there.\n migrateLegacyEntries(settings)\n\n // Pass the install-flag tristate through verbatim — `setPluginEntry`\n // resolves the final value by consulting (in order): the patch, the\n // existing config block, the existing hooks block, then defaulting to\n // `true`. Going through that resolver matters because a 0.0.1 install\n // may have stored the flag only under hooks; if we pre-collapsed it\n // here using existing config alone, we'd lose that signal and silently\n // overwrite the user's intent.\n setPluginEntry(settings, {\n apiKey,\n project,\n baseUrl: envConfig.name === \"production\" ? undefined : envConfig.ingest,\n allowConversationAccess,\n // `debug` is intentionally not passed — `setPluginEntry` preserves\n // hand-edited values; fresh installs leave the key absent (runtime\n // default is `false`).\n })\n\n // Plugins.allow handling. Running `npx install` is itself the trust\n // signal — auto-add unless the user explicitly opted out via --no-trust.\n // Without this, OpenClaw prints a \"plugins.allow is empty\" warning at\n // every gateway start.\n if (!noTrust) {\n addToPluginsAllow(settings)\n }\n\n writeSettings(settings)\n settingsSpinner.stop(`Updated ${SETTINGS_PATH}`)\n if (existsSync(SETTINGS_BACKUP_PATH)) log.info(`Backup saved at ${pc.dim(SETTINGS_BACKUP_PATH)}`)\n if (noTrust) {\n log.warning(\n `--no-trust set; OpenClaw will warn at every gateway start that ${PLUGIN_ID} is untrusted. Add it to plugins.allow yourself when you're ready.`,\n )\n }\n}\n\nfunction ensureSettingsDir(): void {\n const dir = dirname(SETTINGS_PATH)\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true })\n}\n\n/**\n * Verify `openclaw` is on PATH AND its version is >= MIN_OPENCLAW_VERSION.\n * Aborts with a clear upgrade message otherwise. Called before any user\n * prompts so we don't waste their time collecting credentials we can't\n * use.\n */\nfunction ensureOpenclawIsCompatible(): void {\n const v = getOpenclawVersion()\n if (!v.ok) {\n if (v.error === \"missing\") {\n cancel(\"OpenClaw CLI not found on PATH. Install or update via `npm install -g openclaw@latest` and re-run.\")\n process.exit(1)\n }\n cancel(\n `Couldn't parse OpenClaw version output${v.raw ? ` (got: ${pc.dim(v.raw)})` : \"\"}. Run \\`openclaw --version\\` and report the output.`,\n )\n process.exit(1)\n }\n if (compareCalver(v.version, MIN_OPENCLAW_VERSION) < 0) {\n cancel(\n `OpenClaw ${v.version} is older than the minimum supported version (${MIN_OPENCLAW_VERSION}). Run \\`npm install -g openclaw@latest\\` and re-run install.`,\n )\n process.exit(1)\n }\n log.info(`OpenClaw ${pc.dim(v.version)} (>= ${MIN_OPENCLAW_VERSION})`)\n}\n\n/**\n * Resolve the absolute path to our package's root (the directory that\n * contains `package.json` + `openclaw.plugin.json` + `dist/`). The compiled\n * CLI lives at `<package-root>/dist/cli.js`, so `import.meta.url`'s parent\n * directory's parent is our root.\n *\n * `openclaw plugins install <path>` copies files synchronously, so we\n * don't have to keep this directory alive past the spawn return.\n */\nfunction resolvePackageRoot(): string {\n const here = dirname(fileURLToPath(import.meta.url))\n return resolve(here, \"..\")\n}\n\n// ─── Uninstall ──────────────────────────────────────────────────────────────\n\ninterface UninstallFlags {\n noPrompt?: boolean\n}\n\nexport async function runUninstall(flags: UninstallFlags = {}): Promise<void> {\n intro(pc.bgYellow(pc.black(\" Latitude · OpenClaw telemetry — uninstall \")))\n\n const settings = readSettings()\n const hasEntry = hasLatitudePlugin(settings)\n\n // Without an entry, there's nothing for `openclaw plugins uninstall` to\n // remove either — short-circuit cleanly. If the user wants to uninstall\n // even when our entry is gone (e.g. we crashed mid-install), they can\n // run `openclaw plugins uninstall` themselves.\n if (!hasEntry) {\n note(\"No Latitude plugin entry found — nothing to remove.\", \"Status\")\n outro(pc.dim(\"Nothing changed\"))\n return\n }\n\n const plan = [\n `Run \\`openclaw plugins uninstall ${PLUGIN_ID} --force\\` (removes files, install record, and plugin entry)`,\n `Sweep any leftover LATITUDE_* keys from settings.env`,\n `Backup of openclaw.json saved at ${SETTINGS_BACKUP_PATH}`,\n ]\n note(plan.join(\"\\n\"), \"Plan\")\n\n if (!flags.noPrompt && process.stdin.isTTY === true) {\n const ok = await confirm({ message: \"Proceed?\", initialValue: true })\n if (isCancel(ok) || ok !== true) return onCancel()\n }\n\n // 1. Take the backup BEFORE openclaw plugins uninstall touches\n // openclaw.json so it represents the user's true pre-uninstall state\n // (entry + config + plugins.allow). Backing up after would capture\n // the already-stripped state, defeating the point of the backup.\n backupSettings()\n\n // 2. Hand uninstall to OpenClaw. It removes files, the install record,\n // plugins.entries[id], plugins.allow, plugins.deny, plugins.load.paths\n // — see src/plugins/uninstall.ts.\n const s = spinner()\n s.start(\"Reverting via openclaw plugins uninstall\")\n const uninstallResult = runOpenclaw([\"plugins\", \"uninstall\", PLUGIN_ID, \"--force\"], { timeoutMs: 60_000 })\n if (!uninstallResult.ok) {\n s.stop(\"openclaw plugins uninstall failed\")\n if (uninstallResult.reason === \"enoent\") {\n log.warning(\n \"`openclaw` not found on PATH. Falling back to local cleanup — files at ~/.openclaw/extensions/ may remain.\",\n )\n } else {\n const detail =\n uninstallResult.stderr.trim() || uninstallResult.stdout.trim() || `exit code ${uninstallResult.code}`\n log.warning(`openclaw plugins uninstall reported: ${detail}. Continuing with local cleanup.`)\n }\n } else {\n s.stop(\"Plugin removed by OpenClaw\")\n }\n\n // 3. Defensive cleanup — `openclaw plugins uninstall` already strips the\n // entry and plugins.allow on success, but if it failed above (enoent\n // / non-zero) we still want our state out of openclaw.json. These\n // are all idempotent.\n const cleanupSpinner = spinner()\n cleanupSpinner.start(\"Reverting openclaw.json\")\n const post = readSettings()\n removePluginEntry(post)\n removeFromPluginsAllow(post)\n migrateLegacyEntries(post)\n writeSettings(post)\n cleanupSpinner.stop(\"Done\")\n outro(pc.green(\"✓ Uninstalled\"))\n}\n","import { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { normalizeInstallFlags, parseFlags, runInstall, runUninstall } from \"./setup.ts\"\n\nconst USAGE = `usage: latitude-openclaw <command> [options]\n\ncommands:\n install Install the plugin (interactive when stdin is a TTY)\n uninstall Remove the plugin entry and files\n --version, -v Print the package version\n --help, -h Print this message\n\ninstall options:\n --api-key=<key> Pass the API key non-interactively\n --project=<slug> Pass the project slug non-interactively\n --staging Target https://staging.latitude.so / staging-ingest\n --dev Target http://localhost:3000 / 3002\n --no-content Skip raw prompt/response/tool I/O capture\n --allow-conversation Force conversation capture on (overrides existing config)\n --yes / --no-prompt Skip all prompts (required for non-TTY / CI)\n`\n\nfunction readVersion(): string {\n // dist/cli.js → ../package.json\n const here = dirname(fileURLToPath(import.meta.url))\n const pkgPath = join(here, \"..\", \"package.json\")\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version?: string }\n return pkg.version ?? \"unknown\"\n } catch {\n return \"unknown\"\n }\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2)\n // Top-level --version / --help are handled before parseFlags so users can\n // type them as the first arg without hitting the \"unknown subcommand\" path.\n if (argv[0] === \"--version\" || argv[0] === \"-v\") {\n process.stdout.write(`${readVersion()}\\n`)\n return\n }\n if (argv[0] === \"--help\" || argv[0] === \"-h\") {\n process.stdout.write(USAGE)\n return\n }\n\n const { subcommand, flags } = parseFlags(argv)\n if (subcommand === \"install\" || subcommand === undefined) {\n await runInstall(normalizeInstallFlags(flags))\n return\n }\n if (subcommand === \"uninstall\") {\n await runUninstall({ noPrompt: flags[\"no-prompt\"] === true || flags.yes === true })\n return\n }\n process.stderr.write(`unknown subcommand: ${subcommand}\\n`)\n process.stderr.write(USAGE)\n process.exit(1)\n}\n\nmain().catch((err) => {\n process.stderr.write(`${String(err)}\\n`)\n process.exit(1)\n})\n"],"mappings":"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"}
package/dist/plugin.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
2
3
  import { arch, hostname, platform, release } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  //#region src/client.ts
4
7
  async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs = 1e4 }) {
5
8
  const url = `${baseUrl.replace(/\/+$/, "")}/v1/traces`;
@@ -74,7 +77,24 @@ function createLogger(debugEnabled) {
74
77
  //#endregion
75
78
  //#region src/otlp.ts
76
79
  const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
77
- const SCOPE_VERSION = "0.0.2";
80
+ /**
81
+ * Runtime-read package version, so OTLP `scope.version` and `service.version`
82
+ * always reflect what's actually installed. Read once at module load and
83
+ * cached. Falls back to `"unknown"` if the read fails (an exporter accepting
84
+ * any string is more tolerant than a stale hard-coded literal).
85
+ *
86
+ * Same import.meta.url + ../package.json pattern used by `cli.ts` for the
87
+ * `--version` flag — single source of truth in package.json.
88
+ */
89
+ const SCOPE_VERSION = readScopeVersion();
90
+ function readScopeVersion() {
91
+ try {
92
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
93
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "unknown";
94
+ } catch {
95
+ return "unknown";
96
+ }
97
+ }
78
98
  /** Build an OTLP export request for a single completed agent run. */
79
99
  function buildOtlpRequest(run, options) {
80
100
  const spans = buildRunSpans(run, options);
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":[],"sources":["../src/client.ts","../src/config.ts","../src/logger.ts","../src/otlp.ts","../src/turn-builder.ts","../src/plugin.ts"],"sourcesContent":["import type { Logger } from \"./logger.ts\"\nimport type { OtlpExportRequest } from \"./types.ts\"\n\nexport async function postTraces({\n baseUrl,\n apiKey,\n project,\n payload,\n logger,\n timeoutMs = 10_000,\n}: {\n baseUrl: string\n apiKey: string\n project: string\n payload: OtlpExportRequest\n logger: Logger\n timeoutMs?: number\n}): Promise<void> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/v1/traces`\n const bodyText = JSON.stringify(payload)\n logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`)\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"X-Latitude-Project\": project,\n },\n body: bodyText,\n signal: controller.signal,\n })\n if (!res.ok) {\n const text = await res.text().catch(() => \"\")\n logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`)\n } else {\n logger.debug(`ingest HTTP ${res.status}`)\n }\n } catch (err) {\n logger.warn(`ingest failed: ${String(err)}`)\n } finally {\n clearTimeout(timer)\n }\n}\n","export interface Config {\n apiKey: string\n baseUrl: string\n project: string\n enabled: boolean\n debug: boolean\n /**\n * When false, the plugin still emits one span per LLM call / tool / run, but\n * scrubs raw conversation content (input/output messages, system prompt,\n * tool args, tool results, the surfaced first-prompt). Token counts, model\n * names, agent ids, and timings are unaffected.\n */\n allowConversationAccess: boolean\n}\n\nconst DEFAULT_BASE_URL = \"https://ingest.latitude.so\"\n\n/**\n * Build a `Config` from OpenClaw's per-plugin config bucket plus environment\n * variables. The plugin SDK passes `api.pluginConfig` (the user's\n * `plugins.entries[id].config` block) to the registration function — that's\n * the primary source. Env vars are kept as a fallback so existing deployments\n * with `LATITUDE_*` already exported in the gateway environment keep working,\n * and so that `LATITUDE_DEBUG=1` can be flipped without editing openclaw.json.\n */\nexport function loadConfig(\n pluginConfig: Record<string, unknown> | undefined = undefined,\n env: NodeJS.ProcessEnv = process.env,\n): Config {\n const fromOpts = pluginConfig ?? {}\n\n const apiKey = pickString(fromOpts.apiKey) ?? env.LATITUDE_API_KEY ?? \"\"\n const project = pickString(fromOpts.project) ?? env.LATITUDE_PROJECT ?? \"\"\n const baseUrl = pickString(fromOpts.baseUrl) ?? env.LATITUDE_BASE_URL ?? DEFAULT_BASE_URL\n\n const debug = pickBool(fromOpts.debug) ?? env.LATITUDE_DEBUG === \"1\"\n const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false\n\n // Allow either env var or pluginConfig to disable. Falsy `enabled: false` in\n // pluginConfig wins; otherwise we require both api key and project.\n const explicitlyDisabled = pickBool(fromOpts.enabled) === false || (env.LATITUDE_OPENCLAW_ENABLED ?? \"1\") === \"0\"\n const hasCreds = apiKey !== \"\" && project !== \"\"\n\n return {\n apiKey,\n baseUrl,\n project,\n debug,\n allowConversationAccess,\n enabled: hasCreds && !explicitlyDisabled,\n }\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined\n}\n\nfunction pickBool(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined\n}\n","const PREFIX = \"[latitude-openclaw]\"\n\nexport interface Logger {\n debug: (msg: string) => void\n warn: (msg: string) => void\n}\n\nexport function createLogger(debugEnabled: boolean): Logger {\n return {\n debug: debugEnabled ? (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`) : () => {},\n warn: (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`),\n }\n}\n","import { createHash } from \"node:crypto\"\nimport { arch, hostname, platform, release } from \"node:os\"\nimport type {\n LlmCallRecord,\n OtlpExportRequest,\n OtlpKeyValue,\n OtlpResourceSpans,\n OtlpSpan,\n RunRecord,\n ToolCallRecord,\n} from \"./types.ts\"\n\nconst SCOPE_NAME = \"@latitude-data/openclaw-telemetry\"\nconst SCOPE_VERSION = \"0.0.2\"\n\ninterface BuildOptions {\n /**\n * When false, content attributes are scrubbed from spans:\n * - `user_prompt` (interaction)\n * - `gen_ai.system_instructions`, `gen_ai.input.messages`,\n * `gen_ai.output.messages` (llm_request)\n * - `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result` (tool_execution)\n * Timing, token usage, model name, ids, agent name, and counts are\n * always emitted regardless of this flag.\n */\n allowConversationAccess: boolean\n}\n\n/** Build an OTLP export request for a single completed agent run. */\nexport function buildOtlpRequest(run: RunRecord, options: BuildOptions): OtlpExportRequest {\n const spans = buildRunSpans(run, options)\n const rs: OtlpResourceSpans = {\n resource: { attributes: resourceAttrs() },\n scopeSpans: [{ scope: { name: SCOPE_NAME, version: SCOPE_VERSION }, spans }],\n }\n return { resourceSpans: [rs] }\n}\n\nfunction buildRunSpans(run: RunRecord, options: BuildOptions): OtlpSpan[] {\n const traceId = hashHex(`${run.sessionId ?? \"session\"}:${run.runId}`, 32)\n const interactionSpanId = hashHex(`${traceId}:run`, 16)\n const out: OtlpSpan[] = [buildInteractionSpan(traceId, interactionSpanId, run, options)]\n\n run.llmCalls.forEach((call, idx) => {\n const callSpanId = hashHex(`${traceId}:call:${idx}`, 16)\n out.push(buildLlmSpan(traceId, interactionSpanId, callSpanId, call, idx, run, options))\n // Tool spans are siblings of the llm_request, parented on the interaction\n // span so the run timeline renders as: llm → tool → llm → tool → ...\n call.toolCalls.forEach((tool, tIdx) => {\n const toolSpanId = hashHex(`${traceId}:call:${idx}:tool:${tIdx}`, 16)\n out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run, options))\n })\n })\n run.orphanTools.forEach((tool, idx) => {\n const toolSpanId = hashHex(`${traceId}:orphan-tool:${idx}`, 16)\n out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run, options))\n })\n\n return out\n}\n\nfunction buildInteractionSpan(traceId: string, spanId: string, run: RunRecord, options: BuildOptions): OtlpSpan {\n const startNs = msToNs(run.startMs)\n const endNs = msToNs(run.endMs ?? run.startMs)\n const totalTools = run.llmCalls.reduce((sum, c) => sum + c.toolCalls.length, 0) + run.orphanTools.length\n const totalUsage = aggregateUsage(run.llmCalls)\n\n return {\n traceId,\n spanId,\n parentSpanId: \"\",\n name: \"interaction\",\n kind: 1,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: stripUndef([\n str(\"span.type\", \"interaction\"),\n str(\"interaction.kind\", \"agent_run\"),\n str(\"openclaw.run.id\", run.runId),\n run.sessionId ? str(\"openclaw.session.id\", run.sessionId) : undefined,\n run.sessionId ? str(\"session.id\", run.sessionId) : undefined,\n run.sessionKey ? str(\"openclaw.session.key\", run.sessionKey) : undefined,\n run.agentId ? str(\"openclaw.agent.id\", run.agentId) : undefined,\n run.agentId ? str(\"openclaw.agent.name\", run.agentId) : undefined,\n run.workspaceDir ? str(\"openclaw.workspace.dir\", run.workspaceDir) : undefined,\n run.messageProvider ? str(\"openclaw.message.provider\", run.messageProvider) : undefined,\n run.channelId ? str(\"openclaw.channel.id\", run.channelId) : undefined,\n run.trigger ? str(\"openclaw.trigger\", run.trigger) : undefined,\n run.modelProviderId ? str(\"openclaw.model.provider.id\", run.modelProviderId) : undefined,\n run.modelId ? str(\"openclaw.model.id\", run.modelId) : undefined,\n int(\"interaction.duration_ms\", durationMs(run.startMs, run.endMs)),\n int(\"interaction.call_count\", run.llmCalls.length),\n int(\"interaction.tool_call_count\", totalTools),\n run.success !== undefined ? bool(\"openclaw.run.success\", run.success) : undefined,\n run.error ? str(\"openclaw.run.error\", run.error) : undefined,\n totalUsage.input !== undefined ? int(\"gen_ai.usage.input_tokens\", totalUsage.input) : undefined,\n totalUsage.output !== undefined ? int(\"gen_ai.usage.output_tokens\", totalUsage.output) : undefined,\n totalUsage.cacheRead !== undefined\n ? int(\"gen_ai.usage.cache_read_input_tokens\", totalUsage.cacheRead)\n : undefined,\n totalUsage.cacheWrite !== undefined\n ? int(\"gen_ai.usage.cache_creation_input_tokens\", totalUsage.cacheWrite)\n : undefined,\n totalUsage.total !== undefined ? int(\"gen_ai.usage.total_tokens\", totalUsage.total) : undefined,\n // Surface the first user prompt so the Latitude UI has something\n // recognisable in the interaction list — only when the operator opted\n // in to conversation capture.\n options.allowConversationAccess && run.llmCalls[0]?.prompt\n ? str(\"user_prompt\", run.llmCalls[0].prompt)\n : undefined,\n bool(\"latitude.captured.content\", options.allowConversationAccess),\n ]),\n status: { code: run.success === false ? 2 : 1 },\n }\n}\n\nfunction buildLlmSpan(\n traceId: string,\n parentSpanId: string,\n spanId: string,\n call: LlmCallRecord,\n callIdx: number,\n run: RunRecord,\n options: BuildOptions,\n): OtlpSpan {\n const startNs = msToNs(call.startMs)\n const endNs = msToNs(call.endMs ?? call.startMs)\n\n const captureContent = options.allowConversationAccess\n const inputMessages = captureContent ? buildInputMessages(call) : undefined\n const outputMessages = captureContent ? buildOutputMessages(call) : undefined\n const systemInstructions =\n captureContent && call.systemPrompt ? JSON.stringify([{ type: \"text\", content: call.systemPrompt }]) : undefined\n\n return {\n traceId,\n spanId,\n parentSpanId,\n name: \"llm_request\",\n kind: 3,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: stripUndef([\n str(\"span.type\", \"llm_request\"),\n str(\"gen_ai.operation.name\", \"chat\"),\n str(\"llm_request.context\", \"interaction\"),\n int(\"llm_request.call_index\", callIdx),\n // Provider/model — capture every variant OpenClaw exposes so consumers\n // can filter on whichever form they already use.\n str(\"gen_ai.system\", call.provider),\n str(\"openclaw.provider\", call.provider),\n str(\"gen_ai.request.model\", call.requestModel),\n str(\"model\", call.requestModel),\n call.responseModel ? str(\"gen_ai.response.model\", call.responseModel) : undefined,\n call.resolvedRef ? str(\"openclaw.resolved.ref\", call.resolvedRef) : undefined,\n // Identity — the agent name tag the user specifically asked for is here\n // under both canonical and convenience keys.\n run.sessionId ? str(\"session.id\", run.sessionId) : undefined,\n run.sessionKey ? str(\"openclaw.session.key\", run.sessionKey) : undefined,\n str(\"openclaw.run.id\", call.runId),\n call.agentId ? str(\"openclaw.agent.id\", call.agentId) : undefined,\n call.agentId ? str(\"openclaw.agent.name\", call.agentId) : undefined,\n // Token usage — input / output / cache / total, in both gen_ai.* and\n // legacy aliases for backwards compatibility with existing dashboards.\n call.usage?.input !== undefined ? int(\"gen_ai.usage.input_tokens\", call.usage.input) : undefined,\n call.usage?.input !== undefined ? int(\"input_tokens\", call.usage.input) : undefined,\n call.usage?.output !== undefined ? int(\"gen_ai.usage.output_tokens\", call.usage.output) : undefined,\n call.usage?.output !== undefined ? int(\"output_tokens\", call.usage.output) : undefined,\n call.usage?.cacheRead !== undefined\n ? int(\"gen_ai.usage.cache_read_input_tokens\", call.usage.cacheRead)\n : undefined,\n call.usage?.cacheRead !== undefined ? int(\"cache_read_tokens\", call.usage.cacheRead) : undefined,\n call.usage?.cacheWrite !== undefined\n ? int(\"gen_ai.usage.cache_creation_input_tokens\", call.usage.cacheWrite)\n : undefined,\n call.usage?.cacheWrite !== undefined ? int(\"cache_creation_tokens\", call.usage.cacheWrite) : undefined,\n call.usage?.total !== undefined ? int(\"gen_ai.usage.total_tokens\", call.usage.total) : undefined,\n // Content — system prompt + full message arrays. Gated on\n // allowConversationAccess. Even when off, we emit the structural\n // attributes above (model, tokens, ids, agent name).\n systemInstructions ? str(\"gen_ai.system_instructions\", systemInstructions) : undefined,\n inputMessages ? str(\"gen_ai.input.messages\", JSON.stringify(inputMessages)) : undefined,\n outputMessages ? str(\"gen_ai.output.messages\", JSON.stringify(outputMessages)) : undefined,\n bool(\"latitude.captured.content\", captureContent),\n // Misc signals.\n int(\"openclaw.images.count\", call.imagesCount),\n int(\"llm_request.tool_call_count\", call.toolCalls.length),\n int(\"llm_request.duration_ms\", durationMs(call.startMs, call.endMs)),\n call.error ? str(\"error.type\", \"llm_error\") : undefined,\n call.error ? str(\"error.message\", call.error) : undefined,\n str(\"success\", call.error ? \"false\" : \"true\"),\n str(\"llm_request.captured\", \"true\"),\n ]),\n status: { code: call.error ? 2 : 1 },\n }\n}\n\nfunction buildToolSpan(\n traceId: string,\n parentSpanId: string,\n spanId: string,\n tool: ToolCallRecord,\n run: RunRecord,\n options: BuildOptions,\n): OtlpSpan {\n const startNs = msToNs(tool.startMs)\n const endNs = msToNs(tool.endMs ?? tool.startMs)\n const isError = Boolean(tool.error)\n const captureContent = options.allowConversationAccess\n return {\n traceId,\n spanId,\n parentSpanId,\n name: `tool:${tool.toolName}`,\n kind: 1,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: stripUndef([\n str(\"span.type\", \"tool_execution\"),\n str(\"gen_ai.operation.name\", \"execute_tool\"),\n str(\"gen_ai.tool.name\", tool.toolName),\n str(\"gen_ai.tool.call.id\", tool.toolCallId),\n // Tool args + result are content. Gate on allowConversationAccess —\n // when off, we still emit the call's name, id, duration, error state,\n // and agent name so the timeline + counters still work.\n captureContent ? str(\"gen_ai.tool.call.arguments\", safeJson(tool.params)) : undefined,\n captureContent && tool.result !== undefined ? str(\"gen_ai.tool.call.result\", safeJson(tool.result)) : undefined,\n bool(\"latitude.captured.content\", captureContent),\n isError ? str(\"error.type\", \"tool_error\") : undefined,\n isError ? str(\"error.message\", tool.error ?? \"\") : undefined,\n bool(\"tool.is_error\", isError),\n str(\"success\", isError ? \"false\" : \"true\"),\n tool.durationMs !== undefined ? int(\"tool.duration_ms\", tool.durationMs) : undefined,\n run.sessionId ? str(\"session.id\", run.sessionId) : undefined,\n run.sessionKey ? str(\"openclaw.session.key\", run.sessionKey) : undefined,\n str(\"openclaw.run.id\", run.runId),\n tool.agentId ? str(\"openclaw.agent.id\", tool.agentId) : undefined,\n tool.agentId ? str(\"openclaw.agent.name\", tool.agentId) : undefined,\n ]),\n status: { code: isError ? 2 : 1 },\n }\n}\n\n// ─── Message shape helpers ──────────────────────────────────────────────────\n//\n// Latitude UI expects `{ role, parts: [{ type, content|... }] }` objects.\n// Build both the input (history + current prompt) and output (assistant + any\n// tool_calls from this call) arrays in that shape, passing through whatever\n// OpenClaw handed us as-is where the shape is already usable.\n\ninterface MessagePart {\n type: string\n [key: string]: unknown\n}\n\ninterface Message {\n role: \"system\" | \"user\" | \"assistant\" | \"tool\"\n parts: MessagePart[]\n}\n\nfunction buildInputMessages(call: LlmCallRecord): Message[] {\n const out: Message[] = []\n // OpenClaw's `historyMessages` is typed `unknown[]`; we trust it enough to\n // pass through but normalize simple shapes so the Latitude UI has something\n // to render. Complex objects fall back to a JSON stringification.\n for (const msg of call.historyMessages) {\n const normalized = normalizeHistoryMessage(msg)\n if (normalized) out.push(normalized)\n }\n if (call.prompt.length > 0) {\n out.push({ role: \"user\", parts: [{ type: \"text\", content: call.prompt }] })\n }\n return out\n}\n\nconst ALLOWED_ROLES: ReadonlySet<Message[\"role\"]> = new Set([\"system\", \"user\", \"assistant\", \"tool\"])\n\nfunction normalizeRole(raw: unknown): Message[\"role\"] {\n // Provider adapters occasionally emit roles outside the canonical set\n // (e.g. OpenAI's \"developer\"). Coerce unknown roles to \"user\" so the\n // downstream Latitude UI gets a payload it can render.\n if (typeof raw !== \"string\") return \"user\"\n return ALLOWED_ROLES.has(raw as Message[\"role\"]) ? (raw as Message[\"role\"]) : \"user\"\n}\n\nfunction normalizeHistoryMessage(raw: unknown): Message | undefined {\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n const role = normalizeRole(obj.role)\n const content = obj.content ?? obj.text ?? obj.message\n if (typeof content === \"string\") {\n return { role, parts: [{ type: \"text\", content }] }\n }\n if (Array.isArray(content)) {\n const parts: MessagePart[] = []\n for (const block of content) {\n const part = normalizeContentBlock(block)\n if (part) parts.push(part)\n }\n return { role, parts: parts.length > 0 ? parts : [{ type: \"text\", content: JSON.stringify(content) }] }\n }\n // Unknown shape — dump it as JSON so nothing is silently lost.\n return { role, parts: [{ type: \"text\", content: safeJson(raw) }] }\n}\n\nfunction normalizeContentBlock(raw: unknown): MessagePart | undefined {\n if (typeof raw === \"string\") return { type: \"text\", content: raw }\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n const type = typeof obj.type === \"string\" ? obj.type : \"text\"\n if (type === \"text\" && typeof obj.text === \"string\") return { type: \"text\", content: obj.text }\n if (type === \"tool_use\") {\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.input ?? {},\n }\n }\n if (type === \"tool_result\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.tool_use_id === \"string\" ? obj.tool_use_id : \"\",\n response: obj.content ?? \"\",\n }\n }\n if (type === \"image\") {\n return { type: \"uri\", modality: \"image\", uri: safeJson(obj.source ?? obj) }\n }\n return { type, content: safeJson(raw) }\n}\n\nfunction buildOutputMessages(call: LlmCallRecord): Message[] {\n const parts: MessagePart[] = []\n for (const text of call.assistantTexts) {\n if (text.length > 0) parts.push({ type: \"text\", content: text })\n }\n // Attach tool_call parts from tools invoked during this call so the output\n // message reads like the assistant message the model actually produced.\n for (const tool of call.toolCalls) {\n parts.push({\n type: \"tool_call\",\n id: tool.toolCallId,\n name: tool.toolName,\n arguments: tool.params,\n })\n }\n // Fall back to `lastAssistant` if we have no text/tools (edge case — empty\n // SSE response, failed run).\n if (parts.length === 0 && call.lastAssistant !== undefined) {\n parts.push({ type: \"text\", content: safeJson(call.lastAssistant) })\n }\n return [{ role: \"assistant\", parts }]\n}\n\n// ─── Utilities ──────────────────────────────────────────────────────────────\n\nfunction aggregateUsage(calls: LlmCallRecord[]): Required<Partial<import(\"./types.ts\").OpenClawLlmUsage>> {\n const agg = {\n input: undefined as number | undefined,\n output: undefined as number | undefined,\n cacheRead: undefined as number | undefined,\n cacheWrite: undefined as number | undefined,\n total: undefined as number | undefined,\n }\n const add = (k: keyof typeof agg, v: number | undefined): void => {\n if (v === undefined) return\n agg[k] = (agg[k] ?? 0) + v\n }\n for (const c of calls) {\n if (!c.usage) continue\n add(\"input\", c.usage.input)\n add(\"output\", c.usage.output)\n add(\"cacheRead\", c.usage.cacheRead)\n add(\"cacheWrite\", c.usage.cacheWrite)\n add(\"total\", c.usage.total)\n }\n return agg as Required<Partial<import(\"./types.ts\").OpenClawLlmUsage>>\n}\n\nfunction resourceAttrs(): OtlpKeyValue[] {\n return [\n str(\"service.name\", \"openclaw\"),\n str(\"service.version\", SCOPE_VERSION),\n str(\"host.name\", hostname()),\n str(\"host.arch\", arch()),\n str(\"os.type\", platform()),\n str(\"os.version\", release()),\n ]\n}\n\nfunction str(key: string, value: string): OtlpKeyValue {\n return { key, value: { stringValue: value } }\n}\n\nfunction int(key: string, value: number): OtlpKeyValue {\n return { key, value: { intValue: String(Math.trunc(value)) } }\n}\n\nfunction bool(key: string, value: boolean): OtlpKeyValue {\n return { key, value: { boolValue: value } }\n}\n\nfunction stripUndef(items: Array<OtlpKeyValue | undefined>): OtlpKeyValue[] {\n return items.filter((x): x is OtlpKeyValue => x !== undefined)\n}\n\nfunction hashHex(input: string, length: number): string {\n return createHash(\"sha256\").update(input).digest(\"hex\").slice(0, length)\n}\n\nfunction msToNs(ms: number): string {\n return (BigInt(Math.trunc(ms)) * 1_000_000n).toString()\n}\n\nfunction durationMs(startMs: number, endMs: number | undefined): number {\n if (endMs === undefined) return 0\n return Math.max(0, endMs - startMs)\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n","import { randomUUID } from \"node:crypto\"\nimport type {\n LlmCallRecord,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawSessionStartEvent,\n RunRecord,\n ToolCallRecord,\n} from \"./types.ts\"\n\n/**\n * Accumulates OpenClaw hook events per agent run (keyed by `runId`) into a\n * `RunRecord` ready to be converted to OTLP spans. All mutation is synchronous\n * and non-blocking so the hook runner can stay fire-and-forget.\n *\n * Event ordering assumptions (verified against OpenClaw\n * src/agents/pi-embedded-runner/run/attempt.ts):\n *\n * session_start? -> [ llm_input -> (before_tool_call -> after_tool_call)* -> llm_output ]+ -> agent_end\n *\n * Tool calls arriving between an `llm_input` and its `llm_output` are attached\n * to the currently-open LLM call. Tools arriving outside that window (e.g.\n * `after_tool_call` fires after `llm_output` has already closed the call) are\n * stored on the run's `orphanTools` list so we don't drop them.\n */\nexport class TurnBuilder {\n private readonly runs = new Map<string, RunRecord>()\n\n onSessionStart(_evt: OpenClawSessionStartEvent, _ctx: OpenClawAgentContext): void {\n // No-op for now — we lazily create RunRecords on the first `llm_input` for\n // a given runId. Kept as a hook point so we can later emit a\n // session-level span or capture `resumedFrom` metadata.\n }\n\n onLlmInput(evt: OpenClawLlmInputEvent, ctx: OpenClawAgentContext): LlmCallRecord {\n const run = this.ensureRun(evt.runId, ctx)\n const call: LlmCallRecord = {\n runId: evt.runId,\n sessionId: evt.sessionId,\n sessionKey: ctx.sessionKey,\n agentId: ctx.agentId,\n provider: evt.provider,\n requestModel: evt.model,\n responseModel: undefined,\n resolvedRef: undefined,\n systemPrompt: evt.systemPrompt,\n prompt: evt.prompt,\n historyMessages: evt.historyMessages,\n imagesCount: evt.imagesCount,\n assistantTexts: [],\n lastAssistant: undefined,\n usage: undefined,\n startMs: Date.now(),\n endMs: undefined,\n error: undefined,\n toolCalls: [],\n }\n run.llmCalls.push(call)\n return call\n }\n\n onBeforeToolCall(evt: OpenClawBeforeToolCallEvent, ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n // Create the run record lazily if a tool fires before we've seen llm_input\n // for this runId — rare but possible, and we prefer capturing an orphan\n // tool over dropping the event.\n const run = this.runs.get(evt.runId) ?? this.ensureRun(evt.runId, ctx)\n\n const tool: ToolCallRecord = {\n // Use a UUID when OpenClaw elides the id — name+timestamp can collide for\n // multiple invocations of the same tool within the same millisecond,\n // which would then cause `after_tool_call` to update the wrong record.\n toolCallId: evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`,\n toolName: evt.toolName,\n params: evt.params,\n result: undefined,\n error: undefined,\n startMs: Date.now(),\n endMs: undefined,\n durationMs: undefined,\n agentId: ctx.agentId,\n }\n const openCall = this.currentOpenCall(run)\n if (openCall) {\n openCall.toolCalls.push(tool)\n } else {\n run.orphanTools.push(tool)\n }\n }\n\n onAfterToolCall(evt: OpenClawAfterToolCallEvent, _ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n const tool = this.findToolRecord(run, evt.toolCallId, evt.toolName)\n if (!tool) return\n tool.result = evt.result\n tool.error = evt.error\n tool.durationMs = evt.durationMs\n tool.endMs = Date.now()\n }\n\n onLlmOutput(evt: OpenClawLlmOutputEvent, _ctx: OpenClawAgentContext): LlmCallRecord | undefined {\n const run = this.runs.get(evt.runId)\n if (!run) return undefined\n // Close the most recently-opened call that doesn't yet have an endMs —\n // the LLM loop is sequential, so this pairs 1:1 with `llm_input`.\n const openCall = this.currentOpenCall(run)\n if (!openCall) return undefined\n openCall.endMs = Date.now()\n openCall.assistantTexts = evt.assistantTexts\n openCall.lastAssistant = evt.lastAssistant\n openCall.usage = evt.usage\n openCall.responseModel = evt.model\n openCall.resolvedRef = evt.resolvedRef\n return openCall\n }\n\n onAgentEnd(evt: OpenClawAgentEndEvent, ctx: OpenClawAgentContext): RunRecord | undefined {\n const runId = ctx.runId\n if (!runId) return undefined\n const run = this.runs.get(runId)\n if (!run) return undefined\n run.endMs = Date.now()\n run.success = evt.success\n run.error = evt.error\n // Best-effort: close any still-open LLM call that never saw an `llm_output`\n // (e.g. when the run errored mid-call) so the span still has an end time.\n for (const call of run.llmCalls) {\n if (call.endMs === undefined) {\n call.endMs = run.endMs\n if (evt.error && call.error === undefined) call.error = evt.error\n }\n for (const tool of call.toolCalls) {\n if (tool.endMs === undefined) tool.endMs = run.endMs\n }\n }\n for (const tool of run.orphanTools) {\n if (tool.endMs === undefined) tool.endMs = run.endMs\n }\n this.runs.delete(runId)\n return run\n }\n\n /** Drop a run without emitting — used on errors from the emit path. */\n abandon(runId: string): void {\n this.runs.delete(runId)\n }\n\n /** Active runs count, for debug logging. */\n inflightCount(): number {\n return this.runs.size\n }\n\n private ensureRun(runId: string, ctx: OpenClawAgentContext): RunRecord {\n let run = this.runs.get(runId)\n if (run) return run\n run = {\n runId,\n sessionId: ctx.sessionId,\n sessionKey: ctx.sessionKey,\n agentId: ctx.agentId,\n workspaceDir: ctx.workspaceDir,\n messageProvider: ctx.messageProvider,\n trigger: ctx.trigger,\n channelId: ctx.channelId,\n modelProviderId: ctx.modelProviderId,\n modelId: ctx.modelId,\n startMs: Date.now(),\n endMs: undefined,\n success: undefined,\n error: undefined,\n llmCalls: [],\n orphanTools: [],\n }\n this.runs.set(runId, run)\n return run\n }\n\n private currentOpenCall(run: RunRecord): LlmCallRecord | undefined {\n for (let i = run.llmCalls.length - 1; i >= 0; i--) {\n const call = run.llmCalls[i]\n if (call && call.endMs === undefined) return call\n }\n return undefined\n }\n\n private findToolRecord(run: RunRecord, toolCallId: string | undefined, toolName: string): ToolCallRecord | undefined {\n // Try matching by toolCallId first since it's unique. Fall back to the\n // most recent unfinished record for the same name if the id is missing\n // or no record matches — defensive coverage for OpenClaw versions that\n // elide toolCallId on after_tool_call.\n const matchesId = (t: ToolCallRecord): boolean => Boolean(toolCallId && t.toolCallId === toolCallId)\n\n for (const call of run.llmCalls) {\n for (const t of call.toolCalls) if (matchesId(t)) return t\n }\n for (const t of run.orphanTools) if (matchesId(t)) return t\n\n for (let i = run.llmCalls.length - 1; i >= 0; i--) {\n const call = run.llmCalls[i]\n if (!call) continue\n for (let j = call.toolCalls.length - 1; j >= 0; j--) {\n const t = call.toolCalls[j]\n if (t && t.toolName === toolName && t.endMs === undefined) return t\n }\n }\n for (let i = run.orphanTools.length - 1; i >= 0; i--) {\n const t = run.orphanTools[i]\n if (t && t.toolName === toolName && t.endMs === undefined) return t\n }\n return undefined\n }\n}\n","import { postTraces } from \"./client.ts\"\nimport { type Config, loadConfig } from \"./config.ts\"\nimport { createLogger, type Logger } from \"./logger.ts\"\nimport { buildOtlpRequest } from \"./otlp.ts\"\nimport { TurnBuilder } from \"./turn-builder.ts\"\nimport type {\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawSessionStartEvent,\n RunRecord,\n} from \"./types.ts\"\n\n/**\n * Minimal structural type for OpenClaw's plugin API — only the fields we\n * touch. We avoid importing from `openclaw/plugin-sdk` so the package stays\n * usable when OpenClaw isn't installed (the CLI and tests don't need it),\n * and so we're robust to small signature changes across OpenClaw versions.\n *\n * `pluginConfig` is the user's `plugins.entries[id].config` block — that's\n * the canonical place to read credentials and feature flags. The OpenClaw\n * plugin SDK also exposes the same value as `api.pluginConfig` on the\n * builder API; keep both names in sync if the upstream contract evolves.\n */\nexport interface OpenClawPluginApiLike {\n logger?: Logger\n pluginConfig?: Record<string, unknown>\n on: <K extends string>(\n hookName: K,\n handler: (event: unknown, ctx: unknown) => unknown,\n opts?: { priority?: number },\n ) => void\n}\n\nexport interface RegisterOptions {\n /** Override the config, mostly for tests. */\n config?: Config\n /** Override the logger. */\n logger?: Logger\n /**\n * Hook to observe the emitted run right before it's posted. Used by tests;\n * not a stable public API.\n */\n onEmit?: (run: RunRecord) => void\n}\n\n/**\n * Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls\n * this once at plugin activation; we wire up `llm_input`, `llm_output`, tool\n * and lifecycle hooks to stream traces to Latitude.\n *\n * Every handler is fire-and-forget on OpenClaw's side (see\n * `src/plugins/hooks.ts` — runLlmInput/runLlmOutput are documented as\n * parallel and wrapped with `.catch()` at the call site in attempt.ts), so\n * nothing we do here can slow the agent loop.\n */\nexport default function registerLatitudePlugin(api: OpenClawPluginApiLike, opts: RegisterOptions = {}): void {\n // Source of truth: OpenClaw passes the user's `plugins.entries[id].config`\n // as `api.pluginConfig`. Env vars are a fallback so existing deploys with\n // LATITUDE_* exported in the gateway environment keep working.\n const config = opts.config ?? loadConfig(api.pluginConfig)\n const logger = opts.logger ?? createLogger(config.debug)\n\n if (!config.enabled) {\n if (config.apiKey === \"\") logger.debug(\"disabled: apiKey is empty (set plugins.entries[id].config.apiKey)\")\n if (config.project === \"\") logger.debug(\"disabled: project is empty (set plugins.entries[id].config.project)\")\n return\n }\n logger.debug(\n `enabled: project=${config.project} base=${config.baseUrl} allowConversationAccess=${config.allowConversationAccess}`,\n )\n\n const builder = new TurnBuilder()\n\n api.on(\"session_start\", (evt, ctx) => {\n builder.onSessionStart(evt as OpenClawSessionStartEvent, ctx as OpenClawAgentContext)\n })\n\n api.on(\"llm_input\", (evt, ctx) => {\n try {\n builder.onLlmInput(evt as OpenClawLlmInputEvent, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`llm_input handler failed: ${String(err)}`)\n }\n })\n\n api.on(\"before_tool_call\", (evt, ctx) => {\n try {\n builder.onBeforeToolCall(evt as OpenClawBeforeToolCallEvent, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`before_tool_call handler failed: ${String(err)}`)\n }\n })\n\n api.on(\"after_tool_call\", (evt, ctx) => {\n try {\n builder.onAfterToolCall(evt as OpenClawAfterToolCallEvent, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`after_tool_call handler failed: ${String(err)}`)\n }\n })\n\n api.on(\"llm_output\", (evt, ctx) => {\n try {\n builder.onLlmOutput(evt as OpenClawLlmOutputEvent, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`llm_output handler failed: ${String(err)}`)\n }\n })\n\n api.on(\"agent_end\", (evt, ctx) => {\n try {\n const run = builder.onAgentEnd(evt as OpenClawAgentEndEvent, ctx as OpenClawAgentContext)\n if (!run) {\n logger.debug(\"agent_end fired without a matching run in flight\")\n return\n }\n opts.onEmit?.(run)\n const payload = buildOtlpRequest(run, { allowConversationAccess: config.allowConversationAccess })\n void postTraces({\n baseUrl: config.baseUrl,\n apiKey: config.apiKey,\n project: config.project,\n payload,\n logger,\n })\n } catch (err) {\n logger.warn(`agent_end handler failed: ${String(err)}`)\n }\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.js","names":[],"sources":["../src/client.ts","../src/config.ts","../src/logger.ts","../src/otlp.ts","../src/turn-builder.ts","../src/plugin.ts"],"sourcesContent":["import type { Logger } from \"./logger.ts\"\nimport type { OtlpExportRequest } from \"./types.ts\"\n\nexport async function postTraces({\n baseUrl,\n apiKey,\n project,\n payload,\n logger,\n timeoutMs = 10_000,\n}: {\n baseUrl: string\n apiKey: string\n project: string\n payload: OtlpExportRequest\n logger: Logger\n timeoutMs?: number\n}): Promise<void> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/v1/traces`\n const bodyText = JSON.stringify(payload)\n logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`)\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"X-Latitude-Project\": project,\n },\n body: bodyText,\n signal: controller.signal,\n })\n if (!res.ok) {\n const text = await res.text().catch(() => \"\")\n logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`)\n } else {\n logger.debug(`ingest HTTP ${res.status}`)\n }\n } catch (err) {\n logger.warn(`ingest failed: ${String(err)}`)\n } finally {\n clearTimeout(timer)\n }\n}\n","export interface Config {\n apiKey: string\n baseUrl: string\n project: string\n enabled: boolean\n debug: boolean\n /**\n * When false, the plugin still emits one span per LLM call / tool / run, but\n * scrubs raw conversation content (input/output messages, system prompt,\n * tool args, tool results, the surfaced first-prompt). Token counts, model\n * names, agent ids, and timings are unaffected.\n */\n allowConversationAccess: boolean\n}\n\nconst DEFAULT_BASE_URL = \"https://ingest.latitude.so\"\n\n/**\n * Build a `Config` from OpenClaw's per-plugin config bucket plus environment\n * variables. The plugin SDK passes `api.pluginConfig` (the user's\n * `plugins.entries[id].config` block) to the registration function — that's\n * the primary source. Env vars are kept as a fallback so existing deployments\n * with `LATITUDE_*` already exported in the gateway environment keep working,\n * and so that `LATITUDE_DEBUG=1` can be flipped without editing openclaw.json.\n */\nexport function loadConfig(\n pluginConfig: Record<string, unknown> | undefined = undefined,\n env: NodeJS.ProcessEnv = process.env,\n): Config {\n const fromOpts = pluginConfig ?? {}\n\n const apiKey = pickString(fromOpts.apiKey) ?? env.LATITUDE_API_KEY ?? \"\"\n const project = pickString(fromOpts.project) ?? env.LATITUDE_PROJECT ?? \"\"\n const baseUrl = pickString(fromOpts.baseUrl) ?? env.LATITUDE_BASE_URL ?? DEFAULT_BASE_URL\n\n const debug = pickBool(fromOpts.debug) ?? env.LATITUDE_DEBUG === \"1\"\n const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false\n\n // Allow either env var or pluginConfig to disable. Falsy `enabled: false` in\n // pluginConfig wins; otherwise we require both api key and project.\n const explicitlyDisabled = pickBool(fromOpts.enabled) === false || (env.LATITUDE_OPENCLAW_ENABLED ?? \"1\") === \"0\"\n const hasCreds = apiKey !== \"\" && project !== \"\"\n\n return {\n apiKey,\n baseUrl,\n project,\n debug,\n allowConversationAccess,\n enabled: hasCreds && !explicitlyDisabled,\n }\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined\n}\n\nfunction pickBool(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined\n}\n","const PREFIX = \"[latitude-openclaw]\"\n\nexport interface Logger {\n debug: (msg: string) => void\n warn: (msg: string) => void\n}\n\nexport function createLogger(debugEnabled: boolean): Logger {\n return {\n debug: debugEnabled ? (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`) : () => {},\n warn: (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`),\n }\n}\n","import { createHash } from \"node:crypto\"\nimport { readFileSync } from \"node:fs\"\nimport { arch, hostname, platform, release } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport type {\n LlmCallRecord,\n OtlpExportRequest,\n OtlpKeyValue,\n OtlpResourceSpans,\n OtlpSpan,\n RunRecord,\n ToolCallRecord,\n} from \"./types.ts\"\n\nconst SCOPE_NAME = \"@latitude-data/openclaw-telemetry\"\n\n/**\n * Runtime-read package version, so OTLP `scope.version` and `service.version`\n * always reflect what's actually installed. Read once at module load and\n * cached. Falls back to `\"unknown\"` if the read fails (an exporter accepting\n * any string is more tolerant than a stale hard-coded literal).\n *\n * Same import.meta.url + ../package.json pattern used by `cli.ts` for the\n * `--version` flag — single source of truth in package.json.\n */\nconst SCOPE_VERSION = readScopeVersion()\n\nfunction readScopeVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n const pkgPath = join(here, \"..\", \"package.json\")\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version?: string }\n return pkg.version ?? \"unknown\"\n } catch {\n return \"unknown\"\n }\n}\n\ninterface BuildOptions {\n /**\n * When false, content attributes are scrubbed from spans:\n * - `user_prompt` (interaction)\n * - `gen_ai.system_instructions`, `gen_ai.input.messages`,\n * `gen_ai.output.messages` (llm_request)\n * - `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result` (tool_execution)\n * Timing, token usage, model name, ids, agent name, and counts are\n * always emitted regardless of this flag.\n */\n allowConversationAccess: boolean\n}\n\n/** Build an OTLP export request for a single completed agent run. */\nexport function buildOtlpRequest(run: RunRecord, options: BuildOptions): OtlpExportRequest {\n const spans = buildRunSpans(run, options)\n const rs: OtlpResourceSpans = {\n resource: { attributes: resourceAttrs() },\n scopeSpans: [{ scope: { name: SCOPE_NAME, version: SCOPE_VERSION }, spans }],\n }\n return { resourceSpans: [rs] }\n}\n\nfunction buildRunSpans(run: RunRecord, options: BuildOptions): OtlpSpan[] {\n const traceId = hashHex(`${run.sessionId ?? \"session\"}:${run.runId}`, 32)\n const interactionSpanId = hashHex(`${traceId}:run`, 16)\n const out: OtlpSpan[] = [buildInteractionSpan(traceId, interactionSpanId, run, options)]\n\n run.llmCalls.forEach((call, idx) => {\n const callSpanId = hashHex(`${traceId}:call:${idx}`, 16)\n out.push(buildLlmSpan(traceId, interactionSpanId, callSpanId, call, idx, run, options))\n // Tool spans are siblings of the llm_request, parented on the interaction\n // span so the run timeline renders as: llm → tool → llm → tool → ...\n call.toolCalls.forEach((tool, tIdx) => {\n const toolSpanId = hashHex(`${traceId}:call:${idx}:tool:${tIdx}`, 16)\n out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run, options))\n })\n })\n run.orphanTools.forEach((tool, idx) => {\n const toolSpanId = hashHex(`${traceId}:orphan-tool:${idx}`, 16)\n out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run, options))\n })\n\n return out\n}\n\nfunction buildInteractionSpan(traceId: string, spanId: string, run: RunRecord, options: BuildOptions): OtlpSpan {\n const startNs = msToNs(run.startMs)\n const endNs = msToNs(run.endMs ?? run.startMs)\n const totalTools = run.llmCalls.reduce((sum, c) => sum + c.toolCalls.length, 0) + run.orphanTools.length\n const totalUsage = aggregateUsage(run.llmCalls)\n\n return {\n traceId,\n spanId,\n parentSpanId: \"\",\n name: \"interaction\",\n kind: 1,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: stripUndef([\n str(\"span.type\", \"interaction\"),\n str(\"interaction.kind\", \"agent_run\"),\n str(\"openclaw.run.id\", run.runId),\n run.sessionId ? str(\"openclaw.session.id\", run.sessionId) : undefined,\n run.sessionId ? str(\"session.id\", run.sessionId) : undefined,\n run.sessionKey ? str(\"openclaw.session.key\", run.sessionKey) : undefined,\n run.agentId ? str(\"openclaw.agent.id\", run.agentId) : undefined,\n run.agentId ? str(\"openclaw.agent.name\", run.agentId) : undefined,\n run.workspaceDir ? str(\"openclaw.workspace.dir\", run.workspaceDir) : undefined,\n run.messageProvider ? str(\"openclaw.message.provider\", run.messageProvider) : undefined,\n run.channelId ? str(\"openclaw.channel.id\", run.channelId) : undefined,\n run.trigger ? str(\"openclaw.trigger\", run.trigger) : undefined,\n run.modelProviderId ? str(\"openclaw.model.provider.id\", run.modelProviderId) : undefined,\n run.modelId ? str(\"openclaw.model.id\", run.modelId) : undefined,\n int(\"interaction.duration_ms\", durationMs(run.startMs, run.endMs)),\n int(\"interaction.call_count\", run.llmCalls.length),\n int(\"interaction.tool_call_count\", totalTools),\n run.success !== undefined ? bool(\"openclaw.run.success\", run.success) : undefined,\n run.error ? str(\"openclaw.run.error\", run.error) : undefined,\n totalUsage.input !== undefined ? int(\"gen_ai.usage.input_tokens\", totalUsage.input) : undefined,\n totalUsage.output !== undefined ? int(\"gen_ai.usage.output_tokens\", totalUsage.output) : undefined,\n totalUsage.cacheRead !== undefined\n ? int(\"gen_ai.usage.cache_read_input_tokens\", totalUsage.cacheRead)\n : undefined,\n totalUsage.cacheWrite !== undefined\n ? int(\"gen_ai.usage.cache_creation_input_tokens\", totalUsage.cacheWrite)\n : undefined,\n totalUsage.total !== undefined ? int(\"gen_ai.usage.total_tokens\", totalUsage.total) : undefined,\n // Surface the first user prompt so the Latitude UI has something\n // recognisable in the interaction list — only when the operator opted\n // in to conversation capture.\n options.allowConversationAccess && run.llmCalls[0]?.prompt\n ? str(\"user_prompt\", run.llmCalls[0].prompt)\n : undefined,\n bool(\"latitude.captured.content\", options.allowConversationAccess),\n ]),\n status: { code: run.success === false ? 2 : 1 },\n }\n}\n\nfunction buildLlmSpan(\n traceId: string,\n parentSpanId: string,\n spanId: string,\n call: LlmCallRecord,\n callIdx: number,\n run: RunRecord,\n options: BuildOptions,\n): OtlpSpan {\n const startNs = msToNs(call.startMs)\n const endNs = msToNs(call.endMs ?? call.startMs)\n\n const captureContent = options.allowConversationAccess\n const inputMessages = captureContent ? buildInputMessages(call) : undefined\n const outputMessages = captureContent ? buildOutputMessages(call) : undefined\n const systemInstructions =\n captureContent && call.systemPrompt ? JSON.stringify([{ type: \"text\", content: call.systemPrompt }]) : undefined\n\n return {\n traceId,\n spanId,\n parentSpanId,\n name: \"llm_request\",\n kind: 3,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: stripUndef([\n str(\"span.type\", \"llm_request\"),\n str(\"gen_ai.operation.name\", \"chat\"),\n str(\"llm_request.context\", \"interaction\"),\n int(\"llm_request.call_index\", callIdx),\n // Provider/model — capture every variant OpenClaw exposes so consumers\n // can filter on whichever form they already use.\n str(\"gen_ai.system\", call.provider),\n str(\"openclaw.provider\", call.provider),\n str(\"gen_ai.request.model\", call.requestModel),\n str(\"model\", call.requestModel),\n call.responseModel ? str(\"gen_ai.response.model\", call.responseModel) : undefined,\n call.resolvedRef ? str(\"openclaw.resolved.ref\", call.resolvedRef) : undefined,\n // Identity — the agent name tag the user specifically asked for is here\n // under both canonical and convenience keys.\n run.sessionId ? str(\"session.id\", run.sessionId) : undefined,\n run.sessionKey ? str(\"openclaw.session.key\", run.sessionKey) : undefined,\n str(\"openclaw.run.id\", call.runId),\n call.agentId ? str(\"openclaw.agent.id\", call.agentId) : undefined,\n call.agentId ? str(\"openclaw.agent.name\", call.agentId) : undefined,\n // Token usage — input / output / cache / total, in both gen_ai.* and\n // legacy aliases for backwards compatibility with existing dashboards.\n call.usage?.input !== undefined ? int(\"gen_ai.usage.input_tokens\", call.usage.input) : undefined,\n call.usage?.input !== undefined ? int(\"input_tokens\", call.usage.input) : undefined,\n call.usage?.output !== undefined ? int(\"gen_ai.usage.output_tokens\", call.usage.output) : undefined,\n call.usage?.output !== undefined ? int(\"output_tokens\", call.usage.output) : undefined,\n call.usage?.cacheRead !== undefined\n ? int(\"gen_ai.usage.cache_read_input_tokens\", call.usage.cacheRead)\n : undefined,\n call.usage?.cacheRead !== undefined ? int(\"cache_read_tokens\", call.usage.cacheRead) : undefined,\n call.usage?.cacheWrite !== undefined\n ? int(\"gen_ai.usage.cache_creation_input_tokens\", call.usage.cacheWrite)\n : undefined,\n call.usage?.cacheWrite !== undefined ? int(\"cache_creation_tokens\", call.usage.cacheWrite) : undefined,\n call.usage?.total !== undefined ? int(\"gen_ai.usage.total_tokens\", call.usage.total) : undefined,\n // Content — system prompt + full message arrays. Gated on\n // allowConversationAccess. Even when off, we emit the structural\n // attributes above (model, tokens, ids, agent name).\n systemInstructions ? str(\"gen_ai.system_instructions\", systemInstructions) : undefined,\n inputMessages ? str(\"gen_ai.input.messages\", JSON.stringify(inputMessages)) : undefined,\n outputMessages ? str(\"gen_ai.output.messages\", JSON.stringify(outputMessages)) : undefined,\n bool(\"latitude.captured.content\", captureContent),\n // Misc signals.\n int(\"openclaw.images.count\", call.imagesCount),\n int(\"llm_request.tool_call_count\", call.toolCalls.length),\n int(\"llm_request.duration_ms\", durationMs(call.startMs, call.endMs)),\n call.error ? str(\"error.type\", \"llm_error\") : undefined,\n call.error ? str(\"error.message\", call.error) : undefined,\n str(\"success\", call.error ? \"false\" : \"true\"),\n str(\"llm_request.captured\", \"true\"),\n ]),\n status: { code: call.error ? 2 : 1 },\n }\n}\n\nfunction buildToolSpan(\n traceId: string,\n parentSpanId: string,\n spanId: string,\n tool: ToolCallRecord,\n run: RunRecord,\n options: BuildOptions,\n): OtlpSpan {\n const startNs = msToNs(tool.startMs)\n const endNs = msToNs(tool.endMs ?? tool.startMs)\n const isError = Boolean(tool.error)\n const captureContent = options.allowConversationAccess\n return {\n traceId,\n spanId,\n parentSpanId,\n name: `tool:${tool.toolName}`,\n kind: 1,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: stripUndef([\n str(\"span.type\", \"tool_execution\"),\n str(\"gen_ai.operation.name\", \"execute_tool\"),\n str(\"gen_ai.tool.name\", tool.toolName),\n str(\"gen_ai.tool.call.id\", tool.toolCallId),\n // Tool args + result are content. Gate on allowConversationAccess —\n // when off, we still emit the call's name, id, duration, error state,\n // and agent name so the timeline + counters still work.\n captureContent ? str(\"gen_ai.tool.call.arguments\", safeJson(tool.params)) : undefined,\n captureContent && tool.result !== undefined ? str(\"gen_ai.tool.call.result\", safeJson(tool.result)) : undefined,\n bool(\"latitude.captured.content\", captureContent),\n isError ? str(\"error.type\", \"tool_error\") : undefined,\n isError ? str(\"error.message\", tool.error ?? \"\") : undefined,\n bool(\"tool.is_error\", isError),\n str(\"success\", isError ? \"false\" : \"true\"),\n tool.durationMs !== undefined ? int(\"tool.duration_ms\", tool.durationMs) : undefined,\n run.sessionId ? str(\"session.id\", run.sessionId) : undefined,\n run.sessionKey ? str(\"openclaw.session.key\", run.sessionKey) : undefined,\n str(\"openclaw.run.id\", run.runId),\n tool.agentId ? str(\"openclaw.agent.id\", tool.agentId) : undefined,\n tool.agentId ? str(\"openclaw.agent.name\", tool.agentId) : undefined,\n ]),\n status: { code: isError ? 2 : 1 },\n }\n}\n\n// ─── Message shape helpers ──────────────────────────────────────────────────\n//\n// Latitude UI expects `{ role, parts: [{ type, content|... }] }` objects.\n// Build both the input (history + current prompt) and output (assistant + any\n// tool_calls from this call) arrays in that shape, passing through whatever\n// OpenClaw handed us as-is where the shape is already usable.\n\ninterface MessagePart {\n type: string\n [key: string]: unknown\n}\n\ninterface Message {\n role: \"system\" | \"user\" | \"assistant\" | \"tool\"\n parts: MessagePart[]\n}\n\nfunction buildInputMessages(call: LlmCallRecord): Message[] {\n const out: Message[] = []\n // OpenClaw's `historyMessages` is typed `unknown[]`; we trust it enough to\n // pass through but normalize simple shapes so the Latitude UI has something\n // to render. Complex objects fall back to a JSON stringification.\n for (const msg of call.historyMessages) {\n const normalized = normalizeHistoryMessage(msg)\n if (normalized) out.push(normalized)\n }\n if (call.prompt.length > 0) {\n out.push({ role: \"user\", parts: [{ type: \"text\", content: call.prompt }] })\n }\n return out\n}\n\nconst ALLOWED_ROLES: ReadonlySet<Message[\"role\"]> = new Set([\"system\", \"user\", \"assistant\", \"tool\"])\n\nfunction normalizeRole(raw: unknown): Message[\"role\"] {\n // Provider adapters occasionally emit roles outside the canonical set\n // (e.g. OpenAI's \"developer\"). Coerce unknown roles to \"user\" so the\n // downstream Latitude UI gets a payload it can render.\n if (typeof raw !== \"string\") return \"user\"\n return ALLOWED_ROLES.has(raw as Message[\"role\"]) ? (raw as Message[\"role\"]) : \"user\"\n}\n\nfunction normalizeHistoryMessage(raw: unknown): Message | undefined {\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n const role = normalizeRole(obj.role)\n const content = obj.content ?? obj.text ?? obj.message\n if (typeof content === \"string\") {\n return { role, parts: [{ type: \"text\", content }] }\n }\n if (Array.isArray(content)) {\n const parts: MessagePart[] = []\n for (const block of content) {\n const part = normalizeContentBlock(block)\n if (part) parts.push(part)\n }\n return { role, parts: parts.length > 0 ? parts : [{ type: \"text\", content: JSON.stringify(content) }] }\n }\n // Unknown shape — dump it as JSON so nothing is silently lost.\n return { role, parts: [{ type: \"text\", content: safeJson(raw) }] }\n}\n\nfunction normalizeContentBlock(raw: unknown): MessagePart | undefined {\n if (typeof raw === \"string\") return { type: \"text\", content: raw }\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n const type = typeof obj.type === \"string\" ? obj.type : \"text\"\n if (type === \"text\" && typeof obj.text === \"string\") return { type: \"text\", content: obj.text }\n if (type === \"tool_use\") {\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.input ?? {},\n }\n }\n if (type === \"tool_result\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.tool_use_id === \"string\" ? obj.tool_use_id : \"\",\n response: obj.content ?? \"\",\n }\n }\n if (type === \"image\") {\n return { type: \"uri\", modality: \"image\", uri: safeJson(obj.source ?? obj) }\n }\n return { type, content: safeJson(raw) }\n}\n\nfunction buildOutputMessages(call: LlmCallRecord): Message[] {\n const parts: MessagePart[] = []\n for (const text of call.assistantTexts) {\n if (text.length > 0) parts.push({ type: \"text\", content: text })\n }\n // Attach tool_call parts from tools invoked during this call so the output\n // message reads like the assistant message the model actually produced.\n for (const tool of call.toolCalls) {\n parts.push({\n type: \"tool_call\",\n id: tool.toolCallId,\n name: tool.toolName,\n arguments: tool.params,\n })\n }\n // Fall back to `lastAssistant` if we have no text/tools (edge case — empty\n // SSE response, failed run).\n if (parts.length === 0 && call.lastAssistant !== undefined) {\n parts.push({ type: \"text\", content: safeJson(call.lastAssistant) })\n }\n return [{ role: \"assistant\", parts }]\n}\n\n// ─── Utilities ──────────────────────────────────────────────────────────────\n\nfunction aggregateUsage(calls: LlmCallRecord[]): Required<Partial<import(\"./types.ts\").OpenClawLlmUsage>> {\n const agg = {\n input: undefined as number | undefined,\n output: undefined as number | undefined,\n cacheRead: undefined as number | undefined,\n cacheWrite: undefined as number | undefined,\n total: undefined as number | undefined,\n }\n const add = (k: keyof typeof agg, v: number | undefined): void => {\n if (v === undefined) return\n agg[k] = (agg[k] ?? 0) + v\n }\n for (const c of calls) {\n if (!c.usage) continue\n add(\"input\", c.usage.input)\n add(\"output\", c.usage.output)\n add(\"cacheRead\", c.usage.cacheRead)\n add(\"cacheWrite\", c.usage.cacheWrite)\n add(\"total\", c.usage.total)\n }\n return agg as Required<Partial<import(\"./types.ts\").OpenClawLlmUsage>>\n}\n\nfunction resourceAttrs(): OtlpKeyValue[] {\n return [\n str(\"service.name\", \"openclaw\"),\n str(\"service.version\", SCOPE_VERSION),\n str(\"host.name\", hostname()),\n str(\"host.arch\", arch()),\n str(\"os.type\", platform()),\n str(\"os.version\", release()),\n ]\n}\n\nfunction str(key: string, value: string): OtlpKeyValue {\n return { key, value: { stringValue: value } }\n}\n\nfunction int(key: string, value: number): OtlpKeyValue {\n return { key, value: { intValue: String(Math.trunc(value)) } }\n}\n\nfunction bool(key: string, value: boolean): OtlpKeyValue {\n return { key, value: { boolValue: value } }\n}\n\nfunction stripUndef(items: Array<OtlpKeyValue | undefined>): OtlpKeyValue[] {\n return items.filter((x): x is OtlpKeyValue => x !== undefined)\n}\n\nfunction hashHex(input: string, length: number): string {\n return createHash(\"sha256\").update(input).digest(\"hex\").slice(0, length)\n}\n\nfunction msToNs(ms: number): string {\n return (BigInt(Math.trunc(ms)) * 1_000_000n).toString()\n}\n\nfunction durationMs(startMs: number, endMs: number | undefined): number {\n if (endMs === undefined) return 0\n return Math.max(0, endMs - startMs)\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n","import { randomUUID } from \"node:crypto\"\nimport type {\n LlmCallRecord,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawSessionStartEvent,\n RunRecord,\n ToolCallRecord,\n} from \"./types.ts\"\n\n/**\n * Accumulates OpenClaw hook events per agent run (keyed by `runId`) into a\n * `RunRecord` ready to be converted to OTLP spans. All mutation is synchronous\n * and non-blocking so the hook runner can stay fire-and-forget.\n *\n * Event ordering assumptions (verified against OpenClaw\n * src/agents/pi-embedded-runner/run/attempt.ts):\n *\n * session_start? -> [ llm_input -> (before_tool_call -> after_tool_call)* -> llm_output ]+ -> agent_end\n *\n * Tool calls arriving between an `llm_input` and its `llm_output` are attached\n * to the currently-open LLM call. Tools arriving outside that window (e.g.\n * `after_tool_call` fires after `llm_output` has already closed the call) are\n * stored on the run's `orphanTools` list so we don't drop them.\n */\nexport class TurnBuilder {\n private readonly runs = new Map<string, RunRecord>()\n\n onSessionStart(_evt: OpenClawSessionStartEvent, _ctx: OpenClawAgentContext): void {\n // No-op for now — we lazily create RunRecords on the first `llm_input` for\n // a given runId. Kept as a hook point so we can later emit a\n // session-level span or capture `resumedFrom` metadata.\n }\n\n onLlmInput(evt: OpenClawLlmInputEvent, ctx: OpenClawAgentContext): LlmCallRecord {\n const run = this.ensureRun(evt.runId, ctx)\n const call: LlmCallRecord = {\n runId: evt.runId,\n sessionId: evt.sessionId,\n sessionKey: ctx.sessionKey,\n agentId: ctx.agentId,\n provider: evt.provider,\n requestModel: evt.model,\n responseModel: undefined,\n resolvedRef: undefined,\n systemPrompt: evt.systemPrompt,\n prompt: evt.prompt,\n historyMessages: evt.historyMessages,\n imagesCount: evt.imagesCount,\n assistantTexts: [],\n lastAssistant: undefined,\n usage: undefined,\n startMs: Date.now(),\n endMs: undefined,\n error: undefined,\n toolCalls: [],\n }\n run.llmCalls.push(call)\n return call\n }\n\n onBeforeToolCall(evt: OpenClawBeforeToolCallEvent, ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n // Create the run record lazily if a tool fires before we've seen llm_input\n // for this runId — rare but possible, and we prefer capturing an orphan\n // tool over dropping the event.\n const run = this.runs.get(evt.runId) ?? this.ensureRun(evt.runId, ctx)\n\n const tool: ToolCallRecord = {\n // Use a UUID when OpenClaw elides the id — name+timestamp can collide for\n // multiple invocations of the same tool within the same millisecond,\n // which would then cause `after_tool_call` to update the wrong record.\n toolCallId: evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`,\n toolName: evt.toolName,\n params: evt.params,\n result: undefined,\n error: undefined,\n startMs: Date.now(),\n endMs: undefined,\n durationMs: undefined,\n agentId: ctx.agentId,\n }\n const openCall = this.currentOpenCall(run)\n if (openCall) {\n openCall.toolCalls.push(tool)\n } else {\n run.orphanTools.push(tool)\n }\n }\n\n onAfterToolCall(evt: OpenClawAfterToolCallEvent, _ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n const tool = this.findToolRecord(run, evt.toolCallId, evt.toolName)\n if (!tool) return\n tool.result = evt.result\n tool.error = evt.error\n tool.durationMs = evt.durationMs\n tool.endMs = Date.now()\n }\n\n onLlmOutput(evt: OpenClawLlmOutputEvent, _ctx: OpenClawAgentContext): LlmCallRecord | undefined {\n const run = this.runs.get(evt.runId)\n if (!run) return undefined\n // Close the most recently-opened call that doesn't yet have an endMs —\n // the LLM loop is sequential, so this pairs 1:1 with `llm_input`.\n const openCall = this.currentOpenCall(run)\n if (!openCall) return undefined\n openCall.endMs = Date.now()\n openCall.assistantTexts = evt.assistantTexts\n openCall.lastAssistant = evt.lastAssistant\n openCall.usage = evt.usage\n openCall.responseModel = evt.model\n openCall.resolvedRef = evt.resolvedRef\n return openCall\n }\n\n onAgentEnd(evt: OpenClawAgentEndEvent, ctx: OpenClawAgentContext): RunRecord | undefined {\n const runId = ctx.runId\n if (!runId) return undefined\n const run = this.runs.get(runId)\n if (!run) return undefined\n run.endMs = Date.now()\n run.success = evt.success\n run.error = evt.error\n // Best-effort: close any still-open LLM call that never saw an `llm_output`\n // (e.g. when the run errored mid-call) so the span still has an end time.\n for (const call of run.llmCalls) {\n if (call.endMs === undefined) {\n call.endMs = run.endMs\n if (evt.error && call.error === undefined) call.error = evt.error\n }\n for (const tool of call.toolCalls) {\n if (tool.endMs === undefined) tool.endMs = run.endMs\n }\n }\n for (const tool of run.orphanTools) {\n if (tool.endMs === undefined) tool.endMs = run.endMs\n }\n this.runs.delete(runId)\n return run\n }\n\n /** Drop a run without emitting — used on errors from the emit path. */\n abandon(runId: string): void {\n this.runs.delete(runId)\n }\n\n /** Active runs count, for debug logging. */\n inflightCount(): number {\n return this.runs.size\n }\n\n private ensureRun(runId: string, ctx: OpenClawAgentContext): RunRecord {\n let run = this.runs.get(runId)\n if (run) return run\n run = {\n runId,\n sessionId: ctx.sessionId,\n sessionKey: ctx.sessionKey,\n agentId: ctx.agentId,\n workspaceDir: ctx.workspaceDir,\n messageProvider: ctx.messageProvider,\n trigger: ctx.trigger,\n channelId: ctx.channelId,\n modelProviderId: ctx.modelProviderId,\n modelId: ctx.modelId,\n startMs: Date.now(),\n endMs: undefined,\n success: undefined,\n error: undefined,\n llmCalls: [],\n orphanTools: [],\n }\n this.runs.set(runId, run)\n return run\n }\n\n private currentOpenCall(run: RunRecord): LlmCallRecord | undefined {\n for (let i = run.llmCalls.length - 1; i >= 0; i--) {\n const call = run.llmCalls[i]\n if (call && call.endMs === undefined) return call\n }\n return undefined\n }\n\n private findToolRecord(run: RunRecord, toolCallId: string | undefined, toolName: string): ToolCallRecord | undefined {\n // Try matching by toolCallId first since it's unique. Fall back to the\n // most recent unfinished record for the same name if the id is missing\n // or no record matches — defensive coverage for OpenClaw versions that\n // elide toolCallId on after_tool_call.\n const matchesId = (t: ToolCallRecord): boolean => Boolean(toolCallId && t.toolCallId === toolCallId)\n\n for (const call of run.llmCalls) {\n for (const t of call.toolCalls) if (matchesId(t)) return t\n }\n for (const t of run.orphanTools) if (matchesId(t)) return t\n\n for (let i = run.llmCalls.length - 1; i >= 0; i--) {\n const call = run.llmCalls[i]\n if (!call) continue\n for (let j = call.toolCalls.length - 1; j >= 0; j--) {\n const t = call.toolCalls[j]\n if (t && t.toolName === toolName && t.endMs === undefined) return t\n }\n }\n for (let i = run.orphanTools.length - 1; i >= 0; i--) {\n const t = run.orphanTools[i]\n if (t && t.toolName === toolName && t.endMs === undefined) return t\n }\n return undefined\n }\n}\n","import { postTraces } from \"./client.ts\"\nimport { type Config, loadConfig } from \"./config.ts\"\nimport { createLogger, type Logger } from \"./logger.ts\"\nimport { buildOtlpRequest } from \"./otlp.ts\"\nimport { TurnBuilder } from \"./turn-builder.ts\"\nimport type {\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawSessionStartEvent,\n RunRecord,\n} from \"./types.ts\"\n\n/**\n * Minimal structural type for OpenClaw's plugin API — only the fields we\n * touch. We avoid importing from `openclaw/plugin-sdk` so the package stays\n * usable when OpenClaw isn't installed (the CLI and tests don't need it),\n * and so we're robust to small signature changes across OpenClaw versions.\n *\n * `pluginConfig` is the user's `plugins.entries[id].config` block — that's\n * the canonical place to read credentials and feature flags. The OpenClaw\n * plugin SDK also exposes the same value as `api.pluginConfig` on the\n * builder API; keep both names in sync if the upstream contract evolves.\n */\nexport interface OpenClawPluginApiLike {\n logger?: Logger\n pluginConfig?: Record<string, unknown>\n on: <K extends string>(\n hookName: K,\n handler: (event: unknown, ctx: unknown) => unknown,\n opts?: { priority?: number },\n ) => void\n}\n\nexport interface RegisterOptions {\n /** Override the config, mostly for tests. */\n config?: Config\n /** Override the logger. */\n logger?: Logger\n /**\n * Hook to observe the emitted run right before it's posted. Used by tests;\n * not a stable public API.\n */\n onEmit?: (run: RunRecord) => void\n}\n\n/**\n * Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls\n * this once at plugin activation; we wire up `llm_input`, `llm_output`, tool\n * and lifecycle hooks to stream traces to Latitude.\n *\n * Every handler is fire-and-forget on OpenClaw's side (see\n * `src/plugins/hooks.ts` — runLlmInput/runLlmOutput are documented as\n * parallel and wrapped with `.catch()` at the call site in attempt.ts), so\n * nothing we do here can slow the agent loop.\n */\nexport default function registerLatitudePlugin(api: OpenClawPluginApiLike, opts: RegisterOptions = {}): void {\n // Source of truth: OpenClaw passes the user's `plugins.entries[id].config`\n // as `api.pluginConfig`. Env vars are a fallback so existing deploys with\n // LATITUDE_* exported in the gateway environment keep working.\n const config = opts.config ?? loadConfig(api.pluginConfig)\n const logger = opts.logger ?? createLogger(config.debug)\n\n if (!config.enabled) {\n if (config.apiKey === \"\") logger.debug(\"disabled: apiKey is empty (set plugins.entries[id].config.apiKey)\")\n if (config.project === \"\") logger.debug(\"disabled: project is empty (set plugins.entries[id].config.project)\")\n return\n }\n logger.debug(\n `enabled: project=${config.project} base=${config.baseUrl} allowConversationAccess=${config.allowConversationAccess}`,\n )\n\n const builder = new TurnBuilder()\n\n api.on(\"session_start\", (evt, ctx) => {\n builder.onSessionStart(evt as OpenClawSessionStartEvent, ctx as OpenClawAgentContext)\n })\n\n api.on(\"llm_input\", (evt, ctx) => {\n try {\n builder.onLlmInput(evt as OpenClawLlmInputEvent, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`llm_input handler failed: ${String(err)}`)\n }\n })\n\n api.on(\"before_tool_call\", (evt, ctx) => {\n try {\n builder.onBeforeToolCall(evt as OpenClawBeforeToolCallEvent, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`before_tool_call handler failed: ${String(err)}`)\n }\n })\n\n api.on(\"after_tool_call\", (evt, ctx) => {\n try {\n builder.onAfterToolCall(evt as OpenClawAfterToolCallEvent, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`after_tool_call handler failed: ${String(err)}`)\n }\n })\n\n api.on(\"llm_output\", (evt, ctx) => {\n try {\n builder.onLlmOutput(evt as OpenClawLlmOutputEvent, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`llm_output handler failed: ${String(err)}`)\n }\n })\n\n api.on(\"agent_end\", (evt, ctx) => {\n try {\n const run = builder.onAgentEnd(evt as OpenClawAgentEndEvent, ctx as OpenClawAgentContext)\n if (!run) {\n logger.debug(\"agent_end fired without a matching run in flight\")\n return\n }\n opts.onEmit?.(run)\n const payload = buildOtlpRequest(run, { allowConversationAccess: config.allowConversationAccess })\n void postTraces({\n baseUrl: config.baseUrl,\n apiKey: config.apiKey,\n project: config.project,\n payload,\n logger,\n })\n } catch (err) {\n logger.warn(`agent_end handler failed: ${String(err)}`)\n }\n })\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  "id": "@latitude-data/openclaw-telemetry",
3
3
  "name": "Latitude Telemetry",
4
4
  "description": "Streams every OpenClaw agent run to Latitude as OTLP traces — full prompt, message history, assistant output, tool I/O, token usage, and agent name.",
5
- "version": "0.0.3",
5
+ "version": "0.0.4",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latitude-data/openclaw-telemetry",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "OpenClaw plugin that streams LLM calls, tool executions, and agent runs to Latitude as OTLP traces",
5
5
  "author": "Latitude Data SL <hello@latitude.so>",
6
6
  "license": "MIT",