@latitude-data/openclaw-telemetry 0.0.3 → 0.0.5

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.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/settings-file.ts","../src/install-files.ts","../src/setup.ts","../src/cli.ts"],"sourcesContent":["import { copyFileSync, existsSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\nconst CONFIG_DIR = join(homedir(), \".openclaw\")\nexport const SETTINGS_PATH = join(CONFIG_DIR, \"openclaw.json\")\nexport const SETTINGS_BACKUP_PATH = join(CONFIG_DIR, \"openclaw.json.latitude-bak\")\nconst EXTENSIONS_DIR = join(CONFIG_DIR, \"extensions\")\nexport const PLUGIN_INSTALL_DIR = join(EXTENSIONS_DIR, \"latitude-telemetry\")\n\n/** Plugin id used both as the npm package name and as the OpenClaw plugin id. */\nexport const PLUGIN_ID = \"@latitude-data/openclaw-telemetry\"\n\n/**\n * The shape OpenClaw's strict zod schema accepts for a single\n * `plugins.entries[id]` block. We deliberately keep this minimal — only the\n * fields we actually write — so we don't drop anything when round-tripping\n * an existing entry that uses fields we don't know about.\n */\ninterface OpenClawPluginEntry {\n enabled?: boolean\n /**\n * The free-form bucket OpenClaw passes to the plugin at activation. This is\n * where we store all our credentials and feature flags — `hooks` is strict,\n * `env` at root is not a free-form key/value passthrough, and using\n * `.config` keeps everything namespaced to our plugin and survives any\n * version of OpenClaw because the field is a `record(string, unknown)`.\n */\n config?: Record<string, unknown>\n // Pass through anything else that may already be there — `hooks`, `subagent`, etc.\n [key: string]: unknown\n}\n\nexport interface OpenClawSettings {\n plugins?: {\n enabled?: boolean\n entries?: Record<string, OpenClawPluginEntry>\n load?: { paths?: string[] }\n [key: string]: unknown\n }\n [key: string]: unknown\n}\n\nexport interface LatitudePluginConfig {\n apiKey: string\n project: string\n baseUrl?: string | undefined\n allowConversationAccess?: boolean | undefined\n debug?: boolean | undefined\n}\n\nexport function readSettings(): OpenClawSettings {\n if (!existsSync(SETTINGS_PATH)) return {}\n try {\n const raw = readFileSync(SETTINGS_PATH, \"utf-8\")\n const parsed = JSON.parse(raw) as OpenClawSettings\n return parsed && typeof parsed === \"object\" ? parsed : {}\n } catch {\n return {}\n }\n}\n\nexport function writeSettings(settings: OpenClawSettings): void {\n writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\\n`, \"utf-8\")\n}\n\nexport function backupSettings(): void {\n if (existsSync(SETTINGS_PATH)) copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH)\n}\n\ninterface SetPluginEntryPatch {\n /** New API key. Always overwrites — comes from the install prompt. */\n apiKey: string\n /** New project slug. Always overwrites — comes from the install prompt. */\n project: string\n /**\n * New `baseUrl`. `undefined` clears any existing override (used when\n * installing back to production). Anything else overwrites.\n */\n baseUrl: string | undefined\n /**\n * `true`/`false` overwrites; `undefined` preserves the existing value (or\n * leaves the key absent if there is none).\n */\n allowConversationAccess?: boolean | undefined\n /** Same semantics as `allowConversationAccess`. */\n debug?: boolean | undefined\n /**\n * `true`/`false` overwrites; `undefined` preserves the existing value, or\n * defaults to `true` for a fresh install. This keeps a paused plugin\n * (`enabled: false` in openclaw.json) paused across re-installs.\n */\n enabled?: boolean | undefined\n}\n\n/**\n * Set the `plugins.entries[id]` block for our plugin. Writes credentials and\n * options into the `.config` bucket — never under `hooks` (strict zod) and\n * never as top-level `env` keys (root schema rejects them).\n *\n * Re-install idempotency: only `apiKey` / `project` / `baseUrl` always\n * overwrite (these come from the install prompts). `enabled`, `debug`, and\n * `allowConversationAccess` are preserved when not provided in the patch,\n * so a user who hand-edited `enabled: false` or `debug: true` doesn't lose\n * their choice on a re-install.\n */\nexport function setPluginEntry(settings: OpenClawSettings, patch: SetPluginEntryPatch): void {\n const plugins = settings.plugins ?? {}\n const entries = plugins.entries ?? {}\n const existing = entries[PLUGIN_ID] ?? {}\n const existingConfig = (existing.config ?? {}) as Record<string, unknown>\n\n const nextConfig: Record<string, unknown> = {\n ...existingConfig,\n apiKey: patch.apiKey,\n project: patch.project,\n }\n if (patch.baseUrl !== undefined) {\n nextConfig.baseUrl = patch.baseUrl\n } else {\n delete nextConfig.baseUrl\n }\n if (patch.allowConversationAccess !== undefined) {\n nextConfig.allowConversationAccess = patch.allowConversationAccess\n }\n if (patch.debug !== undefined) {\n nextConfig.debug = patch.debug\n }\n\n // Preserve user-edited `enabled: false` across re-installs. Fresh install\n // (no existing entry, no explicit patch) defaults to true.\n const nextEnabled = patch.enabled ?? existing.enabled ?? true\n\n entries[PLUGIN_ID] = {\n ...existing,\n enabled: nextEnabled,\n config: nextConfig,\n }\n plugins.entries = entries\n settings.plugins = plugins\n}\n\n/** Remove the plugin entry entirely. */\nexport function removePluginEntry(settings: OpenClawSettings): boolean {\n const plugins = settings.plugins\n if (!plugins?.entries) return false\n if (!(PLUGIN_ID in plugins.entries)) return false\n delete plugins.entries[PLUGIN_ID]\n return true\n}\n\nexport function hasLatitudePlugin(settings: OpenClawSettings): boolean {\n return Boolean(settings.plugins?.entries && PLUGIN_ID in settings.plugins.entries)\n}\n\n/**\n * Strip any leftover top-level keys our older installer (<= 0.0.1) wrote. The\n * 0.0.1 installer wrote `hooks.allowConversationAccess` (rejected by strict\n * zod) and `LATITUDE_*` keys at root-level `env` (also rejected). Both are\n * cleaned up here so re-running install lands a config OpenClaw will validate.\n */\nexport function migrateLegacyEntries(settings: OpenClawSettings): { changed: boolean } {\n let changed = false\n\n // Wipe `hooks.allowConversationAccess` from our entry — OpenClaw < 2026.4.22\n // rejects unknown keys under the strict `hooks` shape.\n const entry = settings.plugins?.entries?.[PLUGIN_ID]\n if (entry && typeof entry === \"object\") {\n const hooks = (entry as { hooks?: Record<string, unknown> }).hooks\n if (hooks && typeof hooks === \"object\" && \"allowConversationAccess\" in hooks) {\n delete hooks.allowConversationAccess\n // If the hooks object is now empty, drop it entirely so we don't leave\n // a `\"hooks\": {}` carcass behind.\n if (Object.keys(hooks).length === 0) {\n delete (entry as { hooks?: unknown }).hooks\n }\n changed = true\n }\n }\n\n // Strip `LATITUDE_*` keys our 0.0.1 installer mistakenly wrote under\n // `settings.env`. OpenClaw's root schema is strict; the `env` block accepts\n // only `{shellEnv, vars}`, so any `LATITUDE_*` key sitting directly under\n // `env` causes the gateway to quarantine the file. (0.0.1 only ever wrote\n // to `settings.env.LATITUDE_*`, never to top-level `settings.LATITUDE_*`,\n // so we don't bother sweeping the root.)\n const env = settings.env\n if (env && typeof env === \"object\" && !Array.isArray(env)) {\n const envObj = env as Record<string, unknown>\n for (const key of [\"LATITUDE_API_KEY\", \"LATITUDE_PROJECT\", \"LATITUDE_BASE_URL\"]) {\n if (key in envObj) {\n delete envObj[key]\n changed = true\n }\n }\n // Drop the env object if there's nothing left in it AND it's not OpenClaw's\n // canonical {shellEnv, vars} shape — only do this when we know it was ours.\n if (Object.keys(envObj).length === 0) {\n delete settings.env\n }\n }\n\n return { changed }\n}\n","import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { PLUGIN_INSTALL_DIR } from \"./settings-file.ts\"\n\n/**\n * Copies the plugin's runtime files into `~/.openclaw/extensions/latitude-telemetry/`\n * so OpenClaw's discovery (`<configDir>/extensions/<plugin>`) picks it up.\n *\n * `npx -y @latitude-data/openclaw-telemetry` runs the CLI from a temporary npm\n * location — relying on that path persisting across runs (or even across the\n * gateway restart that follows install) is unsafe. So we materialize a stable\n * copy under the user's OpenClaw config dir, the same path OpenClaw scans on\n * startup. Layout we produce:\n *\n * ~/.openclaw/extensions/latitude-telemetry/\n * openclaw.plugin.json <- the manifest, required by discovery\n * package.json <- minimal — keeps node module-resolution happy\n * dist/ <- compiled plugin entrypoint(s)\n */\nexport function installPluginFiles(): { destination: string; entryPath: string } {\n // The CLI runs from `dist/cli.js`. Resolve the package root from there.\n const here = dirname(fileURLToPath(import.meta.url))\n const packageRoot = resolve(here, \"..\") // dist/cli.js -> dist/ -> package root\n\n const manifestSrc = join(packageRoot, \"openclaw.plugin.json\")\n const distSrc = join(packageRoot, \"dist\")\n const pkgSrc = join(packageRoot, \"package.json\")\n\n if (!existsSync(manifestSrc)) {\n throw new Error(\n `Cannot install: missing openclaw.plugin.json at ${manifestSrc}. This is a packaging bug — please file an issue.`,\n )\n }\n if (!existsSync(distSrc)) {\n throw new Error(`Cannot install: missing compiled dist at ${distSrc}.`)\n }\n if (!existsSync(pkgSrc)) {\n // package.json is now load-bearing for OpenClaw discovery (it carries\n // `openclaw.extensions`), so missing it would silently regress to the\n // \"plugin not found\" failure mode 0.0.3 was meant to fix. Match the\n // other packaging-bug checks above and fail loudly instead.\n throw new Error(\n `Cannot install: missing package.json at ${pkgSrc}. This is a packaging bug — please file an issue.`,\n )\n }\n\n // Wipe and recreate so a re-install isn't polluted by a previous version's\n // files. Idempotent.\n if (existsSync(PLUGIN_INSTALL_DIR)) rmSync(PLUGIN_INSTALL_DIR, { recursive: true, force: true })\n mkdirSync(PLUGIN_INSTALL_DIR, { recursive: true })\n\n copyFileSync(manifestSrc, join(PLUGIN_INSTALL_DIR, \"openclaw.plugin.json\"))\n cpSync(distSrc, join(PLUGIN_INSTALL_DIR, \"dist\"), { recursive: true })\n\n // Write a minimal package.json. Three fields matter for OpenClaw:\n // - `name`/`version` for diagnostics.\n // - `type: \"module\"` so node treats `dist/plugin.js` as ESM.\n // - `openclaw.extensions: [...]` — THIS is what OpenClaw's plugin\n // discovery uses to find the plugin entry file. The\n // `openclaw.plugin.json` manifest provides id + configSchema, but\n // discovery's `resolvePackageExtensionEntries` reads\n // `package.json[\"openclaw\"].extensions`. Without this, discovery\n // falls through to looking for `index.{ts,js,mjs,cjs}` at the dir\n // root, doesn't find one, and skips us — which manifests as\n // \"plugin not found\" at gateway start.\n const sourcePkg = JSON.parse(readFileSync(pkgSrc, \"utf-8\")) as {\n name?: string\n version?: string\n type?: string\n main?: string\n openclaw?: { extensions?: string[] }\n }\n const extensions = sourcePkg.openclaw?.extensions ?? [\"./dist/plugin.js\"]\n const minimalPkg = {\n name: sourcePkg.name,\n version: sourcePkg.version,\n type: sourcePkg.type ?? \"module\",\n main: sourcePkg.main ?? \"./dist/plugin.js\",\n private: true,\n openclaw: { extensions },\n }\n writeFileSync(join(PLUGIN_INSTALL_DIR, \"package.json\"), `${JSON.stringify(minimalPkg, null, 2)}\\n`, \"utf-8\")\n\n // Derive the entry path from the resolved extensions rather than\n // hard-coding it, so callers reading `entryPath` stay consistent if the\n // package's openclaw.extensions ever points somewhere other than\n // `./dist/plugin.js` (e.g. multi-entry plugins).\n const primaryEntry = extensions[0] ?? \"./dist/plugin.js\"\n return {\n destination: PLUGIN_INSTALL_DIR,\n entryPath: resolve(PLUGIN_INSTALL_DIR, primaryEntry),\n }\n}\n\n/** Remove the materialized plugin directory under `~/.openclaw/extensions/`. */\nexport function removePluginFiles(): boolean {\n if (!existsSync(PLUGIN_INSTALL_DIR)) return false\n rmSync(PLUGIN_INSTALL_DIR, { recursive: true, force: true })\n return true\n}\n","import { existsSync, mkdirSync } from \"node:fs\"\nimport { dirname } from \"node:path\"\nimport { cancel, confirm, intro, isCancel, log, note, outro, password, spinner, text } from \"@clack/prompts\"\nimport pc from \"picocolors\"\nimport { installPluginFiles, removePluginFiles } from \"./install-files.ts\"\nimport {\n backupSettings,\n hasLatitudePlugin,\n type LatitudePluginConfig,\n migrateLegacyEntries,\n PLUGIN_ID,\n PLUGIN_INSTALL_DIR,\n readSettings,\n removePluginEntry,\n SETTINGS_BACKUP_PATH,\n SETTINGS_PATH,\n setPluginEntry,\n writeSettings,\n} from \"./settings-file.ts\"\n\nconst DOCS_URL = \"https://docs.latitude.so/openclaw-telemetry\"\n\ninterface EnvironmentConfig {\n name: \"production\" | \"staging\" | \"dev\"\n label: string\n app: string\n ingest: string\n}\n\nconst PRODUCTION_ENV: EnvironmentConfig = {\n name: \"production\",\n label: \"production\",\n app: \"https://console.latitude.so\",\n ingest: \"https://ingest.latitude.so\",\n}\nconst STAGING_ENV: EnvironmentConfig = {\n name: \"staging\",\n label: \"staging\",\n app: \"https://staging.latitude.so\",\n ingest: \"https://staging-ingest.latitude.so\",\n}\nconst DEV_ENV: EnvironmentConfig = {\n name: \"dev\",\n label: \"local dev\",\n app: \"http://localhost:3000\",\n ingest: \"http://localhost:3002\",\n}\n\nfunction urlsFor(env: EnvironmentConfig): {\n apiKeys: string\n projects: string\n projectView: (slug: string) => string\n} {\n return {\n apiKeys: `${env.app}/settings/api-keys`,\n projects: env.app,\n projectView: (slug: string) => `${env.app}/projects/${slug}`,\n }\n}\n\ninterface InstallFlags {\n apiKey?: string | undefined\n project?: string | undefined\n environment?: EnvironmentConfig | undefined\n /**\n * Tristate: `true` = capture (the user passed `--allow-conversation`),\n * `false` = scrub (the user passed `--no-content`), `undefined` = preserve\n * existing or fall through to the first-install default. Keeping this\n * tristate is what makes re-installs idempotent for hand-edited values.\n */\n allowConversationAccess?: boolean | undefined\n noPrompt?: boolean\n yes?: boolean\n}\n\nexport function parseFlags(argv: string[]): {\n subcommand: string | undefined\n flags: Record<string, string | boolean>\n} {\n const [subcommand, ...rest] = argv\n const flags: Record<string, string | boolean> = {}\n for (const arg of rest) {\n if (!arg.startsWith(\"--\")) continue\n const eq = arg.indexOf(\"=\")\n if (eq >= 0) {\n flags[arg.slice(2, eq)] = arg.slice(eq + 1)\n } else {\n flags[arg.slice(2)] = true\n }\n }\n return { subcommand, flags }\n}\n\nexport function normalizeInstallFlags(flags: Record<string, string | boolean>): InstallFlags {\n let environment: EnvironmentConfig | undefined\n if (flags.staging === true) environment = STAGING_ENV\n if (flags.dev === true) {\n if (environment) throw new Error(\"--staging and --dev are mutually exclusive\")\n environment = DEV_ENV\n }\n // Tristate: leave undefined unless the user explicitly asked one way or\n // the other. Re-install then preserves whatever's in openclaw.json.\n let allowConversationAccess: boolean | undefined\n if (flags[\"no-content\"] === true || flags[\"no-conversation\"] === true) allowConversationAccess = false\n if (flags[\"allow-conversation\"] === true) allowConversationAccess = true\n\n return {\n apiKey: typeof flags[\"api-key\"] === \"string\" ? flags[\"api-key\"] : undefined,\n project: typeof flags.project === \"string\" ? flags.project : undefined,\n environment,\n allowConversationAccess,\n noPrompt: flags[\"no-prompt\"] === true || flags.yes === true,\n yes: flags.yes === true,\n }\n}\n\n// ─── Install ────────────────────────────────────────────────────────────────\n\nexport async function runInstall(flags: InstallFlags = {}): Promise<void> {\n const canPrompt = !flags.noPrompt && process.stdin.isTTY === true\n if (!canPrompt) return runFlagDrivenInstall(flags)\n await runInteractiveInstall(flags)\n}\n\nasync function runInteractiveInstall(flags: InstallFlags): Promise<void> {\n intro(pc.bgCyan(pc.black(\" Latitude · OpenClaw telemetry \")))\n\n const existing = readSettings()\n const existingConfig =\n (existing.plugins?.entries?.[PLUGIN_ID]?.config as LatitudePluginConfig | undefined) ?? undefined\n const envConfig = flags.environment ?? PRODUCTION_ENV\n const urls = urlsFor(envConfig)\n\n const aboutLines = [\n \"Captures every OpenClaw agent run and ships it to Latitude as\",\n \"OpenTelemetry traces — full system prompt, tool I/O, messages,\",\n \"token usage, and agent name on every span.\",\n \"\",\n `${pc.dim(\"Docs\")} ${pc.cyan(DOCS_URL)}`,\n ]\n if (envConfig.name !== \"production\") {\n aboutLines.push(\"\", pc.yellow(`Using ${envConfig.label} environment (${envConfig.ingest})`))\n }\n note(aboutLines.join(\"\\n\"), \"About\")\n\n log.info(`Get an API key at ${pc.cyan(urls.apiKeys)}`)\n log.info(`Create a project at ${pc.cyan(urls.projects)}`)\n\n const apiKey = await promptApiKey(existingConfig?.apiKey, flags.apiKey)\n const project = await promptProject(existingConfig?.project, flags.project)\n\n await applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess: flags.allowConversationAccess,\n })\n\n note(\n [\n \"Restart the OpenClaw gateway for the plugin to load:\",\n pc.dim(\" openclaw gateway restart\"),\n \"\",\n `View your traces at ${pc.cyan(urls.projectView(project))}`,\n ].join(\"\\n\"),\n \"Next step\",\n )\n outro(pc.green(\"✓ Installed\"))\n}\n\nasync function runFlagDrivenInstall(flags: InstallFlags): Promise<void> {\n const apiKey = flags.apiKey\n const project = flags.project\n if (!apiKey || !project) {\n throw new Error(\"Non-interactive install requires --api-key=... and --project=... (or run in a TTY).\")\n }\n const envConfig = flags.environment ?? PRODUCTION_ENV\n await applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess: flags.allowConversationAccess,\n })\n process.stdout.write(`Installed Latitude plugin in ${SETTINGS_PATH}\\n`)\n process.stdout.write(`Plugin files at ${PLUGIN_INSTALL_DIR}\\n`)\n}\n\nasync function promptApiKey(_existing: string | undefined, flag: string | undefined): Promise<string> {\n if (flag) return flag\n const result = await password({\n message: \"Latitude API key\",\n mask: \"•\",\n validate: (v) => (v && v.length > 0 ? undefined : \"Required\"),\n })\n if (isCancel(result)) return onCancel()\n return result\n}\n\nasync function promptProject(existing: string | undefined, flag: string | undefined): Promise<string> {\n if (flag) return flag\n const result = await text({\n message: \"Latitude project slug\",\n placeholder: existing ?? \"my-openclaw-project\",\n ...(existing ? { initialValue: existing } : {}),\n validate: (v) => (v && v.length > 0 ? undefined : \"Required\"),\n })\n if (isCancel(result)) return onCancel()\n return result\n}\n\nfunction onCancel(): never {\n cancel(\"Cancelled — nothing was changed\")\n process.exit(1)\n}\n\ninterface ApplyParams {\n apiKey: string\n project: string\n envConfig: EnvironmentConfig\n /** Tristate — see `InstallFlags.allowConversationAccess`. */\n allowConversationAccess: boolean | undefined\n}\n\nasync function applyChanges({ apiKey, project, envConfig, allowConversationAccess }: ApplyParams): Promise<void> {\n // 1. Materialize plugin runtime files into ~/.openclaw/extensions/latitude-telemetry/\n // so OpenClaw's plugin discovery picks them up. This MUST happen before\n // we write the openclaw.json entry, otherwise the gateway file-watcher\n // will see the entry, fail to find the plugin, and emit a warning.\n const filesSpinner = spinner()\n filesSpinner.start(\"Installing plugin files\")\n const { destination } = installPluginFiles()\n filesSpinner.stop(`Plugin files installed at ${destination}`)\n\n // 2. Update openclaw.json with the plugin entry.\n const settingsSpinner = spinner()\n settingsSpinner.start(\"Updating openclaw.json\")\n ensureSettingsDir()\n backupSettings()\n const settings = readSettings()\n // Migrate any leftover keys our 0.0.1 installer wrote that the strict zod\n // schema rejects. Without this, re-installing on top of a 0.0.1 install\n // would leave the gateway quarantining the file as `clobbered`.\n migrateLegacyEntries(settings)\n\n // Decide allowConversationAccess for this install:\n // - explicit flag (true|false) wins\n // - else preserve whatever's already in openclaw.json\n // - else first-install default is `true` (matches the README's promise)\n const existingConfig = (settings.plugins?.entries?.[PLUGIN_ID]?.config ?? {}) as Partial<LatitudePluginConfig>\n const finalAllowConversationAccess = allowConversationAccess ?? existingConfig.allowConversationAccess ?? true\n\n setPluginEntry(settings, {\n apiKey,\n project,\n baseUrl: envConfig.name === \"production\" ? undefined : envConfig.ingest,\n allowConversationAccess: finalAllowConversationAccess,\n // `debug` is intentionally not passed — `setPluginEntry` preserves the\n // user's hand-edited value. Fresh installs leave the key absent (the\n // runtime default is `false`).\n })\n writeSettings(settings)\n settingsSpinner.stop(`Updated ${SETTINGS_PATH}`)\n if (existsSync(SETTINGS_BACKUP_PATH)) log.info(`Backup saved at ${pc.dim(SETTINGS_BACKUP_PATH)}`)\n}\n\nfunction ensureSettingsDir(): void {\n const dir = dirname(SETTINGS_PATH)\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true })\n}\n\n// ─── Uninstall ──────────────────────────────────────────────────────────────\n\ninterface UninstallFlags {\n noPrompt?: boolean\n}\n\nexport async function runUninstall(flags: UninstallFlags = {}): Promise<void> {\n intro(pc.bgYellow(pc.black(\" Latitude · OpenClaw telemetry — uninstall \")))\n const settings = readSettings()\n\n const hasEntry = hasLatitudePlugin(settings)\n const hasFiles = existsSync(PLUGIN_INSTALL_DIR)\n\n if (!hasEntry && !hasFiles) {\n note(\"No Latitude plugin entry or files found — nothing to remove.\", \"Status\")\n outro(pc.dim(\"Nothing changed\"))\n return\n }\n\n const plan = [\n hasEntry ? `Remove \"${PLUGIN_ID}\" plugin entry from ${SETTINGS_PATH}` : null,\n hasFiles ? `Delete plugin files at ${PLUGIN_INSTALL_DIR}` : null,\n `Backup of openclaw.json saved at ${SETTINGS_BACKUP_PATH}`,\n ].filter(Boolean) as string[]\n note(plan.join(\"\\n\"), \"Plan\")\n\n if (!flags.noPrompt && process.stdin.isTTY === true) {\n const ok = await confirm({ message: \"Proceed?\", initialValue: true })\n if (isCancel(ok) || ok !== true) return onCancel()\n }\n\n const s = spinner()\n s.start(\"Reverting settings\")\n if (hasEntry) {\n backupSettings()\n removePluginEntry(settings)\n // Also clean any legacy keys our 0.0.1 installer left behind so the file\n // is fully back to a clean state.\n migrateLegacyEntries(settings)\n writeSettings(settings)\n }\n if (hasFiles) removePluginFiles()\n s.stop(\"Done\")\n outro(pc.green(\"✓ Uninstalled\"))\n}\n","import { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { normalizeInstallFlags, parseFlags, runInstall, runUninstall } from \"./setup.ts\"\n\nconst USAGE = `usage: latitude-openclaw <command> [options]\n\ncommands:\n install Install the plugin (interactive when stdin is a TTY)\n uninstall Remove the plugin entry and files\n --version, -v Print the package version\n --help, -h Print this message\n\ninstall options:\n --api-key=<key> Pass the API key non-interactively\n --project=<slug> Pass the project slug non-interactively\n --staging Target https://staging.latitude.so / staging-ingest\n --dev Target http://localhost:3000 / 3002\n --no-content Skip raw prompt/response/tool I/O capture\n --allow-conversation Force conversation capture on (overrides existing config)\n --yes / --no-prompt Skip all prompts (required for non-TTY / CI)\n`\n\nfunction readVersion(): string {\n // dist/cli.js → ../package.json\n const here = dirname(fileURLToPath(import.meta.url))\n const pkgPath = join(here, \"..\", \"package.json\")\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version?: string }\n return pkg.version ?? \"unknown\"\n } catch {\n return \"unknown\"\n }\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2)\n // Top-level --version / --help are handled before parseFlags so users can\n // type them as the first arg without hitting the \"unknown subcommand\" path.\n if (argv[0] === \"--version\" || argv[0] === \"-v\") {\n process.stdout.write(`${readVersion()}\\n`)\n return\n }\n if (argv[0] === \"--help\" || argv[0] === \"-h\") {\n process.stdout.write(USAGE)\n return\n }\n\n const { subcommand, flags } = parseFlags(argv)\n if (subcommand === \"install\" || subcommand === undefined) {\n await runInstall(normalizeInstallFlags(flags))\n return\n }\n if (subcommand === \"uninstall\") {\n await runUninstall({ noPrompt: flags[\"no-prompt\"] === true || flags.yes === true })\n return\n }\n process.stderr.write(`unknown subcommand: ${subcommand}\\n`)\n process.stderr.write(USAGE)\n process.exit(1)\n}\n\nmain().catch((err) => {\n process.stderr.write(`${String(err)}\\n`)\n process.exit(1)\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/openclaw-cli.ts","../src/settings-file.ts","../src/setup.ts","../src/cli.ts"],"sourcesContent":["import { type SpawnSyncReturns, spawnSync } from \"node:child_process\"\n\n/**\n * Lowest OpenClaw version we support. The reporter verified hook-dispatch\n * gating works correctly here; older versions either reject\n * `hooks.allowConversationAccess` outright (≤ 2026.4.21) or have unverified\n * gating behaviour (2026.4.22 – 2026.4.24). Refusing to install on older\n * versions is intentional — we'd rather fail loudly than ship a\n * config the gateway will quarantine or hooks the dispatcher will block.\n */\nexport const MIN_OPENCLAW_VERSION = \"2026.4.25\"\n\nconst DEFAULT_TIMEOUT_MS = 10_000\n\ntype RunResult =\n | { ok: true; stdout: string; stderr: string; code: 0 }\n | { ok: false; reason: \"enoent\"; stdout: \"\"; stderr: \"\"; code: null }\n | { ok: false; reason: \"timeout\"; stdout: string; stderr: string; code: null }\n | { ok: false; reason: \"exit\"; stdout: string; stderr: string; code: number }\n\n/**\n * Spawn `openclaw <args>` synchronously. Reports failure modes structurally so\n * callers can decide how to degrade (missing binary vs. timed out vs. exited\n * non-zero). Never throws; ENOENT becomes `{ reason: \"enoent\" }`.\n */\nexport function runOpenclaw(args: string[], opts: { timeoutMs?: number; stdin?: string } = {}): RunResult {\n const result: SpawnSyncReturns<string> = spawnSync(\"openclaw\", args, {\n encoding: \"utf-8\",\n timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n input: opts.stdin,\n // Inherit env so the user's PATH and any LATITUDE_* / OPENCLAW_* vars\n // reach the child process. We don't need a TTY — install is non-interactive.\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n })\n\n // spawnSync surfaces I/O errors through `result.error` rather than a thrown\n // exception when called this way. Order matters: a timeout shows up as\n // `error.code === \"ETIMEDOUT\"` AND/OR `signal === \"SIGTERM\"|\"SIGKILL\"`\n // depending on platform — classify it before the generic `if (err)`\n // catch-all so callers reliably get `reason: \"timeout\"` (and the\n // timeout-specific error messages in setup.ts aren't dead code).\n const err = result.error as (NodeJS.ErrnoException & { code?: string }) | undefined\n if (err?.code === \"ENOENT\") {\n return { ok: false, reason: \"enoent\", stdout: \"\", stderr: \"\", code: null }\n }\n if (err?.code === \"ETIMEDOUT\" || result.signal === \"SIGTERM\" || result.signal === \"SIGKILL\") {\n return {\n ok: false,\n reason: \"timeout\",\n stdout: result.stdout ?? \"\",\n stderr: result.stderr ?? \"\",\n code: null,\n }\n }\n if (err) {\n // Treat any other spawn error as an unparseable exit failure.\n return {\n ok: false,\n reason: \"exit\",\n stdout: result.stdout ?? \"\",\n stderr: result.stderr ?? String(err),\n code: typeof result.status === \"number\" ? result.status : 1,\n }\n }\n\n if (result.status === 0) {\n return { ok: true, stdout: result.stdout ?? \"\", stderr: result.stderr ?? \"\", code: 0 }\n }\n\n return {\n ok: false,\n reason: \"exit\",\n stdout: result.stdout ?? \"\",\n stderr: result.stderr ?? \"\",\n code: typeof result.status === \"number\" ? result.status : 1,\n }\n}\n\ntype VersionLookup =\n | { ok: true; version: string; raw: string }\n | { ok: false; error: \"missing\" | \"unparseable\"; raw?: string | undefined }\n\n/**\n * Run `openclaw --version` and parse out the version string.\n *\n * Banner format (per OpenClaw `src/cli/banner.ts` `formatCliBannerLine`):\n * `🦞 OpenClaw <version> (<commit-sha>)`\n *\n * The banner is normally suppressed when `--version` is the flag, but the\n * version itself still goes to stdout. We accept either layout (with or\n * without the lobster + commit sha) so we don't break if OpenClaw later\n * prints just the bare version string.\n */\nexport function getOpenclawVersion(): VersionLookup {\n const result = runOpenclaw([\"--version\"], { timeoutMs: 5_000 })\n if (!result.ok) {\n if (result.reason === \"enoent\") return { ok: false, error: \"missing\" }\n return { ok: false, error: \"unparseable\", raw: result.stdout || result.stderr }\n }\n\n const raw = result.stdout.trim()\n // Match a CalVer triple anywhere in the output. Be liberal — banner\n // decorations (emoji, \"OpenClaw\" prefix, trailing \"(sha)\") all just\n // surround a single `YYYY.M.PATCH` somewhere.\n const match = raw.match(/(\\d{4}\\.\\d+\\.\\d+)/)\n if (!match) return { ok: false, error: \"unparseable\", raw }\n return { ok: true, version: match[1] as string, raw }\n}\n\n/**\n * Compare two CalVer strings (`YYYY.M.PATCH`). Returns -1 if `a` < `b`,\n * 0 if equal, 1 if `a` > `b`. Strings with extra components or non-numeric\n * pieces fall back to a per-component string comparison so unexpected\n * formats don't crash the installer.\n */\nexport function compareCalver(a: string, b: string): -1 | 0 | 1 {\n const ap = a.split(\".\")\n const bp = b.split(\".\")\n const len = Math.max(ap.length, bp.length)\n for (let i = 0; i < len; i++) {\n const ai = ap[i] ?? \"0\"\n const bi = bp[i] ?? \"0\"\n const an = Number(ai)\n const bn = Number(bi)\n if (Number.isFinite(an) && Number.isFinite(bn)) {\n if (an < bn) return -1\n if (an > bn) return 1\n continue\n }\n if (ai < bi) return -1\n if (ai > bi) return 1\n }\n return 0\n}\n","import { copyFileSync, existsSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\nconst CONFIG_DIR = join(homedir(), \".openclaw\")\nexport const SETTINGS_PATH = join(CONFIG_DIR, \"openclaw.json\")\nexport const SETTINGS_BACKUP_PATH = join(CONFIG_DIR, \"openclaw.json.latitude-bak\")\n\n/** Plugin id used both as the npm package name and as the OpenClaw plugin id. */\nexport const PLUGIN_ID = \"@latitude-data/openclaw-telemetry\"\n\n/**\n * The shape OpenClaw's strict zod schema accepts for a single\n * `plugins.entries[id]` block. We keep this minimal — only the fields we\n * actually write — so we don't drop anything when round-tripping an entry\n * with fields we don't know about.\n */\ninterface OpenClawPluginEntry {\n enabled?: boolean\n /**\n * Strict zod object on OpenClaw 2026.4.25+: only `allowPromptInjection` and\n * `allowConversationAccess` are accepted. We populate the latter — it's the\n * dispatch gate the OpenClaw runtime checks before forwarding LLM/tool/agent\n * events to non-bundled plugins. Without it, the gateway logs\n * `[plugins] typed hook \"...\" blocked because non-bundled plugins must set\n * plugins.entries.<id>.hooks.allowConversationAccess=true` and the plugin's\n * handlers never fire.\n */\n hooks?: {\n allowPromptInjection?: boolean\n allowConversationAccess?: boolean\n [key: string]: unknown\n }\n /**\n * Free-form bucket OpenClaw passes to the plugin at activation\n * (`api.pluginConfig`). Our credentials and feature flags live here.\n */\n config?: Record<string, unknown>\n // Pass through anything else that may already be there — `subagent`, etc.\n [key: string]: unknown\n}\n\nexport interface OpenClawSettings {\n plugins?: {\n enabled?: boolean\n entries?: Record<string, OpenClawPluginEntry>\n /**\n * Operator-managed allowlist of plugin ids that may auto-load. OpenClaw\n * warns at every gateway start when a non-bundled plugin loads without\n * provenance via `plugins.allow` or an install record.\n */\n allow?: string[]\n load?: { paths?: string[] }\n [key: string]: unknown\n }\n [key: string]: unknown\n}\n\nexport interface LatitudePluginConfig {\n apiKey: string\n project: string\n baseUrl?: string | undefined\n allowConversationAccess?: boolean | undefined\n debug?: boolean | undefined\n}\n\nexport function readSettings(): OpenClawSettings {\n if (!existsSync(SETTINGS_PATH)) return {}\n try {\n const raw = readFileSync(SETTINGS_PATH, \"utf-8\")\n const parsed = JSON.parse(raw) as OpenClawSettings\n return parsed && typeof parsed === \"object\" ? parsed : {}\n } catch {\n return {}\n }\n}\n\nexport function writeSettings(settings: OpenClawSettings): void {\n writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\\n`, \"utf-8\")\n}\n\nexport function backupSettings(): void {\n if (existsSync(SETTINGS_PATH)) copyFileSync(SETTINGS_PATH, SETTINGS_BACKUP_PATH)\n}\n\ninterface SetPluginEntryPatch {\n /** New API key. Always overwrites — comes from the install prompt. */\n apiKey: string\n /** New project slug. Always overwrites — comes from the install prompt. */\n project: string\n /**\n * New `baseUrl`. `undefined` clears any existing override (used when\n * installing back to production). Anything else overwrites.\n */\n baseUrl: string | undefined\n /**\n * `true`/`false` overwrites both `config.allowConversationAccess` AND\n * `hooks.allowConversationAccess` (always coupled — see `setPluginEntry`).\n * `undefined` preserves the existing value, or defaults to `true` for a\n * fresh install.\n */\n allowConversationAccess?: boolean | undefined\n /**\n * `true`/`false` overwrites; `undefined` preserves the existing value (the\n * installer doesn't pass `debug` — it's a hand-edit affordance).\n */\n debug?: boolean | undefined\n /**\n * `true`/`false` overwrites; `undefined` preserves existing, defaulting to\n * `true` for a fresh install. This keeps a paused plugin (`enabled: false`\n * hand-edited in openclaw.json) paused across re-installs.\n */\n enabled?: boolean | undefined\n}\n\n/**\n * Set the `plugins.entries[id]` block for our plugin.\n *\n * Two places in the entry get written:\n *\n * - `.config` (free-form `record(string, unknown)`): credentials, baseUrl,\n * and our copy of `allowConversationAccess`. This is what the plugin\n * runtime reads via `api.pluginConfig`.\n * - `.hooks.allowConversationAccess`: controls whether OpenClaw's hook\n * dispatcher actually forwards `llm_input` / `llm_output` / tool /\n * `agent_end` events to our handlers. Without it set to `true` on\n * OpenClaw 2026.4.25+, every typed hook is blocked at the dispatcher\n * and the plugin's handlers never fire.\n *\n * The two flags mean different things — `hooks.*` is the dispatch gate,\n * `config.*` is the payload-content gate — but for THIS plugin we always\n * couple them: dispatch off + payload on is useless (no payloads to gate),\n * and dispatch on + payload off is a legitimate \"structural-only telemetry\"\n * mode (timing, tokens, ids, agent name; no message bodies). Always writing\n * both from the same source keeps the operator's mental model simple.\n *\n * Re-install idempotency: only `apiKey` / `project` / `baseUrl` always\n * overwrite (these come from install prompts). `enabled`, `debug`, and\n * `allowConversationAccess` are preserved when not provided in the patch.\n */\nexport function setPluginEntry(settings: OpenClawSettings, patch: SetPluginEntryPatch): void {\n const plugins = settings.plugins ?? {}\n const entries = plugins.entries ?? {}\n const existing = entries[PLUGIN_ID] ?? {}\n const existingConfig = (existing.config ?? {}) as Record<string, unknown>\n const existingHooks = existing.hooks ?? {}\n\n const nextConfig: Record<string, unknown> = {\n ...existingConfig,\n apiKey: patch.apiKey,\n project: patch.project,\n }\n if (patch.baseUrl !== undefined) {\n nextConfig.baseUrl = patch.baseUrl\n } else {\n delete nextConfig.baseUrl\n }\n if (patch.allowConversationAccess !== undefined) {\n nextConfig.allowConversationAccess = patch.allowConversationAccess\n }\n if (patch.debug !== undefined) {\n nextConfig.debug = patch.debug\n }\n\n // Resolve the effective allowConversationAccess for the hooks block.\n // We mirror whatever ended up in nextConfig.allowConversationAccess (just\n // computed above) so the two flags always agree. If neither the patch nor\n // the existing config sets it, fall back to the existing hooks value, then\n // to `true` (matches the README's first-install promise).\n const effectiveAccess =\n typeof nextConfig.allowConversationAccess === \"boolean\"\n ? nextConfig.allowConversationAccess\n : typeof existingHooks.allowConversationAccess === \"boolean\"\n ? existingHooks.allowConversationAccess\n : true\n\n const nextHooks: OpenClawPluginEntry[\"hooks\"] = {\n ...existingHooks,\n allowConversationAccess: effectiveAccess,\n }\n\n // Preserve a hand-edited `enabled: false` across re-installs. Fresh install\n // (no existing entry, no explicit patch) defaults to true.\n const nextEnabled = patch.enabled ?? existing.enabled ?? true\n\n entries[PLUGIN_ID] = {\n ...existing,\n enabled: nextEnabled,\n hooks: nextHooks,\n config: nextConfig,\n }\n plugins.entries = entries\n settings.plugins = plugins\n}\n\n/** Remove the plugin entry entirely. Used by uninstall as defense-in-depth. */\nexport function removePluginEntry(settings: OpenClawSettings): boolean {\n const plugins = settings.plugins\n if (!plugins?.entries) return false\n if (!(PLUGIN_ID in plugins.entries)) return false\n delete plugins.entries[PLUGIN_ID]\n return true\n}\n\n/**\n * Add the plugin id to `plugins.allow`. Idempotent — returns `true` only when\n * the array changed. OpenClaw warns at every gateway start when a non-bundled\n * plugin auto-loads without provenance via `plugins.allow` or an install\n * record. We get one warning cleared by going through `openclaw plugins\n * install` (provenance) and the other by adding ourselves to allow.\n *\n * Defensive against hand-edited non-array values: if `plugins.allow` is\n * present but not an array (e.g. someone wrote a string), we replace it\n * with a single-element array rather than spreading the bad value.\n */\nexport function addToPluginsAllow(settings: OpenClawSettings): boolean {\n const plugins = settings.plugins ?? {}\n const existing = plugins.allow\n const allow = Array.isArray(existing) ? existing : []\n if (allow.includes(PLUGIN_ID)) return false\n plugins.allow = [...allow, PLUGIN_ID]\n settings.plugins = plugins\n return true\n}\n\n/**\n * Inverse of `addToPluginsAllow`. Defense-in-depth — `openclaw plugins\n * uninstall` already strips the entry, but the install path can be skipped\n * (e.g. the user removed the plugin manually) and we want re-install/uninstall\n * round-trips to be tidy regardless.\n */\nexport function removeFromPluginsAllow(settings: OpenClawSettings): boolean {\n const allow = settings.plugins?.allow\n if (!Array.isArray(allow) || !allow.includes(PLUGIN_ID)) return false\n if (settings.plugins) {\n settings.plugins.allow = allow.filter((id) => id !== PLUGIN_ID)\n }\n return true\n}\n\nexport function hasLatitudePlugin(settings: OpenClawSettings): boolean {\n return Boolean(settings.plugins?.entries && PLUGIN_ID in settings.plugins.entries)\n}\n\n/**\n * Strip leftover keys from older installers that the strict zod schema\n * rejects on current OpenClaw versions.\n *\n * 0.0.1 wrote `LATITUDE_*` keys directly under `settings.env`. OpenClaw's\n * root schema is strict; the `env` block accepts only `{shellEnv, vars}`,\n * so those keys cause the gateway to quarantine the config as\n * `clobbered.<ts>` and roll back. We sweep them on every install.\n *\n * Note: 0.0.1 also wrote `hooks.allowConversationAccess` (when that key was\n * not yet in the schema). We deliberately do NOT strip it anymore — on\n * OpenClaw 2026.4.25+ the key IS in the schema and IS load-bearing for\n * dispatch. `setPluginEntry` overwrites it on every install with the right\n * value, so any 0.0.1 leftover is reconciled there.\n */\nexport function migrateLegacyEntries(settings: OpenClawSettings): { changed: boolean } {\n let changed = false\n\n const env = settings.env\n if (env && typeof env === \"object\" && !Array.isArray(env)) {\n const envObj = env as Record<string, unknown>\n for (const key of [\"LATITUDE_API_KEY\", \"LATITUDE_PROJECT\", \"LATITUDE_BASE_URL\"]) {\n if (key in envObj) {\n delete envObj[key]\n changed = true\n }\n }\n // Drop the env object only if it's now empty; don't touch a real\n // OpenClaw env block (`{shellEnv, vars}`) that already had those keys.\n if (Object.keys(envObj).length === 0) {\n delete settings.env\n }\n }\n\n return { changed }\n}\n","import { existsSync, mkdirSync } from \"node:fs\"\nimport { dirname, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { cancel, confirm, intro, isCancel, log, note, outro, password, spinner, text } from \"@clack/prompts\"\nimport pc from \"picocolors\"\nimport { compareCalver, getOpenclawVersion, MIN_OPENCLAW_VERSION, runOpenclaw } from \"./openclaw-cli.ts\"\nimport {\n addToPluginsAllow,\n backupSettings,\n hasLatitudePlugin,\n type LatitudePluginConfig,\n migrateLegacyEntries,\n PLUGIN_ID,\n readSettings,\n removeFromPluginsAllow,\n removePluginEntry,\n SETTINGS_BACKUP_PATH,\n SETTINGS_PATH,\n setPluginEntry,\n writeSettings,\n} from \"./settings-file.ts\"\n\nconst DOCS_URL = \"https://docs.latitude.so/openclaw-telemetry\"\n\ninterface EnvironmentConfig {\n name: \"production\" | \"staging\" | \"dev\"\n label: string\n app: string\n ingest: string\n}\n\nconst PRODUCTION_ENV: EnvironmentConfig = {\n name: \"production\",\n label: \"production\",\n app: \"https://console.latitude.so\",\n ingest: \"https://ingest.latitude.so\",\n}\nconst STAGING_ENV: EnvironmentConfig = {\n name: \"staging\",\n label: \"staging\",\n app: \"https://staging.latitude.so\",\n ingest: \"https://staging-ingest.latitude.so\",\n}\nconst DEV_ENV: EnvironmentConfig = {\n name: \"dev\",\n label: \"local dev\",\n app: \"http://localhost:3000\",\n ingest: \"http://localhost:3002\",\n}\n\nfunction urlsFor(env: EnvironmentConfig): {\n apiKeys: string\n projects: string\n projectView: (slug: string) => string\n} {\n return {\n apiKeys: `${env.app}/settings/api-keys`,\n projects: env.app,\n projectView: (slug: string) => `${env.app}/projects/${slug}`,\n }\n}\n\ninterface InstallFlags {\n apiKey?: string | undefined\n project?: string | undefined\n environment?: EnvironmentConfig | undefined\n /**\n * Tristate: `true` = capture (`--allow-conversation`), `false` = scrub\n * (`--no-content`), `undefined` = preserve existing or first-install\n * default. Tristate keeps re-install idempotent for hand-edited values.\n */\n allowConversationAccess?: boolean | undefined\n /** When true, skip adding the plugin id to `plugins.allow`. */\n noTrust?: boolean\n noPrompt?: boolean\n yes?: boolean\n}\n\nexport function parseFlags(argv: string[]): {\n subcommand: string | undefined\n flags: Record<string, string | boolean>\n} {\n const [subcommand, ...rest] = argv\n const flags: Record<string, string | boolean> = {}\n for (const arg of rest) {\n if (!arg.startsWith(\"--\")) continue\n const eq = arg.indexOf(\"=\")\n if (eq >= 0) {\n flags[arg.slice(2, eq)] = arg.slice(eq + 1)\n } else {\n flags[arg.slice(2)] = true\n }\n }\n return { subcommand, flags }\n}\n\nexport function normalizeInstallFlags(flags: Record<string, string | boolean>): InstallFlags {\n let environment: EnvironmentConfig | undefined\n if (flags.staging === true) environment = STAGING_ENV\n if (flags.dev === true) {\n if (environment) throw new Error(\"--staging and --dev are mutually exclusive\")\n environment = DEV_ENV\n }\n // Tristate: leave undefined unless the user explicitly asked one way or the\n // other. Re-install then preserves whatever's already in openclaw.json.\n let allowConversationAccess: boolean | undefined\n if (flags[\"no-content\"] === true || flags[\"no-conversation\"] === true) allowConversationAccess = false\n if (flags[\"allow-conversation\"] === true) allowConversationAccess = true\n\n return {\n apiKey: typeof flags[\"api-key\"] === \"string\" ? flags[\"api-key\"] : undefined,\n project: typeof flags.project === \"string\" ? flags.project : undefined,\n environment,\n allowConversationAccess,\n noTrust: flags[\"no-trust\"] === true,\n noPrompt: flags[\"no-prompt\"] === true || flags.yes === true,\n yes: flags.yes === true,\n }\n}\n\n// ─── Install ────────────────────────────────────────────────────────────────\n\nexport async function runInstall(flags: InstallFlags = {}): Promise<void> {\n const canPrompt = !flags.noPrompt && process.stdin.isTTY === true\n if (!canPrompt) return runFlagDrivenInstall(flags)\n await runInteractiveInstall(flags)\n}\n\nasync function runInteractiveInstall(flags: InstallFlags): Promise<void> {\n intro(pc.bgCyan(pc.black(\" Latitude · OpenClaw telemetry \")))\n\n // Bail before any prompts if the host OpenClaw is too old. We'd rather\n // tell the user to upgrade than walk them through a config we can't make\n // work on their version.\n ensureOpenclawIsCompatible()\n\n const existing = readSettings()\n const existingConfig =\n (existing.plugins?.entries?.[PLUGIN_ID]?.config as LatitudePluginConfig | undefined) ?? undefined\n const envConfig = flags.environment ?? PRODUCTION_ENV\n const urls = urlsFor(envConfig)\n\n const aboutLines = [\n \"Captures every OpenClaw agent run and ships it to Latitude as\",\n \"OpenTelemetry traces — full system prompt, tool I/O, messages,\",\n \"token usage, and agent name on every span.\",\n \"\",\n `${pc.dim(\"Docs\")} ${pc.cyan(DOCS_URL)}`,\n ]\n if (envConfig.name !== \"production\") {\n aboutLines.push(\"\", pc.yellow(`Using ${envConfig.label} environment (${envConfig.ingest})`))\n }\n note(aboutLines.join(\"\\n\"), \"About\")\n\n log.info(`Get an API key at ${pc.cyan(urls.apiKeys)}`)\n log.info(`Create a project at ${pc.cyan(urls.projects)}`)\n\n const apiKey = await promptApiKey(existingConfig?.apiKey, flags.apiKey)\n const project = await promptProject(existingConfig?.project, flags.project)\n\n await applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess: flags.allowConversationAccess,\n noTrust: flags.noTrust === true,\n })\n\n note(\n [\n \"Restart the OpenClaw gateway for the plugin to load:\",\n pc.dim(\" openclaw gateway restart\"),\n \"\",\n `View your traces at ${pc.cyan(urls.projectView(project))}`,\n ].join(\"\\n\"),\n \"Next step\",\n )\n outro(pc.green(\"✓ Installed\"))\n}\n\nasync function runFlagDrivenInstall(flags: InstallFlags): Promise<void> {\n ensureOpenclawIsCompatible()\n const apiKey = flags.apiKey\n const project = flags.project\n if (!apiKey || !project) {\n throw new Error(\"Non-interactive install requires --api-key=... and --project=... (or run in a TTY).\")\n }\n const envConfig = flags.environment ?? PRODUCTION_ENV\n await applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess: flags.allowConversationAccess,\n noTrust: flags.noTrust === true,\n })\n process.stdout.write(`Installed Latitude plugin in ${SETTINGS_PATH}\\n`)\n}\n\nasync function promptApiKey(_existing: string | undefined, flag: string | undefined): Promise<string> {\n if (flag) return flag\n const result = await password({\n message: \"Latitude API key\",\n mask: \"•\",\n validate: (v) => (v && v.length > 0 ? undefined : \"Required\"),\n })\n if (isCancel(result)) return onCancel()\n return result\n}\n\nasync function promptProject(existing: string | undefined, flag: string | undefined): Promise<string> {\n if (flag) return flag\n const result = await text({\n message: \"Latitude project slug\",\n placeholder: existing ?? \"my-openclaw-project\",\n ...(existing ? { initialValue: existing } : {}),\n validate: (v) => (v && v.length > 0 ? undefined : \"Required\"),\n })\n if (isCancel(result)) return onCancel()\n return result\n}\n\nfunction onCancel(): never {\n cancel(\"Cancelled — nothing was changed\")\n process.exit(1)\n}\n\ninterface ApplyParams {\n apiKey: string\n project: string\n envConfig: EnvironmentConfig\n /** Tristate — see `InstallFlags.allowConversationAccess`. */\n allowConversationAccess: boolean | undefined\n noTrust: boolean\n}\n\nasync function applyChanges({\n apiKey,\n project,\n envConfig,\n allowConversationAccess,\n noTrust,\n}: ApplyParams): Promise<void> {\n // 1. Take the backup BEFORE openclaw plugins install touches openclaw.json\n // so it represents the user's true pre-install state. `openclaw plugins\n // install` creates plugins.entries[id], so backing up after would lose\n // the original \"no entry\" state and make recovery harder if any later\n // step fails.\n ensureSettingsDir()\n backupSettings()\n\n // 2. Hand placement off to OpenClaw. `openclaw plugins install <path>`\n // copies our package into ~/.openclaw/extensions/<encoded-id>/, writes\n // the install record into ~/.openclaw/plugins/installs.json, and\n // creates a (disabled, configless) plugins.entries[id] in\n // openclaw.json. We layer config + hooks + allow on top in step 3.\n // --force lets us overwrite an existing install (e.g. when re-running\n // on top of a previous version).\n const packageRoot = resolvePackageRoot()\n const installSpinner = spinner()\n installSpinner.start(`Installing plugin via openclaw plugins install ${packageRoot}`)\n const installResult = runOpenclaw([\"plugins\", \"install\", packageRoot, \"--force\"], { timeoutMs: 60_000 })\n if (!installResult.ok) {\n installSpinner.stop(\"openclaw plugins install failed\")\n if (installResult.reason === \"enoent\") {\n throw new Error(\"`openclaw` not found on PATH. Install OpenClaw first (https://openclaw.ai/install) and re-run.\")\n }\n if (installResult.reason === \"timeout\") {\n throw new Error(\"openclaw plugins install timed out after 60s. Try running it manually to see what's stuck.\")\n }\n const detail = installResult.stderr.trim() || installResult.stdout.trim() || `exit code ${installResult.code}`\n throw new Error(`openclaw plugins install failed: ${detail}`)\n }\n installSpinner.stop(\"Plugin registered with OpenClaw\")\n\n // 3. Layer our config, hooks, and (optionally) plugins.allow on top of the\n // entry OpenClaw just created. We don't touch placement — that's\n // OpenClaw's job — only the policy fields.\n const settingsSpinner = spinner()\n settingsSpinner.start(\"Updating openclaw.json\")\n const settings = readSettings()\n // Sweep `LATITUDE_*` keys our 0.0.1 leaked under settings.env. Idempotent\n // when the keys aren't there.\n migrateLegacyEntries(settings)\n\n // Pass the install-flag tristate through verbatim — `setPluginEntry`\n // resolves the final value by consulting (in order): the patch, the\n // existing config block, the existing hooks block, then defaulting to\n // `true`. Going through that resolver matters because a 0.0.1 install\n // may have stored the flag only under hooks; if we pre-collapsed it\n // here using existing config alone, we'd lose that signal and silently\n // overwrite the user's intent.\n setPluginEntry(settings, {\n apiKey,\n project,\n baseUrl: envConfig.name === \"production\" ? undefined : envConfig.ingest,\n allowConversationAccess,\n // `debug` is intentionally not passed — `setPluginEntry` preserves\n // hand-edited values; fresh installs leave the key absent (runtime\n // default is `false`).\n })\n\n // Plugins.allow handling. Running `npx install` is itself the trust\n // signal — auto-add unless the user explicitly opted out via --no-trust.\n // Without this, OpenClaw prints a \"plugins.allow is empty\" warning at\n // every gateway start.\n if (!noTrust) {\n addToPluginsAllow(settings)\n }\n\n writeSettings(settings)\n settingsSpinner.stop(`Updated ${SETTINGS_PATH}`)\n if (existsSync(SETTINGS_BACKUP_PATH)) log.info(`Backup saved at ${pc.dim(SETTINGS_BACKUP_PATH)}`)\n if (noTrust) {\n log.warning(\n `--no-trust set; OpenClaw will warn at every gateway start that ${PLUGIN_ID} is untrusted. Add it to plugins.allow yourself when you're ready.`,\n )\n }\n}\n\nfunction ensureSettingsDir(): void {\n const dir = dirname(SETTINGS_PATH)\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true })\n}\n\n/**\n * Verify `openclaw` is on PATH AND its version is >= MIN_OPENCLAW_VERSION.\n * Aborts with a clear upgrade message otherwise. Called before any user\n * prompts so we don't waste their time collecting credentials we can't\n * use.\n */\nfunction ensureOpenclawIsCompatible(): void {\n const v = getOpenclawVersion()\n if (!v.ok) {\n if (v.error === \"missing\") {\n cancel(\"OpenClaw CLI not found on PATH. Install or update via `npm install -g openclaw@latest` and re-run.\")\n process.exit(1)\n }\n cancel(\n `Couldn't parse OpenClaw version output${v.raw ? ` (got: ${pc.dim(v.raw)})` : \"\"}. Run \\`openclaw --version\\` and report the output.`,\n )\n process.exit(1)\n }\n if (compareCalver(v.version, MIN_OPENCLAW_VERSION) < 0) {\n cancel(\n `OpenClaw ${v.version} is older than the minimum supported version (${MIN_OPENCLAW_VERSION}). Run \\`npm install -g openclaw@latest\\` and re-run install.`,\n )\n process.exit(1)\n }\n log.info(`OpenClaw ${pc.dim(v.version)} (>= ${MIN_OPENCLAW_VERSION})`)\n}\n\n/**\n * Resolve the absolute path to our package's root (the directory that\n * contains `package.json` + `openclaw.plugin.json` + `dist/`). The compiled\n * CLI lives at `<package-root>/dist/cli.js`, so `import.meta.url`'s parent\n * directory's parent is our root.\n *\n * `openclaw plugins install <path>` copies files synchronously, so we\n * don't have to keep this directory alive past the spawn return.\n */\nfunction resolvePackageRoot(): string {\n const here = dirname(fileURLToPath(import.meta.url))\n return resolve(here, \"..\")\n}\n\n// ─── Uninstall ──────────────────────────────────────────────────────────────\n\ninterface UninstallFlags {\n noPrompt?: boolean\n}\n\nexport async function runUninstall(flags: UninstallFlags = {}): Promise<void> {\n intro(pc.bgYellow(pc.black(\" Latitude · OpenClaw telemetry — uninstall \")))\n\n const settings = readSettings()\n const hasEntry = hasLatitudePlugin(settings)\n\n // Without an entry, there's nothing for `openclaw plugins uninstall` to\n // remove either — short-circuit cleanly. If the user wants to uninstall\n // even when our entry is gone (e.g. we crashed mid-install), they can\n // run `openclaw plugins uninstall` themselves.\n if (!hasEntry) {\n note(\"No Latitude plugin entry found — nothing to remove.\", \"Status\")\n outro(pc.dim(\"Nothing changed\"))\n return\n }\n\n const plan = [\n `Run \\`openclaw plugins uninstall ${PLUGIN_ID} --force\\` (removes files, install record, and plugin entry)`,\n `Sweep any leftover LATITUDE_* keys from settings.env`,\n `Backup of openclaw.json saved at ${SETTINGS_BACKUP_PATH}`,\n ]\n note(plan.join(\"\\n\"), \"Plan\")\n\n if (!flags.noPrompt && process.stdin.isTTY === true) {\n const ok = await confirm({ message: \"Proceed?\", initialValue: true })\n if (isCancel(ok) || ok !== true) return onCancel()\n }\n\n // 1. Take the backup BEFORE openclaw plugins uninstall touches\n // openclaw.json so it represents the user's true pre-uninstall state\n // (entry + config + plugins.allow). Backing up after would capture\n // the already-stripped state, defeating the point of the backup.\n backupSettings()\n\n // 2. Hand uninstall to OpenClaw. It removes files, the install record,\n // plugins.entries[id], plugins.allow, plugins.deny, plugins.load.paths\n // — see src/plugins/uninstall.ts.\n const s = spinner()\n s.start(\"Reverting via openclaw plugins uninstall\")\n const uninstallResult = runOpenclaw([\"plugins\", \"uninstall\", PLUGIN_ID, \"--force\"], { timeoutMs: 60_000 })\n if (!uninstallResult.ok) {\n s.stop(\"openclaw plugins uninstall failed\")\n if (uninstallResult.reason === \"enoent\") {\n log.warning(\n \"`openclaw` not found on PATH. Falling back to local cleanup — files at ~/.openclaw/extensions/ may remain.\",\n )\n } else {\n const detail =\n uninstallResult.stderr.trim() || uninstallResult.stdout.trim() || `exit code ${uninstallResult.code}`\n log.warning(`openclaw plugins uninstall reported: ${detail}. Continuing with local cleanup.`)\n }\n } else {\n s.stop(\"Plugin removed by OpenClaw\")\n }\n\n // 3. Defensive cleanup — `openclaw plugins uninstall` already strips the\n // entry and plugins.allow on success, but if it failed above (enoent\n // / non-zero) we still want our state out of openclaw.json. These\n // are all idempotent.\n const cleanupSpinner = spinner()\n cleanupSpinner.start(\"Reverting openclaw.json\")\n const post = readSettings()\n removePluginEntry(post)\n removeFromPluginsAllow(post)\n migrateLegacyEntries(post)\n writeSettings(post)\n cleanupSpinner.stop(\"Done\")\n outro(pc.green(\"✓ Uninstalled\"))\n}\n","import { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { normalizeInstallFlags, parseFlags, runInstall, runUninstall } from \"./setup.ts\"\n\nconst USAGE = `usage: latitude-openclaw <command> [options]\n\ncommands:\n install Install the plugin (interactive when stdin is a TTY)\n uninstall Remove the plugin entry and files\n --version, -v Print the package version\n --help, -h Print this message\n\ninstall options:\n --api-key=<key> Pass the API key non-interactively\n --project=<slug> Pass the project slug non-interactively\n --staging Target https://staging.latitude.so / staging-ingest\n --dev Target http://localhost:3000 / 3002\n --no-content Skip raw prompt/response/tool I/O capture\n --allow-conversation Force conversation capture on (overrides existing config)\n --yes / --no-prompt Skip all prompts (required for non-TTY / CI)\n`\n\nfunction readVersion(): string {\n // dist/cli.js → ../package.json\n const here = dirname(fileURLToPath(import.meta.url))\n const pkgPath = join(here, \"..\", \"package.json\")\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version?: string }\n return pkg.version ?? \"unknown\"\n } catch {\n return \"unknown\"\n }\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2)\n // Top-level --version / --help are handled before parseFlags so users can\n // type them as the first arg without hitting the \"unknown subcommand\" path.\n if (argv[0] === \"--version\" || argv[0] === \"-v\") {\n process.stdout.write(`${readVersion()}\\n`)\n return\n }\n if (argv[0] === \"--help\" || argv[0] === \"-h\") {\n process.stdout.write(USAGE)\n return\n }\n\n const { subcommand, flags } = parseFlags(argv)\n if (subcommand === \"install\" || subcommand === undefined) {\n await runInstall(normalizeInstallFlags(flags))\n return\n }\n if (subcommand === \"uninstall\") {\n await runUninstall({ noPrompt: flags[\"no-prompt\"] === true || flags.yes === true })\n return\n }\n process.stderr.write(`unknown subcommand: ${subcommand}\\n`)\n process.stderr.write(USAGE)\n process.exit(1)\n}\n\nmain().catch((err) => {\n process.stderr.write(`${String(err)}\\n`)\n process.exit(1)\n})\n"],"mappings":"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"}
package/dist/plugin.d.ts CHANGED
@@ -20,68 +20,56 @@ interface Logger {
20
20
  warn: (msg: string) => void;
21
21
  }
