@latitude-data/openclaw-telemetry 0.0.4 → 0.0.6

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/dist/cli.js DELETED
@@ -1,603 +0,0 @@
1
- #!/usr/bin/env node
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { cancel, confirm, intro, isCancel, log, note, outro, password, spinner, text } from "@clack/prompts";
6
- import pc from "picocolors";
7
- import { spawnSync } from "node:child_process";
8
- import { homedir } from "node:os";
9
- //#region src/openclaw-cli.ts
10
- /**
11
- * Lowest OpenClaw version we support. The reporter verified hook-dispatch
12
- * gating works correctly here; older versions either reject
13
- * `hooks.allowConversationAccess` outright (≤ 2026.4.21) or have unverified
14
- * gating behaviour (2026.4.22 – 2026.4.24). Refusing to install on older
15
- * versions is intentional — we'd rather fail loudly than ship a
16
- * config the gateway will quarantine or hooks the dispatcher will block.
17
- */
18
- const MIN_OPENCLAW_VERSION = "2026.4.25";
19
- const DEFAULT_TIMEOUT_MS = 1e4;
20
- /**
21
- * Spawn `openclaw <args>` synchronously. Reports failure modes structurally so
22
- * callers can decide how to degrade (missing binary vs. timed out vs. exited
23
- * non-zero). Never throws; ENOENT becomes `{ reason: "enoent" }`.
24
- */
25
- function runOpenclaw(args, opts = {}) {
26
- const result = spawnSync("openclaw", args, {
27
- encoding: "utf-8",
28
- timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
29
- input: opts.stdin,
30
- stdio: [
31
- "pipe",
32
- "pipe",
33
- "pipe"
34
- ]
35
- });
36
- const err = result.error;
37
- if (err?.code === "ENOENT") return {
38
- ok: false,
39
- reason: "enoent",
40
- stdout: "",
41
- stderr: "",
42
- code: null
43
- };
44
- if (err?.code === "ETIMEDOUT" || result.signal === "SIGTERM" || result.signal === "SIGKILL") return {
45
- ok: false,
46
- reason: "timeout",
47
- stdout: result.stdout ?? "",
48
- stderr: result.stderr ?? "",
49
- code: null
50
- };
51
- if (err) return {
52
- ok: false,
53
- reason: "exit",
54
- stdout: result.stdout ?? "",
55
- stderr: result.stderr ?? String(err),
56
- code: typeof result.status === "number" ? result.status : 1
57
- };
58
- if (result.status === 0) return {
59
- ok: true,
60
- stdout: result.stdout ?? "",
61
- stderr: result.stderr ?? "",
62
- code: 0
63
- };
64
- return {
65
- ok: false,
66
- reason: "exit",
67
- stdout: result.stdout ?? "",
68
- stderr: result.stderr ?? "",
69
- code: typeof result.status === "number" ? result.status : 1
70
- };
71
- }
72
- /**
73
- * Run `openclaw --version` and parse out the version string.
74
- *
75
- * Banner format (per OpenClaw `src/cli/banner.ts` `formatCliBannerLine`):
76
- * `🦞 OpenClaw <version> (<commit-sha>)`
77
- *
78
- * The banner is normally suppressed when `--version` is the flag, but the
79
- * version itself still goes to stdout. We accept either layout (with or
80
- * without the lobster + commit sha) so we don't break if OpenClaw later
81
- * prints just the bare version string.
82
- */
83
- function getOpenclawVersion() {
84
- const result = runOpenclaw(["--version"], { timeoutMs: 5e3 });
85
- if (!result.ok) {
86
- if (result.reason === "enoent") return {
87
- ok: false,
88
- error: "missing"
89
- };
90
- return {
91
- ok: false,
92
- error: "unparseable",
93
- raw: result.stdout || result.stderr
94
- };
95
- }
96
- const raw = result.stdout.trim();
97
- const match = raw.match(/(\d{4}\.\d+\.\d+)/);
98
- if (!match) return {
99
- ok: false,
100
- error: "unparseable",
101
- raw
102
- };
103
- return {
104
- ok: true,
105
- version: match[1],
106
- raw
107
- };
108
- }
109
- /**
110
- * Compare two CalVer strings (`YYYY.M.PATCH`). Returns -1 if `a` < `b`,
111
- * 0 if equal, 1 if `a` > `b`. Strings with extra components or non-numeric
112
- * pieces fall back to a per-component string comparison so unexpected
113
- * formats don't crash the installer.
114
- */
115
- function compareCalver(a, b) {
116
- const ap = a.split(".");
117
- const bp = b.split(".");
118
- const len = Math.max(ap.length, bp.length);
119
- for (let i = 0; i < len; i++) {
120
- const ai = ap[i] ?? "0";
121
- const bi = bp[i] ?? "0";
122
- const an = Number(ai);
123
- const bn = Number(bi);
124
- if (Number.isFinite(an) && Number.isFinite(bn)) {
125
- if (an < bn) return -1;
126
- if (an > bn) return 1;
127
- continue;
128
- }
129
- if (ai < bi) return -1;
130
- if (ai > bi) return 1;
131
- }
132
- return 0;
133
- }
134
- //#endregion
135
- //#region src/settings-file.ts
136
- const CONFIG_DIR = join(homedir(), ".openclaw");
137
- const SETTINGS_PATH = join(CONFIG_DIR, "openclaw.json");
138
- const SETTINGS_BACKUP_PATH = join(CONFIG_DIR, "openclaw.json.latitude-bak");
139
- /** Plugin id used both as the npm package name and as the OpenClaw plugin id. */
140
- const PLUGIN_ID = "@latitude-data/openclaw-telemetry";
141
- function readSettings() {
142
- if (!existsSync(SETTINGS_PATH)) return {};
143
- try {
144
- const raw = readFileSync(SETTINGS_PATH, "utf-8");
145
- const parsed = JSON.parse(raw);
146
- return parsed && typeof parsed === "object" ? parsed : {};
147
- } catch {
148
- return {};
149
- }
150
- }
151
- function writeSettings(settings) {
152
- writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
153
- }
154
- function backupSettings() {
155
- if (existsSync(SETTINGS_PATH)) copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH);
156
- }
157
- /**
158
- * Set the `plugins.entries[id]` block for our plugin.
159
- *
160
- * Two places in the entry get written:
161
- *
162
- * - `.config` (free-form `record(string, unknown)`): credentials, baseUrl,
163
- * and our copy of `allowConversationAccess`. This is what the plugin
164
- * runtime reads via `api.pluginConfig`.
165
- * - `.hooks.allowConversationAccess`: controls whether OpenClaw's hook
166
- * dispatcher actually forwards `llm_input` / `llm_output` / tool /
167
- * `agent_end` events to our handlers. Without it set to `true` on
168
- * OpenClaw 2026.4.25+, every typed hook is blocked at the dispatcher
169
- * and the plugin's handlers never fire.
170
- *
171
- * The two flags mean different things — `hooks.*` is the dispatch gate,
172
- * `config.*` is the payload-content gate — but for THIS plugin we always
173
- * couple them: dispatch off + payload on is useless (no payloads to gate),
174
- * and dispatch on + payload off is a legitimate "structural-only telemetry"
175
- * mode (timing, tokens, ids, agent name; no message bodies). Always writing
176
- * both from the same source keeps the operator's mental model simple.
177
- *
178
- * Re-install idempotency: only `apiKey` / `project` / `baseUrl` always
179
- * overwrite (these come from install prompts). `enabled`, `debug`, and
180
- * `allowConversationAccess` are preserved when not provided in the patch.
181
- */
182
- function setPluginEntry(settings, patch) {
183
- const plugins = settings.plugins ?? {};
184
- const entries = plugins.entries ?? {};
185
- const existing = entries["@latitude-data/openclaw-telemetry"] ?? {};
186
- const existingConfig = existing.config ?? {};
187
- const existingHooks = existing.hooks ?? {};
188
- const nextConfig = {
189
- ...existingConfig,
190
- apiKey: patch.apiKey,
191
- project: patch.project
192
- };
193
- if (patch.baseUrl !== void 0) nextConfig.baseUrl = patch.baseUrl;
194
- else delete nextConfig.baseUrl;
195
- if (patch.allowConversationAccess !== void 0) nextConfig.allowConversationAccess = patch.allowConversationAccess;
196
- if (patch.debug !== void 0) nextConfig.debug = patch.debug;
197
- const effectiveAccess = typeof nextConfig.allowConversationAccess === "boolean" ? nextConfig.allowConversationAccess : typeof existingHooks.allowConversationAccess === "boolean" ? existingHooks.allowConversationAccess : true;
198
- const nextHooks = {
199
- ...existingHooks,
200
- allowConversationAccess: effectiveAccess
201
- };
202
- const nextEnabled = patch.enabled ?? existing.enabled ?? true;
203
- entries[PLUGIN_ID] = {
204
- ...existing,
205
- enabled: nextEnabled,
206
- hooks: nextHooks,
207
- config: nextConfig
208
- };
209
- plugins.entries = entries;
210
- settings.plugins = plugins;
211
- }
212
- /** Remove the plugin entry entirely. Used by uninstall as defense-in-depth. */
213
- function removePluginEntry(settings) {
214
- const plugins = settings.plugins;
215
- if (!plugins?.entries) return false;
216
- if (!("@latitude-data/openclaw-telemetry" in plugins.entries)) return false;
217
- delete plugins.entries[PLUGIN_ID];
218
- return true;
219
- }
220
- /**
221
- * Add the plugin id to `plugins.allow`. Idempotent — returns `true` only when
222
- * the array changed. OpenClaw warns at every gateway start when a non-bundled
223
- * plugin auto-loads without provenance via `plugins.allow` or an install
224
- * record. We get one warning cleared by going through `openclaw plugins
225
- * install` (provenance) and the other by adding ourselves to allow.
226
- *
227
- * Defensive against hand-edited non-array values: if `plugins.allow` is
228
- * present but not an array (e.g. someone wrote a string), we replace it
229
- * with a single-element array rather than spreading the bad value.
230
- */
231
- function addToPluginsAllow(settings) {
232
- const plugins = settings.plugins ?? {};
233
- const existing = plugins.allow;
234
- const allow = Array.isArray(existing) ? existing : [];
235
- if (allow.includes("@latitude-data/openclaw-telemetry")) return false;
236
- plugins.allow = [...allow, PLUGIN_ID];
237
- settings.plugins = plugins;
238
- return true;
239
- }
240
- /**
241
- * Inverse of `addToPluginsAllow`. Defense-in-depth — `openclaw plugins
242
- * uninstall` already strips the entry, but the install path can be skipped
243
- * (e.g. the user removed the plugin manually) and we want re-install/uninstall
244
- * round-trips to be tidy regardless.
245
- */
246
- function removeFromPluginsAllow(settings) {
247
- const allow = settings.plugins?.allow;
248
- if (!Array.isArray(allow) || !allow.includes("@latitude-data/openclaw-telemetry")) return false;
249
- if (settings.plugins) settings.plugins.allow = allow.filter((id) => id !== PLUGIN_ID);
250
- return true;
251
- }
252
- function hasLatitudePlugin(settings) {
253
- return Boolean(settings.plugins?.entries && "@latitude-data/openclaw-telemetry" in settings.plugins.entries);
254
- }
255
- /**
256
- * Strip leftover keys from older installers that the strict zod schema
257
- * rejects on current OpenClaw versions.
258
- *
259
- * 0.0.1 wrote `LATITUDE_*` keys directly under `settings.env`. OpenClaw's
260
- * root schema is strict; the `env` block accepts only `{shellEnv, vars}`,
261
- * so those keys cause the gateway to quarantine the config as
262
- * `clobbered.<ts>` and roll back. We sweep them on every install.
263
- *
264
- * Note: 0.0.1 also wrote `hooks.allowConversationAccess` (when that key was
265
- * not yet in the schema). We deliberately do NOT strip it anymore — on
266
- * OpenClaw 2026.4.25+ the key IS in the schema and IS load-bearing for
267
- * dispatch. `setPluginEntry` overwrites it on every install with the right
268
- * value, so any 0.0.1 leftover is reconciled there.
269
- */
270
- function migrateLegacyEntries(settings) {
271
- let changed = false;
272
- const env = settings.env;
273
- if (env && typeof env === "object" && !Array.isArray(env)) {
274
- const envObj = env;
275
- for (const key of [
276
- "LATITUDE_API_KEY",
277
- "LATITUDE_PROJECT",
278
- "LATITUDE_BASE_URL"
279
- ]) if (key in envObj) {
280
- delete envObj[key];
281
- changed = true;
282
- }
283
- if (Object.keys(envObj).length === 0) delete settings.env;
284
- }
285
- return { changed };
286
- }
287
- //#endregion
288
- //#region src/setup.ts
289
- const DOCS_URL = "https://docs.latitude.so/openclaw-telemetry";
290
- const PRODUCTION_ENV = {
291
- name: "production",
292
- label: "production",
293
- app: "https://console.latitude.so",
294
- ingest: "https://ingest.latitude.so"
295
- };
296
- const STAGING_ENV = {
297
- name: "staging",
298
- label: "staging",
299
- app: "https://staging.latitude.so",
300
- ingest: "https://staging-ingest.latitude.so"
301
- };
302
- const DEV_ENV = {
303
- name: "dev",
304
- label: "local dev",
305
- app: "http://localhost:3000",
306
- ingest: "http://localhost:3002"
307
- };
308
- function urlsFor(env) {
309
- return {
310
- apiKeys: `${env.app}/settings/api-keys`,
311
- projects: env.app,
312
- projectView: (slug) => `${env.app}/projects/${slug}`
313
- };
314
- }
315
- function parseFlags(argv) {
316
- const [subcommand, ...rest] = argv;
317
- const flags = {};
318
- for (const arg of rest) {
319
- if (!arg.startsWith("--")) continue;
320
- const eq = arg.indexOf("=");
321
- if (eq >= 0) flags[arg.slice(2, eq)] = arg.slice(eq + 1);
322
- else flags[arg.slice(2)] = true;
323
- }
324
- return {
325
- subcommand,
326
- flags
327
- };
328
- }
329
- function normalizeInstallFlags(flags) {
330
- let environment;
331
- if (flags.staging === true) environment = STAGING_ENV;
332
- if (flags.dev === true) {
333
- if (environment) throw new Error("--staging and --dev are mutually exclusive");
334
- environment = DEV_ENV;
335
- }
336
- let allowConversationAccess;
337
- if (flags["no-content"] === true || flags["no-conversation"] === true) allowConversationAccess = false;
338
- if (flags["allow-conversation"] === true) allowConversationAccess = true;
339
- return {
340
- apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : void 0,
341
- project: typeof flags.project === "string" ? flags.project : void 0,
342
- environment,
343
- allowConversationAccess,
344
- noTrust: flags["no-trust"] === true,
345
- noPrompt: flags["no-prompt"] === true || flags.yes === true,
346
- yes: flags.yes === true
347
- };
348
- }
349
- async function runInstall(flags = {}) {
350
- if (!(!flags.noPrompt && process.stdin.isTTY === true)) return runFlagDrivenInstall(flags);
351
- await runInteractiveInstall(flags);
352
- }
353
- async function runInteractiveInstall(flags) {
354
- intro(pc.bgCyan(pc.black(" Latitude · OpenClaw telemetry ")));
355
- ensureOpenclawIsCompatible();
356
- const existingConfig = readSettings().plugins?.entries?.["@latitude-data/openclaw-telemetry"]?.config ?? void 0;
357
- const envConfig = flags.environment ?? PRODUCTION_ENV;
358
- const urls = urlsFor(envConfig);
359
- const aboutLines = [
360
- "Captures every OpenClaw agent run and ships it to Latitude as",
361
- "OpenTelemetry traces — full system prompt, tool I/O, messages,",
362
- "token usage, and agent name on every span.",
363
- "",
364
- `${pc.dim("Docs")} ${pc.cyan(DOCS_URL)}`
365
- ];
366
- if (envConfig.name !== "production") aboutLines.push("", pc.yellow(`Using ${envConfig.label} environment (${envConfig.ingest})`));
367
- note(aboutLines.join("\n"), "About");
368
- log.info(`Get an API key at ${pc.cyan(urls.apiKeys)}`);
369
- log.info(`Create a project at ${pc.cyan(urls.projects)}`);
370
- const apiKey = await promptApiKey(existingConfig?.apiKey, flags.apiKey);
371
- const project = await promptProject(existingConfig?.project, flags.project);
372
- await applyChanges({
373
- apiKey,
374
- project,
375
- envConfig,
376
- allowConversationAccess: flags.allowConversationAccess,
377
- noTrust: flags.noTrust === true
378
- });
379
- note([
380
- "Restart the OpenClaw gateway for the plugin to load:",
381
- pc.dim(" openclaw gateway restart"),
382
- "",
383
- `View your traces at ${pc.cyan(urls.projectView(project))}`
384
- ].join("\n"), "Next step");
385
- outro(pc.green("✓ Installed"));
386
- }
387
- async function runFlagDrivenInstall(flags) {
388
- ensureOpenclawIsCompatible();
389
- const apiKey = flags.apiKey;
390
- const project = flags.project;
391
- if (!apiKey || !project) throw new Error("Non-interactive install requires --api-key=... and --project=... (or run in a TTY).");
392
- await applyChanges({
393
- apiKey,
394
- project,
395
- envConfig: flags.environment ?? PRODUCTION_ENV,
396
- allowConversationAccess: flags.allowConversationAccess,
397
- noTrust: flags.noTrust === true
398
- });
399
- process.stdout.write(`Installed Latitude plugin in ${SETTINGS_PATH}\n`);
400
- }
401
- async function promptApiKey(_existing, flag) {
402
- if (flag) return flag;
403
- const result = await password({
404
- message: "Latitude API key",
405
- mask: "•",
406
- validate: (v) => v && v.length > 0 ? void 0 : "Required"
407
- });
408
- if (isCancel(result)) return onCancel();
409
- return result;
410
- }
411
- async function promptProject(existing, flag) {
412
- if (flag) return flag;
413
- const result = await text({
414
- message: "Latitude project slug",
415
- placeholder: existing ?? "my-openclaw-project",
416
- ...existing ? { initialValue: existing } : {},
417
- validate: (v) => v && v.length > 0 ? void 0 : "Required"
418
- });
419
- if (isCancel(result)) return onCancel();
420
- return result;
421
- }
422
- function onCancel() {
423
- cancel("Cancelled — nothing was changed");
424
- process.exit(1);
425
- }
426
- async function applyChanges({ apiKey, project, envConfig, allowConversationAccess, noTrust }) {
427
- ensureSettingsDir();
428
- backupSettings();
429
- const packageRoot = resolvePackageRoot();
430
- const installSpinner = spinner();
431
- installSpinner.start(`Installing plugin via openclaw plugins install ${packageRoot}`);
432
- const installResult = runOpenclaw([
433
- "plugins",
434
- "install",
435
- packageRoot,
436
- "--force"
437
- ], { timeoutMs: 6e4 });
438
- if (!installResult.ok) {
439
- installSpinner.stop("openclaw plugins install failed");
440
- if (installResult.reason === "enoent") throw new Error("`openclaw` not found on PATH. Install OpenClaw first (https://openclaw.ai/install) and re-run.");
441
- if (installResult.reason === "timeout") throw new Error("openclaw plugins install timed out after 60s. Try running it manually to see what's stuck.");
442
- const detail = installResult.stderr.trim() || installResult.stdout.trim() || `exit code ${installResult.code}`;
443
- throw new Error(`openclaw plugins install failed: ${detail}`);
444
- }
445
- installSpinner.stop("Plugin registered with OpenClaw");
446
- const settingsSpinner = spinner();
447
- settingsSpinner.start("Updating openclaw.json");
448
- const settings = readSettings();
449
- migrateLegacyEntries(settings);
450
- setPluginEntry(settings, {
451
- apiKey,
452
- project,
453
- baseUrl: envConfig.name === "production" ? void 0 : envConfig.ingest,
454
- allowConversationAccess
455
- });
456
- if (!noTrust) addToPluginsAllow(settings);
457
- writeSettings(settings);
458
- settingsSpinner.stop(`Updated ${SETTINGS_PATH}`);
459
- if (existsSync(SETTINGS_BACKUP_PATH)) log.info(`Backup saved at ${pc.dim(SETTINGS_BACKUP_PATH)}`);
460
- if (noTrust) log.warning(`--no-trust set; OpenClaw will warn at every gateway start that ${PLUGIN_ID} is untrusted. Add it to plugins.allow yourself when you're ready.`);
461
- }
462
- function ensureSettingsDir() {
463
- const dir = dirname(SETTINGS_PATH);
464
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
465
- }
466
- /**
467
- * Verify `openclaw` is on PATH AND its version is >= MIN_OPENCLAW_VERSION.
468
- * Aborts with a clear upgrade message otherwise. Called before any user
469
- * prompts so we don't waste their time collecting credentials we can't
470
- * use.
471
- */
472
- function ensureOpenclawIsCompatible() {
473
- const v = getOpenclawVersion();
474
- if (!v.ok) {
475
- if (v.error === "missing") {
476
- cancel("OpenClaw CLI not found on PATH. Install or update via `npm install -g openclaw@latest` and re-run.");
477
- process.exit(1);
478
- }
479
- cancel(`Couldn't parse OpenClaw version output${v.raw ? ` (got: ${pc.dim(v.raw)})` : ""}. Run \`openclaw --version\` and report the output.`);
480
- process.exit(1);
481
- }
482
- if (compareCalver(v.version, "2026.4.25") < 0) {
483
- cancel(`OpenClaw ${v.version} is older than the minimum supported version (${MIN_OPENCLAW_VERSION}). Run \`npm install -g openclaw@latest\` and re-run install.`);
484
- process.exit(1);
485
- }
486
- log.info(`OpenClaw ${pc.dim(v.version)} (>= ${MIN_OPENCLAW_VERSION})`);
487
- }
488
- /**
489
- * Resolve the absolute path to our package's root (the directory that
490
- * contains `package.json` + `openclaw.plugin.json` + `dist/`). The compiled
491
- * CLI lives at `<package-root>/dist/cli.js`, so `import.meta.url`'s parent
492
- * directory's parent is our root.
493
- *
494
- * `openclaw plugins install <path>` copies files synchronously, so we
495
- * don't have to keep this directory alive past the spawn return.
496
- */
497
- function resolvePackageRoot() {
498
- return resolve(dirname(fileURLToPath(import.meta.url)), "..");
499
- }
500
- async function runUninstall(flags = {}) {
501
- intro(pc.bgYellow(pc.black(" Latitude · OpenClaw telemetry — uninstall ")));
502
- if (!hasLatitudePlugin(readSettings())) {
503
- note("No Latitude plugin entry found — nothing to remove.", "Status");
504
- outro(pc.dim("Nothing changed"));
505
- return;
506
- }
507
- note([
508
- `Run \`openclaw plugins uninstall ${PLUGIN_ID} --force\` (removes files, install record, and plugin entry)`,
509
- `Sweep any leftover LATITUDE_* keys from settings.env`,
510
- `Backup of openclaw.json saved at ${SETTINGS_BACKUP_PATH}`
511
- ].join("\n"), "Plan");
512
- if (!flags.noPrompt && process.stdin.isTTY === true) {
513
- const ok = await confirm({
514
- message: "Proceed?",
515
- initialValue: true
516
- });
517
- if (isCancel(ok) || ok !== true) return onCancel();
518
- }
519
- backupSettings();
520
- const s = spinner();
521
- s.start("Reverting via openclaw plugins uninstall");
522
- const uninstallResult = runOpenclaw([
523
- "plugins",
524
- "uninstall",
525
- PLUGIN_ID,
526
- "--force"
527
- ], { timeoutMs: 6e4 });
528
- if (!uninstallResult.ok) {
529
- s.stop("openclaw plugins uninstall failed");
530
- if (uninstallResult.reason === "enoent") log.warning("`openclaw` not found on PATH. Falling back to local cleanup — files at ~/.openclaw/extensions/ may remain.");
531
- else {
532
- const detail = uninstallResult.stderr.trim() || uninstallResult.stdout.trim() || `exit code ${uninstallResult.code}`;
533
- log.warning(`openclaw plugins uninstall reported: ${detail}. Continuing with local cleanup.`);
534
- }
535
- } else s.stop("Plugin removed by OpenClaw");
536
- const cleanupSpinner = spinner();
537
- cleanupSpinner.start("Reverting openclaw.json");
538
- const post = readSettings();
539
- removePluginEntry(post);
540
- removeFromPluginsAllow(post);
541
- migrateLegacyEntries(post);
542
- writeSettings(post);
543
- cleanupSpinner.stop("Done");
544
- outro(pc.green("✓ Uninstalled"));
545
- }
546
- //#endregion
547
- //#region src/cli.ts
548
- const USAGE = `usage: latitude-openclaw <command> [options]
549
-
550
- commands:
551
- install Install the plugin (interactive when stdin is a TTY)
552
- uninstall Remove the plugin entry and files
553
- --version, -v Print the package version
554
- --help, -h Print this message
555
-
556
- install options:
557
- --api-key=<key> Pass the API key non-interactively
558
- --project=<slug> Pass the project slug non-interactively
559
- --staging Target https://staging.latitude.so / staging-ingest
560
- --dev Target http://localhost:3000 / 3002
561
- --no-content Skip raw prompt/response/tool I/O capture
562
- --allow-conversation Force conversation capture on (overrides existing config)
563
- --yes / --no-prompt Skip all prompts (required for non-TTY / CI)
564
- `;
565
- function readVersion() {
566
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
567
- try {
568
- return JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "unknown";
569
- } catch {
570
- return "unknown";
571
- }
572
- }
573
- async function main() {
574
- const argv = process.argv.slice(2);
575
- if (argv[0] === "--version" || argv[0] === "-v") {
576
- process.stdout.write(`${readVersion()}\n`);
577
- return;
578
- }
579
- if (argv[0] === "--help" || argv[0] === "-h") {
580
- process.stdout.write(USAGE);
581
- return;
582
- }
583
- const { subcommand, flags } = parseFlags(argv);
584
- if (subcommand === "install" || subcommand === void 0) {
585
- await runInstall(normalizeInstallFlags(flags));
586
- return;
587
- }
588
- if (subcommand === "uninstall") {
589
- await runUninstall({ noPrompt: flags["no-prompt"] === true || flags.yes === true });
590
- return;
591
- }
592
- process.stderr.write(`unknown subcommand: ${subcommand}\n`);
593
- process.stderr.write(USAGE);
594
- process.exit(1);
595
- }
596
- main().catch((err) => {
597
- process.stderr.write(`${String(err)}\n`);
598
- process.exit(1);
599
- });
600
- //#endregion
601
- export {};
602
-
603
- //# sourceMappingURL=cli.js.map