autotel-devtools 12.1.0 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,6 +150,7 @@ process — point them at the bound port, or free the original.
150
150
  - ✅ Service map visualization
151
151
  - ✅ Resources view (derived from telemetry)
152
152
  - ✅ GenAI run summaries + narrated walkthrough
153
+ - ✅ **Agents view** — observe coding agents (Claude Code, opencode) from their OTel metrics + log events
153
154
  - ✅ Search with debounce (300ms)
154
155
  - ✅ Configurable telemetry limits (env vars)
155
156
  - ✅ Widget position persistence (localStorage)
@@ -172,6 +173,35 @@ on top of the per-span detail:
172
173
  Useful for showing a teammate or a client exactly what the agent did, which
173
174
  tools it called, and where the cost went.
174
175
 
176
+ ### Agents: observe Claude Code (and other coding agents)
177
+
178
+ Coding agents like **Claude Code** emit OpenTelemetry **metrics and log events**
179
+ (no traces). The **Agents** tab reconstructs them into a session-centric view —
180
+ powered by the [`autotel-agents`](../autotel-agents) package, which also handles
181
+ opencode and is one adapter away from Codex.
182
+
183
+ One command starts the receiver _and_ launches Claude Code wired to it:
184
+
185
+ ```bash
186
+ npx autotel-devtools claude
187
+ ```
188
+
189
+ This sets the telemetry env for a live local view — OTLP **`http/protobuf`** to
190
+ this receiver (not the gRPC setup in most guides, which this receiver doesn't
191
+ speak), 1s export intervals, and `session.id` kept on metrics. Then open the UI
192
+ and switch to **Agents**.
193
+
194
+ - `--print-env` — print the env block instead of launching (for managed-settings
195
+ / MDM / VS Code), e.g. `npx autotel-devtools claude --print-env`.
196
+ - `--log-prompts` — capture prompt _text_ (default is private: length only).
197
+
198
+ What you get per session: a **timeline** (prompts → tool calls → API requests →
199
+ decisions), a **rollup** (cost, tokens, requests, lines changed), and breakdowns
200
+ by **tool category**, **MCP server** (`mcp__server__tool`), **sub-agent** (`Task`)
201
+ and **skill** (`Skill`) — plus an aggregate strip across all sessions. Cost uses
202
+ the agent's reported `cost_usd`, falling back to a token estimate (badged). MCP
203
+ protocol internals are out of scope here — that's `autotel-mcp-instrumentation`.
204
+
175
205
  ## Configuration
176
206
 
177
207
  ### Environment Variables
@@ -190,12 +220,18 @@ AUTOTEL_DEVTOOLS_TITLE="My App" # Dashboard title (optional)
190
220
  ```bash
191
221
  npx autotel-devtools 4319 # port as a bare positional
192
222
  npx autotel-devtools --port 4319 --host 0.0.0.0
223
+ npx autotel-devtools claude # receiver + launch Claude Code wired to it
224
+ npx autotel-devtools claude --print-env # print the telemetry env, don't launch
193
225
  ```
194
226
 
195
227
  Arguments:
196
228
 
197
229
  - `[port]` - Port to listen on, shorthand for `--port` (an explicit `--port` always wins)
198
230
 
231
+ Subcommands:
232
+
233
+ - `claude [claude args]` - Start the receiver and launch Claude Code wired to it (open the **Agents** tab). `--print-env` prints the env block instead; `--log-prompts` opts into prompt-text capture (default: private). `--port`/`--host` apply to the receiver; anything else is passed through to `claude`.
234
+
199
235
  Options:
200
236
 
201
237
  - `--port, -p` - Port to listen on (default: 4318). If the port is taken, the receiver walks forward to the next free port and prints a warning.
package/dist/cli.cjs CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- const require_http = require('./http-Yj6iSrMX.cjs');
2
+ const require_http = require('./http-D0sSdp3I.cjs');
3
3
  const require_listen = require('./listen-BBsxO0wm.cjs');
4
4
  let node_http = require("node:http");
5
5
  let node_fs = require("node:fs");
6
6
  let node_path = require("node:path");
7
7
  let node_url = require("node:url");
8
+ let node_child_process = require("node:child_process");
8
9
 
9
10
  //#region src/cli.ts