22
22
  //#endregion
23
- //#region src/types.d.ts
24
- interface OpenClawLlmUsage {
25
- input?: number;
26
- output?: number;
27
- cacheRead?: number;
28
- cacheWrite?: number;
29
- total?: number;
30
- }
31
- interface LlmCallRecord {
32
- runId: string;
33
- sessionId: string;
34
- sessionKey: string | undefined;
35
- agentId: string | undefined;
36
- provider: string;
37
- requestModel: string;
38
- responseModel: string | undefined;
39
- resolvedRef: string | undefined;
40
- systemPrompt: string | undefined;
41
- prompt: string;
42
- historyMessages: unknown[];
43
- imagesCount: number;
44
- assistantTexts: string[];
45
- lastAssistant: unknown;
46
- usage: OpenClawLlmUsage | undefined;
47
- startMs: number;
48
- endMs: number | undefined;
49
- error: string | undefined;
50
- toolCalls: ToolCallRecord[];
51
- }
52
- interface ToolCallRecord {
53
- toolCallId: string;
54
- toolName: string;
55
- params: Record<string, unknown>;
56
- result: unknown;
57
- error: string | undefined;
23
+ //#region src/span-builder.d.ts
24
+ /**
25
+ * Builds the per-trace span tree for an OpenClaw agent run from the granular
26
+ * paired hooks. Replaces the older `turn-builder.ts` model that collapsed the
27
+ * whole attempt into a single `llm_request` span — that shape was wrong on
28
+ * two counts: `llm_input` / `llm_output` fire ONCE per attempt (not per
29
+ * generation), and an attempt is a sequence of generations interleaved with
30
+ * tool executions.
31
+ *
32
+ * Span set this builder produces:
33
+ *
34
+ * agent (root)
35
+ * ├─ compaction (0..1, rare)
36
+ * ├─ model_call (1..N, one per provider API call)
37
+ * ├─ tool_call: ... (interleaved between model_calls; siblings of agent)
38
+ * ├─ subagent (0..N; child agent runs nest INSIDE these via
39
+ * │ └─ agent ... cross-runId trace propagation)
40
+ * └─ model_call (final)
41
+ *
42
+ * Tool spans are siblings of `agent`, not children of `model_call`, because
43
+ * tools run BETWEEN generations — not during them. Nesting under model_call
44
+ * would falsely imply concurrency.
45
+ *
46
+ * `llm_input` / `llm_output` are NOT span boundaries here. They're data-only
47
+ * feeds that enrich the parent `agent` span (full message history, output
48
+ * messages, aggregate token usage).
49
+ */
50
+ interface SpanRecord {
51
+ /** Stable id for the span (16 hex chars). */
52
+ spanId: string;
53
+ /** Span tree id (32 hex chars). */
54
+ traceId: string;
55
+ /** Empty string for root agent spans, parent's spanId otherwise. */
56
+ parentSpanId: string;
57
+ /** OpenClaw event noun (`agent` / `model_call` / `tool_call` / `compaction` / `subagent`). */
58
+ name: string;
58
59
  startMs: number;
59
60
  endMs: number | undefined;
60
- durationMs: number | undefined;
61
- agentId: string | undefined;
61
+ /** Free-form attribute bag — flattened to OTLP key/value at emit time. */
62
+ attrs: Record<string, AttrValue>;
63
+ /** Status — set at close from the event payload's outcome/error. */
64
+ outcome?: "ok" | "error";
65
+ errorMessage?: string | undefined;
62
66
  }
63
- interface RunRecord {
67
+ type AttrValue = string | number | boolean | unknown[] | Record<string, unknown> | undefined;
68
+ interface BuildResult {
69
+ /** Run id this batch belongs to. */
64
70
  runId: string;
65
- sessionId: string | undefined;
66
- sessionKey: string | undefined;
67
- agentId: string | undefined;
68
- workspaceDir: string | undefined;
69
- messageProvider: string | undefined;
70
- trigger: string | undefined;
71
- channelId: string | undefined;
72
- modelProviderId: string | undefined;
73
- modelId: string | undefined;
74
- startMs: number;
75
- endMs: number | undefined;
76
- success: boolean | undefined;
77
- error: string | undefined;
78
- llmCalls: LlmCallRecord[];
79
- /**
80
- * Tool calls queued by `before_tool_call` but not yet matched to an `llm_call`
81
- * because the most recent `llm_output` has already closed the call for this
82
- * runId. Retained so `after_tool_call` can still complete the record.
83
- */
84
- orphanTools: ToolCallRecord[];
71
+ /** All spans ready to be exported (agent + everything beneath it). */
72
+ spans: SpanRecord[];
85
73
  }
86
74
  //#endregion
87
75
  //#region src/plugin.d.ts
@@ -112,17 +100,19 @@ interface RegisterOptions {
112
100
  * Hook to observe the emitted run right before it's posted. Used by tests;
113
101
  * not a stable public API.
114
102
  */
115
- onEmit?: (run: RunRecord) => void;
103
+ onEmit?: (result: BuildResult) => void;
116
104
  }
117
105
  /**
118
106
  * Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls
119
- * this once at plugin activation; we wire up `llm_input`, `llm_output`, tool
120
- * and lifecycle hooks to stream traces to Latitude.
107
+ * this once at plugin activation; we wire up the granular paired hooks
108
+ * (model_call_started/_ended, before_/after_tool_call, before_/after_compaction,
109
+ * subagent_spawned/_ended, before_agent_start/agent_end) plus the
110
+ * data-only feeds (llm_input/llm_output) that enrich the agent span.
121
111
  *
122
- * Every handler is fire-and-forget on OpenClaw's side (see
123
- * `src/plugins/hooks.ts` runLlmInput/runLlmOutput are documented as
124
- * parallel and wrapped with `.catch()` at the call site in attempt.ts), so
125
- * nothing we do here can slow the agent loop.
112
+ * Every typed hook on OpenClaw's side fires fire-and-forget for non-modifying
113
+ * hooks; before_tool_call is a `runModifyingHook` where returning anything
114
+ * other than undefined blocks the tool call. Our handler returns nothing —
115
+ * keep it that way.
126
116
  */
127
117
  declare function registerLatitudePlugin(api: OpenClawPluginApiLike, opts?: RegisterOptions): void;
128
118
  //#endregion
@@ -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;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"}
1
+ {"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/config.ts","../src/logger.ts","../src/span-builder.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;;;ADJT;;;;;;;;;;;;;;;ACEA;;;;;;;;;;;ADFA,UEsDiB,UAAA;;EAEf,MAAA;EAFyB;EAIzB,OAAA;EAQa;EANb,YAAA;EAFA;EAIA,IAAA;EACA,OAAA;EACA,KAAA;EAAA;EAEA,KAAA,EAAO,MAAA,SAAe,SAAA;EAAf;EAEP,OAAA;EACA,YAAA;AAAA;AAAA,KAGU,SAAA,2CAAoD,MAAA;AAAA,UAoD/C,WAAA;EApDL;EAsDV,KAAA;;EAEA,KAAA,EAAO,UAAA;AAAA;;;;;;;;;;;;;;UChGQ,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;EFtCf;EEwCA,MAAA,GAAS,MAAA;EFxCS;EE0ClB,MAAA,GAAS,MAAA;;;;ADQX;ECHE,MAAA,IAAU,MAAA,EAAQ,WAAA;AAAA;;;;;;;;;;;;;iBAeI,sBAAA,CAAuB,GAAA,EAAK,qBAAA,EAAuB,IAAA,GAAM,eAAA"}