diff --git a/.gitignore b/.gitignore index b1bae07..608681a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,8 +39,8 @@ sdk-ts/dist/ # Secrets and env files .env .env.* -ocw.toml -.ocw/ +tj.toml +.tj/ # Claude Code .claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e5f06..bee4b54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [0.1.7] - 2026-04-13 ### Added -- **MCP server (`tj mcp`)** — stdio-based Model Context Protocol server giving Claude Code direct access to OCW observability data. 13 tool handlers: status, traces, alerts, budget headroom, cost summary, drift report, tool stats, trace detail, acknowledge alerts, setup project, list sessions, open dashboard. Dual-mode operation: routes queries through REST API when `tj serve` is running, falls back to read-only DuckDB otherwise. Auto-starts `tj serve` on demand. +- **MCP server (`tj mcp`)** — stdio-based Model Context Protocol server giving Claude Code direct access to TokenJam observability data. 13 tool handlers: status, traces, alerts, budget headroom, cost summary, drift report, tool stats, trace detail, acknowledge alerts, setup project, list sessions, open dashboard. Dual-mode operation: routes queries through REST API when `tj serve` is running, falls back to read-only DuckDB otherwise. Auto-starts `tj serve` on demand. - **Claude Code integration (`tj onboard --claude-code`)** — one-command setup for Claude Code telemetry. Configures OTLP log exporter in `~/.claude/settings.json`, sets project-level `OTEL_RESOURCE_ATTRIBUTES`, adds Docker-compatible endpoint to shell env, and optionally installs background daemon. Re-runs resync the auth header to fix 401s without manual setup. - **Logs ingestion (`POST /v1/logs`)** — new OTLP log endpoint that converts Claude Code log events (`api_request`, `tool_result`, `api_error`, `user_prompt`, `tool_decision`) into NormalizedSpans with deterministic trace/span IDs. Spans flow through the standard ingest pipeline for cost, alerts, and drift. - **`tj drift` CLI** — behavioral drift report with Rich table output showing baseline vs latest session Z-scores per dimension (input tokens, output tokens, duration, tool call count, tool sequence similarity). Color-coded thresholds, `--json` support, exit code 1 if drift detected. @@ -106,7 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Added - `tj stop` command — graceful shutdown of daemon or background process -- `tj uninstall` command — clean removal of all OCW data, config, and daemon +- `tj uninstall` command — clean removal of all TokenJam data, config, and daemon - 16 runnable example agents across 4 tiers: single provider, single framework, multi-agent, and alerts/drift demos - API fallback backend (`ApiBackend`) so CLI works while `tj serve` holds the DB lock diff --git a/CLAUDE.md b/CLAUDE.md index 2195468..52b9703 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -127,7 +127,7 @@ Post-ingest hooks run synchronously after each span is written to DB: | `tj drift` | `cmd_drift.py` | Show drift baselines and Z-scores for recent sessions | | `tj demo [scenario]` | `cmd_demo.py` | Run Agent Incident Library scenarios (zero-config, no API keys). `tj demo` lists all; `tj demo retry-loop` runs one | | `tj mcp` | `cmd_mcp.py` | Start the stdio MCP server for Claude Code integration | -| `tj uninstall` | `cmd_uninstall.py` | Remove all OCW data, config, and daemon | +| `tj uninstall` | `cmd_uninstall.py` | Remove all TokenJam data, config, and daemon | | `tj doctor` | `cmd_doctor.py` | Health checks (config, DB, secrets, webhooks, drift readiness, schema-vs-capture consistency). Exit 0 = ok, 1 = warnings, 2 = errors | All commands support `--json` for machine-readable output. Commands that query alerts use exit code 1 if active (unacknowledged, unsuppressed) alerts exist. diff --git a/Makefile b/Makefile index 6a2a34a..4617948 100644 --- a/Makefile +++ b/Makefile @@ -10,9 +10,9 @@ test-e2e: pytest tests/e2e/ lint: - ruff check ocw/ + ruff check tokenjam/ typecheck: - mypy ocw/ + mypy tokenjam/ all: lint typecheck test diff --git a/README.md b/README.md index d06067c..c510532 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ For any Python agent — Anthropic, OpenAI, Gemini, Bedrock, LangChain, CrewAI, ```bash pip install tokenjam tj onboard # creates config, generates ingest secret -ocw doctor # verify your setup +tj doctor # verify your setup ``` ```python @@ -155,12 +155,12 @@ https://github.com/user-attachments/assets/b94d13f6-1432-40d4-b093-6958d74f0e65 ```bash tj status # current state, cost, active alerts -ocw traces # full span history with waterfall view -ocw cost --since 7d # cost breakdown by agent, model, day -ocw alerts # everything that fired while you were away -ocw budget # view and set daily/session cost limits -ocw drift # behavioral drift Z-scores vs baseline -ocw tools # tool call history with error rates +tj traces # full span history with waterfall view +tj cost --since 7d # cost breakdown by agent, model, day +tj alerts # everything that fired while you were away +tj budget # view and set daily/session cost limits +tj drift # behavioral drift Z-scores vs baseline +tj tools # tool call history with error rates tj serve # start the web UI + REST API ``` @@ -183,7 +183,7 @@ No signup, no cloud — runs entirely on your machine. --- -## ocw vs LangSmith vs Langfuse +## tj vs LangSmith vs Langfuse LangSmith and Langfuse are excellent for tracing LLM API calls and running evals on chat outputs. `tj` solves a different problem: **autonomous agents running unsupervised with real-world consequences**. @@ -251,14 +251,14 @@ The MCP server gives Claude Code direct access to your observability data inside | `get_tool_stats` | Tool call counts and average duration | | `get_drift_report` | Drift baseline vs latest session | | `acknowledge_alert` | Mark an alert as acknowledged | -| `setup_project` | Configure a project for OCW telemetry | +| `setup_project` | Configure a project for TokenJam telemetry | | `open_dashboard` | Open the web UI (starts `tj serve` if needed) | The MCP server opens DuckDB read-only — no lock conflicts with `tj serve`. **Per-project tagging** — after installing globally, ask Claude Code: -> "Set up OCW for this project" +> "Set up TokenJam for this project" Claude calls `setup_project`, which writes `.claude/settings.json` with the right `OTEL_RESOURCE_ATTRIBUTES` for this project. @@ -274,8 +274,8 @@ tj onboard --codex `tj onboard --codex` is project-agnostic. It writes to `~/.codex/config.toml` (Codex's single global config), so you only run it once — not once per project. Codex hardcodes `service.name="codex_exec"` in its binary, so all sessions appear under the same agent ID regardless of which repo you're working in. `tj onboard --codex`: -- Writes an `[otel]` block and `[mcp_servers.ocw]` to `~/.codex/config.toml` -- Registers the MCP server so Codex can call OCW tools directly +- Writes an `[otel]` block and `[mcp_servers.tj]` to `~/.codex/config.toml` +- Registers the MCP server so Codex can call TokenJam tools directly - Installs the background daemon (launchd / systemd) **Codex must be restarted** after running `tj onboard --codex`. @@ -289,14 +289,14 @@ The same 13 MCP tools available to Claude Code are available to Codex after rest ### Uninstalling ```bash -# Remove all OCW data, config, daemon, MCP registration, and env vars: -ocw uninstall --yes +# Remove all TokenJam data, config, daemon, MCP registration, and env vars: +tj uninstall --yes # Then remove the package: pip uninstall tokenjam -y ``` -`tj uninstall` cleans up everything set by `tj onboard --claude-code`: daemon, MCP server, `~/.ocw/`, `~/.config/ocw/`, OTLP env vars in `~/.claude/settings.json`, `OTEL_RESOURCE_ATTRIBUTES` in every onboarded project's `.claude/settings.json`, and the harness env block in `~/.zshrc`. +`tj uninstall` cleans up everything set by `tj onboard --claude-code`: daemon, MCP server, `~/.tj/`, `~/.config/tj/`, OTLP env vars in `~/.claude/settings.json`, `OTEL_RESOURCE_ATTRIBUTES` in every onboarded project's `.claude/settings.json`, and the harness env block in `~/.zshrc`. --- @@ -341,7 +341,7 @@ Full framework support guide: [docs/framework-support.md](docs/framework-support Configure where alerts go. Multiple channels work simultaneously. ```toml -# .ocw/config.toml +# .tj/config.toml [[alerts.channels]] type = "ntfy" @@ -382,10 +382,10 @@ Full event table and configuration: [docs/nemoclaw-integration.md](docs/nemoclaw ## Export and integrate ```bash -ocw export --format otlp # forward to Grafana, Datadog, any OTel backend -ocw export --format openevals # openevals / agentevals trajectory evaluation -ocw export --format json # NDJSON -ocw export --format csv +tj export --format otlp # forward to Grafana, Datadog, any OTel backend +tj export --format openevals # openevals / agentevals trajectory evaluation +tj export --format json # NDJSON +tj export --format csv ``` Prometheus metrics at `http://127.0.0.1:7391/metrics` when `tj serve` is running. @@ -422,7 +422,7 @@ flowchart TD Alerts --> DB Schema --> DB - DB --> CLI["ocw CLI"] + DB --> CLI["tj CLI"] DB --> API["REST API + Web UI\n:7391"] DB --> MCP["MCP Server\n13 tools"] DB --> Prom["Prometheus\n:7391/metrics"] @@ -435,7 +435,7 @@ Full architecture deep-dive — design principles, SDK internals, alert system, ## Configuration ```toml -# .ocw/config.toml — generated by tj onboard +# .tj/config.toml — generated by tj onboard [defaults.budget] daily_usd = 10.00 @@ -462,7 +462,7 @@ completions = false tool_outputs = false [storage] -path = "~/.ocw/telemetry.duckdb" +path = "~/.tj/telemetry.duckdb" retention_days = 90 ``` @@ -503,18 +503,18 @@ See [`examples/README.md`](examples/README.md) for the full list. Reproducible AI agent failures you can run in 30 seconds. No API keys, no config, no setup. ```bash -ocw demo # list all scenarios -ocw demo retry-loop # run one -ocw demo retry-loop --json # machine-readable output +tj demo # list all scenarios +tj demo retry-loop # run one +tj demo retry-loop --json # machine-readable output ``` -| Scenario | What goes wrong | What OCW catches | +| Scenario | What goes wrong | What TokenJam catches | |---|---|---| | [`retry-loop`](incidents/retry-loop/README.md) | Agent retries a failing tool in a loop, burning time and tokens | `retry_loop` + `failure_rate` alerts fire automatically | | [`surprise-cost`](incidents/surprise-cost/README.md) | Model silently escalates from Haiku to Opus mid-chain | Per-model cost breakdown shows the $3+ you didn't expect | | [`hallucination-drift`](incidents/hallucination-drift/README.md) | Agent behavior shifts — different tokens, different tools | `drift_detected` alert fires with Z-scores at session end | -Each scenario runs against an in-memory backend and produces a side-by-side comparison: what `print()` shows vs. what OCW reveals. +Each scenario runs against an in-memory backend and produces a side-by-side comparison: what `print()` shows vs. what TokenJam reveals. --- diff --git a/SECURITY.md b/SECURITY.md index c89a7b2..12165ea 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -26,4 +26,4 @@ Security issues in these areas are especially important to report. ## Not in Scope - Vulnerabilities in upstream dependencies (report to the upstream project) -- Issues that require physical access to the machine running `ocw` +- Issues that require physical access to the machine running `tj` diff --git a/docs/alerts.md b/docs/alerts.md index 4e920af..1bc9d56 100644 --- a/docs/alerts.md +++ b/docs/alerts.md @@ -1,6 +1,6 @@ # Alerts -`ocw` fires alerts the moment something happens — sensitive tool calls, budget breaches, behavioral drift, sandbox violations. Alerts are evaluated after every span ingest and dispatched to your configured channels in real time. +`tj` fires alerts the moment something happens — sensitive tool calls, budget breaches, behavioral drift, sandbox violations. Alerts are evaluated after every span ingest and dispatched to your configured channels in real time. ## Alert types @@ -22,7 +22,7 @@ ## Channels -Configure where alerts go in `.ocw/config.toml`. Multiple channels work simultaneously — you can get push notifications on your phone and a Discord message at the same time. +Configure where alerts go in `.tj/config.toml`. Multiple channels work simultaneously — you can get push notifications on your phone and a Discord message at the same time. ```toml # Push notification (free, no account required) @@ -49,7 +49,7 @@ url = "https://your-endpoint.com/alerts" # Local file log [[alerts.channels]] type = "file" -path = "~/.ocw/alerts.log" +path = "~/.tj/alerts.log" # Stdout (always enabled by default) [[alerts.channels]] @@ -77,7 +77,7 @@ Define which tool calls should trigger immediate alerts: ## Cooldown -To prevent alert storms, `ocw` tracks a cooldown per agent + alert type. Repeat alerts within the cooldown window are suppressed — still persisted to the database, but not dispatched to channels. +To prevent alert storms, `tj` tracks a cooldown per agent + alert type. Repeat alerts within the cooldown window are suppressed — still persisted to the database, but not dispatched to channels. ```toml [alerts] diff --git a/docs/architecture.md b/docs/architecture.md index 1d4f5b9..16d9359 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -This document describes the architecture of TokenJam (ocw) — a local-first, OTel-native observability CLI for autonomous AI agents. +This document describes the architecture of TokenJam (`tj`) — a local-first, OTel-native observability CLI for autonomous AI agents. ## Design principles @@ -10,7 +10,7 @@ This document describes the architecture of TokenJam (ocw) — a local-first, OT 3. **Never crash the agent.** The SDK, ingest pipeline, and all post-ingest hooks are designed to fail silently. Hook failures are logged but never propagated. A span rejection never takes down the instrumented agent. -4. **Core is pure domain logic.** `ocw/core/` has no CLI or HTTP imports. CLI and API layers import from core, never the reverse. This makes the domain logic independently testable and reusable. +4. **Core is pure domain logic.** `tokenjam/core/` has no CLI or HTTP imports. CLI and API layers import from core, never the reverse. This makes the domain logic independently testable and reusable. --- @@ -42,7 +42,7 @@ flowchart TD Alerts --> DB Schema --> DB - DB --> CLI["ocw CLI"] + DB --> CLI["tj CLI"] DB --> API["REST API\n:7391/docs"] DB --> WebUI["Web UI\n:7391/"] DB --> MCP["MCP Server\nstdio"] @@ -91,7 +91,7 @@ Sessions are identified by `conversation_id`. When a span arrives with a `conver ## Package structure and dependency rules ``` -ocw/ +tokenjam/ ├── core/ Pure domain logic — NO imports from cli/ or api/ ├── cli/ Click commands — imports from core/ ├── api/ FastAPI routes — imports from core/ @@ -141,7 +141,7 @@ DuckDB allows only one write connection. When `tj serve` is running, CLI command ### Bootstrap -`ensure_initialised()` in `ocw/sdk/bootstrap.py` is the lazy, thread-safe, idempotent entry point. It bootstraps: config loading → DB connection → IngestPipeline creation → TracerProvider setup. Called automatically by `@watch()` and all `patch_*()` functions. Registers an `atexit` handler to flush pending spans. +`ensure_initialised()` in `tokenjam/sdk/bootstrap.py` is the lazy, thread-safe, idempotent entry point. It bootstraps: config loading → DB connection → IngestPipeline creation → TracerProvider setup. Called automatically by `@watch()` and all `patch_*()` functions. Registers an `atexit` handler to flush pending spans. ### @watch() creates sessions, not LLM spans @@ -256,7 +256,7 @@ A stub endpoint at `/v1/metrics` returns 200 OK to prevent noisy warnings from O ## Web UI -A single-file SPA (`ocw/ui/index.html`) served by `tj serve` at `http://127.0.0.1:7391/`. No build step, no node_modules — vanilla JS with Alpine.js from CDN. +A single-file SPA (`tokenjam/ui/index.html`) served by `tj serve` at `http://127.0.0.1:7391/`. No build step, no node_modules — vanilla JS with Alpine.js from CDN. Six views: Status, Traces (with span waterfall visualization), Cost, Alerts, Budget, Drift. Hash-based routing (`/#/status`, `/#/traces`, etc.). The UI consumes the same REST API endpoints as external clients. All views auto-poll (Status at 5s, all others at 10s). @@ -287,7 +287,7 @@ A fifth layer (`tests/e2e/`) makes real LLM API calls and is auto-skipped withou - **OTel TracerProvider:** Global and set-once per process. In SDK tests, set the provider once at module level (not per-test) and clear spans between tests using a custom `_CollectingExporter`. -- **CLI tests:** Use `click.testing.CliRunner` with `unittest.mock.patch` on `ocw.cli.main.load_config` and `ocw.cli.main.open_db` to inject test fixtures. +- **CLI tests:** Use `click.testing.CliRunner` with `unittest.mock.patch` on `tokenjam.cli.main.load_config` and `tokenjam.cli.main.open_db` to inject test fixtures. - **API tests:** Use `httpx.AsyncClient` with `httpx.ASGITransport(app=app)` against `InMemoryBackend`. @@ -297,7 +297,7 @@ A fifth layer (`tests/e2e/`) makes real LLM API calls and is auto-skipped withou Config is TOML, discovered in order: `tj.toml` → `.tj/config.toml` → `~/.config/tj/config.toml`. Override with `--config` flag or `TJ_CONFIG` env var. -The `TjConfig` dataclass tree in `ocw/core/config.py` defines the full hierarchy: agents (with budgets, sensitive actions, drift config, output schema), storage, export (OTLP, Prometheus), alerts (cooldown, channels), security, API, and capture settings. +The `TjConfig` dataclass tree in `tokenjam/core/config.py` defines the full hierarchy: agents (with budgets, sensitive actions, drift config, output schema), storage, export (OTLP, Prometheus), alerts (cooldown, channels), security, API, and capture settings. `tomllib.load()` requires binary mode (`"rb"`) — text mode raises `TypeError` at runtime. The codebase uses conditional imports: `tomllib` (Python 3.11+) or `tomli` (3.10). Writing config uses `tomli_w`. @@ -305,7 +305,7 @@ The `TjConfig` dataclass tree in `ocw/core/config.py` defines the full hierarchy ## MCP server (Claude Code integration) -`tj mcp` is a stdio-based MCP (Model Context Protocol) server that gives Claude Code direct access to OCW observability data. It exposes 13 tools that Claude Code can call during a session. +`tj mcp` is a stdio-based MCP (Model Context Protocol) server that gives Claude Code direct access to TokenJam observability data. It exposes 13 tools that Claude Code can call during a session. ### Dual-mode operation @@ -348,7 +348,7 @@ If `tj serve` is not running, `cmd_mcp.py` can auto-start it as a detached subpr The `--claude-code` flag configures the full telemetry pipeline in one command: 1. **Derives agent ID** from the git remote origin name (fallback: folder name), prefixed with `claude-code-` -2. **Writes OCW config** to `~/.config/tj/config.toml` (global, shared across all projects) with agent entry and optional daily budget +2. **Writes TokenJam config** to `~/.config/tj/config.toml` (global, shared across all projects) with agent entry and optional daily budget 3. **Updates global Claude settings** (`~/.claude/settings.json`) with OTLP exporter env vars: `CLAUDE_CODE_ENABLE_TELEMETRY=1`, `OTEL_LOGS_EXPORTER=otlp`, endpoint, protocol. On re-runs, always resyncs the `OTEL_EXPORTER_OTLP_HEADERS` auth header to fix 401s without manual setup. 4. **Writes project settings** (`./.claude/settings.json`) with `OTEL_RESOURCE_ATTRIBUTES=service.name={agent_id}` so spans are tagged to the right agent 5. **Updates shell env** (`~/.zshrc`) with Docker-compatible endpoint (`host.docker.internal:{port}`) for harness sessions that can't reach `127.0.0.1` @@ -373,7 +373,7 @@ The converted spans flow through the standard `IngestPipeline.process()` path ### Semantic conventions (`ClaudeCodeEvents`) -`ocw/otel/semconv.py` defines `ClaudeCodeEvents` constants for Claude Code's log event attributes: session context (`session.id`, `prompt.id`, `event.sequence`), API request metrics (`cost_usd`, `duration_ms`, `speed`, token counts), tool result fields (`success`, `error`, `tool_parameters`), and error context (`status_code`, `attempt`). +`tokenjam/otel/semconv.py` defines `ClaudeCodeEvents` constants for Claude Code's log event attributes: session context (`session.id`, `prompt.id`, `event.sequence`), API request metrics (`cost_usd`, `duration_ms`, `speed`, token counts), tool result fields (`success`, `error`, `tool_parameters`), and error context (`status_code`, `attempt`). --- @@ -412,8 +412,8 @@ Z-scores are color-coded: green (|z| < 1.0), yellow (approaching threshold), red ## Packaging -- **PyPI package name:** `tokenjam` (not `ocw`) -- **CLI command:** `ocw` -- **Python package directory:** `ocw/` -- **Build system:** hatchling, with `[tool.hatch.build.targets.wheel] packages = ["ocw"]` because the package name differs from the directory name +- **PyPI package name:** `tokenjam` +- **CLI command:** `tj` +- **Python package directory:** `tokenjam/` +- **Build system:** hatchling, with `[tool.hatch.build.targets.wheel] packages = ["tokenjam"]` - **npm package:** `@tokenjam/sdk` under `sdk-ts/` diff --git a/docs/claude-code-integration.md b/docs/claude-code-integration.md index 6c87660..95d8fc9 100644 --- a/docs/claude-code-integration.md +++ b/docs/claude-code-integration.md @@ -10,7 +10,7 @@ tj status --agent claude-code- ``` `tj onboard --claude-code` does everything in one shot: -- Creates a shared config at `~/.config/ocw/config.toml` (one config for all your projects) +- Creates a shared config at `~/.config/tj/config.toml` (one config for all your projects) - Writes OTLP exporter vars to `~/.claude/settings.json` so Claude Code sends telemetry automatically - Tags this project's sessions by writing `OTEL_RESOURCE_ATTRIBUTES=service.name=claude-code-` to `.claude/settings.json` - Registers the MCP server globally (`claude mcp add --scope user tj -- tj mcp`) @@ -50,32 +50,32 @@ The MCP server is included in the `[mcp]` extra and registered automatically by | `get_tool_stats` | Tool call counts and average duration | | `get_drift_report` | Behavioral drift baseline vs latest session | | `acknowledge_alert` | Mark an alert as acknowledged | -| `setup_project` | Configure a project to send telemetry to OCW | +| `setup_project` | Configure a project to send telemetry to TokenJam | | `open_dashboard` | Open the web UI — starts `tj serve` on demand if needed | The MCP server opens the DuckDB file read-only — no lock conflicts with `tj serve` if both are running. The single write operation (`acknowledge_alert`) opens a short-lived read-write connection only for its UPDATE. **Per-project telemetry tagging** — after installing the MCP server globally, ask Claude Code to set up each project: -> "Set up OCW for this project" +> "Set up TokenJam for this project" Claude calls `setup_project`, which writes `.claude/settings.json` with `OTEL_RESOURCE_ATTRIBUTES=service.name=` so spans from that project are tagged with the right agent ID. ## Uninstalling ```bash -# Remove all OCW data, config, daemon, MCP registration, and env vars from every onboarded project: +# Remove all TokenJam data, config, daemon, MCP registration, and env vars from every onboarded project: tj uninstall --yes -# Then remove the package itself (ocw uninstall intentionally skips this): +# Then remove the package itself (tj uninstall intentionally skips this): pip uninstall tokenjam -y ``` `tj uninstall` cleans up everything set by `tj onboard --claude-code`: - Stops and removes the background daemon (launchd/systemd) - Deregisters the MCP server from Claude Code -- Deletes `~/.ocw/` (telemetry database) -- Deletes `~/.config/ocw/` (global config and projects index) +- Deletes `~/.tj/` (telemetry database) +- Deletes `~/.config/tj/` (global config and projects index) - Removes OTLP env vars from `~/.claude/settings.json` - Removes `OTEL_RESOURCE_ATTRIBUTES` from `.claude/settings.json` in every onboarded project - Removes the harness env block from `~/.zshrc` diff --git a/docs/cli-reference.md b/docs/cli-reference.md index da9f9df..b206c52 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -156,7 +156,7 @@ tj stop ### `tj uninstall` -Remove all OCW data, config, daemon, MCP registration, and env vars. +Remove all TokenJam data, config, daemon, MCP registration, and env vars. ```bash tj uninstall # interactive confirmation diff --git a/docs/configuration.md b/docs/configuration.md index 118edbe..610e7bc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration -`ocw` uses TOML configuration. Run `tj onboard` to generate a starter config, or create one manually. +`tj` uses TOML configuration. Run `tj onboard` to generate a starter config, or create one manually. ## Config file discovery @@ -45,11 +45,11 @@ completions = false tool_outputs = false [storage] -path = "~/.ocw/telemetry.duckdb" +path = "~/.tj/telemetry.duckdb" retention_days = 90 [security] -ingest_secret = "ocw_..." # generated by tj onboard +ingest_secret = "..." # generated by tj onboard [api] host = "127.0.0.1" @@ -90,7 +90,7 @@ Set limits via CLI (`tj budget --daily 10`), the REST API (`POST /api/v1/budget` ## Capture settings -By default, `ocw` does not capture prompt content, completion content, or tool outputs. Enable these selectively if you need them for debugging or evaluation: +By default, `tj` does not capture prompt content, completion content, or tool outputs. Enable these selectively if you need them for debugging or evaluation: ```toml [capture] diff --git a/docs/export.md b/docs/export.md index 473e92b..e6a9809 100644 --- a/docs/export.md +++ b/docs/export.md @@ -1,6 +1,6 @@ # Export and Integrate -`ocw` stores all telemetry in a local DuckDB database. Export it in multiple formats for analysis, evaluation, or forwarding to external backends. +`tj` stores all telemetry in a local DuckDB database. Export it in multiple formats for analysis, evaluation, or forwarding to external backends. ## Export formats diff --git a/docs/framework-support.md b/docs/framework-support.md index ea43e55..6f5011a 100644 --- a/docs/framework-support.md +++ b/docs/framework-support.md @@ -1,17 +1,17 @@ # Framework Support -`ocw` is OTel-native. Any framework that emits OpenTelemetry spans works automatically — point its OTLP exporter at `tj serve` and you're done. For everything else, one-line patches exist. +`tj` is OTel-native. Any framework that emits OpenTelemetry spans works automatically — point its OTLP exporter at `tj serve` and you're done. For everything else, one-line patches exist. ## Python provider patches Intercept at the API level, framework-agnostic: ```python -from ocw.sdk.integrations.anthropic import patch_anthropic # Anthropic — Messages.create + streaming -from ocw.sdk.integrations.openai import patch_openai # OpenAI — chat completions -from ocw.sdk.integrations.gemini import patch_gemini # Google Gemini — GenerativeModel -from ocw.sdk.integrations.bedrock import patch_bedrock # AWS Bedrock — boto3 invoke_model/invoke_agent -from ocw.sdk.integrations.litellm import patch_litellm # LiteLLM — unified interface for 100+ providers +from tokenjam.sdk.integrations.anthropic import patch_anthropic # Anthropic — Messages.create + streaming +from tokenjam.sdk.integrations.openai import patch_openai # OpenAI — chat completions +from tokenjam.sdk.integrations.gemini import patch_gemini # Google Gemini — GenerativeModel +from tokenjam.sdk.integrations.bedrock import patch_bedrock # AWS Bedrock — boto3 invoke_model/invoke_agent +from tokenjam.sdk.integrations.litellm import patch_litellm # LiteLLM — unified interface for 100+ providers ``` `patch_litellm()` covers all providers LiteLLM routes to (OpenAI, Anthropic, Bedrock, Vertex, Cohere, Mistral, Ollama, etc.) with correct per-provider attribution. If you use LiteLLM, you don't need the individual provider patches above. @@ -23,13 +23,13 @@ OpenAI-compatible providers (Groq, Together, Fireworks, xAI, Azure OpenAI) also Instrument the framework's own tool and LLM abstractions: ```python -from ocw.sdk.integrations.langchain import patch_langchain # BaseLLM + BaseTool -from ocw.sdk.integrations.langgraph import patch_langgraph # CompiledGraph -from ocw.sdk.integrations.crewai import patch_crewai # Task + Agent -from ocw.sdk.integrations.autogen import patch_autogen # ConversableAgent -from ocw.sdk.integrations.llamaindex import patch_llamaindex # Native OTel wrapper -from ocw.sdk.integrations.openai_agents_sdk import patch_openai_agents # Native OTel wrapper -from ocw.sdk.integrations.nemoclaw import watch_nemoclaw # NemoClaw Gateway observer +from tokenjam.sdk.integrations.langchain import patch_langchain # BaseLLM + BaseTool +from tokenjam.sdk.integrations.langgraph import patch_langgraph # CompiledGraph +from tokenjam.sdk.integrations.crewai import patch_crewai # Task + Agent +from tokenjam.sdk.integrations.autogen import patch_autogen # ConversableAgent +from tokenjam.sdk.integrations.llamaindex import patch_llamaindex # Native OTel wrapper +from tokenjam.sdk.integrations.openai_agents_sdk import patch_openai_agents # Native OTel wrapper +from tokenjam.sdk.integrations.nemoclaw import watch_nemoclaw # NemoClaw Gateway observer ``` ## Zero-code via OTLP diff --git a/docs/nemoclaw-integration.md b/docs/nemoclaw-integration.md index db14465..f217c57 100644 --- a/docs/nemoclaw-integration.md +++ b/docs/nemoclaw-integration.md @@ -1,9 +1,9 @@ # NemoClaw Integration -Running OpenClaw inside [NVIDIA NemoClaw](https://github.com/NVIDIA/NemoClaw)? `ocw` connects to the OpenShell Gateway WebSocket and turns every sandbox event — blocked network requests, filesystem denials, inference reroutes — into a first-class alert. +Running OpenClaw inside [NVIDIA NemoClaw](https://github.com/NVIDIA/NemoClaw)? `tj` connects to the OpenShell Gateway WebSocket and turns every sandbox event — blocked network requests, filesystem denials, inference reroutes — into a first-class alert. ```python -from ocw.sdk.integrations.nemoclaw import watch_nemoclaw +from tokenjam.sdk.integrations.nemoclaw import watch_nemoclaw observer = watch_nemoclaw() asyncio.create_task(observer.connect()) # non-blocking, runs alongside your agent @@ -13,9 +13,9 @@ This is the observability layer that NemoClaw doesn't ship with. ## What gets captured -The NemoClaw observer listens on the OpenShell Gateway WebSocket for sandbox enforcement events and converts them into OCW alerts: +The NemoClaw observer listens on the OpenShell Gateway WebSocket for sandbox enforcement events and converts them into TokenJam alerts: -| NemoClaw event | OCW alert type | Severity | +| NemoClaw event | TokenJam alert type | Severity | |---|---|---| | Network egress blocked | `network_egress_blocked` | critical | | Filesystem access denied | `filesystem_access_denied` | critical | @@ -26,7 +26,7 @@ These alerts flow through the standard alert pipeline — cooldown, dispatch to ## Configuration -Configure alert channels in `.ocw/config.toml` to get notified when sandbox events fire: +Configure alert channels in `.tj/config.toml` to get notified when sandbox events fire: ```toml [[alerts.channels]] diff --git a/docs/openclaw.md b/docs/openclaw.md index 62eccb1..1bb61a7 100644 --- a/docs/openclaw.md +++ b/docs/openclaw.md @@ -54,20 +54,20 @@ OpenClaw has built-in OpenTelemetry support via its `diagnostics-otel` plugin. P ## How it works -OpenClaw's `diagnostics-otel` plugin exports standard OTLP/HTTP JSON to `{endpoint}/v1/traces`. OCW accepts this at `POST /v1/traces` and maps OpenClaw-specific span patterns: +OpenClaw's `diagnostics-otel` plugin exports standard OTLP/HTTP JSON to `{endpoint}/v1/traces`. TokenJam accepts this at `POST /v1/traces` and maps OpenClaw-specific span patterns: -| OpenClaw span name | OCW interpretation | +| OpenClaw span name | TokenJam interpretation | |---|---| | `openclaw.request` | Root agent session span | | `openclaw.agent.turn` | Agent turn (child of session) | | `tool.Read`, `tool.exec`, `tool.Write`, etc. | Tool call — tool name extracted from span name | | `openclaw.model.usage` | LLM call — token counts extracted for cost tracking | -The `serviceName` field in your OpenClaw config becomes the `agent_id` in OCW (used for filtering, budgets, and alerts). +The `serviceName` field in your OpenClaw config becomes the `agent_id` in TokenJam (used for filtering, budgets, and alerts). ## Sensitive action alerts -Configure alerts for OpenClaw tool calls in `.ocw/config.toml`: +Configure alerts for OpenClaw tool calls in `.tj/config.toml`: ```toml [agents.my-openclaw-agent] diff --git a/examples/README.md b/examples/README.md index e37d507..a02489a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,11 +1,11 @@ -# OCW Examples +# TokenJam Examples -Example agents demonstrating [ocw](https://github.com/Metabuilder-Labs/TokenJam) integrations, from single-provider basics to complex multi-agent workflows. +Example agents demonstrating [tj](https://github.com/Metabuilder-Labs/TokenJam) integrations, from single-provider basics to complex multi-agent workflows. ## Quick Start ```bash -pip install -e ".[dev]" # install ocw in dev mode +pip install -e ".[dev]" # install tokenjam in dev mode export ANTHROPIC_API_KEY=sk-... # set your API key python examples/single_provider/anthropic_agent.py ``` @@ -13,17 +13,17 @@ python examples/single_provider/anthropic_agent.py After running any example, inspect the results with: ```bash -ocw status # agent session overview -ocw traces # list all spans from the run -ocw cost --since 1h # cost breakdown -ocw alerts # any fired alerts +tj status # agent session overview +tj traces # list all spans from the run +tj cost --since 1h # cost breakdown +tj alerts # any fired alerts ``` --- ## Single Provider -One integration per file. Simplest way to see ocw in action with your provider of choice. +One integration per file. Simplest way to see tj in action with your provider of choice. | Example | Env Vars | Extra Deps | Description | |---|---|---|---| @@ -40,7 +40,7 @@ One integration per file. Simplest way to see ocw in action with your provider o ## Single Framework -One framework integration per file. Shows how ocw captures framework-level spans. +One framework integration per file. Shows how tj captures framework-level spans. | Example | Env Vars | Extra Deps | Description | |---|---|---|---| @@ -56,7 +56,7 @@ One framework integration per file. Shows how ocw captures framework-level spans ## Multi-Integration -Complex real-world patterns combining multiple providers and frameworks. These showcase ocw's ability to track cost, performance, and behavior across a heterogeneous agent stack. +Complex real-world patterns combining multiple providers and frameworks. These showcase tj's ability to track cost, performance, and behavior across a heterogeneous agent stack. | Example | Env Vars | Extra Deps | Description | |---|---|---|---| @@ -70,7 +70,7 @@ Complex real-world patterns combining multiple providers and frameworks. These s ## Alerts and Drift -These examples demonstrate what makes ocw unique: real-time alerting and behavioral drift detection. **No API keys required** -- they use simulated instrumentation via `record_llm_call()` and `record_tool_call()`. +These examples demonstrate what makes tj unique: real-time alerting and behavioral drift detection. **No API keys required** -- they use simulated instrumentation via `record_llm_call()` and `record_tool_call()`. | Example | Env Vars | Description | |---|---|---| @@ -78,15 +78,15 @@ These examples demonstrate what makes ocw unique: real-time alerting and behavio | [`budget_breach_demo.py`](alerts_and_drift/budget_breach_demo.py) | None | Exceeds budget limits, shows cost alerts | | [`drift_demo.py`](alerts_and_drift/drift_demo.py) | None | Builds baseline, then triggers drift detection | -These examples include the required `ocw.toml` config snippets as comments at the top of each file. Copy the relevant config to your `ocw.toml` before running. +These examples include the required `tj.toml` config snippets as comments at the top of each file. Copy the relevant config to your `tj.toml` before running. ```bash # No API keys needed -- just run: python examples/alerts_and_drift/budget_breach_demo.py # Then inspect: -ocw alerts # see budget-breach alerts -ocw cost --since 1h # see cost tracking +tj alerts # see budget-breach alerts +tj cost --since 1h # see cost tracking ``` --- @@ -102,5 +102,5 @@ Most examples use in-process telemetry (spans go directly to the local DuckDB). Start the server before running them: ```bash -ocw serve & +tj serve & ``` diff --git a/examples/alerts_and_drift/_shared.py b/examples/alerts_and_drift/_shared.py index 05f59e7..8a23fd7 100644 --- a/examples/alerts_and_drift/_shared.py +++ b/examples/alerts_and_drift/_shared.py @@ -1,9 +1,9 @@ """Helper to let alerts/drift demos seed their own per-agent config. Each demo's alerts (sensitive_actions, budget) live under [agents.] in -ocw config. Without that block the AlertEngine silently no-ops. This helper +tj config. Without that block the AlertEngine silently no-ops. This helper lets each demo idempotently inject the agent block it needs before bootstrap -runs, so demos work out of the box on a fresh `ocw onboard`. +runs, so demos work out of the box on a fresh `tj onboard`. """ from __future__ import annotations @@ -22,15 +22,15 @@ def ensure_demo_agent_config(agent_id: str, agent_block: dict[str, Any]) -> Path: - """Idempotently merge `agent_block` into [agents.] in the active ocw config. + """Idempotently merge `agent_block` into [agents.] in the active tj config. Existing keys are left alone — this only fills in missing config so a tester - can override demo settings if they want. Writes to .ocw/config.toml in the + can override demo settings if they want. Writes to .tj/config.toml in the cwd if no config exists yet. Returns the path written. """ cfg_path = find_config_file() if cfg_path is None: - cfg_path = Path(".ocw/config.toml") + cfg_path = Path(".tj/config.toml") cfg_path.parent.mkdir(parents=True, exist_ok=True) data: dict[str, Any] = {} else: diff --git a/examples/alerts_and_drift/budget_breach_demo.py b/examples/alerts_and_drift/budget_breach_demo.py index e023dc4..89a7151 100644 --- a/examples/alerts_and_drift/budget_breach_demo.py +++ b/examples/alerts_and_drift/budget_breach_demo.py @@ -2,12 +2,12 @@ Budget Breach Alert Demo Simulates an agent that exceeds a very low budget ($0.05 daily, $0.02 session). -Shows how ocw tracks costs and fires budget alerts. +Shows how tj tracks costs and fires budget alerts. No API keys required — uses simulated instrumentation. -The demo seeds its own [agents.budget-demo] block in the active ocw -config on startup, so it works out of the box on a fresh `ocw onboard`. +The demo seeds its own [agents.budget-demo] block in the active tj +config on startup, so it works out of the box on a fresh `tj onboard`. The injected config is equivalent to: [agents.budget-demo.budget] @@ -86,7 +86,7 @@ def run_expensive_agent() -> None: ensure_initialised() print("=" * 60) - print("OCW Budget Breach Alert Demo") + print("TokenJam Budget Breach Alert Demo") print("=" * 60) print( "\nMaking 10 LLM calls with escalating token counts.\n" @@ -100,12 +100,12 @@ def run_expensive_agent() -> None: print("What to observe:") print("=" * 60) print( - "If your ocw.toml has the budget config shown at the top of\n" - "this file, ocw should have fired budget alerts.\n" + "If your tj.toml has the budget config shown at the top of\n" + "this file, tj should have fired budget alerts.\n" "\n" "Run these commands to inspect:\n" "\n" - " ocw cost --since 1h # cost breakdown for the last hour\n" - " ocw alerts # see budget-breach alerts\n" - " ocw status # agent overview with cost totals\n" + " tj cost --since 1h # cost breakdown for the last hour\n" + " tj alerts # see budget-breach alerts\n" + " tj status # agent overview with cost totals\n" ) diff --git a/examples/alerts_and_drift/drift_demo.py b/examples/alerts_and_drift/drift_demo.py index c898697..98665f8 100644 --- a/examples/alerts_and_drift/drift_demo.py +++ b/examples/alerts_and_drift/drift_demo.py @@ -5,7 +5,7 @@ behavior). Phase 2: Runs 1 anomalous session with 5x token usage and different tool calls. -Shows how ocw detects statistical drift and fires DRIFT_DETECTED alerts. +Shows how tj detects statistical drift and fires DRIFT_DETECTED alerts. No API keys required — uses simulated instrumentation. @@ -98,7 +98,7 @@ def run_anomalous_session() -> None: ensure_initialised() print("=" * 60) - print("OCW Behavioral Drift Detection Demo") + print("TokenJam Behavioral Drift Detection Demo") print("=" * 60) # Phase 1 -- build baseline @@ -119,12 +119,12 @@ def run_anomalous_session() -> None: print( "The anomalous session used 5x the normal token count and\n" "a completely different set of tool calls. If drift detection\n" - "is enabled in your ocw.toml, a DRIFT_DETECTED alert should\n" + "is enabled in your tj.toml, a DRIFT_DETECTED alert should\n" "fire when the anomalous session ends.\n" "\n" "Run these commands to inspect:\n" "\n" - " ocw alerts # see drift alerts\n" - " ocw status --agent drift-demo # agent overview\n" - " ocw traces # list all 13 traces\n" + " tj alerts # see drift alerts\n" + " tj status --agent drift-demo # agent overview\n" + " tj traces # list all 13 traces\n" ) diff --git a/examples/alerts_and_drift/sensitive_actions_demo.py b/examples/alerts_and_drift/sensitive_actions_demo.py index 68bcbe8..965b276 100644 --- a/examples/alerts_and_drift/sensitive_actions_demo.py +++ b/examples/alerts_and_drift/sensitive_actions_demo.py @@ -2,12 +2,12 @@ Sensitive Actions Alert Demo Simulates an agent that performs sensitive actions (send_email, delete_file, -submit_form). Shows how ocw detects and alerts on configured sensitive tools. +submit_form). Shows how tj detects and alerts on configured sensitive tools. No API keys required — uses simulated instrumentation. -The demo seeds its own [agents.sensitive-demo] block in the active ocw -config on startup, so it works out of the box on a fresh `ocw onboard`. +The demo seeds its own [agents.sensitive-demo] block in the active tj +config on startup, so it works out of the box on a fresh `tj onboard`. The injected config is equivalent to: [agents.sensitive-demo] @@ -36,7 +36,7 @@ def send_email(to: str, subject: str, body: str) -> dict: """Write an email record to a temp log file.""" - log_path = os.path.join(tempfile.gettempdir(), "ocw_email_log.txt") + log_path = os.path.join(tempfile.gettempdir(), "tj_email_log.txt") with open(log_path, "a") as f: f.write(f"To: {to}\nSubject: {subject}\nBody: {body}\n---\n") return {"status": "sent", "log": log_path} @@ -135,7 +135,7 @@ def run_sensitive_agent() -> None: ensure_initialised() print("=" * 60) - print("OCW Sensitive Actions Alert Demo") + print("TokenJam Sensitive Actions Alert Demo") print("=" * 60) run_sensitive_agent() @@ -144,12 +144,12 @@ def run_sensitive_agent() -> None: print("What to observe:") print("=" * 60) print( - "If your ocw.toml has the sensitive_actions config shown at the\n" - "top of this file, ocw should have fired alerts for each tool.\n" + "If your tj.toml has the sensitive_actions config shown at the\n" + "top of this file, tj should have fired alerts for each tool.\n" "\n" "Run these commands to inspect:\n" "\n" - " ocw alerts # see fired sensitive-action alerts\n" - " ocw status # agent overview with alert count\n" - " ocw traces # list recent traces\n" + " tj alerts # see fired sensitive-action alerts\n" + " tj status # agent overview with alert count\n" + " tj traces # list recent traces\n" ) diff --git a/examples/multi/rag_pipeline.py b/examples/multi/rag_pipeline.py index 11ee365..a94e22d 100644 --- a/examples/multi/rag_pipeline.py +++ b/examples/multi/rag_pipeline.py @@ -2,7 +2,7 @@ RAG Pipeline with Provider Fallback — LlamaIndex + OpenAI/Anthropic. Demonstrates a retrieval-augmented generation pipeline that falls back from -OpenAI to Anthropic when the primary provider fails. Requires `ocw serve` +OpenAI to Anthropic when the primary provider fails. Requires `tj serve` running on localhost:8787 for span ingestion. Extra deps: @@ -12,7 +12,7 @@ OPENAI_API_KEY, ANTHROPIC_API_KEY Prerequisite: - ocw serve (must be running on localhost:8787) + tj serve (must be running on localhost:8787) """ from __future__ import annotations @@ -34,14 +34,14 @@ sys.exit(f"Missing env vars: {', '.join(missing)}") # --------------------------------------------------------------------------- -# Connectivity check — ocw serve must be running +# Connectivity check — tj serve must be running # --------------------------------------------------------------------------- try: resp = httpx.get("http://localhost:8787/api/v1/spans", timeout=3) except httpx.ConnectError: sys.exit( - "Cannot connect to ocw serve on localhost:8787. " - "Start it first: ocw serve" + "Cannot connect to tj serve on localhost:8787. " + "Start it first: tj serve" ) # --------------------------------------------------------------------------- @@ -150,15 +150,15 @@ def main() -> None: # --------------------------------------------------------------------------- # After running, inspect the RAG session: # -# ocw traces --since 10m +# tj traces --since 10m # -> Single trace showing retrieval + LLM spans # -# ocw trace +# tj trace # -> Waterfall: session -> retrieval tool calls -> LLM calls # The second question shows both OpenAI (skipped) and Anthropic spans # -# ocw cost --since 1h +# tj cost --since 1h # -> Cost comparison between OpenAI and Anthropic calls # -# ocw tools +# tj tools # -> "retrieval" tool call count matching the number of questions diff --git a/examples/multi/research_team.py b/examples/multi/research_team.py index cf5084e..1a69588 100644 --- a/examples/multi/research_team.py +++ b/examples/multi/research_team.py @@ -2,7 +2,7 @@ Multi-Framework Research Team — CrewAI agents with LangChain tools. Demonstrates deep span trees from combining CrewAI multi-agent orchestration -with LangChain tool abstractions, all captured in a single ocw session. +with LangChain tool abstractions, all captured in a single tj session. Extra deps: pip install anthropic crewai langchain-core @@ -175,15 +175,15 @@ def main() -> None: # --------------------------------------------------------------------------- # After running, explore the multi-agent session: # -# ocw traces --since 10m +# tj traces --since 10m # -> Single trace containing all three agents' activity # -# ocw trace +# tj trace # -> Deep span tree: session -> agent tasks -> LLM calls + tool calls # -# ocw tools +# tj tools # -> Breakdown of web_search, calculator, and file_reader usage # showing call counts and average durations # -# ocw cost --since 1h +# tj cost --since 1h # -> Total session cost across all LLM calls made by the crew diff --git a/examples/multi/router_agent.py b/examples/multi/router_agent.py index c5a1378..66b2686 100644 --- a/examples/multi/router_agent.py +++ b/examples/multi/router_agent.py @@ -1,7 +1,7 @@ """ Provider Router Agent — routes tasks to the best LLM provider. -Demonstrates multi-provider observability with ocw: each routing decision +Demonstrates multi-provider observability with tj: each routing decision and LLM call appears as a span in a single trace, enabling cost comparison across providers. @@ -136,14 +136,14 @@ def main() -> None: # --------------------------------------------------------------------------- # After running this script, inspect the trace and cost breakdown: # -# ocw traces --since 5m +# tj traces --since 5m # -> Shows a single trace with spans for each provider call # -# ocw trace +# tj trace # -> Waterfall view: session span -> route tool calls + LLM calls # -# ocw cost --since 1h +# tj cost --since 1h # -> Compare cost across gemini, anthropic, and openai in one session # -# ocw tools +# tj tools # -> Shows the "route" tool call count and timing diff --git a/examples/openclaw/README.md b/examples/openclaw/README.md index ffefe34..650c81c 100644 --- a/examples/openclaw/README.md +++ b/examples/openclaw/README.md @@ -1,13 +1,13 @@ -# OpenClaw + OCW Example +# OpenClaw + TokenJam Example This is a config-only integration — no Python code needed. OpenClaw's built-in OTel exporter sends traces directly to `tj serve`. -## Step 1: Start OCW +## Step 1: Start TokenJam ```bash pip install tokenjam -ocw onboard -ocw serve & +tj onboard +tj serve & ``` ## Step 2: Configure OpenClaw @@ -40,7 +40,7 @@ Add this to your `openclaw.json`: ## Step 3: Configure sensitive action alerts (optional) -Add to `.ocw/config.toml`: +Add to `.tj/config.toml`: ```toml [agents.my-openclaw-agent] @@ -64,26 +64,26 @@ Add to `.ocw/config.toml`: ## Step 4: Run OpenClaw and verify -Start your OpenClaw gateway, then check OCW: +Start your OpenClaw gateway, then check TokenJam: ```bash # Agent overview — should show your openclaw agent -ocw status +tj status # Trace listing — shows openclaw.request, openclaw.agent.turn, tool.* spans -ocw traces +tj traces # Span waterfall for a specific trace -ocw trace +tj trace # Cost breakdown — token usage from openclaw.model.usage spans -ocw cost --since 1h +tj cost --since 1h # Tool call summary — shows Read, exec, Write, web_search counts -ocw tools +tj tools # Alerts — shows any sensitive action or budget alerts -ocw alerts +tj alerts ``` ## Expected output from `tj traces` diff --git a/examples/single_framework/autogen_agent.py b/examples/single_framework/autogen_agent.py index 4e41d13..0fa1417 100644 --- a/examples/single_framework/autogen_agent.py +++ b/examples/single_framework/autogen_agent.py @@ -1,8 +1,8 @@ """ -AutoGen agent example with OCW observability. +AutoGen agent example with TokenJam observability. Creates two ConversableAgent debaters that argue for and against a position. -OCW patches ConversableAgent.generate_reply and .initiate_chat for span capture. +TokenJam patches ConversableAgent.generate_reply and .initiate_chat for span capture. Extra deps: pip install pyautogen Run: python examples/single_framework/autogen_agent.py @@ -56,7 +56,7 @@ def main(): print(f"Debate topic: {TOPIC}\n") print("Starting 3-turn debate...\n") - # initiate_chat is patched by OCW to create a span + # initiate_chat is patched by TokenJam to create a span chat_result = debater_for.initiate_chat( debater_against, message=( @@ -74,11 +74,11 @@ def main(): print(f"[{role}] {content}\n") # --- Observation --- - print("--- OCW Observation ---") + print("--- TokenJam Observation ---") print("AutoGen integration captured spans for:") print(" - Chat initiation via ConversableAgent.initiate_chat") print(" - Reply generation via ConversableAgent.generate_reply") - print("Run 'ocw traces' to see the captured telemetry.") + print("Run 'tj traces' to see the captured telemetry.") if __name__ == "__main__": diff --git a/examples/single_framework/crewai_agent.py b/examples/single_framework/crewai_agent.py index 820df3c..6a48499 100644 --- a/examples/single_framework/crewai_agent.py +++ b/examples/single_framework/crewai_agent.py @@ -1,8 +1,8 @@ """ -CrewAI agent example with OCW observability. +CrewAI agent example with TokenJam observability. Creates a 2-agent crew (researcher + writer) that collaborates on a task. -OCW patches Task.execute and Agent.execute_task to capture spans automatically. +TokenJam patches Task.execute and Agent.execute_task to capture spans automatically. Extra deps: pip install crewai Run: python examples/single_framework/crewai_agent.py @@ -78,11 +78,11 @@ def main(): print(f"\n--- Crew Result ---\n{result}\n") # --- Observation --- - print("--- OCW Observation ---") + print("--- TokenJam Observation ---") print("CrewAI integration captured spans for:") print(" - Task execution via Task.execute") print(" - Agent task execution via Agent.execute_task") - print("Run 'ocw traces' to see the captured telemetry.") + print("Run 'tj traces' to see the captured telemetry.") if __name__ == "__main__": diff --git a/examples/single_framework/langchain_agent.py b/examples/single_framework/langchain_agent.py index 70014c8..5416e65 100644 --- a/examples/single_framework/langchain_agent.py +++ b/examples/single_framework/langchain_agent.py @@ -1,8 +1,8 @@ """ -LangChain agent example with OCW observability. +LangChain agent example with TokenJam observability. Uses ChatOpenAI with tool bindings to demonstrate LangChain integration. -OCW patches BaseLLM.generate and BaseTool.run to capture spans automatically. +TokenJam patches BaseLLM.generate and BaseTool.run to capture spans automatically. Extra deps: pip install langchain-core langchain-openai Run: python examples/single_framework/langchain_agent.py @@ -72,7 +72,7 @@ def main(): tool_args = tc["args"] print(f"\nTool call: {tool_name}({tool_args})") tool = tool_map[tool_name] - # BaseTool.run is patched by OCW + # BaseTool.run is patched by TokenJam result = tool.run(tool_args) print(f"Tool result: {result}") tool_results.append({"tool_call_id": tc["id"], "result": result}) @@ -93,11 +93,11 @@ def main(): print(f"\nFinal answer: {final.content}") # --- Observation --- - print("\n--- OCW Observation ---") + print("\n--- TokenJam Observation ---") print("LangChain integration captured spans for:") print(" - LLM calls via ChatOpenAI") print(" - Tool calls via BaseTool.run") - print("Run 'ocw traces' to see the captured telemetry.") + print("Run 'tj traces' to see the captured telemetry.") if __name__ == "__main__": diff --git a/examples/single_framework/langgraph_agent.py b/examples/single_framework/langgraph_agent.py index c0475e6..82d200f 100644 --- a/examples/single_framework/langgraph_agent.py +++ b/examples/single_framework/langgraph_agent.py @@ -1,8 +1,8 @@ """ -LangGraph agent example with OCW observability. +LangGraph agent example with TokenJam observability. Builds a simple 3-node StateGraph (plan -> execute -> review) and runs it. -OCW patches CompiledGraph.invoke to capture the graph execution as a span, +TokenJam patches CompiledGraph.invoke to capture the graph execution as a span, and BaseLLM.generate to capture individual LLM calls within each node. Extra deps: pip install langgraph langchain-openai @@ -105,11 +105,11 @@ def main(): print(f"Review:\n{result['review']}\n") # --- Observation --- - print("--- OCW Observation ---") + print("--- TokenJam Observation ---") print("LangGraph integration captured spans for:") print(" - Graph invocation via CompiledGraph.invoke") print(" - Individual LLM calls within each node (via LangChain patch)") - print("Run 'ocw traces' to see the captured telemetry.") + print("Run 'tj traces' to see the captured telemetry.") if __name__ == "__main__": diff --git a/examples/single_framework/llamaindex_agent.py b/examples/single_framework/llamaindex_agent.py index a85185a..f645a9a 100644 --- a/examples/single_framework/llamaindex_agent.py +++ b/examples/single_framework/llamaindex_agent.py @@ -1,14 +1,14 @@ """ -LlamaIndex agent example with OCW observability. +LlamaIndex agent example with TokenJam observability. -Uses LlamaIndex's native OTel support to export spans to ocw serve. +Uses LlamaIndex's native OTel support to export spans to tj serve. Builds a VectorStoreIndex from sample documents and queries it. -IMPORTANT: This example requires `ocw serve` to be running because +IMPORTANT: This example requires `tj serve` to be running because LlamaIndex integration exports spans over HTTP (not in-process). Extra deps: pip install llama-index opentelemetry-instrumentation-llama-index -Run: ocw serve & +Run: tj serve & python examples/single_framework/llamaindex_agent.py """ import os @@ -26,8 +26,8 @@ httpx.get("http://127.0.0.1:8787/api/v1/traces", timeout=2) except httpx.ConnectError: sys.exit( - "This example requires ocw serve to be running.\n" - "Start it with: ocw serve &" + "This example requires tj serve to be running.\n" + "Start it with: tj serve &" ) from pathlib import Path # noqa: E402 @@ -36,7 +36,7 @@ from tokenjam.sdk import watch, patch_llamaindex # noqa: E402 -# Configure LlamaIndex's native OTel to export to ocw serve +# Configure LlamaIndex's native OTel to export to tj serve patch_llamaindex() # Sample documents directory: examples/multi/sample_docs/ @@ -104,12 +104,12 @@ def main(): print(f"A: {response}\n") # --- Observation --- - print("--- OCW Observation ---") + print("--- TokenJam Observation ---") print("LlamaIndex integration captured spans via native OTel support:") print(" - Document indexing and embedding calls") print(" - Query engine retrieval and LLM synthesis") - print(" - Spans exported to ocw serve over HTTP") - print("Run 'ocw traces' to see the captured telemetry.") + print(" - Spans exported to tj serve over HTTP") + print("Run 'tj traces' to see the captured telemetry.") if __name__ == "__main__": diff --git a/examples/single_provider/anthropic_agent.py b/examples/single_provider/anthropic_agent.py index 9307a7a..a8875fc 100644 --- a/examples/single_provider/anthropic_agent.py +++ b/examples/single_provider/anthropic_agent.py @@ -1,8 +1,8 @@ """ -Anthropic tool-use agent with OCW observability. +Anthropic tool-use agent with TokenJam observability. Demonstrates the full Anthropic tool-use loop: message -> tool_use -> tool_result -> final -response, with each tool invocation recorded via ocw's record_tool_call(). +response, with each tool invocation recorded via tj's record_tool_call(). Requirements: pip install anthropic tokenjam @@ -140,7 +140,7 @@ def run() -> str: else: tool_output = func(**tool_input) - # Record the tool call in ocw for observability + # Record the tool call in tj for observability record_tool_call( tool_name=tool_name, tool_input=tool_input, @@ -182,7 +182,7 @@ def run() -> str: result = run() print(f"\nAgent response:\n{result}") - print("\n--- OCW Observation ---") + print("\n--- TokenJam Observation ---") print("Session and tool spans have been recorded.") - print("Run 'ocw status --agent anthropic-tool-agent' to view telemetry.") - print("Run 'ocw tools --agent anthropic-tool-agent' to see tool call stats.") + print("Run 'tj status --agent anthropic-tool-agent' to view telemetry.") + print("Run 'tj tools --agent anthropic-tool-agent' to see tool call stats.") diff --git a/examples/single_provider/bedrock_agent.py b/examples/single_provider/bedrock_agent.py index 4515177..770c3b2 100644 --- a/examples/single_provider/bedrock_agent.py +++ b/examples/single_provider/bedrock_agent.py @@ -1,8 +1,8 @@ """ -AWS Bedrock agent with OCW observability. +AWS Bedrock agent with TokenJam observability. Demonstrates calling Claude via AWS Bedrock's invoke_model API. All LLM calls -are captured by ocw via the Bedrock integration patch. +are captured by tj via the Bedrock integration patch. SETUP NOTES: This example requires valid AWS credentials configured in your environment. @@ -96,7 +96,7 @@ def run() -> str: result = run() print(f"\nAgent response:\n{result}") - print("\n--- OCW Observation ---") + print("\n--- TokenJam Observation ---") print("Session and LLM spans have been recorded.") - print("Run 'ocw status --agent bedrock-agent' to view telemetry.") - print("Run 'ocw cost --agent bedrock-agent' to see token costs.") + print("Run 'tj status --agent bedrock-agent' to view telemetry.") + print("Run 'tj cost --agent bedrock-agent' to see token costs.") diff --git a/examples/single_provider/gemini_agent.py b/examples/single_provider/gemini_agent.py index d076492..6ed4b26 100644 --- a/examples/single_provider/gemini_agent.py +++ b/examples/single_provider/gemini_agent.py @@ -1,9 +1,9 @@ """ -Google Gemini summarization agent with OCW observability. +Google Gemini summarization agent with TokenJam observability. Demonstrates the Gemini provider path: passes a multi-paragraph text to gemini-2.0-flash and asks for a concise summary. All LLM calls are captured -by ocw via the Gemini integration patch. +by tj via the Gemini integration patch. Requirements: pip install google-generativeai tokenjam @@ -101,7 +101,7 @@ def run() -> str: result = run() print(f"\nSummary:\n{result}") - print("\n--- OCW Observation ---") + print("\n--- TokenJam Observation ---") print("Session and LLM spans have been recorded.") - print("Run 'ocw status --agent gemini-summarizer' to view telemetry.") - print("Run 'ocw cost --agent gemini-summarizer' to see token costs.") + print("Run 'tj status --agent gemini-summarizer' to view telemetry.") + print("Run 'tj cost --agent gemini-summarizer' to see token costs.") diff --git a/examples/single_provider/litellm_agent.py b/examples/single_provider/litellm_agent.py index e1a621d..c7bbcca 100644 --- a/examples/single_provider/litellm_agent.py +++ b/examples/single_provider/litellm_agent.py @@ -1,9 +1,9 @@ """ -LiteLLM multi-provider agent with OCW observability. +LiteLLM multi-provider agent with TokenJam observability. Demonstrates patch_litellm() routing calls to multiple LLM providers through LiteLLM's unified interface. Each call is automatically attributed to the -correct provider in ocw spans. +correct provider in tj spans. Requirements: pip install litellm tokenjam @@ -83,10 +83,10 @@ def run() -> None: if __name__ == "__main__": run() - print("\n--- OCW Observation ---") + print("\n--- TokenJam Observation ---") print("All 3 calls routed through LiteLLM appear as separate spans,") print("each attributed to the correct provider (openai / anthropic).") print() - print(" ocw traces # see the trace with all 3 spans") - print(" ocw cost --since 1h # cost breakdown by provider") - print(" ocw status --agent litellm-multi-provider") + print(" tj traces # see the trace with all 3 spans") + print(" tj cost --since 1h # cost breakdown by provider") + print(" tj status --agent litellm-multi-provider") diff --git a/examples/single_provider/openai_agent.py b/examples/single_provider/openai_agent.py index 4f8770f..6f47f15 100644 --- a/examples/single_provider/openai_agent.py +++ b/examples/single_provider/openai_agent.py @@ -1,9 +1,9 @@ """ -OpenAI tool-use agent with streaming and OCW observability. +OpenAI tool-use agent with streaming and TokenJam observability. Demonstrates the OpenAI function-calling loop with a stub tool, then streams the final response token by token. All LLM calls and tool invocations are -captured by ocw. +captured by tj. Requirements: pip install openai tokenjam @@ -131,7 +131,7 @@ def run() -> str: else: tool_output = func(**func_args) - # Record the tool call in ocw + # Record the tool call in tj record_tool_call( tool_name=func_name, tool_input=func_args, @@ -172,7 +172,7 @@ def run() -> str: if __name__ == "__main__": result = run() - print("\n--- OCW Observation ---") + print("\n--- TokenJam Observation ---") print("Session, LLM, and tool spans have been recorded.") - print("Run 'ocw status --agent openai-tool-agent' to view telemetry.") - print("Run 'ocw tools --agent openai-tool-agent' to see tool call stats.") + print("Run 'tj status --agent openai-tool-agent' to view telemetry.") + print("Run 'tj tools --agent openai-tool-agent' to see tool call stats.") diff --git a/examples/single_provider/openai_agents_sdk_agent.py b/examples/single_provider/openai_agents_sdk_agent.py index 58e6028..89cc12d 100644 --- a/examples/single_provider/openai_agents_sdk_agent.py +++ b/examples/single_provider/openai_agents_sdk_agent.py @@ -1,13 +1,13 @@ """ -OpenAI Agents SDK multi-agent example with OCW observability. +OpenAI Agents SDK multi-agent example with TokenJam observability. Demonstrates a triage/specialist handoff pattern using the OpenAI Agents SDK. The triage agent inspects the user query and delegates to a specialist agent for a detailed answer. This integration works differently from other providers: it configures the -Agents SDK's native OTel support to export traces to `ocw serve` via OTLP. -You must have `ocw serve` running before executing this script. +Agents SDK's native OTel support to export traces to `tj serve` via OTLP. +You must have `tj serve` running before executing this script. Requirements: pip install openai-agents httpx tokenjam @@ -16,7 +16,7 @@ OPENAI_API_KEY — required Prerequisites: - ocw serve must be running: ocw serve --port 8787 + tj serve must be running: tj serve --port 8787 Usage: python examples/single_provider/openai_agents_sdk_agent.py @@ -32,21 +32,21 @@ print(" export OPENAI_API_KEY=sk-...") sys.exit(1) -# Check that ocw serve is reachable before proceeding. +# Check that tj serve is reachable before proceeding. try: import httpx resp = httpx.get("http://127.0.0.1:8787/api/v1/traces", timeout=2) if resp.status_code >= 500: - raise ConnectionError(f"ocw serve returned {resp.status_code}") + raise ConnectionError(f"tj serve returned {resp.status_code}") except Exception: - print("ERROR: Cannot reach ocw serve at http://127.0.0.1:8787") + print("ERROR: Cannot reach tj serve at http://127.0.0.1:8787") print() - print("This example requires a running ocw serve instance because the") + print("This example requires a running tj serve instance because the") print("OpenAI Agents SDK exports traces via OTLP HTTP to the local server.") print() print("Start it in another terminal:") - print(" ocw serve --port 8787") + print(" tj serve --port 8787") sys.exit(1) from agents import Agent, Runner # noqa: E402 @@ -54,9 +54,9 @@ from tokenjam.sdk import watch # noqa: E402 from tokenjam.sdk.integrations.openai_agents_sdk import patch_openai_agents # noqa: E402 -# Configure the Agents SDK's native OTel to export to ocw serve. +# Configure the Agents SDK's native OTel to export to tj serve. # NOTE: patch_openai_agents() does NOT call ensure_initialised() — it sets up -# OTLP export to ocw serve instead. We still use @watch() for session tracking. +# OTLP export to tj serve instead. We still use @watch() for session tracking. patch_openai_agents() # --------------------------------------------------------------------------- @@ -121,7 +121,7 @@ def run() -> str: result = run() print(f"\nAgent response:\n{result}") - print("\n--- OCW Observation ---") + print("\n--- TokenJam Observation ---") print("Session and agent handoff spans have been recorded.") - print("Run 'ocw status --agent openai-agents-sdk-demo' to view telemetry.") - print("Run 'ocw traces --agent openai-agents-sdk-demo' to see the trace.") + print("Run 'tj status --agent openai-agents-sdk-demo' to view telemetry.") + print("Run 'tj traces --agent openai-agents-sdk-demo' to see the trace.") diff --git a/incidents/hallucination-drift/BLOG.md b/incidents/hallucination-drift/BLOG.md index 341f8c1..47142ee 100644 --- a/incidents/hallucination-drift/BLOG.md +++ b/incidents/hallucination-drift/BLOG.md @@ -37,7 +37,7 @@ Five new tools. 50x the tokens. Every metric off the chart. Your `print()` logs said: *output looks reasonable. Moving on.* -OCW fired `drift_detected` the moment the session closed. +TokenJam fired `drift_detected` the moment the session closed. --- @@ -57,7 +57,7 @@ No API keys. Runs entirely in-process. Watch 5 normal sessions, then 1 anomalous Enable it for your real agent: ```toml -# ocw.toml +# tj.toml [agents.my-agent.drift] enabled = true baseline_sessions = 10 diff --git a/incidents/hallucination-drift/README.md b/incidents/hallucination-drift/README.md index 225e3ec..ba31317 100644 --- a/incidents/hallucination-drift/README.md +++ b/incidents/hallucination-drift/README.md @@ -25,7 +25,7 @@ This is the worst kind of bug. No stack trace. No error. No crash. Just behavior [agent] But hey, it completed successfully. Moving on. ``` -## What you see with OCW +## What you see with TokenJam ``` Sessions: 5 baseline + 1 anomalous @@ -39,13 +39,13 @@ The anomalous session had: Tool sequence: 5 new tools never seen in baseline ``` -Five sessions averaged 1,000 input tokens with tools `[search, summarize]`. Session 6 came in with 50,000 tokens and tools `[fetch_url, parse_html, extract_entities, classify, store_results]`. Every metric was off the chart. OCW fired `drift_detected` the moment the session closed. +Five sessions averaged 1,000 input tokens with tools `[search, summarize]`. Session 6 came in with 50,000 tokens and tools `[fetch_url, parse_html, extract_entities, classify, store_results]`. Every metric was off the chart. TokenJam fired `drift_detected` the moment the session closed. The `DriftDetector` builds a rolling baseline from prior sessions. When a new session's token counts exceed a Z-score of 2.0, or the tool sequence Jaccard distance exceeds 0.4 — it fires. You find out in seconds, not after a week of "huh, that output seemed weird." ## Enable drift detection -In `ocw.toml`: +In `tj.toml`: ```toml [agents.my-agent.drift] @@ -66,7 +66,7 @@ tj demo hallucination-drift Runs entirely in-process. No API keys, no real model calls, no network traffic. -To track drift on your real agent, wire up the OCW SDK, enable drift in `ocw.toml`, and run `tj serve`. Then `tj drift` shows Z-scores; `tj alerts` shows the events. +To track drift on your real agent, wire up the TokenJam SDK, enable drift in `tj.toml`, and run `tj serve`. Then `tj drift` shows Z-scores; `tj alerts` shows the events. ## Next in the incident library @@ -75,4 +75,4 @@ To track drift on your real agent, wire up the OCW SDK, enable drift in `ocw.tom --- -[OCW](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does. +[TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does. diff --git a/incidents/hallucination-drift/scenario.py b/incidents/hallucination-drift/scenario.py index d69d66b..dbc681c 100644 --- a/incidents/hallucination-drift/scenario.py +++ b/incidents/hallucination-drift/scenario.py @@ -2,7 +2,7 @@ Incident: "My agent worked yesterday. Today it's possessed." Behavioral drift: the agent's token usage and tool patterns have shifted -significantly. With print(), you notice "outputs look different." OCW +significantly. With print(), you notice "outputs look different." TokenJam measures the deviation with Z-scores and fires a DRIFT_DETECTED alert. No API keys required. @@ -10,7 +10,7 @@ from __future__ import annotations AGENT_ID = "demo-hallucination-drift" -DESCRIPTION = "Agent behavior shifts unexpectedly — OCW catches statistical drift and fires an alert" +DESCRIPTION = "Agent behavior shifts unexpectedly — TokenJam catches statistical drift and fires an alert" _PRINT_SIMULATION = """\ [agent] Session 1... output looks reasonable @@ -170,8 +170,8 @@ def _render(console, result) -> None: " Input tokens: 50,000 vs baseline ~1,000 (Z-score: inf)\n" " Tool sequence: 5 new tools never seen in baseline\n\n" "[dim]In your real agent:[/dim]\n" - " ocw drift [dim]# Z-scores and baseline stats[/dim]\n" - " ocw alerts [dim]# see the drift_detected alert[/dim]" + " tj drift [dim]# Z-scores and baseline stats[/dim]\n" + " tj alerts [dim]# see the drift_detected alert[/dim]" ) - console.print(Panel(ocw_output, title="[green]What OCW reveals[/green]", border_style="green")) + console.print(Panel(ocw_output, title="[green]What TokenJam reveals[/green]", border_style="green")) console.print() diff --git a/incidents/retry-loop/BLOG.md b/incidents/retry-loop/BLOG.md index b74dab7..610f6a8 100644 --- a/incidents/retry-loop/BLOG.md +++ b/incidents/retry-loop/BLOG.md @@ -28,7 +28,7 @@ This is why people say agents are "flaky" — there's no error to grep for, just --- -When I designed the `retry_loop` detector for OCW, the rule I landed on was deliberately boring: fire when the same tool name shows up 4+ times in the last 6 spans. No ML, no per-agent tuning. Most real loops are tighter than that — they're 6+ identical calls in a row — so 4-of-6 catches them early without false positives on legitimate retries. +When I designed the `retry_loop` detector for TokenJam, the rule I landed on was deliberately boring: fire when the same tool name shows up 4+ times in the last 6 spans. No ML, no per-agent tuning. Most real loops are tighter than that — they're 6+ identical calls in a row — so 4-of-6 catches them early without false positives on legitimate retries. It runs alongside `failure_rate`, which trips when more than 20% of recent spans error out. Both default-on. Together they cover the two flavors of "stuck": looping on success and looping on failure. @@ -55,14 +55,14 @@ pip install tokenjam tj demo retry-loop ``` -It synthesizes the span sequence above, runs both detectors against it, and shows you the `print()` view next to the OCW view. About 30 seconds. +It synthesizes the span sequence above, runs both detectors against it, and shows you the `print()` view next to the TokenJam view. About 30 seconds. --- To wire it into a real agent, the SDK is three lines: ```python -from ocw.sdk import patch_anthropic, watch +from tokenjam.sdk import patch_anthropic, watch patch_anthropic() diff --git a/incidents/retry-loop/README.md b/incidents/retry-loop/README.md index 6d108dd..260a5e1 100644 --- a/incidents/retry-loop/README.md +++ b/incidents/retry-loop/README.md @@ -38,7 +38,7 @@ The logs said "tool called, tool returned." They were right. They just didn't te Technically correct. Completely useless. -## What you see with OCW +## What you see with TokenJam ``` Spans ingested: 6 @@ -63,7 +63,7 @@ tj demo retry-loop 30 seconds, no API keys, no config file. The demo runs against an in-memory backend so nothing persists to disk. -To catch this in your real agent, wire up the OCW SDK (`@watch()` + `patch_anthropic()` or `patch_openai()`) and run `tj serve` in the background. After that, `tj alerts` and `tj traces` work against your live data. +To catch this in your real agent, wire up the TokenJam SDK (`@watch()` + `patch_anthropic()` or `patch_openai()`) and run `tj serve` in the background. After that, `tj alerts` and `tj traces` work against your live data. ## Next in the incident library @@ -72,4 +72,4 @@ To catch this in your real agent, wire up the OCW SDK (`@watch()` + `patch_anthr --- -[OCW](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does. +[TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does. diff --git a/incidents/retry-loop/scenario.py b/incidents/retry-loop/scenario.py index 16b1bf1..114e8ad 100644 --- a/incidents/retry-loop/scenario.py +++ b/incidents/retry-loop/scenario.py @@ -3,14 +3,14 @@ A tool call silently fails and retries itself in a loop. The agent keeps calling the same broken tool, burning time and money. With print(), you see -"tool called" over and over. OCW sees the loop pattern and fires an alert. +"tool called" over and over. TokenJam sees the loop pattern and fires an alert. No API keys required. """ from __future__ import annotations AGENT_ID = "demo-retry-loop" -DESCRIPTION = "Agent stuck retrying a failing tool — OCW detects the loop and fires an alert" +DESCRIPTION = "Agent stuck retrying a failing tool — TokenJam detects the loop and fires an alert" _PRINT_SIMULATION = """\ [agent] Starting task... @@ -29,7 +29,7 @@ def run() -> None: """ Inject a retry-loop pattern directly into IngestPipeline + InMemoryBackend. - Renders Rich panels, or JSON if --json was passed to `ocw demo`. + Renders Rich panels, or JSON if --json was passed to `tj demo`. """ import click @@ -127,8 +127,8 @@ def _render(console, result) -> None: f"[bold]Traces:[/bold] {result.trace_count}\n\n" f"[bold]Alerts fired:[/bold]\n{alert_str}\n\n" "[dim]In your real agent:[/dim]\n" - " ocw alerts [dim]# see the retry_loop alert[/dim]\n" - " ocw traces [dim]# see the loop pattern[/dim]" + " tj alerts [dim]# see the retry_loop alert[/dim]\n" + " tj traces [dim]# see the loop pattern[/dim]" ) - console.print(Panel(ocw_output, title="[green]What OCW reveals[/green]", border_style="green")) + console.print(Panel(ocw_output, title="[green]What TokenJam reveals[/green]", border_style="green")) console.print() diff --git a/incidents/surprise-cost/BLOG.md b/incidents/surprise-cost/BLOG.md index 3b419c5..f762d59 100644 --- a/incidents/surprise-cost/BLOG.md +++ b/incidents/surprise-cost/BLOG.md @@ -38,7 +38,7 @@ Total $3.5192 Three calls to Opus. 92% of the bill. The `model=` config said Haiku. A fallback router in the chain was escalating harder subtasks — exactly as configured, two weeks ago, by someone who then forgot. -`print()` has no way to tell you which model handled which call. HTTP responses don't include "by the way, this one cost $1.20." OCW does. +`print()` has no way to tell you which model handled which call. HTTP responses don't include "by the way, this one cost $1.20." TokenJam does. --- @@ -59,14 +59,14 @@ pip install tokenjam tj demo surprise-cost ``` -8 synthetic LLM spans with real pricing math — same model mix, same token counts as the real scenario. Side-by-side: what `print()` shows vs. what OCW reveals. +8 synthetic LLM spans with real pricing math — same model mix, same token counts as the real scenario. Side-by-side: what `print()` shows vs. what TokenJam reveals. --- Wire up your real agent: ```python -from ocw.sdk import patch_anthropic, watch +from tokenjam.sdk import patch_anthropic, watch patch_anthropic() @@ -78,12 +78,12 @@ def run(): Set a budget cap: ```toml -# ocw.toml +# tj.toml [agents.my-agent.budget] session_usd = 5.00 ``` -OCW fires an alert when you cross it. Not on the bill. When the call happens. +TokenJam fires an alert when you cross it. Not on the bill. When the call happens. --- diff --git a/incidents/surprise-cost/README.md b/incidents/surprise-cost/README.md index f1e0c6f..7ff23db 100644 --- a/incidents/surprise-cost/README.md +++ b/incidents/surprise-cost/README.md @@ -37,7 +37,7 @@ Eight responses. All 200. Nothing wrong. Except three of those calls weren't Hai Eight successes. No errors. Nothing to investigate. -## What you see with OCW +## What you see with TokenJam ``` Model Calls Cost (USD) @@ -51,11 +51,11 @@ Total session cost: $3.5192 Two Haiku calls: $0.009. Three Opus calls: $3.23. You paid 350x more for Opus than Haiku, in the same session, and `print()` gave you eight identical lines. -OCW records `model`, `input_tokens`, and `output_tokens` on every LLM span. The `CostEngine` prices each call using per-model rates from `pricing/models.toml`. The escalation is visible the moment it happens — not at the end of the month on a bill. +TokenJam records `model`, `input_tokens`, and `output_tokens` on every LLM span. The `CostEngine` prices each call using per-model rates from `pricing/models.toml`. The escalation is visible the moment it happens — not at the end of the month on a bill. ## Set a budget before it happens -Add to `ocw.toml`: +Add to `tj.toml`: ```toml [agents.my-agent.budget] @@ -70,7 +70,7 @@ Or set a global default for all agents: daily_usd = 10.00 ``` -OCW fires `cost_budget_session` and `cost_budget_daily` alerts when limits are crossed. +TokenJam fires `cost_budget_session` and `cost_budget_daily` alerts when limits are crossed. ## Try it yourself @@ -81,7 +81,7 @@ tj demo surprise-cost Emits 8 synthetic LLM spans with real pricing math. No API keys, no model calls, no network traffic. -To track real spend, instrument your agent with the OCW SDK and run `tj serve`. Then `tj cost --by model` shows live per-model attribution. +To track real spend, instrument your agent with the tokenjam SDK and run `tj serve`. Then `tj cost --by model` shows live per-model attribution. ## Next in the incident library @@ -90,4 +90,4 @@ To track real spend, instrument your agent with the OCW SDK and run `tj serve`. --- -[OCW](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does. +[TokenJam](https://github.com/Metabuilder-Labs/TokenJam) is a local-first, zero-signup observability CLI for AI agents. No cloud. No account. Just `pip install tokenjam` and start seeing what your agent actually does. diff --git a/incidents/surprise-cost/scenario.py b/incidents/surprise-cost/scenario.py index d8141cb..4f28790 100644 --- a/incidents/surprise-cost/scenario.py +++ b/incidents/surprise-cost/scenario.py @@ -2,14 +2,14 @@ Incident: "Why did my agent just spend $47 on a hello world?" The model silently escalated from cheap Haiku to expensive Opus mid-chain. -With print(), you see "response received". OCW shows cost per model, per call. +With print(), you see "response received". TokenJam shows cost per model, per call. No API keys required. """ from __future__ import annotations AGENT_ID = "demo-surprise-cost" -DESCRIPTION = "Agent silently burns budget on expensive models — OCW tracks every dollar" +DESCRIPTION = "Agent silently burns budget on expensive models — TokenJam tracks every dollar" _PRINT_SIMULATION = """\ [agent] Starting document analysis... @@ -40,7 +40,7 @@ def run() -> None: """ Inject multi-model LLM calls showing escalating costs. - Renders Rich panels, or JSON if --json was passed to `ocw demo`. + Renders Rich panels, or JSON if --json was passed to `tj demo`. """ import click @@ -148,8 +148,8 @@ def _render(console, result, model_breakdown) -> None: tmp.print(table) tmp.print(Text(f"Total session cost: ${result.total_cost_usd:.4f}", style="bold red")) tmp.print("\n[dim]In your real agent:[/dim]") - tmp.print(" ocw cost --by model [dim]# per-model spend[/dim]") - tmp.print(" ocw cost [dim]# daily breakdown[/dim]") + tmp.print(" tj cost --by model [dim]# per-model spend[/dim]") + tmp.print(" tj cost [dim]# daily breakdown[/dim]") - console.print(Panel(buf.getvalue(), title="[green]What OCW reveals[/green]", border_style="green")) + console.print(Panel(buf.getvalue(), title="[green]What TokenJam reveals[/green]", border_style="green")) console.print() diff --git a/sdk-ts/README.md b/sdk-ts/README.md index 98b5982..6cb3404 100644 --- a/sdk-ts/README.md +++ b/sdk-ts/README.md @@ -2,7 +2,7 @@ TypeScript SDK for [TokenJam](https://github.com/Metabuilder-Labs/tokenjam) — local-first, OTel-native observability for AI agents. -Communicates with a running `ocw serve` instance via HTTP. No in-process OTel pipeline — spans are built with `SpanBuilder` and sent by `TjClient`. +Communicates with a running `tj serve` instance via HTTP. No in-process OTel pipeline — spans are built with `SpanBuilder` and sent by `TjClient`. > **Note:** Provider auto-instrumentation (the `patch_anthropic()`, `patch_openai()`, etc. convenience wrappers from the Python SDK) does not exist in this package. Every LLM call and tool call must be manually instrumented using `SpanBuilder`. @@ -12,11 +12,11 @@ Communicates with a running `ocw serve` instance via HTTP. No in-process OTel pi npm install @tokenjam/sdk ``` -Requires Node.js >= 18. Start the OCW server before sending spans: +Requires Node.js >= 18. Start the TokenJam server before sending spans: ```bash pip install tokenjam -ocw serve +tj serve ``` ## Quick start @@ -25,8 +25,8 @@ ocw serve import { TjClient, SpanBuilder, SpanStatus } from "@tokenjam/sdk"; const client = new TjClient({ - ingestSecret: "your-ingest-secret", // from ocw.toml security.ingest_secret - serviceName: "my-agent", // shown as agent ID in ocw status + ingestSecret: "your-ingest-secret", // from tj.toml security.ingest_secret + serviceName: "my-agent", // shown as agent ID in tj status }).start(); // Record an LLM call @@ -56,9 +56,9 @@ new TjClient(options: TjClientOptions) | Option | Type | Default | Description | |---|---|---|---| -| `ingestSecret` | `string` | required | Bearer token from `security.ingest_secret` in `ocw.toml` | -| `baseUrl` | `string` | `http://127.0.0.1:7391` | OCW server base URL | -| `serviceName` | `string` | `"ocw-ts-sdk"` | Reported as `service.name` in OTLP resource attributes; used as fallback agent ID | +| `ingestSecret` | `string` | required | Bearer token from `security.ingest_secret` in `tj.toml` | +| `baseUrl` | `string` | `http://127.0.0.1:7391` | `tj serve` base URL | +| `serviceName` | `string` | `"tj-ts-sdk"` | Reported as `service.name` in OTLP resource attributes; used as fallback agent ID | | `batchSize` | `number` | `50` | Max spans buffered before auto-flush | | `flushIntervalMs` | `number` | `5000` | Interval between automatic flushes (ms) | | `maxRetries` | `number` | `3` | Retry attempts on network errors and 5xx responses; 4xx errors are not retried | @@ -135,9 +135,9 @@ GenAIAttributes.REQUEST_MODEL // "gen_ai.request.model" GenAIAttributes.CACHE_CREATE_TOKENS // "gen_ai.usage.cache_creation_tokens" // ... -// OCW-specific attribute name strings -TjAttributes.COST_USD // "ocw.cost_usd" -TjAttributes.SANDBOX_EVENT // "ocw.sandbox.event" +// tj-specific attribute name strings +TjAttributes.COST_USD // "tokenjam.cost_usd" +TjAttributes.SANDBOX_EVENT // "tokenjam.sandbox.event" // ... // Claude Code OTel log event names and attribute constants @@ -148,7 +148,7 @@ ClaudeCodeEvents.INPUT_TOKENS // "input_tokens" // ... ``` -Use `ClaudeCodeEvents` when writing agents that consume Claude Code's own OTel log output (e.g. via the `ocw` MCP server or a log subscriber). +Use `ClaudeCodeEvents` when writing agents that consume Claude Code's own OTel log output (e.g. via the `tj` MCP server or a log subscriber). ## SpanKind and SpanStatus @@ -173,6 +173,6 @@ Unlike the Python SDK (`pip install tokenjam`), this package does **not** includ - **Session management** (`@watch()` decorator / `AgentSession` context manager) — you must manually build and send `invoke_agent` session spans. - **Provider auto-instrumentation** — no `patchAnthropic()`, `patchOpenAI()`, etc. Every LLM call requires an explicit `SpanBuilder`. - **Framework patches** — no LangChain JS, OpenAI Agents SDK, or Vercel AI SDK integration. -- **In-process OTel pipeline** — all telemetry goes over HTTP to `ocw serve`. +- **In-process OTel pipeline** — all telemetry goes over HTTP to `tj serve`. See the [Python SDK docs](https://github.com/Metabuilder-Labs/tokenjam#python-sdk) for the full-featured in-process instrumentation path. diff --git a/sdk-ts/src/client.test.ts b/sdk-ts/src/client.test.ts index 41070a0..ab35139 100644 --- a/sdk-ts/src/client.test.ts +++ b/sdk-ts/src/client.test.ts @@ -7,7 +7,7 @@ import { SpanKind, SpanStatus } from "./types.js"; /** * Spin up a local HTTP server that captures requests, so we can test - * the client without a real OCW server. + * the client without a real TokenJam server. */ function createMockServer(): { server: ReturnType; diff --git a/sdk-ts/src/client.ts b/sdk-ts/src/client.ts index 0e346d9..b974f86 100644 --- a/sdk-ts/src/client.ts +++ b/sdk-ts/src/client.ts @@ -1,5 +1,5 @@ /** - * TjClient — sends spans to the OCW REST API. + * TjClient — sends spans to the TokenJam REST API. * Communicates via HTTP POST to /api/v1/spans in OTLP JSON format. */ import { GenAIAttributes } from "./semconv.js"; @@ -7,7 +7,7 @@ import type { IngestResult, OtlpSpan, OtlpValue, Span, SpanBatch } from "./types import { SpanKind, SpanStatus } from "./types.js"; export interface TjClientOptions { - /** Base URL of the OCW server (default: http://127.0.0.1:7391) */ + /** Base URL of the TokenJam server (default: http://127.0.0.1:7391) */ baseUrl?: string; /** Ingest secret for authentication */ ingestSecret: string; @@ -15,7 +15,7 @@ export interface TjClientOptions { batchSize?: number; /** Flush interval in milliseconds (default: 5000) */ flushIntervalMs?: number; - /** Service name reported in OTLP resource attributes (default: "ocw-ts-sdk") */ + /** Service name reported in OTLP resource attributes (default: "tj-ts-sdk") */ serviceName?: string; /** Maximum retry attempts on network errors or 5xx responses (default: 3) */ maxRetries?: number; @@ -104,7 +104,7 @@ export class TjClient { this.ingestSecret = options.ingestSecret; this.batchSize = options.batchSize ?? 50; this.flushIntervalMs = options.flushIntervalMs ?? 5000; - this.serviceName = options.serviceName ?? "ocw-ts-sdk"; + this.serviceName = options.serviceName ?? "tj-ts-sdk"; this.maxRetries = options.maxRetries ?? 3; } @@ -216,7 +216,7 @@ export class TjClient { const text = await response.text().catch(() => ""); const error = new Error( - `OCW ingest failed: ${response.status} ${response.statusText} — ${text}` + `TokenJam ingest failed: ${response.status} ${response.statusText} — ${text}` ); // 4xx: not retriable (auth/validation failure) @@ -228,6 +228,6 @@ export class TjClient { lastError = error; } - throw lastError ?? new Error("OCW ingest failed after retries"); + throw lastError ?? new Error("TokenJam ingest failed after retries"); } } diff --git a/sdk-ts/src/semconv.test.ts b/sdk-ts/src/semconv.test.ts index 3ff3756..218d501 100644 --- a/sdk-ts/src/semconv.test.ts +++ b/sdk-ts/src/semconv.test.ts @@ -21,9 +21,9 @@ describe("GenAIAttributes", () => { }); describe("TjAttributes", () => { - it("has ocw-specific attribute keys", () => { - assert.equal(TjAttributes.COST_USD, "ocw.cost_usd"); - assert.equal(TjAttributes.ALERT_TYPE, "ocw.alert.type"); - assert.equal(TjAttributes.SANDBOX_EVENT, "ocw.sandbox.event"); + it("has tj-specific attribute keys", () => { + assert.equal(TjAttributes.COST_USD, "tokenjam.cost_usd"); + assert.equal(TjAttributes.ALERT_TYPE, "tokenjam.alert.type"); + assert.equal(TjAttributes.SANDBOX_EVENT, "tokenjam.sandbox.event"); }); }); diff --git a/sdk-ts/src/semconv.ts b/sdk-ts/src/semconv.ts index 1db78a8..96a1d31 100644 --- a/sdk-ts/src/semconv.ts +++ b/sdk-ts/src/semconv.ts @@ -1,6 +1,6 @@ /** * OpenTelemetry GenAI Semantic Convention attribute names. - * Mirrors ocw/otel/semconv.py — keep in sync. + * Mirrors tokenjam/otel/semconv.py — keep in sync. */ export const GenAIAttributes = { AGENT_ID: "gen_ai.agent.id", @@ -27,19 +27,19 @@ export const GenAIAttributes = { } as const; export const TjAttributes = { - COST_USD: "ocw.cost_usd", - ALERT_TYPE: "ocw.alert.type", - ALERT_SEVERITY: "ocw.alert.severity", - SANDBOX_EVENT: "ocw.sandbox.event", - EGRESS_HOST: "ocw.sandbox.egress_host", - EGRESS_PORT: "ocw.sandbox.egress_port", - FILESYSTEM_PATH: "ocw.sandbox.filesystem_path", - SYSCALL_NAME: "ocw.sandbox.syscall_name", + COST_USD: "tokenjam.cost_usd", + ALERT_TYPE: "tokenjam.alert.type", + ALERT_SEVERITY: "tokenjam.alert.severity", + SANDBOX_EVENT: "tokenjam.sandbox.event", + EGRESS_HOST: "tokenjam.sandbox.egress_host", + EGRESS_PORT: "tokenjam.sandbox.egress_port", + FILESYSTEM_PATH: "tokenjam.sandbox.filesystem_path", + SYSCALL_NAME: "tokenjam.sandbox.syscall_name", } as const; /** * Event names and attribute constants from Claude Code's OTel log exporter. - * Mirrors ClaudeCodeEvents in ocw/otel/semconv.py — keep in sync. + * Mirrors ClaudeCodeEvents in tokenjam/otel/semconv.py — keep in sync. */ export const ClaudeCodeEvents = { // Event names (logRecord body values) diff --git a/sdk-ts/src/types.ts b/sdk-ts/src/types.ts index 48d308b..664fa80 100644 --- a/sdk-ts/src/types.ts +++ b/sdk-ts/src/types.ts @@ -1,5 +1,5 @@ /** - * Core types for the OCW TypeScript SDK. + * Core types for the TokenJam TypeScript SDK. */ export enum SpanKind { diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py index 8c9f068..307cc21 100644 --- a/tests/integration/test_api.py +++ b/tests/integration/test_api.py @@ -258,7 +258,7 @@ async def test_get_metrics_returns_prometheus_format(client): resp = await client.get("/metrics") assert resp.status_code == 200 text = resp.text - assert "ocw_cost_usd_total" in text + assert "tj_cost_usd_total" in text assert "# HELP" in text assert "# TYPE" in text @@ -354,7 +354,7 @@ async def test_post_budget_zero_clears_limit(db): app = create_app(config=cfg, db=db, ingest_pipeline=pipeline) transport = httpx.ASGITransport(app=app) - with patch("tokenjam.api.routes.budget.find_config_file", return_value="/fake/ocw.toml"), \ + with patch("tokenjam.api.routes.budget.find_config_file", return_value="/fake/tj.toml"), \ patch("tokenjam.api.routes.budget.write_config"): async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: resp = await c.post("/api/v1/budget", json={"scope": "my-agent", "daily_usd": 0}) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index e59d4b8..a172553 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -383,7 +383,7 @@ def test_onboard_claude_code_preserves_existing(runner, tmp_path): def test_onboard_claude_code_creates_tj_config(runner, tmp_path): - """ocw config is created when none exists.""" + """tj config is created when none exists.""" fake_home = tmp_path / "home" fake_home.mkdir() @@ -422,7 +422,7 @@ def test_onboard_claude_code_resyncs_secret_on_rerun(runner, tmp_path): Regression test: previously the guard `if OTEL_EXPORTER_OTLP_ENDPOINT not in global_env` silently skipped updating the secret when settings.json already existed, causing 401s - whenever the OCW config was regenerated without re-running onboard --claude-code. + whenever the TokenJam config was regenerated without re-running onboard --claude-code. """ fake_home = tmp_path / "home" (fake_home / ".claude").mkdir(parents=True) @@ -462,14 +462,14 @@ def test_onboard_claude_code_resyncs_secret_on_rerun(runner, tmp_path): assert result.exit_code == 0 data = json.loads(settings_path.read_text()) - # Secret must be updated to match the current OCW config, not left as stale old value + # Secret must be updated to match the current TokenJam config, not left as stale old value assert data["env"]["OTEL_EXPORTER_OTLP_HEADERS"] == f"Authorization=Bearer {new_secret}" def test_onboard_claude_code_preserves_custom_otlp_headers(runner, tmp_path): """Re-running --claude-code does NOT overwrite manually customised OTEL_EXPORTER_OTLP_HEADERS. - Only headers that were previously written by ocw (i.e. contain 'Authorization=Bearer') + Only headers that were previously written by tj (i.e. contain 'Authorization=Bearer') are eligible for syncing. A header set by the user to something else is left untouched. """ fake_home = tmp_path / "home" @@ -513,7 +513,7 @@ def test_onboard_claude_code_preserves_custom_otlp_headers(runner, tmp_path): def test_onboard_does_not_prompt_for_daemon(runner, tmp_path): - """Regression: ocw onboard should auto-install the daemon without + """Regression: tj onboard should auto-install the daemon without prompting. The prompt was removed in v0.1.6 but reappeared. See v0.1.7 fix.""" with patch("tokenjam.cli.cmd_onboard.find_config_file", return_value=None), \ @@ -536,14 +536,14 @@ def test_onboard_no_daemon_skips_install(runner, tmp_path): def test_budget_show_displays_defaults(runner, db, config): - """ocw budget with no flags shows current budgets.""" + """tj budget with no flags shows current budgets.""" result = _invoke(runner, db, config, ["budget"]) assert result.exit_code == 0 assert "5.00" in result.output # fixture has daily_usd=5.0 def test_budget_set_global_writes_config(runner, db, config, tmp_path): - """ocw budget --daily updates global defaults and writes config.""" + """tj budget --daily updates global defaults and writes config.""" config_file = tmp_path / "config.toml" config_file.write_text("") @@ -560,7 +560,7 @@ def test_budget_set_global_writes_config(runner, db, config, tmp_path): def test_budget_set_agent_writes_config(runner, db, config, tmp_path): - """ocw budget --agent --daily --session updates per-agent budget.""" + """tj budget --agent --daily --session updates per-agent budget.""" config_file = tmp_path / "config.toml" config_file.write_text("") @@ -580,7 +580,7 @@ def test_budget_set_agent_writes_config(runner, db, config, tmp_path): def test_budget_set_negative_daily_rejected(runner, db, config, tmp_path): - """ocw budget --daily -5 should error, not silently clear the limit.""" + """tj budget --daily -5 should error, not silently clear the limit.""" config_file = tmp_path / "config.toml" config_file.write_text("") diff --git a/tests/integration/test_demos.py b/tests/integration/test_demos.py index 11a8b3c..63728c7 100644 --- a/tests/integration/test_demos.py +++ b/tests/integration/test_demos.py @@ -1,4 +1,4 @@ -"""Integration tests for `ocw demo` CLI command.""" +"""Integration tests for `tj demo` CLI command.""" from __future__ import annotations import json diff --git a/tests/synthetic/test_ingest.py b/tests/synthetic/test_ingest.py index 13eb1a0..c3e67c6 100644 --- a/tests/synthetic/test_ingest.py +++ b/tests/synthetic/test_ingest.py @@ -1,4 +1,4 @@ -"""Tests for ocw.core.ingest — sanitizer, pipeline, session resolution, capture stripping.""" +"""Tests for tokenjam.core.ingest — sanitizer, pipeline, session resolution, capture stripping.""" from __future__ import annotations from dataclasses import dataclass, field diff --git a/tests/synthetic/test_schema_validation.py b/tests/synthetic/test_schema_validation.py index d0d54d0..71f9b63 100644 --- a/tests/synthetic/test_schema_validation.py +++ b/tests/synthetic/test_schema_validation.py @@ -1,4 +1,4 @@ -"""Tests for ocw.core.schema_validator — schema validation, inference, skipping.""" +"""Tests for tokenjam.core.schema_validator — schema validation, inference, skipping.""" from __future__ import annotations import json diff --git a/tests/unit/test_cmd_stop.py b/tests/unit/test_cmd_stop.py index fe8c30e..1a28298 100644 --- a/tests/unit/test_cmd_stop.py +++ b/tests/unit/test_cmd_stop.py @@ -1,4 +1,4 @@ -"""Unit tests for `ocw stop` lifecycle behavior.""" +"""Unit tests for `tj stop` lifecycle behavior.""" from __future__ import annotations from unittest.mock import MagicMock, patch @@ -9,9 +9,9 @@ class TestStopSweepsForegroundProcesses: - """`ocw stop` must reap orphan foreground `ocw serve &` processes after + """`tj stop` must reap orphan foreground `tj serve &` processes after a successful launchctl unload — otherwise port 7391 stays held and - "ocw stop didn't actually stop ocw" (C6).""" + "tj stop didn't actually stop tj" (C6).""" def test_kills_foreground_after_launchd_unload(self, tmp_path, monkeypatch): # Pretend a plist exists so the launchd branch runs. @@ -43,7 +43,7 @@ def test_kills_foreground_after_launchd_unload(self, tmp_path, monkeypatch): def test_does_not_loop_on_slow_shutdown(self, tmp_path, monkeypatch): """SIGTERM is async — if the target process hasn't exited before the next pgrep, the sweep must NOT re-signal the same PID. Otherwise a - slow shutdown handler can make `ocw stop` hang forever.""" + slow shutdown handler can make `tj stop` hang forever.""" monkeypatch.setattr("tokenjam.cli.cmd_stop.Path.home", lambda: tmp_path) kill_mock = MagicMock() # pgrep keeps returning the same PID — simulates a process whose diff --git a/tests/unit/test_cost.py b/tests/unit/test_cost.py index e8ce1a6..7d2fc15 100644 --- a/tests/unit/test_cost.py +++ b/tests/unit/test_cost.py @@ -1,4 +1,4 @@ -"""Unit tests for ocw.core.cost and ocw.core.pricing.""" +"""Unit tests for tokenjam.core.cost and tokenjam.core.pricing.""" from __future__ import annotations import logging diff --git a/tests/unit/test_litellm_integration.py b/tests/unit/test_litellm_integration.py index fc233bd..bd743ba 100644 --- a/tests/unit/test_litellm_integration.py +++ b/tests/unit/test_litellm_integration.py @@ -1,4 +1,4 @@ -"""Tests for the LiteLLM integration (ocw.sdk.integrations.litellm).""" +"""Tests for the LiteLLM integration (tokenjam.sdk.integrations.litellm).""" from __future__ import annotations import asyncio diff --git a/tests/unit/test_mcp_server.py b/tests/unit/test_mcp_server.py index 3df5ea3..7c1120c 100644 --- a/tests/unit/test_mcp_server.py +++ b/tests/unit/test_mcp_server.py @@ -1,4 +1,4 @@ -"""Unit tests for OCW MCP server tool handlers.""" +"""Unit tests for TokenJam MCP server tool handlers.""" from __future__ import annotations import json diff --git a/tests/unit/test_onboard_daemon.py b/tests/unit/test_onboard_daemon.py index 442ffd1..9dbb5e8 100644 --- a/tests/unit/test_onboard_daemon.py +++ b/tests/unit/test_onboard_daemon.py @@ -67,12 +67,12 @@ def test_unsupported_platform(self, monkeypatch): class TestLaunchdInstallUsesWFlag: """`_install_launchd` must pass -w to both unload and load so it clears - the Disabled=true flag that `ocw stop` writes (C1).""" + the Disabled=true flag that `tj stop` writes (C1).""" def test_load_uses_w_flag(self, tmp_path, monkeypatch): from tokenjam.cli.cmd_onboard import _install_launchd monkeypatch.setattr("tokenjam.cli.cmd_onboard.Path.home", lambda: tmp_path) - monkeypatch.setattr("tokenjam.cli.cmd_onboard.shutil.which", lambda _: "/usr/bin/ocw") + monkeypatch.setattr("tokenjam.cli.cmd_onboard.shutil.which", lambda _: "/usr/bin/tj") run_mock = MagicMock(return_value=MagicMock(returncode=0)) with patch("tokenjam.cli.cmd_onboard.subprocess.run", run_mock): diff --git a/tokenjam/api/routes/metrics.py b/tokenjam/api/routes/metrics.py index 8f7c6a9..e23fff8 100644 --- a/tokenjam/api/routes/metrics.py +++ b/tokenjam/api/routes/metrics.py @@ -20,30 +20,30 @@ async def prometheus_metrics(request: Request) -> PlainTextResponse: lines: list[str] = [] # -- Cost per agent -- - _add_header(lines, "ocw_cost_usd_total", "gauge", "Running cost total per agent") + _add_header(lines, "tj_cost_usd_total", "gauge", "Running cost total per agent") cost_rows = db.get_cost_summary(CostFilters(group_by="agent")) for row in cost_rows: agent = row.agent_id or "unknown" - lines.append(f'ocw_cost_usd_total{{agent_id="{_escape(agent)}"}} {row.cost_usd}') + lines.append(f'tj_cost_usd_total{{agent_id="{_escape(agent)}"}} {row.cost_usd}') # -- Tokens per agent and type -- - _add_header(lines, "ocw_tokens_total", "counter", "Token usage by type") + _add_header(lines, "tj_tokens_total", "counter", "Token usage by type") for row in cost_rows: agent = row.agent_id or "unknown" - lines.append(f'ocw_tokens_total{{agent_id="{_escape(agent)}",type="input"}} {row.input_tokens}') - lines.append(f'ocw_tokens_total{{agent_id="{_escape(agent)}",type="output"}} {row.output_tokens}') + lines.append(f'tj_tokens_total{{agent_id="{_escape(agent)}",type="input"}} {row.input_tokens}') + lines.append(f'tj_tokens_total{{agent_id="{_escape(agent)}",type="output"}} {row.output_tokens}') # -- Tool calls per agent -- tool_rows = db.get_tool_calls(None, None, None) - _add_header(lines, "ocw_tool_calls_total", "counter", "Total tool calls per agent and tool") + _add_header(lines, "tj_tool_calls_total", "counter", "Total tool calls per agent and tool") for row in tool_rows: agent = row.get("agent_id") or "unknown" tool = row.get("tool_name") or "unknown" count = row.get("call_count", 0) - lines.append(f'ocw_tool_calls_total{{agent_id="{_escape(agent)}",tool_name="{_escape(tool)}"}} {count}') + lines.append(f'tj_tool_calls_total{{agent_id="{_escape(agent)}",tool_name="{_escape(tool)}"}} {count}') # -- Alerts per agent, type, severity -- - _add_header(lines, "ocw_alerts_total", "counter", "Total alerts fired") + _add_header(lines, "tj_alerts_total", "counter", "Total alerts fired") alerts = db.get_alerts(AlertFilters(limit=10000)) alert_counts: dict[tuple[str, str, str], int] = {} for a in alerts: @@ -51,19 +51,19 @@ async def prometheus_metrics(request: Request) -> PlainTextResponse: alert_counts[key] = alert_counts.get(key, 0) + 1 for (agent, atype, sev), count in alert_counts.items(): lines.append( - f'ocw_alerts_total{{agent_id="{_escape(agent)}",' + f'tj_alerts_total{{agent_id="{_escape(agent)}",' f'type="{_escape(atype)}",severity="{_escape(sev)}"}} {count}' ) # -- Session duration (latest completed per agent) -- - _add_header(lines, "ocw_session_duration_seconds", "gauge", "Duration of last completed session") + _add_header(lines, "tj_session_duration_seconds", "gauge", "Duration of last completed session") # Collect unique agent_ids from cost rows agent_ids = {row.agent_id for row in cost_rows if row.agent_id} for agent_id in sorted(agent_ids): sessions = db.get_completed_sessions(agent_id, limit=1) if sessions and sessions[0].duration_seconds is not None: lines.append( - f'ocw_session_duration_seconds{{agent_id="{_escape(agent_id)}"}} ' + f'tj_session_duration_seconds{{agent_id="{_escape(agent_id)}"}} ' f'{sessions[0].duration_seconds:.1f}' ) diff --git a/tokenjam/cli/cmd_demo.py b/tokenjam/cli/cmd_demo.py index 61399ca..890b9e8 100644 --- a/tokenjam/cli/cmd_demo.py +++ b/tokenjam/cli/cmd_demo.py @@ -69,7 +69,7 @@ def _list_scenarios(scenarios: dict[str, ModuleType]) -> None: console.print() console.print( - "[bold]OCW Agent Incident Library[/bold]\n" + "[bold]TokenJam Agent Incident Library[/bold]\n" "Reproducible AI agent failures — no API keys, no config needed.\n" ) table = Table(box=box.SIMPLE, show_header=True, header_style="bold") diff --git a/tokenjam/cli/cmd_mcp.py b/tokenjam/cli/cmd_mcp.py index f93faef..2dc41df 100644 --- a/tokenjam/cli/cmd_mcp.py +++ b/tokenjam/cli/cmd_mcp.py @@ -28,7 +28,7 @@ def _port_open(host: str, port: int) -> bool: def _start_and_wait(host: str, port: int, timeout: float = 10.0) -> bool: """Start tj serve in the background and wait up to *timeout* seconds for it to accept connections. Returns True if the server is ready in time.""" - ocw_bin = shutil.which("tj") or sys.argv[0] + tj_bin = shutil.which("tj") or sys.argv[0] popen_kwargs: dict = { "stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, @@ -39,7 +39,7 @@ def _start_and_wait(host: str, port: int, timeout: float = 10.0) -> bool: popen_kwargs["start_new_session"] = True try: - subprocess.Popen([ocw_bin, "serve"], **popen_kwargs) + subprocess.Popen([tj_bin, "serve"], **popen_kwargs) except (FileNotFoundError, OSError): return False @@ -54,7 +54,7 @@ def _start_and_wait(host: str, port: int, timeout: float = 10.0) -> bool: @click.command("mcp") @click.pass_context def cmd_mcp(ctx: click.Context) -> None: - """Start the OCW MCP server (stdio transport for Claude Code).""" + """Start the TokenJam MCP server (stdio transport for Claude Code).""" from tokenjam.mcp.server import mcp, init config_path = find_config_file() diff --git a/tokenjam/cli/cmd_onboard.py b/tokenjam/cli/cmd_onboard.py index 051d111..8561ab9 100644 --- a/tokenjam/cli/cmd_onboard.py +++ b/tokenjam/cli/cmd_onboard.py @@ -217,7 +217,7 @@ def _onboard_claude_code( global_settings = {} # Write global OTLP config — always overwrite endpoint vars so reinstall stays in sync. - # Custom headers (non-OCW) are preserved; only OCW-generated "Authorization=Bearer" + # Custom headers (non-TokenJam) are preserved; only TokenJam-generated "Authorization=Bearer" # headers are replaced when the secret rotates. port = config.api.port secret = config.security.ingest_secret @@ -346,7 +346,7 @@ def _onboard_codex( # `--codex` always writes to the global config, mirroring `--claude-code`. # Codex's own config (~/.codex/config.toml) is global and the agent_id # `codex_exec` is project-agnostic by design (Codex hardcodes service.name - # in its binary). Per-project OCW configs would rotate the secret on every + # in its binary). Per-project TokenJam configs would rotate the secret on every # onboard, breaking the running server. config_path = Path.home() / ".config" / "tj" / "config.toml" @@ -462,7 +462,7 @@ def _onboard_codex( console.print() console.print("[bold green]Codex CLI observability configured.[/bold green]") console.print(f" Codex config: {codex_config_path}") - console.print(f" OCW config: {config_path}") + console.print(f" TokenJam config: {config_path}") if budget and budget > 0: console.print(f" Daily budget: ${budget:.2f}") console.print(f" OTLP endpoint: http://127.0.0.1:{port}/v1/logs") @@ -475,7 +475,7 @@ def _onboard_codex( if not want_daemon: console.print("[dim]Start the server:[/dim] tj serve") console.print( - "[dim]Codex can now call OCW tools (open_dashboard, get_status, etc.) directly.[/dim]" + "[dim]Codex can now call TokenJam tools (open_dashboard, get_status, etc.) directly.[/dim]" ) console.print("[dim]Then run:[/dim] tj traces") @@ -513,7 +513,7 @@ def _codex_mcp_toml_block() -> str: """Return the [mcp_servers.tj] TOML block for ~/.codex/config.toml.""" return ( "[mcp_servers.tj]\n" - "# Managed by tj — gives Codex access to OCW observability tools\n" + "# Managed by tj — gives Codex access to TokenJam observability tools\n" 'command = "tj"\n' 'args = ["mcp"]\n' ) @@ -660,7 +660,7 @@ def _derive_project_name() -> str: def _daemon_already_running() -> bool: - """Check if the OCW daemon is already installed and loaded.""" + """Check if the TokenJam daemon is already installed and loaded.""" system = platform.system() if system == "Darwin": plist = Path.home() / "Library/LaunchAgents/com.tokenjam.serve.plist" diff --git a/tokenjam/cli/cmd_uninstall.py b/tokenjam/cli/cmd_uninstall.py index 60c1192..8ee9e4d 100644 --- a/tokenjam/cli/cmd_uninstall.py +++ b/tokenjam/cli/cmd_uninstall.py @@ -15,10 +15,10 @@ @click.option("--yes", is_flag=True, help="Skip confirmation prompt") @click.pass_context def cmd_uninstall(ctx: click.Context, yes: bool) -> None: - """Remove all OCW data, config, and daemon.""" + """Remove all TokenJam data, config, and daemon.""" if not yes: confirmed = click.confirm( - "This will delete all OCW data including telemetry history. Continue?", + "This will delete all TokenJam data including telemetry history. Continue?", default=False, ) if not confirmed: @@ -58,10 +58,10 @@ def cmd_uninstall(ctx: click.Context, yes: bool) -> None: console.print(f" Removed {systemd_path}") # 5. Delete ~/.tj/ (telemetry DB) - ocw_dir = Path.home() / ".tj" - if ocw_dir.exists(): - shutil.rmtree(ocw_dir) - console.print(f" Removed {ocw_dir}") + tj_dir = Path.home() / ".tj" + if tj_dir.exists(): + shutil.rmtree(tj_dir) + console.print(f" Removed {tj_dir}") # 6. Read projects index BEFORE deleting the global config dir. global_config_dir = Path.home() / ".config" / "tj" @@ -80,10 +80,10 @@ def cmd_uninstall(ctx: click.Context, yes: bool) -> None: console.print(f" Removed {global_config_dir}") # 8. Delete local .tj/ if present - local_ocw = Path(".tj") - if local_ocw.exists(): - shutil.rmtree(local_ocw) - console.print(f" Removed {local_ocw}") + local_tj = Path(".tj") + if local_tj.exists(): + shutil.rmtree(local_tj) + console.print(f" Removed {local_tj}") # 9. Delete temp files for tmp_file in ["/tmp/tj-serve.out", "/tmp/tj-serve.err"]: @@ -92,7 +92,7 @@ def cmd_uninstall(ctx: click.Context, yes: bool) -> None: p.unlink() console.print(f" Removed {tmp_file}") - # 10. Remove OCW env vars from ~/.claude/settings.json + # 10. Remove TokenJam env vars from ~/.claude/settings.json _GLOBAL_TJ_KEYS = { "CLAUDE_CODE_ENABLE_TELEMETRY", "OTEL_LOGS_EXPORTER", @@ -111,7 +111,7 @@ def cmd_uninstall(ctx: click.Context, yes: bool) -> None: if removed: gs["env"] = env global_settings_path.write_text(json.dumps(gs, indent=2) + "\n") - console.print(f" Cleaned {len(removed)} OCW env vars from {global_settings_path}") + console.print(f" Cleaned {len(removed)} TokenJam env vars from {global_settings_path}") except Exception as exc: console.print(f" [yellow]Could not clean {global_settings_path}: {exc}[/yellow]") @@ -150,7 +150,7 @@ def cmd_uninstall(ctx: click.Context, yes: bool) -> None: ) if cleaned != text: zshrc.write_text(cleaned) - console.print(f" Removed OCW env block from {zshrc}") + console.print(f" Removed TokenJam env block from {zshrc}") except Exception as exc: console.print(f" [yellow]Could not clean {zshrc}: {exc}[/yellow]") diff --git a/tokenjam/mcp/server.py b/tokenjam/mcp/server.py index 35a1142..b0c9c3a 100644 --- a/tokenjam/mcp/server.py +++ b/tokenjam/mcp/server.py @@ -1,4 +1,4 @@ -"""OCW MCP server — exposes observability data to Claude Code via stdio.""" +"""TokenJam MCP server — exposes observability data to Claude Code via stdio.""" from __future__ import annotations from fastmcp import FastMCP @@ -28,7 +28,7 @@ def init(ro_conn, config, serve_url: str | None = None) -> None: def _no_config() -> dict: return { "error": ( - "No OCW config found. " + "No TokenJam config found. " "Run 'tj onboard --claude-code' (Claude Code) " "or 'tj onboard --codex' (Codex CLI) to set up." ) @@ -744,7 +744,7 @@ def _tool_setup_project( existing["env"] = env settings_path.write_text(json.dumps(existing, indent=2) + "\n") - # Add agent entry to OCW config + # Add agent entry to TokenJam config if agent_id not in config.agents: config.agents[agent_id] = AgentConfig() write_config(config, Path(config_path)) @@ -800,7 +800,7 @@ def _tool_open_dashboard(config) -> dict: # Spawn tj serve detached from this process. # start_new_session is Unix-only; use DETACHED_PROCESS on Windows instead. import shutil as _shutil - ocw_bin = _shutil.which("tj") or sys.argv[0] + tj_bin = _shutil.which("tj") or sys.argv[0] import sys as _sys popen_kwargs: dict = { "stdout": subprocess.DEVNULL, @@ -811,9 +811,9 @@ def _tool_open_dashboard(config) -> dict: else: popen_kwargs["start_new_session"] = True try: - subprocess.Popen([ocw_bin, "serve"], **popen_kwargs) + subprocess.Popen([tj_bin, "serve"], **popen_kwargs) except (FileNotFoundError, OSError): - return {"error": f"Could not find '{ocw_bin}' on PATH. Run 'tj serve' manually."} + return {"error": f"Could not find '{tj_bin}' on PATH. Run 'tj serve' manually."} # Wait up to 5 seconds for the port to open for _ in range(10): @@ -867,7 +867,7 @@ def get_budget_headroom(agent_id: str) -> dict: @mcp.tool() def list_agents() -> dict: """ - List all agents OCW has ever seen, with first/last seen timestamps and lifetime cost. + List all agents TokenJam has ever seen, with first/last seen timestamps and lifetime cost. Use this when the user asks which agents are being tracked, wants an overview of all projects, or asks about total spend across agents. """ @@ -1034,10 +1034,10 @@ def acknowledge_alert(alert_id: str) -> dict: @mcp.tool() def setup_project(agent_id: str | None = None, project_path: str | None = None) -> dict: """ - Configure the current project to send telemetry to OCW. Writes OTEL_RESOURCE_ATTRIBUTES + Configure the current project to send telemetry to TokenJam. Writes OTEL_RESOURCE_ATTRIBUTES into .claude/settings.json so Claude Code tags spans with the right agent ID. For Codex CLI users the agent ID is set in ~/.codex/config.toml via 'tj onboard --codex'. Use - this when the user wants to start monitoring a new project, or asks how to set up OCW + this when the user wants to start monitoring a new project, or asks how to set up TokenJam for this repo. Infers agent_id from the git remote if not provided. """ try: @@ -1056,7 +1056,7 @@ def setup_project(agent_id: str | None = None, project_path: str | None = None) @mcp.tool() def open_dashboard() -> dict: """ - Open the OCW web dashboard in the browser. Starts `tj serve` in the background if it + Open the TokenJam web dashboard in the browser. Starts `tj serve` in the background if it is not already running — do NOT start tj serve manually via Bash. Call this tool whenever the user asks to open the dashboard, view the UI, or browse observability data visually. Returns the URL to open. Safe to call repeatedly — detects if already running. diff --git a/tokenjam/sdk/bootstrap.py b/tokenjam/sdk/bootstrap.py index 1063133..72b44ea 100644 --- a/tokenjam/sdk/bootstrap.py +++ b/tokenjam/sdk/bootstrap.py @@ -1,5 +1,5 @@ """ -Auto-bootstrap: lazily initialise the OCW TracerProvider + IngestPipeline +Auto-bootstrap: lazily initialise the TokenJam TracerProvider + IngestPipeline the first time @watch() or a provider patch creates a span. This ensures that SDK users don't need to manually wire up the pipeline. @@ -57,7 +57,7 @@ def ensure_initialised() -> None: # Ensure spans are flushed on exit atexit.register(_shutdown) - logger.debug("OCW: writing spans to local DuckDB (%s)", config.storage.path) + logger.debug("TokenJam: writing spans to local DuckDB (%s)", config.storage.path) except Exception as exc: # DuckDB lock error — try HTTP fallback @@ -71,7 +71,7 @@ def ensure_initialised() -> None: return except Exception: pass - logger.warning("OCW bootstrap failed — spans will not be recorded: %s", exc) + logger.warning("TokenJam bootstrap failed — spans will not be recorded: %s", exc) _initialised = True # Don't retry on every call diff --git a/tokenjam/sdk/integrations/nemoclaw.py b/tokenjam/sdk/integrations/nemoclaw.py index c5c5be0..223307e 100644 --- a/tokenjam/sdk/integrations/nemoclaw.py +++ b/tokenjam/sdk/integrations/nemoclaw.py @@ -85,27 +85,27 @@ async def connect(self) -> None: def _translate_event(self, event: dict) -> NormalizedSpan | None: """Convert an OpenShell gateway event to a NormalizedSpan.""" event_type = event.get("type", "") - ocw_event = SANDBOX_EVENT_MAP.get(event_type) - if not ocw_event: + tj_event = SANDBOX_EVENT_MAP.get(event_type) + if not tj_event: return None now = utcnow() attrs: dict = { - TjAttributes.SANDBOX_EVENT: ocw_event, + TjAttributes.SANDBOX_EVENT: tj_event, } - if ocw_event == "network_blocked": + if tj_event == "network_blocked": attrs[TjAttributes.EGRESS_HOST] = event.get("host", "unknown") attrs[TjAttributes.EGRESS_PORT] = event.get("port") - elif ocw_event == "fs_denied": + elif tj_event == "fs_denied": attrs[TjAttributes.FILESYSTEM_PATH] = event.get("path", "unknown") - elif ocw_event == "syscall_denied": + elif tj_event == "syscall_denied": attrs[TjAttributes.SYSCALL_NAME] = event.get("syscall", "unknown") return NormalizedSpan( span_id=new_span_id(), trace_id=new_trace_id(), - name=f"sandbox.{ocw_event}", + name=f"sandbox.{tj_event}", kind=SpanKind.INTERNAL, status_code=SpanStatus.ERROR, start_time=now, diff --git a/tokenjam/ui/index.html b/tokenjam/ui/index.html index 2dca1a1..a3024e2 100644 --- a/tokenjam/ui/index.html +++ b/tokenjam/ui/index.html @@ -527,7 +527,7 @@ const html = htm.bind(h); // --- API --- -const apiKeyMeta = document.querySelector('meta[name="ocw-api-key"]'); +const apiKeyMeta = document.querySelector('meta[name="tj-api-key"]'); const API_KEY = apiKeyMeta ? apiKeyMeta.getAttribute('content') : null; async function api(path, params = {}) { @@ -975,7 +975,7 @@

${friendlySpanName(sel.name)}

- ${alerts.length === 0 ? html`
No alerts fired. Configure sensitive actions in ${'`.ocw/config.toml`'}.
` : html` + ${alerts.length === 0 ? html`
No alerts fired. Configure sensitive actions in ${'`tj.toml`'}.
` : html`
TimeSeverityTypeTitleAgent