@latitude-data/openclaw-telemetry 0.0.1 → 0.0.2
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 +30 -35
- package/dist/cli.js +164 -54
- package/dist/cli.js.map +1 -1
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +55 -30
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +40 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,7 +8,10 @@ OpenClaw plugin that streams every agent run to [Latitude](https://latitude.so)
|
|
|
8
8
|
npx -y @latitude-data/openclaw-telemetry install
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The installer prompts for your Latitude API key and project slug, then
|
|
11
|
+
The installer prompts for your Latitude API key and project slug, then:
|
|
12
|
+
|
|
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.
|
|
12
15
|
|
|
13
16
|
Restart the OpenClaw gateway after install:
|
|
14
17
|
|
|
@@ -27,6 +30,7 @@ That's it. Traces show up at `https://console.latitude.so/projects/<your-slug>`.
|
|
|
27
30
|
| `--staging` | Target `https://staging.latitude.so` / `https://staging-ingest.latitude.so`. |
|
|
28
31
|
| `--dev` | Target `http://localhost:3000` / `http://localhost:3002`. |
|
|
29
32
|
| `--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. |
|
|
30
34
|
|
|
31
35
|
Re-running `install` is idempotent — existing values are preserved.
|
|
32
36
|
|
|
@@ -36,7 +40,7 @@ Re-running `install` is idempotent — existing values are preserved.
|
|
|
36
40
|
npx -y @latitude-data/openclaw-telemetry uninstall
|
|
37
41
|
```
|
|
38
42
|
|
|
39
|
-
Shows a plan, asks for confirmation, then removes the plugin entry and the
|
|
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`.
|
|
40
44
|
|
|
41
45
|
## What gets sent
|
|
42
46
|
|
|
@@ -66,48 +70,39 @@ OpenClaw runs LLM hooks **fire-and-forget** (see [`src/plugins/hooks.ts`](https:
|
|
|
66
70
|
|
|
67
71
|
## Configuration reference
|
|
68
72
|
|
|
69
|
-
|
|
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:
|
|
70
74
|
|
|
71
|
-
### `
|
|
75
|
+
### `plugins.entries["@latitude-data/openclaw-telemetry"].config`
|
|
72
76
|
|
|
73
|
-
|
|
|
77
|
+
| Key | Required | Default | Description |
|
|
74
78
|
| --- | --- | --- | --- |
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
79
|
+
| `apiKey` | yes | — | Bearer token for Latitude ingestion. |
|
|
80
|
+
| `project` | yes | — | Slug of the project to route traces into. |
|
|
81
|
+
| `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. |
|
|
83
|
+
| `enabled` | no | `true` | Set to `false` to pause emission without uninstalling. |
|
|
84
|
+
| `debug` | no | `false` | Log diagnostic lines to stderr (visible in the gateway log). |
|
|
80
85
|
|
|
81
|
-
###
|
|
86
|
+
### Environment variable fallbacks
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
"@latitude-data/openclaw-telemetry": {
|
|
86
|
-
"enabled": true,
|
|
87
|
-
"hooks": {
|
|
88
|
-
"allowConversationAccess": true
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
`allowConversationAccess` is load-bearing — OpenClaw scrubs payloads from `llm_input` / `llm_output` / `agent_end` for third-party plugins unless they opt in explicitly.
|
|
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`.
|
|
95
89
|
|
|
96
90
|
### Manual installation
|
|
97
91
|
|
|
98
|
-
If the installer doesn't fit your setup,
|
|
92
|
+
If the installer doesn't fit your setup, you need two things:
|
|
93
|
+
|
|
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`:
|
|
99
96
|
|
|
100
97
|
```jsonc
|
|
101
98
|
{
|
|
102
|
-
"env": {
|
|
103
|
-
"LATITUDE_API_KEY": "lat_xxx",
|
|
104
|
-
"LATITUDE_PROJECT": "my-openclaw-project"
|
|
105
|
-
},
|
|
106
99
|
"plugins": {
|
|
107
100
|
"entries": {
|
|
108
101
|
"@latitude-data/openclaw-telemetry": {
|
|
109
102
|
"enabled": true,
|
|
110
|
-
"
|
|
103
|
+
"config": {
|
|
104
|
+
"apiKey": "lat_xxx",
|
|
105
|
+
"project": "my-openclaw-project",
|
|
111
106
|
"allowConversationAccess": true
|
|
112
107
|
}
|
|
113
108
|
}
|
|
@@ -116,17 +111,17 @@ If the installer doesn't fit your setup, the equivalent `openclaw.json` is:
|
|
|
116
111
|
}
|
|
117
112
|
```
|
|
118
113
|
|
|
119
|
-
|
|
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`.
|
|
120
117
|
|
|
121
118
|
## Privacy
|
|
122
119
|
|
|
123
|
-
|
|
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).
|
|
124
121
|
|
|
125
|
-
|
|
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.
|
|
126
123
|
|
|
127
|
-
|
|
128
|
-
- Set `LATITUDE_OPENCLAW_ENABLED=0` in your shell before starting a sensitive session.
|
|
129
|
-
- Run `uninstall`.
|
|
124
|
+
To pause emission entirely without uninstalling, set `LATITUDE_OPENCLAW_ENABLED=0` in the gateway environment.
|
|
130
125
|
|
|
131
126
|
## Supported OpenClaw versions
|
|
132
127
|
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { cancel, confirm, intro, isCancel, log, note, outro, password, spinner, text } from "@clack/prompts";
|
|
5
5
|
import pc from "picocolors";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
//#region src/settings-file.ts
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const CONFIG_DIR = join(homedir(), ".openclaw");
|
|
10
|
+
const SETTINGS_PATH = join(CONFIG_DIR, "openclaw.json");
|
|
11
|
+
const SETTINGS_BACKUP_PATH = join(CONFIG_DIR, "openclaw.json.latitude-bak");
|
|
12
|
+
const PLUGIN_INSTALL_DIR = join(join(CONFIG_DIR, "extensions"), "latitude-telemetry");
|
|
13
|
+
/** Plugin id used both as the npm package name and as the OpenClaw plugin id. */
|
|
10
14
|
const PLUGIN_ID = "@latitude-data/openclaw-telemetry";
|
|
11
15
|
function readSettings() {
|
|
12
16
|
if (!existsSync(SETTINGS_PATH)) return {};
|
|
@@ -24,23 +28,40 @@ function writeSettings(settings) {
|
|
|
24
28
|
function backupSettings() {
|
|
25
29
|
if (existsSync(SETTINGS_PATH)) copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH);
|
|
26
30
|
}
|
|
27
|
-
|
|
31
|
+
/**
|
|
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).
|
|
35
|
+
*
|
|
36
|
+
* 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.
|
|
41
|
+
*/
|
|
42
|
+
function setPluginEntry(settings, patch) {
|
|
28
43
|
const plugins = settings.plugins ?? {};
|
|
29
44
|
const entries = plugins.entries ?? {};
|
|
30
45
|
const existing = entries["@latitude-data/openclaw-telemetry"] ?? {};
|
|
31
|
-
const
|
|
46
|
+
const nextConfig = {
|
|
47
|
+
...existing.config ?? {},
|
|
48
|
+
apiKey: patch.apiKey,
|
|
49
|
+
project: patch.project
|
|
50
|
+
};
|
|
51
|
+
if (patch.baseUrl !== void 0) nextConfig.baseUrl = patch.baseUrl;
|
|
52
|
+
else delete nextConfig.baseUrl;
|
|
53
|
+
if (patch.allowConversationAccess !== void 0) nextConfig.allowConversationAccess = patch.allowConversationAccess;
|
|
54
|
+
if (patch.debug !== void 0) nextConfig.debug = patch.debug;
|
|
55
|
+
const nextEnabled = patch.enabled ?? existing.enabled ?? true;
|
|
56
|
+
entries[PLUGIN_ID] = {
|
|
32
57
|
...existing,
|
|
33
|
-
enabled:
|
|
34
|
-
|
|
35
|
-
...existing.hooks ?? {},
|
|
36
|
-
allowConversationAccess: true
|
|
37
|
-
}
|
|
58
|
+
enabled: nextEnabled,
|
|
59
|
+
config: nextConfig
|
|
38
60
|
};
|
|
39
|
-
entries[PLUGIN_ID] = entry;
|
|
40
61
|
plugins.entries = entries;
|
|
41
62
|
settings.plugins = plugins;
|
|
42
|
-
return entry;
|
|
43
63
|
}
|
|
64
|
+
/** Remove the plugin entry entirely. */
|
|
44
65
|
function removePluginEntry(settings) {
|
|
45
66
|
const plugins = settings.plugins;
|
|
46
67
|
if (!plugins?.entries) return false;
|
|
@@ -48,23 +69,96 @@ function removePluginEntry(settings) {
|
|
|
48
69
|
delete plugins.entries[PLUGIN_ID];
|
|
49
70
|
return true;
|
|
50
71
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
env[key] = value;
|
|
54
|
-
settings.env = env;
|
|
72
|
+
function hasLatitudePlugin(settings) {
|
|
73
|
+
return Boolean(settings.plugins?.entries && "@latitude-data/openclaw-telemetry" in settings.plugins.entries);
|
|
55
74
|
}
|
|
56
|
-
|
|
75
|
+
/**
|
|
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.
|
|
80
|
+
*/
|
|
81
|
+
function migrateLegacyEntries(settings) {
|
|
82
|
+
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
|
+
}
|
|
57
92
|
const env = settings.env;
|
|
58
|
-
if (!env)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
93
|
+
if (env && typeof env === "object" && !Array.isArray(env)) {
|
|
94
|
+
const envObj = env;
|
|
95
|
+
for (const key of [
|
|
96
|
+
"LATITUDE_API_KEY",
|
|
97
|
+
"LATITUDE_PROJECT",
|
|
98
|
+
"LATITUDE_BASE_URL"
|
|
99
|
+
]) if (key in envObj) {
|
|
100
|
+
delete envObj[key];
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
if (Object.keys(envObj).length === 0) delete settings.env;
|
|
63
104
|
}
|
|
64
|
-
return
|
|
105
|
+
return { changed };
|
|
65
106
|
}
|
|
66
|
-
|
|
67
|
-
|
|
107
|
+
//#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(PLUGIN_INSTALL_DIR)) rmSync(PLUGIN_INSTALL_DIR, {
|
|
132
|
+
recursive: true,
|
|
133
|
+
force: true
|
|
134
|
+
});
|
|
135
|
+
mkdirSync(PLUGIN_INSTALL_DIR, { recursive: true });
|
|
136
|
+
copyFileSync(manifestSrc, join(PLUGIN_INSTALL_DIR, "openclaw.plugin.json"));
|
|
137
|
+
cpSync(distSrc, join(PLUGIN_INSTALL_DIR, "dist"), { recursive: true });
|
|
138
|
+
if (existsSync(pkgSrc)) {
|
|
139
|
+
const sourcePkg = JSON.parse(readFileSync(pkgSrc, "utf-8"));
|
|
140
|
+
const minimalPkg = {
|
|
141
|
+
name: sourcePkg.name,
|
|
142
|
+
version: sourcePkg.version,
|
|
143
|
+
type: sourcePkg.type ?? "module",
|
|
144
|
+
main: sourcePkg.main ?? "./dist/plugin.js",
|
|
145
|
+
private: true
|
|
146
|
+
};
|
|
147
|
+
writeFileSync(join(PLUGIN_INSTALL_DIR, "package.json"), `${JSON.stringify(minimalPkg, null, 2)}\n`, "utf-8");
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
destination: PLUGIN_INSTALL_DIR,
|
|
151
|
+
entryPath: join(PLUGIN_INSTALL_DIR, "dist", "plugin.js")
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Remove the materialized plugin directory under `~/.openclaw/extensions/`. */
|
|
155
|
+
function removePluginFiles() {
|
|
156
|
+
if (!existsSync(PLUGIN_INSTALL_DIR)) return false;
|
|
157
|
+
rmSync(PLUGIN_INSTALL_DIR, {
|
|
158
|
+
recursive: true,
|
|
159
|
+
force: true
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
68
162
|
}
|
|
69
163
|
//#endregion
|
|
70
164
|
//#region src/setup.ts
|
|
@@ -115,10 +209,14 @@ function normalizeInstallFlags(flags) {
|
|
|
115
209
|
if (environment) throw new Error("--staging and --dev are mutually exclusive");
|
|
116
210
|
environment = DEV_ENV;
|
|
117
211
|
}
|
|
212
|
+
let allowConversationAccess;
|
|
213
|
+
if (flags["no-content"] === true || flags["no-conversation"] === true) allowConversationAccess = false;
|
|
214
|
+
if (flags["allow-conversation"] === true) allowConversationAccess = true;
|
|
118
215
|
return {
|
|
119
216
|
apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : void 0,
|
|
120
217
|
project: typeof flags.project === "string" ? flags.project : void 0,
|
|
121
218
|
environment,
|
|
219
|
+
allowConversationAccess,
|
|
122
220
|
noPrompt: flags["no-prompt"] === true || flags.yes === true,
|
|
123
221
|
yes: flags.yes === true
|
|
124
222
|
};
|
|
@@ -129,7 +227,7 @@ async function runInstall(flags = {}) {
|
|
|
129
227
|
}
|
|
130
228
|
async function runInteractiveInstall(flags) {
|
|
131
229
|
intro(pc.bgCyan(pc.black(" Latitude · OpenClaw telemetry ")));
|
|
132
|
-
const
|
|
230
|
+
const existingConfig = readSettings().plugins?.entries?.["@latitude-data/openclaw-telemetry"]?.config ?? void 0;
|
|
133
231
|
const envConfig = flags.environment ?? PRODUCTION_ENV;
|
|
134
232
|
const urls = urlsFor(envConfig);
|
|
135
233
|
const aboutLines = [
|
|
@@ -143,12 +241,13 @@ async function runInteractiveInstall(flags) {
|
|
|
143
241
|
note(aboutLines.join("\n"), "About");
|
|
144
242
|
log.info(`Get an API key at ${pc.cyan(urls.apiKeys)}`);
|
|
145
243
|
log.info(`Create a project at ${pc.cyan(urls.projects)}`);
|
|
146
|
-
const apiKey = await promptApiKey(
|
|
147
|
-
const project = await promptProject(
|
|
244
|
+
const apiKey = await promptApiKey(existingConfig?.apiKey, flags.apiKey);
|
|
245
|
+
const project = await promptProject(existingConfig?.project, flags.project);
|
|
148
246
|
await applyChanges({
|
|
149
247
|
apiKey,
|
|
150
248
|
project,
|
|
151
|
-
envConfig
|
|
249
|
+
envConfig,
|
|
250
|
+
allowConversationAccess: flags.allowConversationAccess
|
|
152
251
|
});
|
|
153
252
|
note([
|
|
154
253
|
"Restart the OpenClaw gateway for the plugin to load:",
|
|
@@ -165,9 +264,11 @@ async function runFlagDrivenInstall(flags) {
|
|
|
165
264
|
await applyChanges({
|
|
166
265
|
apiKey,
|
|
167
266
|
project,
|
|
168
|
-
envConfig: flags.environment ?? PRODUCTION_ENV
|
|
267
|
+
envConfig: flags.environment ?? PRODUCTION_ENV,
|
|
268
|
+
allowConversationAccess: flags.allowConversationAccess
|
|
169
269
|
});
|
|
170
270
|
process.stdout.write(`Installed Latitude plugin in ${SETTINGS_PATH}\n`);
|
|
271
|
+
process.stdout.write(`Plugin files at ${PLUGIN_INSTALL_DIR}\n`);
|
|
171
272
|
}
|
|
172
273
|
async function promptApiKey(_existing, flag) {
|
|
173
274
|
if (flag) return flag;
|
|
@@ -194,19 +295,27 @@ function onCancel() {
|
|
|
194
295
|
cancel("Cancelled — nothing was changed");
|
|
195
296
|
process.exit(1);
|
|
196
297
|
}
|
|
197
|
-
async function applyChanges({ apiKey, project, envConfig }) {
|
|
198
|
-
const
|
|
199
|
-
|
|
298
|
+
async function applyChanges({ apiKey, project, envConfig, allowConversationAccess }) {
|
|
299
|
+
const filesSpinner = spinner();
|
|
300
|
+
filesSpinner.start("Installing plugin files");
|
|
301
|
+
const { destination } = installPluginFiles();
|
|
302
|
+
filesSpinner.stop(`Plugin files installed at ${destination}`);
|
|
303
|
+
const settingsSpinner = spinner();
|
|
304
|
+
settingsSpinner.start("Updating openclaw.json");
|
|
200
305
|
ensureSettingsDir();
|
|
201
306
|
backupSettings();
|
|
202
307
|
const settings = readSettings();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
308
|
+
migrateLegacyEntries(settings);
|
|
309
|
+
const existingConfig = settings.plugins?.entries?.["@latitude-data/openclaw-telemetry"]?.config ?? {};
|
|
310
|
+
const finalAllowConversationAccess = allowConversationAccess ?? existingConfig.allowConversationAccess ?? true;
|
|
311
|
+
setPluginEntry(settings, {
|
|
312
|
+
apiKey,
|
|
313
|
+
project,
|
|
314
|
+
baseUrl: envConfig.name === "production" ? void 0 : envConfig.ingest,
|
|
315
|
+
allowConversationAccess: finalAllowConversationAccess
|
|
316
|
+
});
|
|
208
317
|
writeSettings(settings);
|
|
209
|
-
|
|
318
|
+
settingsSpinner.stop(`Updated ${SETTINGS_PATH}`);
|
|
210
319
|
if (existsSync(SETTINGS_BACKUP_PATH)) log.info(`Backup saved at ${pc.dim(SETTINGS_BACKUP_PATH)}`);
|
|
211
320
|
}
|
|
212
321
|
function ensureSettingsDir() {
|
|
@@ -216,16 +325,18 @@ function ensureSettingsDir() {
|
|
|
216
325
|
async function runUninstall(flags = {}) {
|
|
217
326
|
intro(pc.bgYellow(pc.black(" Latitude · OpenClaw telemetry — uninstall ")));
|
|
218
327
|
const settings = readSettings();
|
|
219
|
-
|
|
220
|
-
|
|
328
|
+
const hasEntry = hasLatitudePlugin(settings);
|
|
329
|
+
const hasFiles = existsSync(PLUGIN_INSTALL_DIR);
|
|
330
|
+
if (!hasEntry && !hasFiles) {
|
|
331
|
+
note("No Latitude plugin entry or files found — nothing to remove.", "Status");
|
|
221
332
|
outro(pc.dim("Nothing changed"));
|
|
222
333
|
return;
|
|
223
334
|
}
|
|
224
335
|
note([
|
|
225
|
-
`Remove "${PLUGIN_ID}" plugin entry from ${SETTINGS_PATH}
|
|
226
|
-
|
|
227
|
-
`Backup saved at ${SETTINGS_BACKUP_PATH}`
|
|
228
|
-
].join("\n"), "Plan");
|
|
336
|
+
hasEntry ? `Remove "${PLUGIN_ID}" plugin entry from ${SETTINGS_PATH}` : null,
|
|
337
|
+
hasFiles ? `Delete plugin files at ${PLUGIN_INSTALL_DIR}` : null,
|
|
338
|
+
`Backup of openclaw.json saved at ${SETTINGS_BACKUP_PATH}`
|
|
339
|
+
].filter(Boolean).join("\n"), "Plan");
|
|
229
340
|
if (!flags.noPrompt && process.stdin.isTTY === true) {
|
|
230
341
|
const ok = await confirm({
|
|
231
342
|
message: "Proceed?",
|
|
@@ -235,14 +346,13 @@ async function runUninstall(flags = {}) {
|
|
|
235
346
|
}
|
|
236
347
|
const s = spinner();
|
|
237
348
|
s.start("Reverting settings");
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
writeSettings(settings);
|
|
349
|
+
if (hasEntry) {
|
|
350
|
+
backupSettings();
|
|
351
|
+
removePluginEntry(settings);
|
|
352
|
+
migrateLegacyEntries(settings);
|
|
353
|
+
writeSettings(settings);
|
|
354
|
+
}
|
|
355
|
+
if (hasFiles) removePluginFiles();
|
|
246
356
|
s.stop("Done");
|
|
247
357
|
outro(pc.green("✓ Uninstalled"));
|
|
248
358
|
}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/settings-file.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\nexport const SETTINGS_PATH = join(homedir(), \".openclaw\", \"openclaw.json\")\nexport const SETTINGS_BACKUP_PATH = join(homedir(), \".openclaw\", \"openclaw.json.latitude-bak\")\n\nexport const PLUGIN_ID = \"@latitude-data/openclaw-telemetry\"\n\ninterface OpenClawPluginEntry {\n enabled?: boolean\n hooks?: {\n allowPromptInjection?: boolean\n allowConversationAccess?: boolean\n }\n config?: Record<string, unknown>\n}\n\ninterface OpenClawSettings {\n plugins?: {\n enabled?: boolean\n entries?: Record<string, OpenClawPluginEntry>\n [key: string]: unknown\n }\n env?: Record<string, string>\n [key: string]: unknown\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\nexport function ensurePluginEntry(settings: OpenClawSettings): OpenClawPluginEntry {\n const plugins = settings.plugins ?? {}\n const entries = plugins.entries ?? {}\n const existing = entries[PLUGIN_ID] ?? {}\n const entry: OpenClawPluginEntry = {\n ...existing,\n enabled: true,\n hooks: {\n ...(existing.hooks ?? {}),\n // Required for third-party plugins to read raw conversation content\n // from llm_input/llm_output/agent_end. Without this the hooks fire but\n // payloads are scrubbed — which is exactly the silent failure mode that\n // the existing third-party OpenClaw observability plugin runs into.\n allowConversationAccess: true,\n },\n }\n entries[PLUGIN_ID] = entry\n plugins.entries = entries\n settings.plugins = plugins\n return entry\n}\n\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 setEnv(settings: OpenClawSettings, key: string, value: string): void {\n const env = settings.env ?? {}\n env[key] = value\n settings.env = env\n}\n\nexport function removeEnv(settings: OpenClawSettings, keys: string[]): boolean {\n const env = settings.env\n if (!env) return false\n let removed = false\n for (const k of keys) {\n if (k in env) {\n delete env[k]\n removed = true\n }\n }\n return removed\n}\n\nexport function hasLatitudePlugin(settings: OpenClawSettings): boolean {\n return Boolean(settings.plugins?.entries && PLUGIN_ID in settings.plugins.entries)\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 {\n backupSettings,\n ensurePluginEntry,\n hasLatitudePlugin,\n PLUGIN_ID,\n readSettings,\n removeEnv,\n removePluginEntry,\n SETTINGS_BACKUP_PATH,\n SETTINGS_PATH,\n setEnv,\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 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 return {\n apiKey: typeof flags[\"api-key\"] === \"string\" ? flags[\"api-key\"] : undefined,\n project: typeof flags.project === \"string\" ? flags.project : undefined,\n environment,\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 existingEnv = existing.env ?? {}\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(existingEnv.LATITUDE_API_KEY, flags.apiKey)\n const project = await promptProject(existingEnv.LATITUDE_PROJECT, flags.project)\n\n await applyChanges({ apiKey, project, envConfig })\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({ apiKey, project, envConfig })\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}\n\nasync function applyChanges({ apiKey, project, envConfig }: ApplyParams): Promise<void> {\n const s = spinner()\n s.start(\"Updating openclaw.json\")\n ensureSettingsDir()\n backupSettings()\n const settings = readSettings()\n ensurePluginEntry(settings)\n setEnv(settings, \"LATITUDE_API_KEY\", apiKey)\n setEnv(settings, \"LATITUDE_PROJECT\", project)\n if (envConfig.name !== \"production\") {\n setEnv(settings, \"LATITUDE_BASE_URL\", envConfig.ingest)\n } else {\n // Clear any stale BASE_URL left over from a prior --staging/--dev install so\n // re-running `install` without flags is idempotent back to production.\n removeEnv(settings, [\"LATITUDE_BASE_URL\"])\n }\n writeSettings(settings)\n s.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 if (!hasLatitudePlugin(settings)) {\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 `Remove \"${PLUGIN_ID}\" plugin entry from ${SETTINGS_PATH}`,\n \"Remove LATITUDE_API_KEY / LATITUDE_PROJECT / LATITUDE_BASE_URL from env\",\n `Backup 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 const s = spinner()\n s.start(\"Reverting settings\")\n backupSettings()\n removePluginEntry(settings)\n removeEnv(settings, [\"LATITUDE_API_KEY\", \"LATITUDE_PROJECT\", \"LATITUDE_BASE_URL\"])\n writeSettings(settings)\n s.stop(\"Done\")\n outro(pc.green(\"✓ Uninstalled\"))\n}\n","import { normalizeInstallFlags, parseFlags, runInstall, runUninstall } from \"./setup.ts\"\n\nasync function main(): Promise<void> {\n const { subcommand, flags } = parseFlags(process.argv.slice(2))\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(\n \"usage: latitude-openclaw [install|uninstall] [--api-key=...] [--project=...] [--staging|--dev] [--yes]\\n\",\n )\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/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\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 so anything that runs `require(\"./dist/plugin.js\")`\n // from a `type: module` context still resolves cleanly (we mirror the source\n // package's `type` and `main` fields).\n if (existsSync(pkgSrc)) {\n const sourcePkg = JSON.parse(readFileSync(pkgSrc, \"utf-8\")) as {\n name?: string\n version?: string\n type?: string\n main?: string\n }\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 }\n writeFileSync(join(PLUGIN_INSTALL_DIR, \"package.json\"), `${JSON.stringify(minimalPkg, null, 2)}\\n`, \"utf-8\")\n }\n\n return {\n destination: PLUGIN_INSTALL_DIR,\n entryPath: join(PLUGIN_INSTALL_DIR, \"dist\", \"plugin.js\"),\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 { normalizeInstallFlags, parseFlags, runInstall, runUninstall } from \"./setup.ts\"\n\nasync function main(): Promise<void> {\n const { subcommand, flags } = parseFlags(process.argv.slice(2))\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(\n \"usage: latitude-openclaw [install|uninstall] [--api-key=...] [--project=...] [--staging|--dev] [--yes]\\n\",\n )\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.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ interface Config {
|
|
|
5
5
|
project: string;
|
|
6
6
|
enabled: boolean;
|
|
7
7
|
debug: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* When false, the plugin still emits one span per LLM call / tool / run, but
|
|
10
|
+
* scrubs raw conversation content (input/output messages, system prompt,
|
|
11
|
+
* tool args, tool results, the surfaced first-prompt). Token counts, model
|
|
12
|
+
* names, agent ids, and timings are unaffected.
|
|
13
|
+
*/
|
|
14
|
+
allowConversationAccess: boolean;
|
|
8
15
|
}
|
|
9
16
|
//#endregion
|
|
10
17
|
//#region src/logger.d.ts
|
|
@@ -83,9 +90,15 @@ interface RunRecord {
|
|
|
83
90
|
* touch. We avoid importing from `openclaw/plugin-sdk` so the package stays
|
|
84
91
|
* usable when OpenClaw isn't installed (the CLI and tests don't need it),
|
|
85
92
|
* and so we're robust to small signature changes across OpenClaw versions.
|
|
93
|
+
*
|
|
94
|
+
* `pluginConfig` is the user's `plugins.entries[id].config` block — that's
|
|
95
|
+
* the canonical place to read credentials and feature flags. The OpenClaw
|
|
96
|
+
* plugin SDK also exposes the same value as `api.pluginConfig` on the
|
|
97
|
+
* builder API; keep both names in sync if the upstream contract evolves.
|
|
86
98
|
*/
|
|
87
99
|
interface OpenClawPluginApiLike {
|
|
88
100
|
logger?: Logger;
|
|
101
|
+
pluginConfig?: Record<string, unknown>;
|
|
89
102
|
on: <K extends string>(hookName: K, handler: (event: unknown, ctx: unknown) => unknown, opts?: {
|
|
90
103
|
priority?: number;
|
|
91
104
|
}) => void;
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/config.ts","../src/logger.ts","../src/types.ts","../src/plugin.ts"],"mappings":";UAAiB,MAAA;EACf,MAAA;EACA,OAAA;EACA,OAAA;EACA,OAAA;EACA,KAAA;AAAA;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/config.ts","../src/logger.ts","../src/types.ts","../src/plugin.ts"],"mappings":";UAAiB,MAAA;EACf,MAAA;EACA,OAAA;EACA,OAAA;EACA,OAAA;EACA,KAAA;EAHA;;;;;;EAUA,uBAAA;AAAA;;;UCVe,MAAA;EACf,KAAA,GAAQ,GAAA;EACR,IAAA,GAAO,GAAA;AAAA;;;UCwDQ,gBAAA;EACf,KAAA;EACA,MAAA;EACA,SAAA;EACA,UAAA;EACA,KAAA;AAAA;AAAA,UA4De,aAAA;EACf,KAAA;EACA,SAAA;EACA,UAAA;EACA,OAAA;EACA,QAAA;EACA,YAAA;EACA,aAAA;EACA,WAAA;EACA,YAAA;EACA,MAAA;EACA,eAAA;EACA,WAAA;EACA,cAAA;EACA,aAAA;EACA,KAAA,EAAO,gBAAA;EACP,OAAA;EACA,KAAA;EACA,KAAA;EACA,SAAA,EAAW,cAAA;AAAA;AAAA,UAGI,cAAA;EACf,UAAA;EACA,QAAA;EACA,MAAA,EAAQ,MAAA;EACR,MAAA;EACA,KAAA;EACA,OAAA;EACA,KAAA;EACA,UAAA;EACA,OAAA;AAAA;AAAA,UAGe,SAAA;EACf,KAAA;EACA,SAAA;EACA,UAAA;EACA,OAAA;EACA,YAAA;EACA,eAAA;EACA,OAAA;EACA,SAAA;EACA,eAAA;EACA,OAAA;EACA,OAAA;EACA,KAAA;EACA,OAAA;EACA,KAAA;EACA,QAAA,EAAU,aAAA;ECnHmC;;;;;EDyH7C,WAAA,EAAa,cAAA;AAAA;;;;;;;;;;;;;;UCzJE,qBAAA;EACf,MAAA,GAAS,MAAA;EACT,YAAA,GAAe,MAAA;EACf,EAAA,qBACE,QAAA,EAAU,CAAA,EACV,OAAA,GAAU,KAAA,WAAgB,GAAA,uBAC1B,IAAA;IAAS,QAAA;EAAA;AAAA;AAAA,UAII,eAAA;EFjCf;EEmCA,MAAA,GAAS,MAAA;EFnCS;EEqClB,MAAA,GAAS,MAAA;;;;ADmBX;ECdE,MAAA,IAAU,GAAA,EAAK,SAAA;AAAA;;;;;;;;;AD+EjB;;iBClEwB,sBAAA,CAAuB,GAAA,EAAK,qBAAA,EAAuB,IAAA,GAAM,eAAA"}
|
package/dist/plugin.js
CHANGED
|
@@ -30,18 +30,38 @@ async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs
|
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/config.ts
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const DEFAULT_BASE_URL = "https://ingest.latitude.so";
|
|
34
|
+
/**
|
|
35
|
+
* Build a `Config` from OpenClaw's per-plugin config bucket plus environment
|
|
36
|
+
* variables. The plugin SDK passes `api.pluginConfig` (the user's
|
|
37
|
+
* `plugins.entries[id].config` block) to the registration function — that's
|
|
38
|
+
* the primary source. Env vars are kept as a fallback so existing deployments
|
|
39
|
+
* with `LATITUDE_*` already exported in the gateway environment keep working,
|
|
40
|
+
* and so that `LATITUDE_DEBUG=1` can be flipped without editing openclaw.json.
|
|
41
|
+
*/
|
|
42
|
+
function loadConfig(pluginConfig = void 0, env = process.env) {
|
|
43
|
+
const fromOpts = pluginConfig ?? {};
|
|
44
|
+
const apiKey = pickString(fromOpts.apiKey) ?? env.LATITUDE_API_KEY ?? "";
|
|
45
|
+
const project = pickString(fromOpts.project) ?? env.LATITUDE_PROJECT ?? "";
|
|
46
|
+
const baseUrl = pickString(fromOpts.baseUrl) ?? env.LATITUDE_BASE_URL ?? DEFAULT_BASE_URL;
|
|
47
|
+
const debug = pickBool(fromOpts.debug) ?? env.LATITUDE_DEBUG === "1";
|
|
48
|
+
const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false;
|
|
49
|
+
const explicitlyDisabled = pickBool(fromOpts.enabled) === false || (env.LATITUDE_OPENCLAW_ENABLED ?? "1") === "0";
|
|
37
50
|
return {
|
|
38
51
|
apiKey,
|
|
39
52
|
baseUrl,
|
|
40
53
|
project,
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
debug,
|
|
55
|
+
allowConversationAccess,
|
|
56
|
+
enabled: apiKey !== "" && project !== "" && !explicitlyDisabled
|
|
43
57
|
};
|
|
44
58
|
}
|
|
59
|
+
function pickString(value) {
|
|
60
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
61
|
+
}
|
|
62
|
+
function pickBool(value) {
|
|
63
|
+
return typeof value === "boolean" ? value : void 0;
|
|
64
|
+
}
|
|
45
65
|
//#endregion
|
|
46
66
|
//#region src/logger.ts
|
|
47
67
|
const PREFIX = "[latitude-openclaw]";
|
|
@@ -54,10 +74,10 @@ function createLogger(debugEnabled) {
|
|
|
54
74
|
//#endregion
|
|
55
75
|
//#region src/otlp.ts
|
|
56
76
|
const SCOPE_NAME = "@latitude-data/openclaw-telemetry";
|
|
57
|
-
const SCOPE_VERSION = "0.0.
|
|
77
|
+
const SCOPE_VERSION = "0.0.2";
|
|
58
78
|
/** Build an OTLP export request for a single completed agent run. */
|
|
59
|
-
function buildOtlpRequest(run) {
|
|
60
|
-
const spans = buildRunSpans(run);
|
|
79
|
+
function buildOtlpRequest(run, options) {
|
|
80
|
+
const spans = buildRunSpans(run, options);
|
|
61
81
|
return { resourceSpans: [{
|
|
62
82
|
resource: { attributes: resourceAttrs() },
|
|
63
83
|
scopeSpans: [{
|
|
@@ -69,25 +89,25 @@ function buildOtlpRequest(run) {
|
|
|
69
89
|
}]
|
|
70
90
|
}] };
|
|
71
91
|
}
|
|
72
|
-
function buildRunSpans(run) {
|
|
92
|
+
function buildRunSpans(run, options) {
|
|
73
93
|
const traceId = hashHex(`${run.sessionId ?? "session"}:${run.runId}`, 32);
|
|
74
94
|
const interactionSpanId = hashHex(`${traceId}:run`, 16);
|
|
75
|
-
const out = [buildInteractionSpan(traceId, interactionSpanId, run)];
|
|
95
|
+
const out = [buildInteractionSpan(traceId, interactionSpanId, run, options)];
|
|
76
96
|
run.llmCalls.forEach((call, idx) => {
|
|
77
97
|
const callSpanId = hashHex(`${traceId}:call:${idx}`, 16);
|
|
78
|
-
out.push(buildLlmSpan(traceId, interactionSpanId, callSpanId, call, idx, run));
|
|
98
|
+
out.push(buildLlmSpan(traceId, interactionSpanId, callSpanId, call, idx, run, options));
|
|
79
99
|
call.toolCalls.forEach((tool, tIdx) => {
|
|
80
100
|
const toolSpanId = hashHex(`${traceId}:call:${idx}:tool:${tIdx}`, 16);
|
|
81
|
-
out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run));
|
|
101
|
+
out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run, options));
|
|
82
102
|
});
|
|
83
103
|
});
|
|
84
104
|
run.orphanTools.forEach((tool, idx) => {
|
|
85
105
|
const toolSpanId = hashHex(`${traceId}:orphan-tool:${idx}`, 16);
|
|
86
|
-
out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run));
|
|
106
|
+
out.push(buildToolSpan(traceId, interactionSpanId, toolSpanId, tool, run, options));
|
|
87
107
|
});
|
|
88
108
|
return out;
|
|
89
109
|
}
|
|
90
|
-
function buildInteractionSpan(traceId, spanId, run) {
|
|
110
|
+
function buildInteractionSpan(traceId, spanId, run, options) {
|
|
91
111
|
const startNs = msToNs(run.startMs);
|
|
92
112
|
const endNs = msToNs(run.endMs ?? run.startMs);
|
|
93
113
|
const totalTools = run.llmCalls.reduce((sum, c) => sum + c.toolCalls.length, 0) + run.orphanTools.length;
|
|
@@ -125,17 +145,19 @@ function buildInteractionSpan(traceId, spanId, run) {
|
|
|
125
145
|
totalUsage.cacheRead !== void 0 ? int("gen_ai.usage.cache_read_input_tokens", totalUsage.cacheRead) : void 0,
|
|
126
146
|
totalUsage.cacheWrite !== void 0 ? int("gen_ai.usage.cache_creation_input_tokens", totalUsage.cacheWrite) : void 0,
|
|
127
147
|
totalUsage.total !== void 0 ? int("gen_ai.usage.total_tokens", totalUsage.total) : void 0,
|
|
128
|
-
run.llmCalls[0]?.prompt ? str("user_prompt", run.llmCalls[0].prompt) : void 0
|
|
148
|
+
options.allowConversationAccess && run.llmCalls[0]?.prompt ? str("user_prompt", run.llmCalls[0].prompt) : void 0,
|
|
149
|
+
bool("latitude.captured.content", options.allowConversationAccess)
|
|
129
150
|
]),
|
|
130
151
|
status: { code: run.success === false ? 2 : 1 }
|
|
131
152
|
};
|
|
132
153
|
}
|
|
133
|
-
function buildLlmSpan(traceId, parentSpanId, spanId, call, callIdx, run) {
|
|
154
|
+
function buildLlmSpan(traceId, parentSpanId, spanId, call, callIdx, run, options) {
|
|
134
155
|
const startNs = msToNs(call.startMs);
|
|
135
156
|
const endNs = msToNs(call.endMs ?? call.startMs);
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
157
|
+
const captureContent = options.allowConversationAccess;
|
|
158
|
+
const inputMessages = captureContent ? buildInputMessages(call) : void 0;
|
|
159
|
+
const outputMessages = captureContent ? buildOutputMessages(call) : void 0;
|
|
160
|
+
const systemInstructions = captureContent && call.systemPrompt ? JSON.stringify([{
|
|
139
161
|
type: "text",
|
|
140
162
|
content: call.systemPrompt
|
|
141
163
|
}]) : void 0;
|
|
@@ -173,8 +195,9 @@ function buildLlmSpan(traceId, parentSpanId, spanId, call, callIdx, run) {
|
|
|
173
195
|
call.usage?.cacheWrite !== void 0 ? int("cache_creation_tokens", call.usage.cacheWrite) : void 0,
|
|
174
196
|
call.usage?.total !== void 0 ? int("gen_ai.usage.total_tokens", call.usage.total) : void 0,
|
|
175
197
|
systemInstructions ? str("gen_ai.system_instructions", systemInstructions) : void 0,
|
|
176
|
-
str("gen_ai.input.messages", JSON.stringify(inputMessages)),
|
|
177
|
-
str("gen_ai.output.messages", JSON.stringify(outputMessages)),
|
|
198
|
+
inputMessages ? str("gen_ai.input.messages", JSON.stringify(inputMessages)) : void 0,
|
|
199
|
+
outputMessages ? str("gen_ai.output.messages", JSON.stringify(outputMessages)) : void 0,
|
|
200
|
+
bool("latitude.captured.content", captureContent),
|
|
178
201
|
int("openclaw.images.count", call.imagesCount),
|
|
179
202
|
int("llm_request.tool_call_count", call.toolCalls.length),
|
|
180
203
|
int("llm_request.duration_ms", durationMs(call.startMs, call.endMs)),
|
|
@@ -186,10 +209,11 @@ function buildLlmSpan(traceId, parentSpanId, spanId, call, callIdx, run) {
|
|
|
186
209
|
status: { code: call.error ? 2 : 1 }
|
|
187
210
|
};
|
|
188
211
|
}
|
|
189
|
-
function buildToolSpan(traceId, parentSpanId, spanId, tool, run) {
|
|
212
|
+
function buildToolSpan(traceId, parentSpanId, spanId, tool, run, options) {
|
|
190
213
|
const startNs = msToNs(tool.startMs);
|
|
191
214
|
const endNs = msToNs(tool.endMs ?? tool.startMs);
|
|
192
215
|
const isError = Boolean(tool.error);
|
|
216
|
+
const captureContent = options.allowConversationAccess;
|
|
193
217
|
return {
|
|
194
218
|
traceId,
|
|
195
219
|
spanId,
|
|
@@ -203,8 +227,9 @@ function buildToolSpan(traceId, parentSpanId, spanId, tool, run) {
|
|
|
203
227
|
str("gen_ai.operation.name", "execute_tool"),
|
|
204
228
|
str("gen_ai.tool.name", tool.toolName),
|
|
205
229
|
str("gen_ai.tool.call.id", tool.toolCallId),
|
|
206
|
-
str("gen_ai.tool.call.arguments", safeJson(tool.params)),
|
|
207
|
-
tool.result !== void 0 ? str("gen_ai.tool.call.result", safeJson(tool.result)) : void 0,
|
|
230
|
+
captureContent ? str("gen_ai.tool.call.arguments", safeJson(tool.params)) : void 0,
|
|
231
|
+
captureContent && tool.result !== void 0 ? str("gen_ai.tool.call.result", safeJson(tool.result)) : void 0,
|
|
232
|
+
bool("latitude.captured.content", captureContent),
|
|
208
233
|
isError ? str("error.type", "tool_error") : void 0,
|
|
209
234
|
isError ? str("error.message", tool.error ?? "") : void 0,
|
|
210
235
|
bool("tool.is_error", isError),
|
|
@@ -579,14 +604,14 @@ var TurnBuilder = class {
|
|
|
579
604
|
* nothing we do here can slow the agent loop.
|
|
580
605
|
*/
|
|
581
606
|
function registerLatitudePlugin(api, opts = {}) {
|
|
582
|
-
const config = opts.config ?? loadConfig();
|
|
607
|
+
const config = opts.config ?? loadConfig(api.pluginConfig);
|
|
583
608
|
const logger = opts.logger ?? createLogger(config.debug);
|
|
584
609
|
if (!config.enabled) {
|
|
585
|
-
if (config.apiKey === "") logger.debug("disabled:
|
|
586
|
-
if (config.project === "") logger.debug("disabled:
|
|
610
|
+
if (config.apiKey === "") logger.debug("disabled: apiKey is empty (set plugins.entries[id].config.apiKey)");
|
|
611
|
+
if (config.project === "") logger.debug("disabled: project is empty (set plugins.entries[id].config.project)");
|
|
587
612
|
return;
|
|
588
613
|
}
|
|
589
|
-
logger.debug(`enabled: project=${config.project} base=${config.baseUrl}`);
|
|
614
|
+
logger.debug(`enabled: project=${config.project} base=${config.baseUrl} allowConversationAccess=${config.allowConversationAccess}`);
|
|
590
615
|
const builder = new TurnBuilder();
|
|
591
616
|
api.on("session_start", (evt, ctx) => {
|
|
592
617
|
builder.onSessionStart(evt, ctx);
|
|
@@ -627,7 +652,7 @@ function registerLatitudePlugin(api, opts = {}) {
|
|
|
627
652
|
return;
|
|
628
653
|
}
|
|
629
654
|
opts.onEmit?.(run);
|
|
630
|
-
const payload = buildOtlpRequest(run);
|
|
655
|
+
const payload = buildOtlpRequest(run, { allowConversationAccess: config.allowConversationAccess });
|
|
631
656
|
postTraces({
|
|
632
657
|
baseUrl: config.baseUrl,
|
|
633
658
|
apiKey: config.apiKey,
|
package/dist/plugin.js.map
CHANGED
|
@@ -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\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const apiKey = env.LATITUDE_API_KEY ?? \"\"\n const baseUrl = env.LATITUDE_BASE_URL ?? \"https://ingest.latitude.so\"\n const project = env.LATITUDE_PROJECT ?? \"\"\n const enabled = (env.LATITUDE_OPENCLAW_ENABLED ?? \"1\") !== \"0\" && apiKey !== \"\" && project !== \"\"\n const debug = env.LATITUDE_DEBUG === \"1\"\n return { apiKey, baseUrl, project, enabled, debug }\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.1\"\n\n/** Build an OTLP export request for a single completed agent run. */\nexport function buildOtlpRequest(run: RunRecord): OtlpExportRequest {\n const spans = buildRunSpans(run)\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): 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)]\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))\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))\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))\n })\n\n return out\n}\n\nfunction buildInteractionSpan(traceId: string, spanId: string, run: RunRecord): 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.\n run.llmCalls[0]?.prompt ? str(\"user_prompt\", run.llmCalls[0].prompt) : undefined,\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): OtlpSpan {\n const startNs = msToNs(call.startMs)\n const endNs = msToNs(call.endMs ?? call.startMs)\n\n const inputMessages = buildInputMessages(call)\n const outputMessages = buildOutputMessages(call)\n const systemInstructions = call.systemPrompt\n ? JSON.stringify([{ type: \"text\", content: call.systemPrompt }])\n : 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.\n systemInstructions ? str(\"gen_ai.system_instructions\", systemInstructions) : undefined,\n str(\"gen_ai.input.messages\", JSON.stringify(inputMessages)),\n str(\"gen_ai.output.messages\", JSON.stringify(outputMessages)),\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): OtlpSpan {\n const startNs = msToNs(tool.startMs)\n const endNs = msToNs(tool.endMs ?? tool.startMs)\n const isError = Boolean(tool.error)\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 str(\"gen_ai.tool.call.arguments\", safeJson(tool.params)),\n tool.result !== undefined ? str(\"gen_ai.tool.call.result\", safeJson(tool.result)) : undefined,\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 */\nexport interface OpenClawPluginApiLike {\n logger?: Logger\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 const config = opts.config ?? loadConfig()\n const logger = opts.logger ?? createLogger(config.debug)\n\n if (!config.enabled) {\n if (config.apiKey === \"\") logger.debug(\"disabled: LATITUDE_API_KEY is empty\")\n if (config.project === \"\") logger.debug(\"disabled: LATITUDE_PROJECT is empty\")\n return\n }\n logger.debug(`enabled: project=${config.project} base=${config.baseUrl}`)\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)\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 { 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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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "@latitude-data/openclaw-telemetry",
|
|
3
|
+
"name": "Latitude Telemetry",
|
|
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.2",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": true,
|
|
9
|
+
"properties": {
|
|
10
|
+
"apiKey": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Latitude bearer token. Required."
|
|
13
|
+
},
|
|
14
|
+
"project": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Latitude project slug to route traces into. Required."
|
|
17
|
+
},
|
|
18
|
+
"baseUrl": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "OTLP ingest origin. Defaults to https://ingest.latitude.so. Override for staging/dev."
|
|
21
|
+
},
|
|
22
|
+
"allowConversationAccess": {
|
|
23
|
+
"type": "boolean",
|
|
24
|
+
"default": false,
|
|
25
|
+
"description": "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 ids."
|
|
26
|
+
},
|
|
27
|
+
"debug": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"default": false,
|
|
30
|
+
"description": "Log diagnostic lines to stderr."
|
|
31
|
+
},
|
|
32
|
+
"enabled": {
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"default": true,
|
|
35
|
+
"description": "Set to false to pause emission without uninstalling."
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": ["apiKey", "project"]
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latitude-data/openclaw-telemetry",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"homepage": "https://github.com/latitude-dev/latitude-llm/tree/main/packages/telemetry/openclaw#readme",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"files": [
|
|
22
|
-
"dist"
|
|
22
|
+
"dist",
|
|
23
|
+
"openclaw.plugin.json"
|
|
23
24
|
],
|
|
24
25
|
"bin": {
|
|
25
26
|
"latitude-openclaw": "./dist/cli.js"
|