teley-cli
v0.5.0
Published
Terminal trace and log viewer for Teley. Point your OpenTelemetry or Sentry SDK at a room DSN and watch traces stream in as a live waterfall.
Maintainers
Readme
teley-cli generates a DSN and renders traces, logs, and metrics as they stream in. No account, no config file.
Built with OpenTUI (React bindings), so the waterfall needs FFI: Bun, or Node 26.4+ with --experimental-ffi.
⚡ Run
bunx teley-cli # live room, waterfall in your terminal
bunx teley-cli mcp # serve the room to a coding agent over MCP
bunx teley-cli --demo # sample data, no network
bunx teley-cli --new # start a fresh room (new DSN)
bunx teley-cli --json # no TUI, newline-delimited JSON on stdout
bunx teley-cli --local # receive telemetry here, nothing leaves the machineThe DSN and OTLP endpoint are printed in the header. Point your SDK at either one, run your app, and spans appear.
The TUI needs FFI, so either Bun, or Node 26.4+ with
node --experimental-ffiare needed. Everything else (--json,mcp,--local) runs on Node/Bun alike.
📡 Send your data
Both protocols land in the same room. Swap in the session ID from the header.
| Purpose | Endpoint |
| ----------- | ------------------------------- |
| Sentry DSN | https://<room-id>@teley.dev/0 |
| OTLP ingest | https://teley.dev/r/<room-id> |
Sentry.init({ dsn: 'https://<room-id>@teley.dev/0', tracesSampleRate: 1.0 });
// or
new OTLPTraceExporter({ url: 'https://teley.dev/r/<room-id>' });Traces, logs, and metrics all go to that one OTLP endpoint, in JSON or protobuf, gzipped or not. See the main README for full SDK snippets.
✨ What you get
- Live waterfall. Time-proportional span bars with the real hierarchy, span-kind badges, and errors in red.
- Span details. Kind, duration, status, span id, and every attribute for the selected span.
- Logs view. The full log stream with severity colors, correlated with the traces in the same room.
- Metric charts. Drawn in the terminal itself, out of braille: counters and gauges as lines, histogram buckets as bars. A metric split by attributes is charted as one series per attribute set, not folded into an average.
- Both protocols. OTLP and Sentry envelopes render in one timeline, each tagged with its source.
- Sessions that persist. Your room is reused across runs, so a restart does not invalidate the DSN you configured.
- Pipeable.
--jsondrops the TUI and streams the room as newline-delimited JSON, forjq, a file, or CI. - Readable by agents.
teley mcphands the same room to a coding agent as MCP tools, so it can run your app and read the span tree back. - Offline if you want.
--localmakes the CLI the ingest endpoint, so telemetry never leaves your machine.
⌨️ Keys
| Key | Action |
| ------------------------ | --------------------------------------------- |
| ← / → | Cycle the Traces, Logs, and Metrics views |
| ↑ / ↓ (or j / k) | Navigate the focused panel |
| tab | Cycle focus: list → detail → connection links |
| ↵ / y | Copy the focused DSN or OTLP endpoint |
| c | Clear the local view |
| q | Quit |
With the trace list focused, ↑/↓ moves between traces. With the waterfall focused, it moves between spans and the attribute panel follows along. In the metrics view ↑/↓ always moves between series, since a chart has nothing to walk inside one, and tab swaps the series list for a stats and attributes panel.
🧾 JSON output
--json skips the TUI entirely and streams the room to stdout as newline-delimited JSON, one object per line. Same room, no terminal rendering, so it pipes and greps.
bunx teley-cli --json | jq 'select(.type == "trace" and .trace.status_code == 2)'
bunx teley-cli --json > run.ndjson # keep a run for later
bunx teley-cli --json --demo # sample data, to see the shapeThe first line is the session, so a script can read the DSN it should point an SDK at. Every line after it is telemetry:
{ "type": "session", "version": "0.1.7", "room_id": "…", "host": "teley.dev", "dsn": "…", "otlp": "…", "time": "…" }
{ "type": "trace", "trace": { … }, "spans": [ … ], "time": "…" }
{ "type": "log", "log": { … }, "time": "…" }
{ "type": "metric", "metric": { … }, "time": "…" }trace, log, and metric carry the same shapes the web app uses (shared/parsers/types.ts). A metric line is one data point, so a series arrives as many lines; group them by name and attributes to reassemble it. A trace appears on a new line every time more of it arrives: trace is the running summary over every span seen so far, while spans holds only the spans from that update, so lines never repeat a span.
Nothing else is written to stdout. Connection state stays out of the stream, and a fatal error (a room already claimed by another token) goes to stderr with exit code 1. ctrl-c exits 0.
🏕️ Local mode
--local makes the cli process the ingest endpoint. Your app posts OTLP or Sentry envelopes straight to it on localhost, and nothing leaves the machine.
bunx teley-cli --local # ingest on 127.0.0.1:8788
bunx teley-cli --local --port 4318 # the conventional OTLP/HTTP port
bunx teley-cli --local --port 0 # let the OS pick, printed in the header
bunx teley-cli mcp --local # same, serving an agentIt accepts the same formats as teley.dev: JSON or protobuf, gzipped or not, and Sentry envelopes. The CLI still reports its own crashes; see below.
🤖 MCP for Coding agents
teley mcp serves the room over MCP on stdio, so an agent can instrument your app, run it, and read the traces back without a terminal.
claude mcp add teley -- bunx teley-cli mcp// or by hand, in .mcp.json
{
"mcpServers": {
"teley": { "command": "bunx", "args": ["teley-cli", "mcp"] },
},
}| Tool | What it does |
| ----------------- | -------------------------------------------------------------------------------- |
| get_dsn | The room's DSN and OTLP endpoint, to point an SDK at |
| wait_for_traces | Blocks until the room goes quiet, returns what arrived (idle_ms, timeout_ms) |
| list_traces | Captured traces, newest first (limit, errors_only, service) |
| get_trace | One trace as an indented span tree (include_attributes for the metadata) |
| list_logs | Captured logs (min_severity, trace_id) |
| list_metrics | Captured metric series with their current reading and range (name, service) |
| clear_captured | Drops what the server is holding, so the next run starts clean |
The loop is: get_dsn → point the SDK at it → run the app → wait_for_traces → get_trace on whatever looks wrong. Results come back as text sized for a model to read, not raw payloads:
+0ms POST /checkout 213.0ms server ERROR
+10.0ms GET inventory-service 59.0ms client
+80.0ms db.query orders 120.0ms client ERRORCall wait_for_traces before or while the app runs, not after it exits: it reports what arrives from the moment it is called.
It watches the same room as the TUI and the web app, so you can follow along while the agent works.
It starts empty every time your MCP client starts it.
📮 What the CLI reports about itself
teley-cli reports its own faults, the way any installed program does. That is the CLI's health, and it is a different thing from the telemetry you point at it.
Never sent, in any mode: your spans, logs, metrics, attribute values or payload bodies.
Sent: crashes and stack traces from the CLI's own code, and counters about its operation, which mode it ran in, how many payloads it accepted or rejected and why, connection close codes, and how long each MCP tool took and how it ended. A crash also carries the runtime it ran under, the OS and architecture, and the CLI's resolved dependency versions.
🛠️ Local development
From this directory:
bun install
bun run dev # bun run src/index.tsx
bun run dev --demo
bun run typecheck
bun run test # bun test; chart.ts and metrics.ts are pure, so they
# are covered directly
bun run build # bundle to dist/ (what gets published)The published package is a single Bun-bundled dist/index.js with deps kept external. The cross-package imports from shared/ are inlined at build time.
With the CLI running and a worker up, inject a sample trace:
bun run scripts/send-test-trace.ts --host localhost:8787