10
11
  function printHelp() {
11
- process.stdout.write("autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"><\/script>\n\n");
12
+ process.stdout.write("autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n autotel-devtools claude [claude args] [--print-env] [--log-prompts]\n\nSubcommands:\n claude Start the receiver AND launch Claude Code wired to it\n (HTTP/protobuf → this receiver, 1s intervals, session id on).\n Open the \"Agents\" tab to watch sessions, tokens, cost,\n tool / MCP / sub-agent / skill usage live.\n --print-env Print the telemetry env block and exit (don't launch)\n --log-prompts Capture prompt text (default: length only / private)\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n npx autotel-devtools claude # watch Claude Code in the Agents tab\n npx autotel-devtools claude --print-env # just print the env (for MDM / VS Code)\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"><\/script>\n\n");
12
13
  }
13
14
  function printVersion() {
14
15
  try {
@@ -70,9 +71,7 @@ function parsePort(value) {
70
71
  }
71
72
  return n;
72
73
  }
73
- async function main() {
74
- const options = parseArgs(process.argv.slice(2));
75
- if (!options) process.exit(0);
74
+ async function startReceiver(options) {
76
75
  const httpServer = (0, node_http.createServer)();
77
76
  const loopbackOnly = require_http.hostHeaderIsLoopback(options.host);
78
77
  const wsServer = new require_http.DevtoolsServer({
@@ -90,7 +89,117 @@ async function main() {
90
89
  const { addresses, warnings, port: boundPort } = await listeners.ready;
91
90
  if (boundPort !== options.port) if (await require_http.probePortHolder(options.host, options.port) === "autotel-devtools") warnings.push(`another autotel-devtools is already running on port ${options.port}; this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`);
92
91
  else warnings.push(`port ${options.port} is held by another process that is NOT autotel-devtools. Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. Point your exporter at :${boundPort}, or free :${options.port} and restart.`);
93
- const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
92
+ return {
93
+ wsServer,
94
+ closeAll: () => Promise.all([wsServer.close(), listeners.closeSibling()]).then(() => void 0),
95
+ addresses,
96
+ warnings,
97
+ boundPort,
98
+ uiBase: `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`
99
+ };
100
+ }
101
+ function buildAgentEnv(uiBase, logPrompts) {
102
+ const env = {
103
+ CLAUDE_CODE_ENABLE_TELEMETRY: "1",
104
+ OTEL_METRICS_EXPORTER: "otlp",
105
+ OTEL_LOGS_EXPORTER: "otlp",
106
+ OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
107
+ OTEL_EXPORTER_OTLP_ENDPOINT: uiBase,
108
+ OTEL_METRIC_EXPORT_INTERVAL: "1000",
109
+ OTEL_LOGS_EXPORT_INTERVAL: "1000",
110
+ OTEL_METRICS_INCLUDE_SESSION_ID: "true"
111
+ };
112
+ if (logPrompts) env.OTEL_LOG_USER_PROMPTS = "1";
113
+ return env;
114
+ }
115
+ function printEnvBlock(env) {
116
+ for (const [key, value] of Object.entries(env)) process.stdout.write(`export ${key}=${value}\n`);
117
+ }
118
+ function parseClaudeArgs(argv) {
119
+ const opts = {
120
+ port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || "4318"),
121
+ host: process.env.AUTOTEL_DEVTOOLS_HOST || "127.0.0.1",
122
+ printEnv: false,
123
+ logPrompts: false,
124
+ claudeArgs: []
125
+ };
126
+ for (let i = 0; i < argv.length; i++) {
127
+ const arg = argv[i];
128
+ const next = argv[i + 1];
129
+ if (arg === "--") {
130
+ opts.claudeArgs.push(...argv.slice(i + 1));
131
+ break;
132
+ }
133
+ if (arg === "--print-env") {
134
+ opts.printEnv = true;
135
+ continue;
136
+ }
137
+ if (arg === "--log-prompts") {
138
+ opts.logPrompts = true;
139
+ continue;
140
+ }
141
+ if ((arg === "--port" || arg === "-p") && next) {
142
+ opts.port = parsePort(next);
143
+ i++;
144
+ continue;
145
+ }
146
+ if ((arg === "--host" || arg === "-H") && next) {
147
+ opts.host = next;
148
+ i++;
149
+ continue;
150
+ }
151
+ opts.claudeArgs.push(arg);
152
+ }
153
+ return opts;
154
+ }
155
+ async function runClaudeSubcommand(argv) {
156
+ const opts = parseClaudeArgs(argv);
157
+ if (opts.printEnv) {
158
+ printEnvBlock(buildAgentEnv(`http://${opts.host === "localhost" ? "127.0.0.1" : opts.host}:${opts.port}`, opts.logPrompts));
159
+ return;
160
+ }
161
+ const receiver = await startReceiver({
162
+ port: opts.port,
163
+ host: opts.host
164
+ });
165
+ const env = buildAgentEnv(receiver.uiBase, opts.logPrompts);
166
+ process.stdout.write(`\n autotel-devtools — Claude Code\n\n`);
167
+ process.stdout.write(` Receiver: ${receiver.uiBase}\n`);
168
+ process.stdout.write(` UI: ${receiver.uiBase} → open the "Agents" tab\n`);
169
+ if (!opts.logPrompts) process.stdout.write(` Prompts: private (length only) — add --log-prompts to capture text\n`);
170
+ for (const w of receiver.warnings) process.stdout.write(` ⚠ ${w}\n`);
171
+ process.stdout.write(`\n Launching claude…\n\n`);
172
+ const child = (0, node_child_process.spawn)("claude", opts.claudeArgs, {
173
+ stdio: "inherit",
174
+ env: {
175
+ ...process.env,
176
+ ...env
177
+ }
178
+ });
179
+ const shutdown = () => {
180
+ receiver.closeAll().then(() => process.exit(0));
181
+ };
182
+ process.on("SIGINT", shutdown);
183
+ process.on("SIGTERM", shutdown);
184
+ child.on("error", (err) => {
185
+ const reason = err.code === "ENOENT" ? `'claude' not found on PATH. Install Claude Code, or run the receiver alone with 'npx autotel-devtools' and point claude at ${receiver.uiBase} using --print-env.` : err.message;
186
+ process.stderr.write(`[autotel-devtools] ${reason}\n`);
187
+ receiver.closeAll().then(() => process.exit(1));
188
+ });
189
+ child.on("exit", (code) => {
190
+ receiver.closeAll().then(() => process.exit(code ?? 0));
191
+ });
192
+ }
193
+ async function main() {
194
+ const argv = process.argv.slice(2);
195
+ if (argv[0] === "claude") {
196
+ await runClaudeSubcommand(argv.slice(1));
197
+ return;
198
+ }
199
+ const options = parseArgs(argv);
200
+ if (!options) process.exit(0);
201
+ const receiver = await startReceiver(options);
202
+ const { addresses, warnings, uiBase } = receiver;
94
203
  const title = options.title || "autotel-devtools";
95
204
  process.stdout.write(`\n ${title}\n\n`);
96
205
  process.stdout.write(` Listening: ${addresses.join(" + ")}\n`);
@@ -108,7 +217,7 @@ async function main() {
108
217
  for (const w of warnings) process.stdout.write(` ⚠ ${w}\n`);
109
218
  if (warnings.length > 0) process.stdout.write("\n");
110
219
  const shutdown = () => {
111
- Promise.all([wsServer.close(), listeners.closeSibling()]).then(() => process.exit(0));
220
+ receiver.closeAll().then(() => process.exit(0));
112
221
  };
113
222
  process.on("SIGINT", shutdown);
114
223
  process.on("SIGTERM", shutdown);
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","names":["hostHeaderIsLoopback","DevtoolsServer","listenLoopbackDualStack","attachDevtoolsRoutes","probePortHolder"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// src/cli.ts\nimport { createServer } from 'node:http'\nimport { readFileSync } from 'node:fs'\nimport { resolve, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { DevtoolsServer } from './server/server'\nimport { attachDevtoolsRoutes } from './server/http'\nimport { hostHeaderIsLoopback } from './server/origin-guard'\nimport { listenLoopbackDualStack } from './server/listen'\nimport { probePortHolder } from './server/identity'\n\ninterface CliOptions {\n port: number\n host: string\n title?: string\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n `autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"></script>\n` + '\\n',\n )\n}\n\nfunction printVersion(): void {\n try {\n const dir = dirname(fileURLToPath(import.meta.url))\n const pkgPath = resolve(dir, '..', 'package.json')\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))\n process.stdout.write(`${pkg.version}\\n`)\n } catch {\n process.stdout.write('unknown\\n')\n }\n}\n\nfunction parseArgs(argv: string[]): CliOptions | null {\n const options: CliOptions = {\n port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || '4318'),\n host: process.env.AUTOTEL_DEVTOOLS_HOST || '127.0.0.1',\n title: process.env.AUTOTEL_DEVTOOLS_TITLE,\n }\n\n // An explicit `--port`/`-p` always wins, regardless of where it sits in\n // argv. A bare numeric positional is shorthand for `--port`, but only when\n // no explicit flag was given — and only the first positional counts, so a\n // stray \"4318\" can't override an earlier positional either.\n let portWasExplicit = false\n let positionalPortConsumed = false\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n const next = argv[i + 1]\n if (arg === '--help' || arg === '-h') { printHelp(); return null }\n if (arg === '--version' || arg === '-v') { printVersion(); return null }\n if ((arg === '--port' || arg === '-p') && next) { options.port = parsePort(next); portWasExplicit = true; i++; continue }\n if ((arg === '--host' || arg === '-H') && next) { options.host = next; i++; continue }\n if ((arg === '--title' || arg === '-t') && next) { options.title = next; i++; continue }\n if (/^\\d+$/.test(arg) && !positionalPortConsumed) {\n if (!portWasExplicit) options.port = parsePort(arg)\n positionalPortConsumed = true\n continue\n }\n }\n\n return options\n}\n\nfunction parsePort(value: string): number {\n const n = Number(value)\n if (!Number.isInteger(n) || n < 0 || n > 65535) {\n process.stderr.write(`[autotel-devtools] invalid port: ${value}\\n`)\n process.exit(2)\n }\n return n\n}\n\nasync function main(): Promise<void> {\n const options = parseArgs(process.argv.slice(2))\n if (!options) { process.exit(0) }\n\n const httpServer = createServer()\n const loopbackOnly = hostHeaderIsLoopback(options.host)\n const wsServer = new DevtoolsServer({ server: httpServer, host: options.host, verbose: true })\n attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly })\n\n const listeners = listenLoopbackDualStack({\n primary: httpServer,\n port: options.port,\n host: options.host,\n attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly }),\n })\n\n const { addresses, warnings, port: boundPort } = await listeners.ready\n\n // Falling forward to a different port means the one we wanted is held by\n // someone else. Classify them: another autotel-devtools (benign) versus a\n // foreign process. The foreign case is the silent footgun — apps keep\n // exporting OTLP to the requested port and reach that process, not us, so\n // this UI stays empty while they see export errors. Say so, with the fix.\n if (boundPort !== options.port) {\n const holder = await probePortHolder(options.host, options.port)\n if (holder === 'autotel-devtools') {\n warnings.push(\n `another autotel-devtools is already running on port ${options.port}; ` +\n `this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`,\n )\n } else {\n warnings.push(\n `port ${options.port} is held by another process that is NOT autotel-devtools. ` +\n `Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. ` +\n `Point your exporter at :${boundPort}, or free :${options.port} and restart.`,\n )\n }\n }\n\n const uiBase = `http://${options.host === 'localhost' ? '127.0.0.1' : options.host}:${boundPort}`\n const title = options.title || 'autotel-devtools'\n process.stdout.write(`\\n ${title}\\n\\n`)\n process.stdout.write(` Listening: ${addresses.join(' + ')}\\n`)\n process.stdout.write(` UI: ${uiBase} (open in a browser)\\n`)\n process.stdout.write(` OTLP: ${uiBase}/v1/traces\\n`)\n process.stdout.write(` WebSocket: ${uiBase.replace('http', 'ws')}/ws\\n\\n`)\n // The widget bundle auto-mounts on load, so the bare <script> tag is all the\n // user needs — spell that out, plus the two opt-in variations, so nobody\n // wonders whether they also have to add an element by hand.\n process.stdout.write(\n ` Embed in your app — paste into your HTML; a floating panel appears automatically:\\n`,\n )\n process.stdout.write(` <script src=\"${uiBase}/widget.js\"></script>\\n\\n`)\n process.stdout.write(\n ` full screen instead: <script src=\"${uiBase}/widget.js?mode=fullpage\"></script>\\n`,\n )\n process.stdout.write(\n ` choose where it goes: add <autotel-devtools></autotel-devtools> to your markup\\n\\n`,\n )\n process.stdout.write(` Or point any OTLP exporter at this receiver:\\n`)\n process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json\\n`)\n process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}\\n\\n`)\n // Self-check: confirm the collector is reachable AND that ingestion works,\n // not just that something is listening. Reading /v1/traces back is the same\n // check a test should make instead of trusting \"the client tried to send\".\n process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces\\n\\n`)\n for (const w of warnings) {\n process.stdout.write(` ⚠ ${w}\\n`)\n }\n if (warnings.length > 0) process.stdout.write('\\n')\n\n const shutdown = () => {\n Promise.all([wsServer.close(), listeners.closeSibling()]).then(() =>\n process.exit(0),\n )\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n\nmain().catch((error) => {\n process.stderr.write(`[autotel-devtools] failed to start: ${error instanceof Error ? error.message : String(error)}\\n`)\n process.exit(1)\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.cjs","names":["hostHeaderIsLoopback","DevtoolsServer","listenLoopbackDualStack","attachDevtoolsRoutes","probePortHolder"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// src/cli.ts\nimport { createServer } from 'node:http'\nimport { spawn } from 'node:child_process'\nimport { readFileSync } from 'node:fs'\nimport { resolve, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { DevtoolsServer } from './server/server'\nimport { attachDevtoolsRoutes } from './server/http'\nimport { hostHeaderIsLoopback } from './server/origin-guard'\nimport { listenLoopbackDualStack } from './server/listen'\nimport { probePortHolder } from './server/identity'\n\ninterface CliOptions {\n port: number\n host: string\n title?: string\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n `autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n autotel-devtools claude [claude args] [--print-env] [--log-prompts]\n\nSubcommands:\n claude Start the receiver AND launch Claude Code wired to it\n (HTTP/protobuf → this receiver, 1s intervals, session id on).\n Open the \"Agents\" tab to watch sessions, tokens, cost,\n tool / MCP / sub-agent / skill usage live.\n --print-env Print the telemetry env block and exit (don't launch)\n --log-prompts Capture prompt text (default: length only / private)\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n npx autotel-devtools claude # watch Claude Code in the Agents tab\n npx autotel-devtools claude --print-env # just print the env (for MDM / VS Code)\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"></script>\n` + '\\n',\n )\n}\n\nfunction printVersion(): void {\n try {\n const dir = dirname(fileURLToPath(import.meta.url))\n const pkgPath = resolve(dir, '..', 'package.json')\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))\n process.stdout.write(`${pkg.version}\\n`)\n } catch {\n process.stdout.write('unknown\\n')\n }\n}\n\nfunction parseArgs(argv: string[]): CliOptions | null {\n const options: CliOptions = {\n port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || '4318'),\n host: process.env.AUTOTEL_DEVTOOLS_HOST || '127.0.0.1',\n title: process.env.AUTOTEL_DEVTOOLS_TITLE,\n }\n\n // An explicit `--port`/`-p` always wins, regardless of where it sits in\n // argv. A bare numeric positional is shorthand for `--port`, but only when\n // no explicit flag was given — and only the first positional counts, so a\n // stray \"4318\" can't override an earlier positional either.\n let portWasExplicit = false\n let positionalPortConsumed = false\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n const next = argv[i + 1]\n if (arg === '--help' || arg === '-h') { printHelp(); return null }\n if (arg === '--version' || arg === '-v') { printVersion(); return null }\n if ((arg === '--port' || arg === '-p') && next) { options.port = parsePort(next); portWasExplicit = true; i++; continue }\n if ((arg === '--host' || arg === '-H') && next) { options.host = next; i++; continue }\n if ((arg === '--title' || arg === '-t') && next) { options.title = next; i++; continue }\n if (/^\\d+$/.test(arg) && !positionalPortConsumed) {\n if (!portWasExplicit) options.port = parsePort(arg)\n positionalPortConsumed = true\n continue\n }\n }\n\n return options\n}\n\nfunction parsePort(value: string): number {\n const n = Number(value)\n if (!Number.isInteger(n) || n < 0 || n > 65535) {\n process.stderr.write(`[autotel-devtools] invalid port: ${value}\\n`)\n process.exit(2)\n }\n return n\n}\n\ninterface RunningReceiver {\n wsServer: DevtoolsServer\n closeAll: () => Promise<void>\n addresses: string[]\n warnings: string[]\n boundPort: number\n uiBase: string\n}\n\nasync function startReceiver(options: CliOptions): Promise<RunningReceiver> {\n const httpServer = createServer()\n const loopbackOnly = hostHeaderIsLoopback(options.host)\n const wsServer = new DevtoolsServer({ server: httpServer, host: options.host, verbose: true })\n attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly })\n\n const listeners = listenLoopbackDualStack({\n primary: httpServer,\n port: options.port,\n host: options.host,\n attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly }),\n })\n\n const { addresses, warnings, port: boundPort } = await listeners.ready\n\n // Falling forward to a different port means the one we wanted is held by\n // someone else. Classify them: another autotel-devtools (benign) versus a\n // foreign process. The foreign case is the silent footgun — apps keep\n // exporting OTLP to the requested port and reach that process, not us, so\n // this UI stays empty while they see export errors. Say so, with the fix.\n if (boundPort !== options.port) {\n const holder = await probePortHolder(options.host, options.port)\n if (holder === 'autotel-devtools') {\n warnings.push(\n `another autotel-devtools is already running on port ${options.port}; ` +\n `this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`,\n )\n } else {\n warnings.push(\n `port ${options.port} is held by another process that is NOT autotel-devtools. ` +\n `Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. ` +\n `Point your exporter at :${boundPort}, or free :${options.port} and restart.`,\n )\n }\n }\n\n const uiBase = `http://${options.host === 'localhost' ? '127.0.0.1' : options.host}:${boundPort}`\n return {\n wsServer,\n closeAll: () => Promise.all([wsServer.close(), listeners.closeSibling()]).then(() => undefined),\n addresses,\n warnings,\n boundPort,\n uiBase,\n }\n}\n\n// Telemetry env that wires Claude Code (and any OTel-via-env CLI) to this\n// receiver for a *live* local view: HTTP/protobuf (NOT gRPC — this receiver\n// speaks HTTP only), 1s export intervals so events show up promptly, and\n// session.id kept on metrics so metric-only signals join their session.\nfunction buildAgentEnv(uiBase: string, logPrompts: boolean): Record<string, string> {\n const env: Record<string, string> = {\n CLAUDE_CODE_ENABLE_TELEMETRY: '1',\n OTEL_METRICS_EXPORTER: 'otlp',\n OTEL_LOGS_EXPORTER: 'otlp',\n OTEL_EXPORTER_OTLP_PROTOCOL: 'http/protobuf',\n OTEL_EXPORTER_OTLP_ENDPOINT: uiBase,\n OTEL_METRIC_EXPORT_INTERVAL: '1000',\n OTEL_LOGS_EXPORT_INTERVAL: '1000',\n OTEL_METRICS_INCLUDE_SESSION_ID: 'true',\n }\n // Private by default: prompt *text* only flows when explicitly opted in.\n if (logPrompts) env.OTEL_LOG_USER_PROMPTS = '1'\n return env\n}\n\nfunction printEnvBlock(env: Record<string, string>): void {\n for (const [key, value] of Object.entries(env)) {\n process.stdout.write(`export ${key}=${value}\\n`)\n }\n}\n\ninterface ClaudeOptions {\n port: number\n host: string\n printEnv: boolean\n logPrompts: boolean\n claudeArgs: string[]\n}\n\nfunction parseClaudeArgs(argv: string[]): ClaudeOptions {\n const opts: ClaudeOptions = {\n port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || '4318'),\n host: process.env.AUTOTEL_DEVTOOLS_HOST || '127.0.0.1',\n printEnv: false,\n logPrompts: false,\n claudeArgs: [],\n }\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n const next = argv[i + 1]\n if (arg === '--') { opts.claudeArgs.push(...argv.slice(i + 1)); break }\n if (arg === '--print-env') { opts.printEnv = true; continue }\n if (arg === '--log-prompts') { opts.logPrompts = true; continue }\n if ((arg === '--port' || arg === '-p') && next) { opts.port = parsePort(next); i++; continue }\n if ((arg === '--host' || arg === '-H') && next) { opts.host = next; i++; continue }\n // Anything else is passed straight through to `claude`.\n opts.claudeArgs.push(arg)\n }\n return opts\n}\n\nasync function runClaudeSubcommand(argv: string[]): Promise<void> {\n const opts = parseClaudeArgs(argv)\n\n // --print-env needs no running server: emit the block for the configured\n // port so users can paste it into a shell / managed-settings file.\n if (opts.printEnv) {\n const uiBase = `http://${opts.host === 'localhost' ? '127.0.0.1' : opts.host}:${opts.port}`\n printEnvBlock(buildAgentEnv(uiBase, opts.logPrompts))\n return\n }\n\n const receiver = await startReceiver({ port: opts.port, host: opts.host })\n const env = buildAgentEnv(receiver.uiBase, opts.logPrompts)\n\n process.stdout.write(`\\n autotel-devtools — Claude Code\\n\\n`)\n process.stdout.write(` Receiver: ${receiver.uiBase}\\n`)\n process.stdout.write(` UI: ${receiver.uiBase} → open the \"Agents\" tab\\n`)\n if (!opts.logPrompts) {\n process.stdout.write(` Prompts: private (length only) — add --log-prompts to capture text\\n`)\n }\n for (const w of receiver.warnings) process.stdout.write(` ⚠ ${w}\\n`)\n process.stdout.write(`\\n Launching claude…\\n\\n`)\n\n const child = spawn('claude', opts.claudeArgs, {\n stdio: 'inherit',\n env: { ...process.env, ...env },\n })\n\n const shutdown = () => { receiver.closeAll().then(() => process.exit(0)) }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n\n child.on('error', (err) => {\n const reason = (err as NodeJS.ErrnoException).code === 'ENOENT'\n ? `'claude' not found on PATH. Install Claude Code, or run the receiver alone with 'npx autotel-devtools' and point claude at ${receiver.uiBase} using --print-env.`\n : err.message\n process.stderr.write(`[autotel-devtools] ${reason}\\n`)\n receiver.closeAll().then(() => process.exit(1))\n })\n child.on('exit', (code) => {\n receiver.closeAll().then(() => process.exit(code ?? 0))\n })\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2)\n\n // Subcommand: `autotel-devtools claude [claude args] [--print-env] [--log-prompts]`\n if (argv[0] === 'claude') {\n await runClaudeSubcommand(argv.slice(1))\n return\n }\n\n const options = parseArgs(argv)\n if (!options) { process.exit(0) }\n\n const receiver = await startReceiver(options)\n const { addresses, warnings, uiBase } = receiver\n const title = options.title || 'autotel-devtools'\n process.stdout.write(`\\n ${title}\\n\\n`)\n process.stdout.write(` Listening: ${addresses.join(' + ')}\\n`)\n process.stdout.write(` UI: ${uiBase} (open in a browser)\\n`)\n process.stdout.write(` OTLP: ${uiBase}/v1/traces\\n`)\n process.stdout.write(` WebSocket: ${uiBase.replace('http', 'ws')}/ws\\n\\n`)\n // The widget bundle auto-mounts on load, so the bare <script> tag is all the\n // user needs — spell that out, plus the two opt-in variations, so nobody\n // wonders whether they also have to add an element by hand.\n process.stdout.write(\n ` Embed in your app — paste into your HTML; a floating panel appears automatically:\\n`,\n )\n process.stdout.write(` <script src=\"${uiBase}/widget.js\"></script>\\n\\n`)\n process.stdout.write(\n ` full screen instead: <script src=\"${uiBase}/widget.js?mode=fullpage\"></script>\\n`,\n )\n process.stdout.write(\n ` choose where it goes: add <autotel-devtools></autotel-devtools> to your markup\\n\\n`,\n )\n process.stdout.write(` Or point any OTLP exporter at this receiver:\\n`)\n process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json\\n`)\n process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}\\n\\n`)\n // Self-check: confirm the collector is reachable AND that ingestion works,\n // not just that something is listening. Reading /v1/traces back is the same\n // check a test should make instead of trusting \"the client tried to send\".\n process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces\\n\\n`)\n for (const w of warnings) {\n process.stdout.write(` ⚠ ${w}\\n`)\n }\n if (warnings.length > 0) process.stdout.write('\\n')\n\n const shutdown = () => {\n receiver.closeAll().then(() => process.exit(0))\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n\nmain().catch((error) => {\n process.stderr.write(`[autotel-devtools] failed to start: ${error instanceof Error ? error.message : String(error)}\\n`)\n process.exit(1)\n})\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { d as DevtoolsServer, i as probePortHolder, p as hostHeaderIsLoopback, t as attachDevtoolsRoutes } from "./http-BkkKa9C_.js";
2
+ import { d as DevtoolsServer, i as probePortHolder, p as hostHeaderIsLoopback, t as attachDevtoolsRoutes } from "./http-BXPux5tQ.js";
3
3
  import { t as listenLoopbackDualStack } from "./listen-DfOCquUq.js";
4
4
  import { createServer } from "node:http";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { dirname, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { spawn } from "node:child_process";
8
9
 
9
10
  //#region src/cli.ts
10
11
  function printHelp() {
11
- process.stdout.write("autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"><\/script>\n\n");
12
+ process.stdout.write("autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n autotel-devtools claude [claude args] [--print-env] [--log-prompts]\n\nSubcommands:\n claude Start the receiver AND launch Claude Code wired to it\n (HTTP/protobuf → this receiver, 1s intervals, session id on).\n Open the \"Agents\" tab to watch sessions, tokens, cost,\n tool / MCP / sub-agent / skill usage live.\n --print-env Print the telemetry env block and exit (don't launch)\n --log-prompts Capture prompt text (default: length only / private)\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n npx autotel-devtools claude # watch Claude Code in the Agents tab\n npx autotel-devtools claude --print-env # just print the env (for MDM / VS Code)\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"><\/script>\n\n");
12
13
  }
13
14
  function printVersion() {
14
15
  try {
@@ -70,9 +71,7 @@ function parsePort(value) {
70
71
  }
71
72
  return n;
72
73
  }
73
- async function main() {
74
- const options = parseArgs(process.argv.slice(2));
75
- if (!options) process.exit(0);
74
+ async function startReceiver(options) {
76
75
  const httpServer = createServer();
77
76
  const loopbackOnly = hostHeaderIsLoopback(options.host);
78
77
  const wsServer = new DevtoolsServer({
@@ -90,7 +89,117 @@ async function main() {
90
89
  const { addresses, warnings, port: boundPort } = await listeners.ready;
91
90
  if (boundPort !== options.port) if (await probePortHolder(options.host, options.port) === "autotel-devtools") warnings.push(`another autotel-devtools is already running on port ${options.port}; this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`);
92
91
  else warnings.push(`port ${options.port} is held by another process that is NOT autotel-devtools. Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. Point your exporter at :${boundPort}, or free :${options.port} and restart.`);
93
- const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
92
+ return {
93
+ wsServer,
94
+ closeAll: () => Promise.all([wsServer.close(), listeners.closeSibling()]).then(() => void 0),
95
+ addresses,
96
+ warnings,
97
+ boundPort,
98
+ uiBase: `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`
99
+ };
100
+ }
101
+ function buildAgentEnv(uiBase, logPrompts) {
102
+ const env = {
103
+ CLAUDE_CODE_ENABLE_TELEMETRY: "1",
104
+ OTEL_METRICS_EXPORTER: "otlp",
105
+ OTEL_LOGS_EXPORTER: "otlp",
106
+ OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
107
+ OTEL_EXPORTER_OTLP_ENDPOINT: uiBase,
108
+ OTEL_METRIC_EXPORT_INTERVAL: "1000",
109
+ OTEL_LOGS_EXPORT_INTERVAL: "1000",
110
+ OTEL_METRICS_INCLUDE_SESSION_ID: "true"
111
+ };
112
+ if (logPrompts) env.OTEL_LOG_USER_PROMPTS = "1";
113
+ return env;
114
+ }
115
+ function printEnvBlock(env) {
116
+ for (const [key, value] of Object.entries(env)) process.stdout.write(`export ${key}=${value}\n`);
117
+ }
118
+ function parseClaudeArgs(argv) {
119
+ const opts = {
120
+ port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || "4318"),
121
+ host: process.env.AUTOTEL_DEVTOOLS_HOST || "127.0.0.1",
122
+ printEnv: false,
123
+ logPrompts: false,
124
+ claudeArgs: []
125
+ };
126
+ for (let i = 0; i < argv.length; i++) {
127
+ const arg = argv[i];
128
+ const next = argv[i + 1];
129
+ if (arg === "--") {
130
+ opts.claudeArgs.push(...argv.slice(i + 1));
131
+ break;
132
+ }
133
+ if (arg === "--print-env") {
134
+ opts.printEnv = true;
135
+ continue;
136
+ }
137
+ if (arg === "--log-prompts") {
138
+ opts.logPrompts = true;
139
+ continue;
140
+ }
141
+ if ((arg === "--port" || arg === "-p") && next) {
142
+ opts.port = parsePort(next);
143
+ i++;
144
+ continue;
145
+ }
146
+ if ((arg === "--host" || arg === "-H") && next) {
147
+ opts.host = next;
148
+ i++;
149
+ continue;
150
+ }
151
+ opts.claudeArgs.push(arg);
152
+ }
153
+ return opts;
154
+ }
155
+ async function runClaudeSubcommand(argv) {
156
+ const opts = parseClaudeArgs(argv);
157
+ if (opts.printEnv) {
158
+ printEnvBlock(buildAgentEnv(`http://${opts.host === "localhost" ? "127.0.0.1" : opts.host}:${opts.port}`, opts.logPrompts));
159
+ return;
160
+ }
161
+ const receiver = await startReceiver({
162
+ port: opts.port,
163
+ host: opts.host
164
+ });
165
+ const env = buildAgentEnv(receiver.uiBase, opts.logPrompts);
166
+ process.stdout.write(`\n autotel-devtools — Claude Code\n\n`);
167
+ process.stdout.write(` Receiver: ${receiver.uiBase}\n`);
168
+ process.stdout.write(` UI: ${receiver.uiBase} → open the "Agents" tab\n`);
169
+ if (!opts.logPrompts) process.stdout.write(` Prompts: private (length only) — add --log-prompts to capture text\n`);
170
+ for (const w of receiver.warnings) process.stdout.write(` ⚠ ${w}\n`);
171
+ process.stdout.write(`\n Launching claude…\n\n`);
172
+ const child = spawn("claude", opts.claudeArgs, {
173
+ stdio: "inherit",
174
+ env: {
175
+ ...process.env,
176
+ ...env
177
+ }
178
+ });
179
+ const shutdown = () => {
180
+ receiver.closeAll().then(() => process.exit(0));
181
+ };
182
+ process.on("SIGINT", shutdown);
183
+ process.on("SIGTERM", shutdown);
184
+ child.on("error", (err) => {
185
+ const reason = err.code === "ENOENT" ? `'claude' not found on PATH. Install Claude Code, or run the receiver alone with 'npx autotel-devtools' and point claude at ${receiver.uiBase} using --print-env.` : err.message;
186
+ process.stderr.write(`[autotel-devtools] ${reason}\n`);
187
+ receiver.closeAll().then(() => process.exit(1));
188
+ });
189
+ child.on("exit", (code) => {
190
+ receiver.closeAll().then(() => process.exit(code ?? 0));
191
+ });
192
+ }
193
+ async function main() {
194
+ const argv = process.argv.slice(2);
195
+ if (argv[0] === "claude") {
196
+ await runClaudeSubcommand(argv.slice(1));
197
+ return;
198
+ }
199
+ const options = parseArgs(argv);
200
+ if (!options) process.exit(0);
201
+ const receiver = await startReceiver(options);
202
+ const { addresses, warnings, uiBase } = receiver;
94
203
  const title = options.title || "autotel-devtools";
95
204
  process.stdout.write(`\n ${title}\n\n`);
96
205
  process.stdout.write(` Listening: ${addresses.join(" + ")}\n`);
@@ -108,7 +217,7 @@ async function main() {
108
217
  for (const w of warnings) process.stdout.write(` ⚠ ${w}\n`);
109
218
  if (warnings.length > 0) process.stdout.write("\n");
110
219
  const shutdown = () => {
111
- Promise.all([wsServer.close(), listeners.closeSibling()]).then(() => process.exit(0));
220
+ receiver.closeAll().then(() => process.exit(0));
112
221
  };
113
222
  process.on("SIGINT", shutdown);
114
223
  process.on("SIGTERM", shutdown);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// src/cli.ts\nimport { createServer } from 'node:http'\nimport { readFileSync } from 'node:fs'\nimport { resolve, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { DevtoolsServer } from './server/server'\nimport { attachDevtoolsRoutes } from './server/http'\nimport { hostHeaderIsLoopback } from './server/origin-guard'\nimport { listenLoopbackDualStack } from './server/listen'\nimport { probePortHolder } from './server/identity'\n\ninterface CliOptions {\n port: number\n host: string\n title?: string\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n `autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"></script>\n` + '\\n',\n )\n}\n\nfunction printVersion(): void {\n try {\n const dir = dirname(fileURLToPath(import.meta.url))\n const pkgPath = resolve(dir, '..', 'package.json')\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))\n process.stdout.write(`${pkg.version}\\n`)\n } catch {\n process.stdout.write('unknown\\n')\n }\n}\n\nfunction parseArgs(argv: string[]): CliOptions | null {\n const options: CliOptions = {\n port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || '4318'),\n host: process.env.AUTOTEL_DEVTOOLS_HOST || '127.0.0.1',\n title: process.env.AUTOTEL_DEVTOOLS_TITLE,\n }\n\n // An explicit `--port`/`-p` always wins, regardless of where it sits in\n // argv. A bare numeric positional is shorthand for `--port`, but only when\n // no explicit flag was given — and only the first positional counts, so a\n // stray \"4318\" can't override an earlier positional either.\n let portWasExplicit = false\n let positionalPortConsumed = false\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n const next = argv[i + 1]\n if (arg === '--help' || arg === '-h') { printHelp(); return null }\n if (arg === '--version' || arg === '-v') { printVersion(); return null }\n if ((arg === '--port' || arg === '-p') && next) { options.port = parsePort(next); portWasExplicit = true; i++; continue }\n if ((arg === '--host' || arg === '-H') && next) { options.host = next; i++; continue }\n if ((arg === '--title' || arg === '-t') && next) { options.title = next; i++; continue }\n if (/^\\d+$/.test(arg) && !positionalPortConsumed) {\n if (!portWasExplicit) options.port = parsePort(arg)\n positionalPortConsumed = true\n continue\n }\n }\n\n return options\n}\n\nfunction parsePort(value: string): number {\n const n = Number(value)\n if (!Number.isInteger(n) || n < 0 || n > 65535) {\n process.stderr.write(`[autotel-devtools] invalid port: ${value}\\n`)\n process.exit(2)\n }\n return n\n}\n\nasync function main(): Promise<void> {\n const options = parseArgs(process.argv.slice(2))\n if (!options) { process.exit(0) }\n\n const httpServer = createServer()\n const loopbackOnly = hostHeaderIsLoopback(options.host)\n const wsServer = new DevtoolsServer({ server: httpServer, host: options.host, verbose: true })\n attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly })\n\n const listeners = listenLoopbackDualStack({\n primary: httpServer,\n port: options.port,\n host: options.host,\n attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly }),\n })\n\n const { addresses, warnings, port: boundPort } = await listeners.ready\n\n // Falling forward to a different port means the one we wanted is held by\n // someone else. Classify them: another autotel-devtools (benign) versus a\n // foreign process. The foreign case is the silent footgun — apps keep\n // exporting OTLP to the requested port and reach that process, not us, so\n // this UI stays empty while they see export errors. Say so, with the fix.\n if (boundPort !== options.port) {\n const holder = await probePortHolder(options.host, options.port)\n if (holder === 'autotel-devtools') {\n warnings.push(\n `another autotel-devtools is already running on port ${options.port}; ` +\n `this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`,\n )\n } else {\n warnings.push(\n `port ${options.port} is held by another process that is NOT autotel-devtools. ` +\n `Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. ` +\n `Point your exporter at :${boundPort}, or free :${options.port} and restart.`,\n )\n }\n }\n\n const uiBase = `http://${options.host === 'localhost' ? '127.0.0.1' : options.host}:${boundPort}`\n const title = options.title || 'autotel-devtools'\n process.stdout.write(`\\n ${title}\\n\\n`)\n process.stdout.write(` Listening: ${addresses.join(' + ')}\\n`)\n process.stdout.write(` UI: ${uiBase} (open in a browser)\\n`)\n process.stdout.write(` OTLP: ${uiBase}/v1/traces\\n`)\n process.stdout.write(` WebSocket: ${uiBase.replace('http', 'ws')}/ws\\n\\n`)\n // The widget bundle auto-mounts on load, so the bare <script> tag is all the\n // user needs — spell that out, plus the two opt-in variations, so nobody\n // wonders whether they also have to add an element by hand.\n process.stdout.write(\n ` Embed in your app — paste into your HTML; a floating panel appears automatically:\\n`,\n )\n process.stdout.write(` <script src=\"${uiBase}/widget.js\"></script>\\n\\n`)\n process.stdout.write(\n ` full screen instead: <script src=\"${uiBase}/widget.js?mode=fullpage\"></script>\\n`,\n )\n process.stdout.write(\n ` choose where it goes: add <autotel-devtools></autotel-devtools> to your markup\\n\\n`,\n )\n process.stdout.write(` Or point any OTLP exporter at this receiver:\\n`)\n process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json\\n`)\n process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}\\n\\n`)\n // Self-check: confirm the collector is reachable AND that ingestion works,\n // not just that something is listening. Reading /v1/traces back is the same\n // check a test should make instead of trusting \"the client tried to send\".\n process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces\\n\\n`)\n for (const w of warnings) {\n process.stdout.write(` ⚠ ${w}\\n`)\n }\n if (warnings.length > 0) process.stdout.write('\\n')\n\n const shutdown = () => {\n Promise.all([wsServer.close(), listeners.closeSibling()]).then(() =>\n process.exit(0),\n )\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n\nmain().catch((error) => {\n process.stderr.write(`[autotel-devtools] failed to start: ${error instanceof Error ? error.message : String(error)}\\n`)\n process.exit(1)\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n// src/cli.ts\nimport { createServer } from 'node:http'\nimport { spawn } from 'node:child_process'\nimport { readFileSync } from 'node:fs'\nimport { resolve, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { DevtoolsServer } from './server/server'\nimport { attachDevtoolsRoutes } from './server/http'\nimport { hostHeaderIsLoopback } from './server/origin-guard'\nimport { listenLoopbackDualStack } from './server/listen'\nimport { probePortHolder } from './server/identity'\n\ninterface CliOptions {\n port: number\n host: string\n title?: string\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n `autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n autotel-devtools claude [claude args] [--print-env] [--log-prompts]\n\nSubcommands:\n claude Start the receiver AND launch Claude Code wired to it\n (HTTP/protobuf → this receiver, 1s intervals, session id on).\n Open the \"Agents\" tab to watch sessions, tokens, cost,\n tool / MCP / sub-agent / skill usage live.\n --print-env Print the telemetry env block and exit (don't launch)\n --log-prompts Capture prompt text (default: length only / private)\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n npx autotel-devtools claude # watch Claude Code in the Agents tab\n npx autotel-devtools claude --print-env # just print the env (for MDM / VS Code)\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"></script>\n` + '\\n',\n )\n}\n\nfunction printVersion(): void {\n try {\n const dir = dirname(fileURLToPath(import.meta.url))\n const pkgPath = resolve(dir, '..', 'package.json')\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))\n process.stdout.write(`${pkg.version}\\n`)\n } catch {\n process.stdout.write('unknown\\n')\n }\n}\n\nfunction parseArgs(argv: string[]): CliOptions | null {\n const options: CliOptions = {\n port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || '4318'),\n host: process.env.AUTOTEL_DEVTOOLS_HOST || '127.0.0.1',\n title: process.env.AUTOTEL_DEVTOOLS_TITLE,\n }\n\n // An explicit `--port`/`-p` always wins, regardless of where it sits in\n // argv. A bare numeric positional is shorthand for `--port`, but only when\n // no explicit flag was given — and only the first positional counts, so a\n // stray \"4318\" can't override an earlier positional either.\n let portWasExplicit = false\n let positionalPortConsumed = false\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n const next = argv[i + 1]\n if (arg === '--help' || arg === '-h') { printHelp(); return null }\n if (arg === '--version' || arg === '-v') { printVersion(); return null }\n if ((arg === '--port' || arg === '-p') && next) { options.port = parsePort(next); portWasExplicit = true; i++; continue }\n if ((arg === '--host' || arg === '-H') && next) { options.host = next; i++; continue }\n if ((arg === '--title' || arg === '-t') && next) { options.title = next; i++; continue }\n if (/^\\d+$/.test(arg) && !positionalPortConsumed) {\n if (!portWasExplicit) options.port = parsePort(arg)\n positionalPortConsumed = true\n continue\n }\n }\n\n return options\n}\n\nfunction parsePort(value: string): number {\n const n = Number(value)\n if (!Number.isInteger(n) || n < 0 || n > 65535) {\n process.stderr.write(`[autotel-devtools] invalid port: ${value}\\n`)\n process.exit(2)\n }\n return n\n}\n\ninterface RunningReceiver {\n wsServer: DevtoolsServer\n closeAll: () => Promise<void>\n addresses: string[]\n warnings: string[]\n boundPort: number\n uiBase: string\n}\n\nasync function startReceiver(options: CliOptions): Promise<RunningReceiver> {\n const httpServer = createServer()\n const loopbackOnly = hostHeaderIsLoopback(options.host)\n const wsServer = new DevtoolsServer({ server: httpServer, host: options.host, verbose: true })\n attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly })\n\n const listeners = listenLoopbackDualStack({\n primary: httpServer,\n port: options.port,\n host: options.host,\n attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer, { loopbackOnly }),\n })\n\n const { addresses, warnings, port: boundPort } = await listeners.ready\n\n // Falling forward to a different port means the one we wanted is held by\n // someone else. Classify them: another autotel-devtools (benign) versus a\n // foreign process. The foreign case is the silent footgun — apps keep\n // exporting OTLP to the requested port and reach that process, not us, so\n // this UI stays empty while they see export errors. Say so, with the fix.\n if (boundPort !== options.port) {\n const holder = await probePortHolder(options.host, options.port)\n if (holder === 'autotel-devtools') {\n warnings.push(\n `another autotel-devtools is already running on port ${options.port}; ` +\n `this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`,\n )\n } else {\n warnings.push(\n `port ${options.port} is held by another process that is NOT autotel-devtools. ` +\n `Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. ` +\n `Point your exporter at :${boundPort}, or free :${options.port} and restart.`,\n )\n }\n }\n\n const uiBase = `http://${options.host === 'localhost' ? '127.0.0.1' : options.host}:${boundPort}`\n return {\n wsServer,\n closeAll: () => Promise.all([wsServer.close(), listeners.closeSibling()]).then(() => undefined),\n addresses,\n warnings,\n boundPort,\n uiBase,\n }\n}\n\n// Telemetry env that wires Claude Code (and any OTel-via-env CLI) to this\n// receiver for a *live* local view: HTTP/protobuf (NOT gRPC — this receiver\n// speaks HTTP only), 1s export intervals so events show up promptly, and\n// session.id kept on metrics so metric-only signals join their session.\nfunction buildAgentEnv(uiBase: string, logPrompts: boolean): Record<string, string> {\n const env: Record<string, string> = {\n CLAUDE_CODE_ENABLE_TELEMETRY: '1',\n OTEL_METRICS_EXPORTER: 'otlp',\n OTEL_LOGS_EXPORTER: 'otlp',\n OTEL_EXPORTER_OTLP_PROTOCOL: 'http/protobuf',\n OTEL_EXPORTER_OTLP_ENDPOINT: uiBase,\n OTEL_METRIC_EXPORT_INTERVAL: '1000',\n OTEL_LOGS_EXPORT_INTERVAL: '1000',\n OTEL_METRICS_INCLUDE_SESSION_ID: 'true',\n }\n // Private by default: prompt *text* only flows when explicitly opted in.\n if (logPrompts) env.OTEL_LOG_USER_PROMPTS = '1'\n return env\n}\n\nfunction printEnvBlock(env: Record<string, string>): void {\n for (const [key, value] of Object.entries(env)) {\n process.stdout.write(`export ${key}=${value}\\n`)\n }\n}\n\ninterface ClaudeOptions {\n port: number\n host: string\n printEnv: boolean\n logPrompts: boolean\n claudeArgs: string[]\n}\n\nfunction parseClaudeArgs(argv: string[]): ClaudeOptions {\n const opts: ClaudeOptions = {\n port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || '4318'),\n host: process.env.AUTOTEL_DEVTOOLS_HOST || '127.0.0.1',\n printEnv: false,\n logPrompts: false,\n claudeArgs: [],\n }\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n const next = argv[i + 1]\n if (arg === '--') { opts.claudeArgs.push(...argv.slice(i + 1)); break }\n if (arg === '--print-env') { opts.printEnv = true; continue }\n if (arg === '--log-prompts') { opts.logPrompts = true; continue }\n if ((arg === '--port' || arg === '-p') && next) { opts.port = parsePort(next); i++; continue }\n if ((arg === '--host' || arg === '-H') && next) { opts.host = next; i++; continue }\n // Anything else is passed straight through to `claude`.\n opts.claudeArgs.push(arg)\n }\n return opts\n}\n\nasync function runClaudeSubcommand(argv: string[]): Promise<void> {\n const opts = parseClaudeArgs(argv)\n\n // --print-env needs no running server: emit the block for the configured\n // port so users can paste it into a shell / managed-settings file.\n if (opts.printEnv) {\n const uiBase = `http://${opts.host === 'localhost' ? '127.0.0.1' : opts.host}:${opts.port}`\n printEnvBlock(buildAgentEnv(uiBase, opts.logPrompts))\n return\n }\n\n const receiver = await startReceiver({ port: opts.port, host: opts.host })\n const env = buildAgentEnv(receiver.uiBase, opts.logPrompts)\n\n process.stdout.write(`\\n autotel-devtools — Claude Code\\n\\n`)\n process.stdout.write(` Receiver: ${receiver.uiBase}\\n`)\n process.stdout.write(` UI: ${receiver.uiBase} → open the \"Agents\" tab\\n`)\n if (!opts.logPrompts) {\n process.stdout.write(` Prompts: private (length only) — add --log-prompts to capture text\\n`)\n }\n for (const w of receiver.warnings) process.stdout.write(` ⚠ ${w}\\n`)\n process.stdout.write(`\\n Launching claude…\\n\\n`)\n\n const child = spawn('claude', opts.claudeArgs, {\n stdio: 'inherit',\n env: { ...process.env, ...env },\n })\n\n const shutdown = () => { receiver.closeAll().then(() => process.exit(0)) }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n\n child.on('error', (err) => {\n const reason = (err as NodeJS.ErrnoException).code === 'ENOENT'\n ? `'claude' not found on PATH. Install Claude Code, or run the receiver alone with 'npx autotel-devtools' and point claude at ${receiver.uiBase} using --print-env.`\n : err.message\n process.stderr.write(`[autotel-devtools] ${reason}\\n`)\n receiver.closeAll().then(() => process.exit(1))\n })\n child.on('exit', (code) => {\n receiver.closeAll().then(() => process.exit(code ?? 0))\n })\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2)\n\n // Subcommand: `autotel-devtools claude [claude args] [--print-env] [--log-prompts]`\n if (argv[0] === 'claude') {\n await runClaudeSubcommand(argv.slice(1))\n return\n }\n\n const options = parseArgs(argv)\n if (!options) { process.exit(0) }\n\n const receiver = await startReceiver(options)\n const { addresses, warnings, uiBase } = receiver\n const title = options.title || 'autotel-devtools'\n process.stdout.write(`\\n ${title}\\n\\n`)\n process.stdout.write(` Listening: ${addresses.join(' + ')}\\n`)\n process.stdout.write(` UI: ${uiBase} (open in a browser)\\n`)\n process.stdout.write(` OTLP: ${uiBase}/v1/traces\\n`)\n process.stdout.write(` WebSocket: ${uiBase.replace('http', 'ws')}/ws\\n\\n`)\n // The widget bundle auto-mounts on load, so the bare <script> tag is all the\n // user needs — spell that out, plus the two opt-in variations, so nobody\n // wonders whether they also have to add an element by hand.\n process.stdout.write(\n ` Embed in your app — paste into your HTML; a floating panel appears automatically:\\n`,\n )\n process.stdout.write(` <script src=\"${uiBase}/widget.js\"></script>\\n\\n`)\n process.stdout.write(\n ` full screen instead: <script src=\"${uiBase}/widget.js?mode=fullpage\"></script>\\n`,\n )\n process.stdout.write(\n ` choose where it goes: add <autotel-devtools></autotel-devtools> to your markup\\n\\n`,\n )\n process.stdout.write(` Or point any OTLP exporter at this receiver:\\n`)\n process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json\\n`)\n process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}\\n\\n`)\n // Self-check: confirm the collector is reachable AND that ingestion works,\n // not just that something is listening. Reading /v1/traces back is the same\n // check a test should make instead of trusting \"the client tried to send\".\n process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces\\n\\n`)\n for (const w of warnings) {\n process.stdout.write(` ⚠ ${w}\\n`)\n }\n if (warnings.length > 0) process.stdout.write('\\n')\n\n const shutdown = () => {\n receiver.closeAll().then(() => process.exit(0))\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n\nmain().catch((error) => {\n process.stderr.write(`[autotel-devtools] failed to start: ${error instanceof Error ? error.message : String(error)}\\n`)\n process.exit(1)\n})\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as ErrorGroup, o as ErrorOccurrence, u as TraceData } from "./exporter-1Y3GmLVS.cjs";
1
+ import { a as ErrorGroup, o as ErrorOccurrence, u as TraceData } from "./exporter-kXTGDFoH.cjs";
2
2
 
3
3
  //#region src/server/error-aggregator.d.ts
4
4
  interface ErrorAggregatorOptions {
@@ -117,4 +117,4 @@ declare class ErrorAggregator {
117
117
  }
118
118
  //#endregion
119
119
  export { ErrorAggregator as t };
120
- //# sourceMappingURL=error-aggregator-nnfbpSR7.d.cts.map
120
+ //# sourceMappingURL=error-aggregator-7Q2H7htn.d.cts.map
@@ -1,4 +1,4 @@
1
- import { a as ErrorGroup, o as ErrorOccurrence, u as TraceData } from "./exporter-CZ5HdD3o.js";
1
+ import { a as ErrorGroup, o as ErrorOccurrence, u as TraceData } from "./exporter-yqXiHw1Q.js";
2
2
 
3
3
  //#region src/server/error-aggregator.d.ts
4
4
  interface ErrorAggregatorOptions {
@@ -117,4 +117,4 @@ declare class ErrorAggregator {
117
117
  }
118
118
  //#endregion
119
119
  export { ErrorAggregator as t };
120
- //# sourceMappingURL=error-aggregator-BvNmgn7E.d.ts.map
120
+ //# sourceMappingURL=error-aggregator-C8X-RQRi.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { Server } from "node:http";
2
- import { ExportResult } from "@opentelemetry/core";
2
+ import { AgentRawEvent, AgentSession, OtelMetricRecord } from "autotel-agents";
3
3
  import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
4
+ import { ExportResult } from "@opentelemetry/core";
4
5
 
5
6
  //#region src/server/types.d.ts
6
7
  interface SpanData {
@@ -94,6 +95,9 @@ interface DevtoolsData {
94
95
  metrics: MetricData[];
95
96
  logs: LogData[];
96
97
  errors: ErrorGroup[];
98
+ /** Full-state on every broadcast (client replaces, like `errors`). Coding-agent
99
+ * sessions reconstructed from agent metrics + log events. */
100
+ agents?: AgentSession[];
97
101
  }
98
102
  //#endregion
99
103
  //#region src/server/server.d.ts
@@ -126,6 +130,7 @@ declare class DevtoolsServer {
126
130
  private traces;
127
131
  private logs;
128
132
  private metrics;
133
+ private agentSessions;
129
134
  private errorAggregator;
130
135
  private limits;
131
136
  private verbose;
@@ -139,6 +144,11 @@ declare class DevtoolsServer {
139
144
  addLog(log: LogData): void;
140
145
  addLogs(logs: LogData[]): void;
141
146
  addMetric(metric: MetricData): void;
147
+ /** Fold decoded agent log events into sessions and broadcast the full set. */
148
+ ingestAgentEvents(records: AgentRawEvent[]): void;
149
+ /** Fold decoded agent metric records into sessions and broadcast the full set. */
150
+ ingestAgentMetrics(records: OtelMetricRecord[]): void;
151
+ private broadcastAgents;
142
152
  getCurrentData(): DevtoolsData;
143
153
  clearData(): void;
144
154
  private broadcast;
@@ -179,4 +189,4 @@ declare class DevtoolsSpanExporter implements SpanExporter {
179
189
  }
180
190
  //#endregion
181
191
  export { ErrorGroup as a, MetricData as c, DevtoolsData as i, SpanData as l, DevtoolsServer as n, ErrorOccurrence as o, DevtoolsServerOptions as r, LogData as s, DevtoolsSpanExporter as t, TraceData as u };
182
- //# sourceMappingURL=exporter-CZ5HdD3o.d.ts.map
192
+ //# sourceMappingURL=exporter-kXTGDFoH.d.cts.map