From 2654cfb5bff8a1a0731bd9714a34d3b11829e873 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 30 Jul 2026 23:07:45 +0800 Subject: [PATCH 1/2] Switch coding-agent plugins to the Rust daemon Replace the legacy Codex TypeScript runtime and Claude shell state machine with fail-open launchers for bt agents hook. Switch the real-agent integration harness from generated direct-hook fixtures to the production plugin packages so the stacked PR validates the actual cutover on every platform. Signed-off-by: Stephen Belanger --- .github/workflows/smoke-codex.yml | 2 +- bt-daemon/tests/support/agents/claude.rs | 4 +- bt-daemon/tests/support/agents/codex.rs | 6 +- bt-daemon/tests/support/agents/mod.rs | 8 +- bt-daemon/tests/support/agents/test_plugin.rs | 95 - src/plugins/claude/content/CONTRIBUTING.md | 249 +- src/plugins/claude/content/Makefile | 7 +- src/plugins/claude/content/README.md | 44 +- .../.claude-plugin/plugin.json | 2 +- .../trace-claude-code/bin/claude-hook.cmd | 38 + .../trace-claude-code/bin/claude-hook.sh | 73 + .../plugins/trace-claude-code/hooks/common.sh | 1323 ------ .../trace-claude-code/hooks/hooks.json | 60 +- .../hooks/permission_denied.sh | 95 - .../trace-claude-code/hooks/post_tool_use.sh | 217 - .../hooks/post_tool_use_failure.sh | 114 - .../trace-claude-code/hooks/record_event.sh | 41 - .../trace-claude-code/hooks/session_end.sh | 83 - .../trace-claude-code/hooks/session_start.sh | 145 - .../trace-claude-code/hooks/stop_hook.sh | 529 --- .../hooks/user_prompt_expansion.sh | 92 - .../hooks/user_prompt_submit.sh | 158 - .../plugins/trace-claude-code/hooks/worker.sh | 138 - .../plugins/trace-claude-code/setup.sh | 247 +- .../trace-claude-code/test/helpers/assert.sh | 207 - .../test/helpers/curl_stub.sh | 224 - .../test/helpers/fixtures.sh | 127 - .../trace-claude-code/test/helpers/harness.sh | 121 - .../trace-claude-code/test/helpers/replay.sh | 134 - .../test/helpers/span_tree.sh | 88 - .../trace-claude-code/test/reconcile_usage.sh | 166 - .../trace-claude-code/test/record_session.sh | 100 - .../trace-claude-code/test/run_tests.sh | 117 - .../trace-claude-code/test/test_common.sh | 460 -- .../test/test_fixture_replay.sh | 361 -- .../test/test_full_pipeline.sh | 174 - .../test/test_insert_span.sh | 262 -- .../test/test_post_tool_use.sh | 322 -- .../trace-claude-code/test/test_queue.sh | 592 --- .../trace-claude-code/test/test_replay.sh | 150 - .../test/test_session_start.sh | 244 - .../trace-claude-code/test/test_stop_hook.sh | 375 -- .../test/test_user_prompt_expansion.sh | 120 - .../test/test_user_prompt_submit.sh | 155 - src/plugins/claude/validate.sh | 24 + src/plugins/codex/build.sh | 49 +- src/plugins/codex/content/AGENTS.md | 3 +- src/plugins/codex/content/install.sh | 49 +- .../trace-codex/.codex-plugin/plugin.json | 2 +- .../content/plugins/trace-codex/.gitignore | 10 - .../content/plugins/trace-codex/AGENTS.md | 227 +- .../content/plugins/trace-codex/Makefile | 117 +- .../content/plugins/trace-codex/README.md | 172 +- .../plugins/trace-codex/bin/codex-hook.cmd | 41 +- .../plugins/trace-codex/bin/codex-hook.sh | 104 +- .../content/plugins/trace-codex/biome.json | 29 - .../plugins/trace-codex/config.json.example | 16 - .../content/plugins/trace-codex/package.json | 39 - .../plugins/trace-codex/pnpm-lock.yaml | 4059 ----------------- .../plugins/trace-codex/pnpm-workspace.yaml | 11 - .../plugins/trace-codex/scripts/build.ts | 104 - .../trace-codex/scripts/mock-collector.ts | 128 - .../plugins/trace-codex/scripts/smoke-test.sh | 177 - .../trace-codex/scripts/token-proxy.ts | 390 -- .../src/agents/codex/event-builder.test.ts | 171 - .../src/agents/codex/event-builder.ts | 125 - .../src/agents/codex/event-processor.test.ts | 2442 ---------- .../src/agents/codex/event-processor.ts | 2507 ---------- .../trace-codex/src/agents/codex/register.ts | 63 - .../src/agents/codex/resume.test.ts | 671 --- .../src/agents/codex/settings.test.ts | 188 - .../trace-codex/src/agents/codex/settings.ts | 160 - .../src/agents/codex/snapshot-store.test.ts | 103 - .../src/agents/codex/snapshot-store.ts | 161 - .../src/agents/codex/state-snapshot.test.ts | 62 - .../src/agents/codex/state-snapshot.ts | 154 - .../src/agents/codex/test-helpers.ts | 410 -- .../src/agents/codex/transcript-reader.ts | 155 - .../src/agents/codex/transcript.test.ts | 47 - .../src/agents/codex/transcript.ts | 81 - .../trace-codex/src/braintrust/logger.test.ts | 29 - .../trace-codex/src/braintrust/logger.ts | 230 - .../plugins/trace-codex/src/client/client.ts | 107 - .../src/client/ensure-server.test.ts | 121 - .../trace-codex/src/client/ensure-server.ts | 141 - .../trace-codex/src/client/spawn-server.ts | 47 - .../plugins/trace-codex/src/config.test.ts | 37 - .../content/plugins/trace-codex/src/config.ts | 68 - .../trace-codex/src/git-metadata.test.ts | 73 - .../plugins/trace-codex/src/git-metadata.ts | 53 - .../content/plugins/trace-codex/src/index.ts | 84 - .../content/plugins/trace-codex/src/log.ts | 68 - .../src/processor/event-processor.ts | 35 - .../trace-codex/src/processor/lru-map.test.ts | 51 - .../trace-codex/src/processor/lru-map.ts | 62 - .../src/processor/processor-registry.test.ts | 115 - .../src/processor/processor-registry.ts | 113 - .../trace-codex/src/replay/replay.test.ts | 71 - .../plugins/trace-codex/src/replay/replay.ts | 82 - .../trace-codex/src/server/enqueue-client.ts | 63 - .../src/server/event-queue.test.ts | 207 - .../trace-codex/src/server/event-queue.ts | 179 - .../trace-codex/src/server/mutex.test.ts | 60 - .../plugins/trace-codex/src/server/mutex.ts | 35 - .../trace-codex/src/server/recorder.test.ts | 128 - .../trace-codex/src/server/recorder.ts | 73 - .../trace-codex/src/server/routes.test.ts | 163 - .../plugins/trace-codex/src/server/routes.ts | 130 - .../trace-codex/src/server/server.test.ts | 184 - .../plugins/trace-codex/src/server/server.ts | 227 - .../trace-codex/src/server/state.test.ts | 44 - .../plugins/trace-codex/src/server/state.ts | 37 - .../plugins/trace-codex/src/test-helpers.ts | 377 -- .../plugins/trace-codex/src/version.ts | 9 - .../content/plugins/trace-codex/tsconfig.json | 19 - .../plugins/trace-codex/tsup.config.ts | 24 - .../plugins/trace-codex/vitest.config.ts | 11 - src/plugins/codex/publish.sh | 11 +- src/plugins/codex/validate.sh | 19 + 119 files changed, 547 insertions(+), 24560 deletions(-) delete mode 100644 bt-daemon/tests/support/agents/test_plugin.rs create mode 100644 src/plugins/claude/content/plugins/trace-claude-code/bin/claude-hook.cmd create mode 100755 src/plugins/claude/content/plugins/trace-claude-code/bin/claude-hook.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/hooks/common.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/hooks/permission_denied.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/hooks/post_tool_use.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/hooks/post_tool_use_failure.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/hooks/record_event.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/hooks/session_end.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/hooks/session_start.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/hooks/stop_hook.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/hooks/user_prompt_expansion.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/hooks/user_prompt_submit.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/hooks/worker.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/test/helpers/assert.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/test/helpers/curl_stub.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/test/helpers/fixtures.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/test/helpers/harness.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/test/helpers/replay.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/test/helpers/span_tree.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/reconcile_usage.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/record_session.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/run_tests.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_common.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_fixture_replay.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_full_pipeline.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_insert_span.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_post_tool_use.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_queue.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_replay.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_session_start.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_stop_hook.sh delete mode 100644 src/plugins/claude/content/plugins/trace-claude-code/test/test_user_prompt_expansion.sh delete mode 100755 src/plugins/claude/content/plugins/trace-claude-code/test/test_user_prompt_submit.sh delete mode 100644 src/plugins/codex/content/plugins/trace-codex/.gitignore delete mode 100644 src/plugins/codex/content/plugins/trace-codex/biome.json delete mode 100644 src/plugins/codex/content/plugins/trace-codex/config.json.example delete mode 100644 src/plugins/codex/content/plugins/trace-codex/package.json delete mode 100644 src/plugins/codex/content/plugins/trace-codex/pnpm-lock.yaml delete mode 100644 src/plugins/codex/content/plugins/trace-codex/pnpm-workspace.yaml delete mode 100644 src/plugins/codex/content/plugins/trace-codex/scripts/build.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/scripts/mock-collector.ts delete mode 100755 src/plugins/codex/content/plugins/trace-codex/scripts/smoke-test.sh delete mode 100644 src/plugins/codex/content/plugins/trace-codex/scripts/token-proxy.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/event-builder.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/event-builder.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/event-processor.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/event-processor.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/register.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/resume.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/settings.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/settings.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/snapshot-store.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/snapshot-store.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/state-snapshot.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/state-snapshot.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/test-helpers.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/transcript-reader.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/transcript.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/agents/codex/transcript.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/braintrust/logger.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/braintrust/logger.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/client/client.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/client/ensure-server.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/client/ensure-server.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/client/spawn-server.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/config.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/config.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/git-metadata.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/git-metadata.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/index.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/log.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/processor/event-processor.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/processor/lru-map.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/processor/lru-map.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/processor/processor-registry.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/processor/processor-registry.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/replay/replay.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/replay/replay.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/enqueue-client.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/event-queue.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/event-queue.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/mutex.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/mutex.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/recorder.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/recorder.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/routes.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/routes.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/server.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/server.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/state.test.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/server/state.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/test-helpers.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/src/version.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/tsconfig.json delete mode 100644 src/plugins/codex/content/plugins/trace-codex/tsup.config.ts delete mode 100644 src/plugins/codex/content/plugins/trace-codex/vitest.config.ts diff --git a/.github/workflows/smoke-codex.yml b/.github/workflows/smoke-codex.yml index 250b83c..abf5f85 100644 --- a/.github/workflows/smoke-codex.yml +++ b/.github/workflows/smoke-codex.yml @@ -77,7 +77,7 @@ jobs: - name: Require daemon-capable bt run: | - "$HOME/.local/bin/bt" daemon hook --help + "$HOME/.local/bin/bt" agents hook --help - name: Install plugin from ${{ inputs.dist_repo }} env: diff --git a/bt-daemon/tests/support/agents/claude.rs b/bt-daemon/tests/support/agents/claude.rs index 261ae5b..7fb32ec 100644 --- a/bt-daemon/tests/support/agents/claude.rs +++ b/bt-daemon/tests/support/agents/claude.rs @@ -1,4 +1,4 @@ -use super::{command_from_env, test_plugin, AgentOutput, ProcessOptions}; +use super::{command_from_env, repository_root, AgentOutput, ProcessOptions}; use crate::support::agent_process::AgentTestWorld; use std::ffi::OsString; use std::path::PathBuf; @@ -64,7 +64,7 @@ impl ClaudeAgent { pub async fn run(&self, world: &AgentTestWorld, run: ClaudeRun) -> AgentOutput { let session_id = Uuid::new_v4().to_string(); - let plugin = test_plugin::claude_plugin(world); + let plugin = repository_root().join("src/plugins/claude/content/plugins/trace-claude-code"); let mut command = command_from_env("CLAUDE_BIN", "claude"); command .args([ diff --git a/bt-daemon/tests/support/agents/codex.rs b/bt-daemon/tests/support/agents/codex.rs index cdedb26..be33da0 100644 --- a/bt-daemon/tests/support/agents/codex.rs +++ b/bt-daemon/tests/support/agents/codex.rs @@ -1,4 +1,4 @@ -use super::{command_from_env, configured_home, test_plugin, AgentOutput, ProcessOptions}; +use super::{command_from_env, configured_home, repository_root, AgentOutput, ProcessOptions}; use crate::support::agent_process::AgentTestWorld; use std::ffi::OsString; use std::path::PathBuf; @@ -56,7 +56,7 @@ impl CodexAgent { let home = world.temp_path("codex-home"); std::fs::create_dir_all(&home).expect("create Codex home"); - let marketplace = test_plugin::codex_marketplace(world); + let marketplace = repository_root().join("src/plugins/codex/content"); let mut add_marketplace = command_from_env("CODEX_BIN", "codex"); add_marketplace .arg("plugin") @@ -69,7 +69,7 @@ impl CodexAgent { let mut add_plugin = command_from_env("CODEX_BIN", "codex"); add_plugin - .args(["plugin", "add", "trace-codex-test@braintrust-daemon-tests"]) + .args(["plugin", "add", "trace-codex@braintrust-codex-plugins"]) .env("CODEX_HOME", &home); world.configure(&mut add_plugin); AgentOutput::from(world.output(&mut add_plugin).await).assert_success(); diff --git a/bt-daemon/tests/support/agents/mod.rs b/bt-daemon/tests/support/agents/mod.rs index 47447bd..9a83fdb 100644 --- a/bt-daemon/tests/support/agents/mod.rs +++ b/bt-daemon/tests/support/agents/mod.rs @@ -1,6 +1,5 @@ mod claude; mod codex; -mod test_plugin; #[allow(unused_imports)] pub use claude::{ClaudeAgent, ClaudeRun}; @@ -82,6 +81,13 @@ fn command_from_env(name: &str, fallback: &str) -> Command { Command::new(fallback) } +fn repository_root() -> PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("repository root") + .to_path_buf() +} + fn configured_home(config_env: &str, directory: &str) -> Option { std::env::var_os(config_env).map(PathBuf::from).or_else(|| { std::env::var_os("HOME") diff --git a/bt-daemon/tests/support/agents/test_plugin.rs b/bt-daemon/tests/support/agents/test_plugin.rs deleted file mode 100644 index 99279ee..0000000 --- a/bt-daemon/tests/support/agents/test_plugin.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::support::agent_process::AgentTestWorld; -use serde_json::{json, Map, Value}; -use std::path::{Path, PathBuf}; - -const HOOK_EVENTS: &[&str] = &[ - "SessionStart", - "UserPromptSubmit", - "PreToolUse", - "PermissionRequest", - "PermissionDenied", - "PostToolUse", - "PostToolUseFailure", - "PostToolBatch", - "PreCompact", - "PostCompact", - "SubagentStart", - "SubagentStop", - "Stop", - "StopFailure", - "SessionEnd", - "TaskCreated", - "TaskCompleted", -]; - -pub fn codex_marketplace(world: &AgentTestWorld) -> PathBuf { - let root = world.temp_path("codex-test-marketplace"); - write_json( - &root.join(".agents/plugins/marketplace.json"), - json!({ - "name": "braintrust-daemon-tests", - "plugins": [{ - "name": "trace-codex-test", - "source": { - "source": "local", - "path": "./plugins/trace-codex-test" - } - }] - }), - ); - - let plugin = root.join("plugins/trace-codex-test"); - write_json( - &plugin.join(".codex-plugin/plugin.json"), - json!({ - "name": "trace-codex-test", - "version": "0.0.0", - "description": "Direct bt daemon hook fixture", - "hooks": "./hooks/hooks.json" - }), - ); - write_json( - &plugin.join("hooks/hooks.json"), - hook_config("bt agents hook --source codex --source-version test"), - ); - root -} - -pub fn claude_plugin(world: &AgentTestWorld) -> PathBuf { - let root = world.temp_path("claude-test-plugin"); - write_json( - &root.join(".claude-plugin/plugin.json"), - json!({ - "name": "trace-claude-test", - "version": "0.0.0", - "description": "Direct bt daemon hook fixture" - }), - ); - write_json( - &root.join("hooks/hooks.json"), - hook_config("bt agents hook --source claude-code --source-version test"), - ); - root -} - -fn hook_config(command: &str) -> Value { - let hook = json!([{ - "hooks": [{ - "type": "command", - "command": command, - "async": false - }] - }]); - let hooks = HOOK_EVENTS - .iter() - .map(|event| ((*event).to_string(), hook.clone())) - .collect::>(); - json!({ "hooks": hooks }) -} - -fn write_json(path: &Path, value: Value) { - std::fs::create_dir_all(path.parent().expect("test plugin file has parent")) - .expect("create test plugin directory"); - std::fs::write(path, serde_json::to_vec_pretty(&value).unwrap()) - .expect("write test plugin file"); -} diff --git a/src/plugins/claude/content/CONTRIBUTING.md b/src/plugins/claude/content/CONTRIBUTING.md index dbe4473..adb2185 100644 --- a/src/plugins/claude/content/CONTRIBUTING.md +++ b/src/plugins/claude/content/CONTRIBUTING.md @@ -1,246 +1,89 @@ -# Development of the plugin itself +# Development of the plugins ## Prerequisites -- Python 3.12+ -- [uv](https://docs.astral.sh/uv/) package manager +- Python 3.12+ and [uv](https://docs.astral.sh/uv/) for the Braintrust skill + evals. +- Rust for the shared tracing daemon. +- `jq` for plugin manifest validation and the optional fixture recorder. ## Local testing -Test a plugin without installing from marketplace: +Load a plugin directly without installing it from the marketplace: ```bash -claude --plugin-dir /path/to/thisrepo/plugins/{plugin dir here} -# example -claude --plugin-dir /path/to/thisrepo/plugins/braintrust +claude --plugin-dir /path/to/repo/plugins/braintrust +claude --plugin-dir /path/to/repo/plugins/trace-claude-code ``` ## Running evals -The `evals/` directory contains tests that verify the plugin works correctly (e.g., Claude generates valid SQL queries, logs data properly). +The `evals/` directory verifies that Claude can use Braintrust workflows: ```bash cd evals export BRAINTRUST_API_KEY="your-key" - -# Run all evals uv run braintrust eval . - -# Run specific eval -uv run braintrust eval eval_e2e_log_fetch.py ``` -## Pre-commit hooks - -```bash -# Install hooks -uv run pre-commit install +## Testing `trace-claude-code` -# Run all hooks -uv run pre-commit run --all-files -``` +The plugin contains only a fail-open `bt` hook shim. All event translation and +Braintrust delivery live in the shared Rust daemon at `bt-daemon/`. -## Testing the `trace-claude-code` plugin +From the monorepo root: -Bash test suite for the hook scripts. Tests run the hooks against a -stubbed `curl`, capture the resulting HTTP requests, and assert on the -inferred span tree. - -### Running - -```sh -# From the repo root: +```bash +cargo test --manifest-path bt-daemon/Cargo.toml --all-features +cargo clippy --manifest-path bt-daemon/Cargo.toml --all-targets --all-features -- -D warnings make test - -# Or run a specific test file: -bash plugins/trace-claude-code/test/run_tests.sh test_e2e -bash plugins/trace-claude-code/test/run_tests.sh test_replay test_queue ``` -### Layout - -``` -plugins/trace-claude-code/test/ -├── helpers/ -│ ├── assert.sh # describe / it / assert_eq / assert_contains, color output -│ ├── harness.sh # setup_test_env, teardown_test_env, run_hook -│ ├── curl_stub.sh # curl() shell function that captures requests + returns canned responses -│ ├── fixtures.sh # builders for hook input JSON (fixture_session_start, etc.) -│ ├── span_tree.sh # all_spans, span_count_by_type, span_by_name, children_of, ... -│ └── replay.sh # replay_session, describe_fixture -├── fixtures/ -│ └── sessions/ # captured Claude sessions used by test_replay.sh -├── test_*.sh # one file per area -├── record_session.sh # CLI to prep a fixture directory for capturing -└── run_tests.sh # entry point -``` +`bt-daemon/tests/claude_translator.rs` covers synthetic lifecycle cases and +replays the immutable captured sessions under +`plugins/trace-claude-code/test/fixtures/sessions/`. Add translator behavior and +assertions there, not as another hook script. -### Writing a test +### Capturing a fixture -Each `test_*.sh` follows this pattern: +Set `BRAINTRUST_RECORD_DIR` to a new absolute directory before running Claude: ```bash -#!/bin/bash -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/helpers/assert.sh" -source "$SCRIPT_DIR/helpers/harness.sh" - -describe "my feature" - -t_my_test_body() { - # setup_test_env has already created an isolated $HOME and stubbed curl - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' - - run_hook session_start.sh "$(fixture_session_start "s1" "/tmp/x")" - - assert_eq "$(span_count_by_type task)" "1" -} - -it "does the thing" t_my_test_body -``` - -Key conventions: - -- `describe "..."` is a section header (purely visual). -- `it "name" function_name` runs `function_name` between `setup_test_env` - and `teardown_test_env`, then prints a ✓ or ✗. -- Assertions (`assert_eq`, `assert_contains`, `assert_failure`, ...) record - failures into the current test but do **not** abort. Multiple assertions - per test are fine. -- Hooks are run synchronously in tests via `BRAINTRUST_SYNC_QUEUE=true` - set by `setup_test_env`. Span queue tests opt out of this when needed. - -### Capturing a real session as a test fixture - -The hooks support recording every invocation to disk when the env var -`BRAINTRUST_RECORD_DIR` is set. The recorded data can then be replayed -in a test. - -#### 1. Prepare a fixture directory - -```sh -plugins/trace-claude-code/test/record_session.sh my-fixture +export BRAINTRUST_RECORD_DIR=/absolute/path/to/new-fixture +claude --plugin-dir /path/to/plugins/trace-claude-code ``` -This prints a `BRAINTRUST_RECORD_DIR` value pointing at -`test/fixtures/sessions/my-fixture/`. - -#### 2. Run Claude Code with recording on +The shim appends `{ts, hook, payload}` records to `events.ndjson` and copies +referenced main/subagent transcripts under `transcripts/`. Move a reviewed, +credential-free capture under `test/fixtures/sessions/`, add its contract to +the Rust test, and run the full daemon suite. The daemon’s normal recovery +journal independently embeds transcript snapshots at lifecycle boundaries. -```sh -export BRAINTRUST_RECORD_DIR=/abs/path/to/test/fixtures/sessions/my-fixture -claude -# ... use Claude Code normally ... -``` - -While `BRAINTRUST_RECORD_DIR` is set: - -- Every hook invocation appends one NDJSON record to - `events.ndjson` containing `{ts, hook, payload}`. -- The `stop_hook` also copies the referenced transcript file into - `transcripts/.jsonl`. - -You do not need to modify hook scripts or set anything else - the recorder -runs inside the existing hooks. - -#### 3. Inspect the fixture - -```sh -plugins/trace-claude-code/test/record_session.sh --describe my-fixture -``` - -Output: - -``` -Fixture: .../test/fixtures/sessions/my-fixture - Events: 14 - Hook counts: - post_tool_use: 8 - session_end: 1 - session_start: 1 - stop_hook: 3 - user_prompt_submit: 1 - Transcripts: 1 -``` - -#### 4. Replay it in a test +## Pre-commit hooks ```bash -t_replay_my_fixture() { - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' - - local n - n=$(replay_session "$SCRIPT_DIR/fixtures/sessions/my-fixture") - assert_success "$?" - assert_eq "$n" "14" - - # Now assert on the span tree the hooks produced - assert_eq "$(span_count_by_type tool)" "8" - assert_eq "$(span_count_by_type llm)" "3" -} - -it "my real-world fixture produces the expected spans" t_replay_my_fixture +uv run pre-commit install +uv run pre-commit run --all-files ``` -The replayer: - -- Reads `events.ndjson` line by line in order. -- For `stop_hook` events, rewrites `payload.transcript_path` to point at - the bundled transcript so the replayed hook can read it. -- Invokes the matching hook script via `run_hook` with the recorded - payload. - -#### When to use replay vs. synthetic fixtures - -- **Synthetic fixtures** (`fixture_session_start`, etc.) - fast to write, - test specific scenarios in isolation, no real Claude needed. -- **Replayed fixtures** - high-fidelity regression tests of real-world - interactions. Use when you want to lock in behavior on a specific - pattern of hooks you saw in the wild (e.g. a session with parallel - tool calls, or a long multi-turn conversation). - -### Span-tree queries - -The captured HTTP requests are parsed to extract the inserted spans. Available helpers: - -| Function | Returns | -|---|---| -| `all_spans` | JSON array of every span sent to any `/insert` endpoint | -| `span_count` | total number of spans | -| `span_count_by_type "tool"` | count of spans with `span_attributes.type == "tool"` | -| `spans_named "^Turn "` | array of spans whose name matches the regex | -| `span_by_name "^Turn 1$"` | first matching span (or `null`) | -| `span_by_type "llm"` | first span of that type | -| `span_by_id "..."` | span with the given `span_id` | -| `children_of ""` | array of spans whose first parent is the given id | -| `is_child_of "" ""` | exit 0 if true | - -All return JSON on stdout; combine with `jq` for further drilling. - # Releasing a plugin -Releases are manual and git-driven. There are no git tags or publish automation: pushing to `main` is the release. - -## How version resolution works - -Claude Code resolves a plugin's version from the first of these that is set: - -1. `version` in the plugin's `plugins//.claude-plugin/plugin.json` -2. `version` in the plugin's entry in `.claude-plugin/marketplace.json` -3. The git commit SHA of the plugin's source +Releases are manual and git-driven. There are no git tags or publish +automation: pushing to `main` is the release. -Both plugins set `version` in their own `plugin.json`, and the marketplace entries do **not** declare a per-plugin `version`. So **each plugin's `plugin.json` is the sole authority for its version**, and bumping it is what triggers updates for users. +Claude Code resolves a plugin version from the first available source: -The top-level `version` field in `marketplace.json` is just marketplace-manifest metadata. It does **not** gate plugin updates. +1. `version` in `plugins//.claude-plugin/plugin.json` +2. `version` in its marketplace entry +3. the source commit SHA -> [!WARNING] -> Do not add a `version` field to a plugin's entry in `marketplace.json`. The `plugin.json` value always wins silently, so a stale marketplace version can mask the real one. Keep the version in `plugin.json` only. +Each plugin’s `plugin.json` is authoritative. Do not add a per-plugin version +to `marketplace.json`; a stale duplicate can mask the real version. -## Release steps +Release steps: -1. Bump `version` in the plugin's manifest: - - `plugins/braintrust/.claude-plugin/plugin.json`, or - - `plugins/trace-claude-code/.claude-plugin/plugin.json` -2. (Optional) Bump the top-level `version` in `.claude-plugin/marketplace.json` for bookkeeping. This is cosmetic and does not affect whether users receive the update. -3. Commit and push to `main` (via PR). -4. Users update with: `claude plugin marketplace update braintrust-claude-plugin` +1. Bump the plugin’s `.claude-plugin/plugin.json` version. +2. Optionally bump the marketplace manifest’s top-level bookkeeping version. +3. Commit and merge through a PR. +4. Users update with + `claude plugin marketplace update braintrust-claude-plugin`. diff --git a/src/plugins/claude/content/Makefile b/src/plugins/claude/content/Makefile index a30d4c4..e3e3d9c 100644 --- a/src/plugins/claude/content/Makefile +++ b/src/plugins/claude/content/Makefile @@ -1,8 +1,9 @@ .PHONY: test test-trace-claude-code -# Run all plugin tests test: test-trace-claude-code -# Run trace-claude-code plugin tests test-trace-claude-code: - @bash plugins/trace-claude-code/test/run_tests.sh + @sh -n plugins/trace-claude-code/bin/claude-hook.sh + @jq empty plugins/trace-claude-code/hooks/hooks.json + @grep -q "'agents','hook','--source','claude-code'" plugins/trace-claude-code/bin/claude-hook.cmd + @echo "trace-claude-code shims OK" diff --git a/src/plugins/claude/content/README.md b/src/plugins/claude/content/README.md index 61b49d9..716df13 100644 --- a/src/plugins/claude/content/README.md +++ b/src/plugins/claude/content/README.md @@ -10,7 +10,8 @@ A Claude Code plugin marketplace for [Braintrust](https://braintrust.dev) integr ## Prerequisites - A [Braintrust account](https://braintrust.dev) -- `BRAINTRUST_API_KEY` exported in your environment +- The [`bt` CLI](https://bt.dev/cli/install.sh), authenticated with `bt auth login`, + or `BRAINTRUST_API_KEY` exported in your environment ## Installation @@ -37,7 +38,9 @@ claude plugin install braintrust@braintrust-claude-plugin ### trace-claude-code -Automatically traces Claude Code conversations to Braintrust. Captures sessions, conversation turns, and tool calls as hierarchical traces. +Automatically traces Claude Code conversations to Braintrust through the +shared local `bt` daemon. Captures sessions, turns, model calls, tool calls, +subagents, tool failures, and permission denials as hierarchical traces. ```bash claude plugin install trace-claude-code@braintrust-claude-plugin @@ -45,23 +48,35 @@ claude plugin install trace-claude-code@braintrust-claude-plugin $HOME/.claude/plugins/marketplaces/braintrust-claude-plugin/plugins/trace-claude-code/setup.sh ``` -Traces are sent to the `claude-code` project by default. +The setup script enables tracing in the daemon's shared non-credential +`config.json`. The project selected there applies to every coding-agent plugin +connected to the daemon. + +The tracing launchers live under `plugins/trace-claude-code/bin/` as +`claude-hook.sh` and `claude-hook.cmd`. The Windows launcher forwards the same +configuration to `bt`, whose daemon uses a local named pipe on Windows. #### manual configuration -Instead of running `setup.sh`, you can manually edit `~/.claude/settings.json` or your project's `.claude/settings.local.json`: +Instead of running `setup.sh`, create the daemon's shared config at +`$BT_DAEMON_CONFIG`, or at `config.json` inside `$BT_DAEMON_DATA_DIR` (by +default `~/.braintrust/state/bt-daemon/config.json` on Unix): ```json { - "env": { - "TRACE_TO_BRAINTRUST": "true", - "BRAINTRUST_CC_PROJECT": "project-name-to-send-cc-traces-to", - "BRAINTRUST_API_KEY": "sk-yourkey", - "BRAINTRUST_DEBUG": "false" - } + "traceToBraintrust": true, + "project": "coding-agents", + "flushOnTurnEnd": false, + "additionalMetadata": {"team": "platform"} } ``` +This file intentionally contains no API keys, auth tokens, credentials, or +backend URLs. Those come from the authenticated `bt` CLI. The legacy +`TRACE_TO_BRAINTRUST`, `BRAINTRUST_CC_PROJECT`, +`BRAINTRUST_FLUSH_ON_TURN_END`, and `BRAINTRUST_ADDITIONAL_METADATA` +environment variables remain fallbacks for omitted shared settings. + #### add claude code trace to an existing trace You can attach a Claude Code session to an existing Braintrust trace by passing `CC_PARENT_SPAN_ID`: @@ -78,11 +93,12 @@ claude --settings '{"env":{"CC_PARENT_SPAN_ID":"parent-span-id","CC_ROOT_SPAN_ID The Claude Code session and all its turns/tools will appear as children of your parent span in Braintrust. -To attach claude code to an experiment's trace, specify CC_EXPERIMENT_ID as well: +To route the session into an existing experiment instead of project logs, set +`CC_EXPERIMENT_ID`. -```bash -claude --settings '{"env":{"CC_PARENT_SPAN_ID":"parent-span-id","CC_ROOT_SPAN_ID":"root-span-id", "CC_EXPERIMENT_ID":"the-experiment-id"}}' -p "task" -``` +Set `BRAINTRUST_RECORD_DIR` to capture native hook events and transcript +snapshots for a reproducible local fixture. The daemon also embeds transcript +snapshots in its redacted recovery journal at lifecycle boundaries. #### token accounting diff --git a/src/plugins/claude/content/plugins/trace-claude-code/.claude-plugin/plugin.json b/src/plugins/claude/content/plugins/trace-claude-code/.claude-plugin/plugin.json index ae258fc..4e0b29b 100644 --- a/src/plugins/claude/content/plugins/trace-claude-code/.claude-plugin/plugin.json +++ b/src/plugins/claude/content/plugins/trace-claude-code/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "trace-claude-code", "description": "Automatically trace Claude Code conversations to Braintrust for observability. Captures sessions, conversation turns, and tool calls as hierarchical traces.", - "version": "1.5.0", + "version": "2.0.0", "author": { "name": "Braintrust" } diff --git a/src/plugins/claude/content/plugins/trace-claude-code/bin/claude-hook.cmd b/src/plugins/claude/content/plugins/trace-claude-code/bin/claude-hook.cmd new file mode 100644 index 0000000..124f2e7 --- /dev/null +++ b/src/plugins/claude/content/plugins/trace-claude-code/bin/claude-hook.cmd @@ -0,0 +1,38 @@ +@echo off +REM Thin, fail-open Claude Code hook shim for the shared Braintrust daemon. +REM Invokes: bt agents hook --source claude-code +setlocal EnableExtensions DisableDelayedExpansion + +set "BT_HOOK_BIN=" +for /f "delims=" %%B in ('where bt.exe 2^>nul') do if not defined BT_HOOK_BIN set "BT_HOOK_BIN=%%B" +if not defined BT_HOOK_BIN for /f "delims=" %%B in ('where bt.cmd 2^>nul') do if not defined BT_HOOK_BIN set "BT_HOOK_BIN=%%B" +if not defined BT_HOOK_BIN for /f "delims=" %%B in ('where bt 2^>nul') do if not defined BT_HOOK_BIN set "BT_HOOK_BIN=%%B" +if not defined BT_HOOK_BIN if exist "%USERPROFILE%\.local\bin\bt.exe" set "BT_HOOK_BIN=%USERPROFILE%\.local\bin\bt.exe" +if not defined BT_HOOK_BIN ( + echo trace-claude-code: bt CLI is unavailable; tracing disabled for this event.>&2 + exit /b 0 +) + +call "%BT_HOOK_BIN%" agents hook --help >nul 2>&1 +if errorlevel 1 ( + echo trace-claude-code: a daemon-capable bt CLI is unavailable; tracing disabled for this event.>&2 + exit /b 0 +) + +if not defined BRAINTRUST_PROJECT if defined BRAINTRUST_CC_PROJECT set "BRAINTRUST_PROJECT=%BRAINTRUST_CC_PROJECT%" +if not defined BRAINTRUST_DEFAULT_PROJECT if defined BRAINTRUST_PROJECT set "BRAINTRUST_DEFAULT_PROJECT=%BRAINTRUST_PROJECT%" + +set "BT_PLUGIN_JSON=%~dp0..\.claude-plugin\plugin.json" +powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ^ + "$ErrorActionPreference='SilentlyContinue';" ^ + "$a=@('agents','hook','--source','claude-code');" ^ + "if(Test-Path $env:BT_PLUGIN_JSON){$v=(Get-Content -Raw $env:BT_PLUGIN_JSON|ConvertFrom-Json).version;if($v){$a+=@('--source-version',[string]$v)}};" ^ + "if($env:BRAINTRUST_FLUSH_ON_TURN_END -match '^(?i:1|true|yes|on)$'){$a+='--flush-on-turn-end'};" ^ + "if($env:CC_PARENT_SPAN_ID){$a+=@('--parent-span-id',$env:CC_PARENT_SPAN_ID)};" ^ + "if($env:CC_ROOT_SPAN_ID){$a+=@('--root-span-id',$env:CC_ROOT_SPAN_ID)};" ^ + "if($env:BRAINTRUST_ADDITIONAL_METADATA){$a+=@('--additional-metadata',$env:BRAINTRUST_ADDITIONAL_METADATA)};" ^ + "if($env:CC_EXPERIMENT_ID){$a+=@('--experiment-id',$env:CC_EXPERIMENT_ID)};" ^ + "& $env:BT_HOOK_BIN @a;" ^ + "if($LASTEXITCODE -ne 0){[Console]::Error.WriteLine('trace-claude-code: bt agents hook failed non-fatally')};exit 0" + +exit /b 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/bin/claude-hook.sh b/src/plugins/claude/content/plugins/trace-claude-code/bin/claude-hook.sh new file mode 100755 index 0000000..cd12a5a --- /dev/null +++ b/src/plugins/claude/content/plugins/trace-claude-code/bin/claude-hook.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# Thin, fail-open Claude Code hook shim for the shared Braintrust daemon. + +set -u + +log() { printf 'trace-claude-code: %s\n' "$1" >&2; } +truthy() { + case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in + 1 | true | yes | on) return 0 ;; + *) return 1 ;; + esac +} + +compatible_bt() { + [ -n "${1:-}" ] && [ -x "$1" ] && "$1" agents hook --help >/dev/null 2>&1 +} + +BT_BIN=$(command -v bt 2>/dev/null || true) +LOCAL_BT="${XDG_BIN_HOME:-$HOME/.local/bin}/bt" +if ! compatible_bt "$BT_BIN" && compatible_bt "$LOCAL_BT"; then + BT_BIN="$LOCAL_BT" +fi +if ! compatible_bt "$BT_BIN"; then + INSTALL_LOCK="${TMPDIR:-/tmp}/braintrust-bt-daemon-install-${UID:-user}" + if command -v curl >/dev/null 2>&1 && mkdir "$INSTALL_LOCK" 2>/dev/null; then + nohup sh -c 'curl -fsSL --max-time 20 https://bt.dev/cli/install.sh | sh; rmdir "$1"' \ + sh "$INSTALL_LOCK" /dev/null 2>&1 & + log "a daemon-capable bt CLI is unavailable; started a background install or upgrade" + else + log "a daemon-capable bt CLI is unavailable; tracing disabled for this event" + fi + exit 0 +fi + +# Preserve the Claude plugin's project-name compatibility while bt owns auth. +if [ -z "${BRAINTRUST_PROJECT:-}" ] && [ -n "${BRAINTRUST_CC_PROJECT:-}" ]; then + export BRAINTRUST_PROJECT="$BRAINTRUST_CC_PROJECT" +fi +if [ -z "${BRAINTRUST_DEFAULT_PROJECT:-}" ] && [ -n "${BRAINTRUST_PROJECT:-}" ]; then + export BRAINTRUST_DEFAULT_PROJECT="$BRAINTRUST_PROJECT" +fi + +PLUGIN_JSON="$(dirname "$0")/../.claude-plugin/plugin.json" +PLUGIN_VERSION="" +if [ -f "$PLUGIN_JSON" ]; then + PLUGIN_VERSION=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PLUGIN_JSON" | head -1) +fi + +set -- agents hook --source claude-code +[ -z "$PLUGIN_VERSION" ] || set -- "$@" --source-version "$PLUGIN_VERSION" +truthy "${BRAINTRUST_FLUSH_ON_TURN_END:-false}" && set -- "$@" --flush-on-turn-end +[ -z "${CC_PARENT_SPAN_ID:-}" ] || set -- "$@" --parent-span-id "$CC_PARENT_SPAN_ID" +[ -z "${CC_ROOT_SPAN_ID:-}" ] || set -- "$@" --root-span-id "$CC_ROOT_SPAN_ID" +[ -z "${BRAINTRUST_ADDITIONAL_METADATA:-}" ] \ + || set -- "$@" --additional-metadata "$BRAINTRUST_ADDITIONAL_METADATA" +[ -z "${CC_EXPERIMENT_ID:-}" ] || set -- "$@" --experiment-id "$CC_EXPERIMENT_ID" + +INPUT=$(cat) +if [ -n "${BRAINTRUST_RECORD_DIR:-}" ] && command -v jq >/dev/null 2>&1; then + mkdir -p "$BRAINTRUST_RECORD_DIR/transcripts" 2>/dev/null || true + printf '%s' "$INPUT" | jq -c --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + '{hook:(.hook_event_name // ""),ts:$ts,payload:.}' \ + >>"$BRAINTRUST_RECORD_DIR/events.ndjson" 2>/dev/null || true + for field in transcript_path agent_transcript_path; do + transcript=$(printf '%s' "$INPUT" | jq -r --arg field "$field" '.[$field] // empty' 2>/dev/null) + if [ -n "$transcript" ] && [ -f "$transcript" ]; then + cp "$transcript" "$BRAINTRUST_RECORD_DIR/transcripts/$(basename "$transcript")" 2>/dev/null || true + fi + done +fi + +printf '%s' "$INPUT" | "$BT_BIN" "$@" || log "bt agents hook failed non-fatally" +exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/common.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/common.sh deleted file mode 100755 index 151c1c8..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/common.sh +++ /dev/null @@ -1,1323 +0,0 @@ -#!/bin/bash -### -# Common utilities for Braintrust Claude Code tracing hooks -### - -# Config -export LOG_FILE="$HOME/.claude/state/braintrust_hook.log" -export CACHE_FILE="$HOME/.claude/state/braintrust_cache.json" -export SESSION_STATE_DIR="$HOME/.claude/state/braintrust_sessions" -export QUEUE_DIR="$HOME/.claude/state/braintrust_queue" -export DEBUG="${BRAINTRUST_CC_DEBUG:-false}" -export API_KEY="${BRAINTRUST_API_KEY}" -export PROJECT="${BRAINTRUST_CC_PROJECT:-claude-code}" -export APP_URL="${BRAINTRUST_APP_URL:-https://www.braintrust.dev}" - -# If true, enqueue_span processes jobs inline rather than spawning a worker. -# Used by tests and as a debugging knob. Default: false (async worker). -export BRAINTRUST_SYNC_QUEUE="${BRAINTRUST_SYNC_QUEUE:-false}" - -# How long drain_queue will wait (seconds) before giving up. Hooks must -# never block Claude Code forever. -export BRAINTRUST_DRAIN_TIMEOUT="${BRAINTRUST_DRAIN_TIMEOUT:-60}" - -# How long the worker.lock mtime can be stale (seconds) before a session -# is considered crashed and its queue dir is swept on the next -# session_start. The worker refreshes its lock every loop iteration -# (~0.2s), so 5 minutes is ~1500x margin against transient slowness. -export BRAINTRUST_WORKER_STALE_SECS="${BRAINTRUST_WORKER_STALE_SECS:-300}" - -# Parent span configuration (for attaching to an existing trace) -# If either is set, we're attaching to an existing trace -# Each defaults to the other if not set -if [ -n "${CC_PARENT_SPAN_ID:-}" ] && [ -z "${CC_ROOT_SPAN_ID:-}" ]; then - export CC_ROOT_SPAN_ID="$CC_PARENT_SPAN_ID" -elif [ -n "${CC_ROOT_SPAN_ID:-}" ] && [ -z "${CC_PARENT_SPAN_ID:-}" ]; then - export CC_PARENT_SPAN_ID="$CC_ROOT_SPAN_ID" -fi -export CC_PARENT_SPAN_ID="${CC_PARENT_SPAN_ID:-}" -export CC_ROOT_SPAN_ID="${CC_ROOT_SPAN_ID:-}" - -# Experiment mode configuration -# If CC_EXPERIMENT_ID is set, spans are inserted into the experiment instead of project_logs -export CC_EXPERIMENT_ID="${CC_EXPERIMENT_ID:-}" - -# Ensure top-level directories exist. Per-session sub-trees under -# $QUEUE_DIR// are created on demand by enqueue_span. -mkdir -p "$(dirname "$LOG_FILE")" -mkdir -p "$(dirname "$CACHE_FILE")" -mkdir -p "$SESSION_STATE_DIR" -mkdir -p "$QUEUE_DIR" - -# Logging (defined early so other functions can use it) -log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" >> "$LOG_FILE"; } - -# Check if a value is truthy (true, 1, yes, on - case insensitive) -is_truthy() { - local val="$(echo "$1" | tr '[:upper:]' '[:lower:]')" - [[ "$val" == "true" || "$val" == "1" || "$val" == "yes" || "$val" == "on" ]] -} - -debug() { is_truthy "$DEBUG" && log "DEBUG" "$1" || true; } - -### -# Hook input recording (for capturing real Claude Code sessions to use -# as test fixtures). -# -# When BRAINTRUST_RECORD_DIR is set, every hook calls record_hook_input -# right after reading stdin. The function appends an NDJSON record to -# $BRAINTRUST_RECORD_DIR/events.ndjson and, for the Stop hook, copies the -# transcript file referenced in the payload to -# $BRAINTRUST_RECORD_DIR/transcripts/. -# -# To capture a session: -# export BRAINTRUST_RECORD_DIR=~/my-session-fixture -# -# -# Recordings are then replayed in tests via test/helpers/replay.sh. -### -record_hook_input() { - local hook_name="$1" - local payload="$2" - - [ -z "${BRAINTRUST_RECORD_DIR:-}" ] && return 0 - - mkdir -p "$BRAINTRUST_RECORD_DIR" "$BRAINTRUST_RECORD_DIR/transcripts" 2>/dev/null || return 0 - - local ts events_file - ts=$(get_timestamp 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%S.000Z") - events_file="$BRAINTRUST_RECORD_DIR/events.ndjson" - - # Build the record. payload may already be valid JSON; if not, treat as string. - local payload_field - if [ -n "$payload" ] && echo "$payload" | jq -e . >/dev/null 2>&1; then - payload_field=$(echo "$payload" | jq -c .) - else - payload_field=$(jq -nc --arg p "$payload" '$p') - fi - - # Always label the event with Claude Code's canonical event name. The - # payload carries it as `hook_event_name` (e.g. "SessionStart", - # "PostToolUse", "PreCompact"); fall back to the caller-supplied name - # only when the payload doesn't include it. This gives the recording a - # single CamelCase namespace so replay can dispatch every event through - # hooks.json exactly the way Claude Code does. - local event_name - event_name=$(echo "$payload_field" | jq -r '.hook_event_name // empty' 2>/dev/null) - [ -z "$event_name" ] && event_name="$hook_name" - hook_name="$event_name" - - local record - record=$(jq -nc \ - --arg ts "$ts" \ - --arg hook "$hook_name" \ - --argjson payload "$payload_field" \ - '{ts: $ts, hook: $hook, payload: $payload}' 2>/dev/null) || return 0 - - # Atomic append: claim an exclusive lock via mkdir, write, release. - # Parallel PostToolUse hooks can otherwise interleave bytes into the - # NDJSON file, corrupting fixture lines (one torn line makes jq reject - # the whole file at replay time). mkdir is the only portable lock - # primitive we have (flock is not available on macOS by default). - # - # If we can't acquire the lock within ~500ms we DROP the record and - # log a warning rather than fall back to an unlocked write. Recording - # is opt-in and best-effort; losing one record on contention is - # strictly better than producing a fixture that won't parse. - # - # If a previous writer was killed and left the lock dir behind, the - # directory's mtime will be older than RECORD_LOCK_STALE_SECS and we - # forcibly remove it before retrying (same pattern as worker.lock). - # - # 30s threshold balances two concerns: - # - Crash recovery: a SIGKILLed holder needs to be reclaimed. - # - False preemption: the recording lock has no heartbeat, so a - # healthy holder stuck on slow disk I/O (Time Machine snapshots, - # disk pressure) must not be preempted while still working. The - # critical section is one printf >> file, so 30s is ~6 orders of - # magnitude above the expected duration. - local lock_dir="$events_file.lock" - local stale_secs=30 - local i acquired=0 - for i in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - acquired=1 - break - fi - # Lock is held - check whether it's stale. - local lock_mtime now age - lock_mtime=$(stat -f '%m' "$lock_dir" 2>/dev/null \ - || stat -c '%Y' "$lock_dir" 2>/dev/null \ - || echo 0) - now=$(date +%s) - age=$((now - lock_mtime)) - if [ "$age" -gt "$stale_secs" ]; then - # Lock is stale; reclaim it. - rmdir "$lock_dir" 2>/dev/null || true - continue - fi - sleep 0.05 - done - - if [ "$acquired" -eq 1 ]; then - printf '%s\n' "$record" >> "$events_file" 2>/dev/null || true - rmdir "$lock_dir" 2>/dev/null || true - else - log "WARN" "record_hook_input: dropped $hook_name record after lock contention timeout" - return 0 - fi - - # Snapshot any transcript files referenced by this event so the - # recording can be replayed deterministically. (Path rewriting from - # absolute to fixture-relative happens at replay time in - # test/helpers/replay.sh, not here; we only copy the files.) - # - # Two cases: - # - Stop carries `transcript_path` (the main conversation transcript). - # - SubagentStop carries `agent_transcript_path` (the sub-agent's own - # transcript, which holds its model calls - e.g. haiku - and which - # Claude Code may clean up shortly after the agent finishes, so we - # must snapshot it now, while it still exists). - # - # All transcripts land flat in transcripts/. Basenames are globally - # unique (main: ".jsonl"; agent: "agent-.jsonl"), so there - # is no collision and replay can resolve any of them by basename. - _snapshot_transcript() { - local src="$1" - [ -n "$src" ] && [ -f "$src" ] || return 0 - local base - base=$(basename "$src") - cp "$src" "$BRAINTRUST_RECORD_DIR/transcripts/$base" 2>/dev/null || true - } - - case "$hook_name" in - Stop) - _snapshot_transcript "$(echo "$payload" | jq -r '.transcript_path // empty' 2>/dev/null)" - ;; - SubagentStop) - _snapshot_transcript "$(echo "$payload" | jq -r '.agent_transcript_path // empty' 2>/dev/null)" - ;; - esac -} - -### -# Cache management (shared across sessions, used for API URL and project IDs) -# Uses simple file-based caching - minor races here are harmless (just extra API calls) -### - -get_cache_value() { - local key="$1" - # Use --arg + bracket lookup so keys containing dashes/dots/etc work - [ -f "$CACHE_FILE" ] && jq -r --arg k "$key" '.[$k] // empty' "$CACHE_FILE" 2>/dev/null || echo "" -} - -set_cache_value() { - local key="$1" - local value="$2" - local cache - cache=$([ -f "$CACHE_FILE" ] && cat "$CACHE_FILE" 2>/dev/null || echo '{}') - cache=$(echo "$cache" | jq --arg k "$key" --arg v "$value" '.[$k] = $v' 2>/dev/null) || return 0 - local tmp="$CACHE_FILE.tmp.$$" - echo "$cache" > "$tmp" && mv "$tmp" "$CACHE_FILE" -} - -# Resolve API URL via login endpoint (with caching) -resolve_api_url() { - # Check for explicit override first - if [ -n "${BRAINTRUST_API_URL:-}" ]; then - echo "$BRAINTRUST_API_URL" - return 0 - fi - - # Check cache - local cached_url - cached_url=$(get_cache_value "api_url") - if [ -n "$cached_url" ]; then - echo "$cached_url" - return 0 - fi - - # Login to discover API URL - if [ -z "$API_KEY" ]; then - echo "https://api.braintrust.dev" - return 0 - fi - - local resp http_code - resp=$(curl -s -w "\n%{http_code}" -X POST -H "Authorization: Bearer $API_KEY" "$APP_URL/api/apikey/login" 2>/dev/null) - http_code=$(echo "$resp" | tail -1) - resp=$(echo "$resp" | sed '$d') - - if [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then - log "ERROR" "Braintrust authentication failed (HTTP $http_code) at $APP_URL/api/apikey/login - BRAINTRUST_API_KEY appears to be invalid or expired. Check your API key at $APP_URL/app/settings?subroute=api-keys" - # Fall back to default API URL so callers can produce a definitive auth error too - echo "https://api.braintrust.dev" - return 0 - fi - - if [ "$http_code" != "200" ]; then - log "WARN" "Braintrust login endpoint returned HTTP $http_code at $APP_URL/api/apikey/login: $resp" - fi - - local api_url - local org_name="${BRAINTRUST_ORG_NAME:-}" - - if [ -n "$org_name" ]; then - # Filter by org name if specified - api_url=$(echo "$resp" | jq -r --arg name "$org_name" \ - '.org_info[] | select(.name == $name) | .api_url // empty' 2>/dev/null | head -1) - else - # Use first org - api_url=$(echo "$resp" | jq -r '.org_info[0].api_url // empty' 2>/dev/null) - fi - - if [ -n "$api_url" ]; then - set_cache_value "api_url" "$api_url" - echo "$api_url" - return 0 - fi - - # Fall back to default - echo "https://api.braintrust.dev" -} - -# Initialize API_URL (call resolve_api_url lazily when needed) -get_api_url() { - if [ -z "${_RESOLVED_API_URL:-}" ]; then - _RESOLVED_API_URL=$(resolve_api_url) - fi - echo "$_RESOLVED_API_URL" -} - -# Check if tracing is enabled -tracing_enabled() { - is_truthy "$TRACE_TO_BRAINTRUST" -} - -# Validate requirements -check_requirements() { - for cmd in jq curl uuidgen; do - command -v "$cmd" &>/dev/null || { log "ERROR" "$cmd not installed"; return 1; } - done - [ -z "$API_KEY" ] && { log "ERROR" "BRAINTRUST_API_KEY not set"; return 1; } - return 0 -} - -# Get or create project ID (cached per project name) -get_project_id() { - local name="$1" - local cache_key="project_id_$name" - - # Check cache first - local cached_id - cached_id=$(get_cache_value "$cache_key") - if [ -n "$cached_id" ]; then - echo "$cached_id" - return 0 - fi - - local encoded_name - encoded_name=$(printf '%s' "$name" | jq -sRr @uri) - - # Try to get existing project - local api_url - api_url=$(get_api_url) - local resp http_code - resp=$(curl -s -w "\n%{http_code}" -H "Authorization: Bearer $API_KEY" "$api_url/v1/project?project_name=$encoded_name" 2>/dev/null) - http_code=$(echo "$resp" | tail -1) - resp=$(echo "$resp" | sed '$d') - - if [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then - log "ERROR" "Braintrust authentication failed (HTTP $http_code) - BRAINTRUST_API_KEY is invalid, expired, or lacks permission. Get a new key at $APP_URL/app/settings?subroute=api-keys and set BRAINTRUST_API_KEY. Response: $resp" - return 1 - fi - - local pid - pid=$(echo "$resp" | jq -r '.id // empty' 2>/dev/null) - - if [ -n "$pid" ]; then - set_cache_value "$cache_key" "$pid" - echo "$pid" - return 0 - fi - - if [ "$http_code" != "200" ] && [ "$http_code" != "404" ]; then - log "WARN" "Project lookup returned HTTP $http_code at $api_url/v1/project: $resp" - fi - - # Create project. Build the JSON body with jq so any special chars - # (quotes, backslashes, control chars) in the project name are - # properly escaped rather than interpolated raw into a string literal. - debug "Creating project: $name" - local create_body - create_body=$(jq -nc --arg name "$name" '{name: $name}') - resp=$(curl -s -w "\n%{http_code}" -X POST -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ - -d "$create_body" "$api_url/v1/project" 2>/dev/null) - http_code=$(echo "$resp" | tail -1) - resp=$(echo "$resp" | sed '$d') - - if [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then - log "ERROR" "Braintrust authentication failed (HTTP $http_code) while creating project '$name' - BRAINTRUST_API_KEY is invalid, expired, or lacks permission. Get a new key at $APP_URL/app/settings?subroute=api-keys. Response: $resp" - return 1 - fi - - pid=$(echo "$resp" | jq -r '.id // empty' 2>/dev/null) - - if [ -n "$pid" ]; then - set_cache_value "$cache_key" "$pid" - echo "$pid" - return 0 - fi - - log "ERROR" "Failed to create project '$name' (HTTP $http_code) at $api_url/v1/project: $resp" - return 1 -} - -# Check if we're in experiment mode -is_experiment_mode() { - [ -n "$CC_EXPERIMENT_ID" ] -} - -# Get the insert endpoint URL based on mode (experiment vs project_logs) -get_insert_endpoint() { - local object_id="$1" - local api_url - api_url=$(get_api_url) - - if is_experiment_mode; then - echo "$api_url/v1/experiment/$CC_EXPERIMENT_ID/insert" - else - echo "$api_url/v1/project_logs/$object_id/insert" - fi -} - -# Low-level HTTP insert: POSTs a single span event to the Braintrust insert -# endpoint and returns the inserted row id on stdout. Used by the queue -# worker; hooks should call enqueue_span() instead. -# -# In experiment mode, project_id is ignored and CC_EXPERIMENT_ID is used. -_http_insert_span() { - local project_id="$1" - local event_json="$2" - - event_json=$(add_span_origin_context "$event_json") || { - log "ERROR" "Insert aborted: failed to add span origin context" - return 1 - } - - debug "Inserting span: $(echo "$event_json" | jq -c '.')" - - if [ -z "$API_KEY" ]; then - log "ERROR" "API_KEY is empty - check BRAINTRUST_API_KEY env var" - return 1 - fi - - local endpoint - endpoint=$(get_insert_endpoint "$project_id") - debug "Insert endpoint: $endpoint" - - # Wrap the (already-jq-built) event in the insert envelope via jq. - # This validates the event is well-formed JSON before we POST it and - # avoids hand-crafted string concatenation around the body. - local body - body=$(jq -nc --argjson event "$event_json" '{events: [$event]}') || { - log "ERROR" "Insert aborted: event JSON failed to parse" - return 1 - } - - local resp http_code - resp=$(curl -s -w "\n%{http_code}" -X POST \ - -H "Authorization: Bearer $API_KEY" \ - -H "Content-Type: application/json" \ - -d "$body" \ - "$endpoint" 2>&1) - - http_code=$(echo "$resp" | tail -1) - resp=$(echo "$resp" | sed '$d') - - if [ "$http_code" != "200" ]; then - log "ERROR" "Insert failed (HTTP $http_code) to $endpoint: $resp" - return 1 - fi - - local row_id - row_id=$(echo "$resp" | jq -r '.row_ids[0] // empty' 2>/dev/null) - - if [ -n "$row_id" ]; then - echo "$row_id" - return 0 - else - log "WARN" "Insert returned empty row_ids: $resp" - return 1 - fi -} - -detect_span_origin_environment_json() { - if [ -n "${BRAINTRUST_ENVIRONMENT_TYPE:-}" ] || [ -n "${BRAINTRUST_ENVIRONMENT_NAME:-}" ]; then - jq -nc \ - --arg type "$BRAINTRUST_ENVIRONMENT_TYPE" \ - --arg name "${BRAINTRUST_ENVIRONMENT_NAME:-}" \ - '$ARGS.named | with_entries(select(.value != ""))' - return 0 - fi - if [ -n "${GITHUB_ACTIONS:-}" ]; then jq -nc '{type:"ci", name:"github_actions"}'; return 0; fi - if [ -n "${GITLAB_CI:-}" ]; then jq -nc '{type:"ci", name:"gitlab_ci"}'; return 0; fi - if [ -n "${CIRCLECI:-}" ]; then jq -nc '{type:"ci", name:"circleci"}'; return 0; fi - if [ -n "${BUILDKITE:-}" ]; then jq -nc '{type:"ci", name:"buildkite"}'; return 0; fi - if [ -n "${CI:-}" ]; then jq -nc '{type:"ci", name:"ci"}'; return 0; fi - if [ -n "${VERCEL:-}" ]; then jq -nc '{type:"server", name:"vercel"}'; return 0; fi - if [ -n "${NETLIFY:-}" ]; then jq -nc '{type:"server", name:"netlify"}'; return 0; fi - if [ -n "${AWS_LAMBDA_FUNCTION_NAME:-}" ] || [ -n "${AWS_EXECUTION_ENV:-}" ]; then jq -nc '{type:"server", name:"aws_lambda"}'; return 0; fi - if [ "${NODE_ENV:-}" = "production" ] || [ "${NODE_ENV:-}" = "staging" ]; then - jq -nc --arg name "$NODE_ENV" '{type:"server", name:$name}' - return 0 - fi - if [ "${NODE_ENV:-}" = "development" ] || [ "${NODE_ENV:-}" = "local" ]; then - jq -nc --arg name "$NODE_ENV" '{type:"local", name:$name}' - return 0 - fi - jq -nc 'null' -} - -add_span_origin_context() { - local event_json="$1" - local version environment - version=$(get_plugin_version) - environment=$(detect_span_origin_environment_json) - jq -c \ - --arg version "$version" \ - --argjson environment "$environment" \ - '.context = ((.context // {}) + { - span_origin: ({ - name: "braintrust.plugin.claude-code", - version: $version, - instrumentation: {name: "claude-code-hooks"} - } + (if $environment == null then {} else {environment: $environment} end)) - })' <<< "$event_json" -} - -### -# Queue layer (per-session) -# -# Hooks call enqueue_span() to schedule a span insert without blocking. -# Each Claude Code session gets its own queue subtree and its own -# background worker (hooks/worker.sh). This isolates sessions from one -# another: one session's slow inserts can't delay another's, and one -# session's worker crash doesn't strand another's spans. -# -# Filesystem layout: -# $QUEUE_DIR//pending/-.json -# $QUEUE_DIR//processing/-.json -# $QUEUE_DIR//worker.lock -# -# The worker.lock file holds the worker PID, and the worker refreshes its -# mtime on every loop iteration. A future session_start invocation can -# detect a crashed session by finding stale lock files (mtime older than -# $BRAINTRUST_WORKER_STALE_SECS). -# -# Job file is one JSON object: {project_id, experiment_id, event} -### - -# Return the queue dir for a session (no trailing slash). -session_queue_dir() { - local session_id="$1" - echo "$QUEUE_DIR/$session_id" -} - -# Create the on-disk layout for a session's queue if it doesn't already exist. -_ensure_session_queue() { - local session_id="$1" - local dir - dir=$(session_queue_dir "$session_id") - mkdir -p "$dir/pending" "$dir/processing" -} - -# Generate a monotonic-ish job filename. Uses epoch-ns + uuid suffix so the -# directory listing sorts in roughly insertion order without needing a -# central counter (multiple processes can enqueue concurrently safely). -# -# FIFO caveats: -# - Across distinct hook invocations, timestamps differ enough that -# create-then-merge orderings (e.g. user_prompt_submit creating a -# Turn span and a later stop_hook enqueuing a merge update to it) -# are reliably ordered correctly. -# - Within a SINGLE process that enqueues multiple spans back-to-back, -# timestamps can collide. We tie-break on uuid suffix, which is -# effectively random - meaning two enqueues from the same process at -# the same timestamp are NOT guaranteed FIFO. Don't enqueue a span -# and its merge from the same hook script. -# - On macOS without python3, the fallback drops to second-level -# precision (appending `000000000`), widening the collision window -# dramatically. Prefer python3 (or coreutils `gdate +%s%N`) when -# available to keep nanosecond precision. -_queue_job_name() { - local ts uuid - # Linux: `date +%s%N` gives epoch-nanoseconds directly. - if date +%s%N 2>/dev/null | grep -qv 'N$'; then - ts=$(date +%s%N) - elif command -v python3 >/dev/null 2>&1; then - # macOS: prefer python's time_ns for true ns precision. - ts=$(python3 -c 'import time; print(time.time_ns())') - elif command -v gdate >/dev/null 2>&1; then - # macOS with GNU coreutils installed. - ts=$(gdate +%s%N) - else - # Last resort: second-level precision (FIFO collisions likely - # under high load). See caveat above. - ts=$(date +%s)000000000 - fi - uuid=$(generate_uuid 2>/dev/null || echo "$$-$RANDOM") - echo "${ts}-${uuid}.json" -} - -# Enqueue a span insert for a specific session. Returns immediately after -# writing the job file. If BRAINTRUST_SYNC_QUEUE is truthy, processes the -# job inline instead (used in tests and as a debug fallback). -# -# Args: session_id project_id event_json -enqueue_span() { - local session_id="$1" - local project_id="$2" - local event_json="$3" - - if [ -z "$session_id" ]; then - log "ERROR" "enqueue_span called without session_id" - return 1 - fi - - # Build the job JSON - local job - job=$(jq -nc \ - --arg pid "$project_id" \ - --arg exp "${CC_EXPERIMENT_ID:-}" \ - --argjson event "$event_json" \ - '{type: "insert_span", project_id: $pid, experiment_id: $exp, event: $event}') - - # Synchronous mode: process inline. This is what tests use, and it's - # also the fallback if a user wants the old blocking behavior. - if is_truthy "$BRAINTRUST_SYNC_QUEUE"; then - _process_job_inline "$job" - return $? - fi - - # Async mode: write to /pending/ and ensure a worker is running. - _ensure_session_queue "$session_id" - local sdir - sdir=$(session_queue_dir "$session_id") - local job_file="$sdir/pending/$(_queue_job_name)" - - # Write atomically: write to tmp, then rename. - local tmp="${job_file}.tmp.$$" - echo "$job" > "$tmp" || return 1 - mv "$tmp" "$job_file" || return 1 - - debug "Enqueued job for session $session_id: $(basename "$job_file")" - ensure_worker_running "$session_id" - return 0 -} - -# Process a job in the current process. Returns the same exit code as -# _http_insert_span. Used by both sync mode and the worker. -_process_job_inline() { - local job="$1" - local project_id experiment_id event_json - project_id=$(echo "$job" | jq -r '.project_id // ""') - experiment_id=$(echo "$job" | jq -r '.experiment_id // ""') - event_json=$(echo "$job" | jq -c '.event') - - # Temporarily set CC_EXPERIMENT_ID so _http_insert_span routes correctly. - local prev_exp="${CC_EXPERIMENT_ID:-}" - CC_EXPERIMENT_ID="$experiment_id" - _http_insert_span "$project_id" "$event_json" >/dev/null - local rc=$? - CC_EXPERIMENT_ID="$prev_exp" - return $rc -} - -# Ensure a background worker is running for the given session. If one is -# already alive (worker.lock exists with a live PID), do nothing. -# Otherwise fork a new worker. -# -# Args: session_id -ensure_worker_running() { - is_truthy "$BRAINTRUST_SYNC_QUEUE" && return 0 - - local session_id="$1" - if [ -z "$session_id" ]; then - log "ERROR" "ensure_worker_running called without session_id" - return 1 - fi - - local sdir lock_file - sdir=$(session_queue_dir "$session_id") - lock_file="$sdir/worker.lock" - - # If a live worker holds the lock, we're done. - if [ -f "$lock_file" ]; then - local pid - pid=$(cat "$lock_file" 2>/dev/null) - if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then - return 0 - fi - # Stale lock; clean it up before spawning. - rm -f "$lock_file" - fi - - # Fork a new worker, scoped to this session. - local script_dir worker_script - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - worker_script="$script_dir/worker.sh" - - if [ ! -f "$worker_script" ]; then - log "ERROR" "Worker script not found: $worker_script" - return 1 - fi - - _ensure_session_queue "$session_id" - - # Detach the worker so it persists after this hook exits. - nohup bash "$worker_script" "$session_id" >/dev/null 2>&1 & - disown 2>/dev/null || true - debug "Spawned worker for session $session_id (parent pid=$$)" - return 0 -} - -# Block until this session's queue is drained, or BRAINTRUST_DRAIN_TIMEOUT -# elapses. Used by session_end.sh to ensure all spans are flushed before -# Claude Code exits. -# -# Args: session_id [timeout_secs] -drain_queue() { - is_truthy "$BRAINTRUST_SYNC_QUEUE" && return 0 - - local session_id="$1" - local timeout="${2:-$BRAINTRUST_DRAIN_TIMEOUT}" - - if [ -z "$session_id" ]; then - log "ERROR" "drain_queue called without session_id" - return 1 - fi - - local deadline=$(( $(date +%s) + timeout )) - debug "Draining queue for session $session_id (timeout=${timeout}s)" - - while [ "$(date +%s)" -lt "$deadline" ]; do - if ! _queue_has_jobs "$session_id"; then - debug "Queue drained successfully for session $session_id" - return 0 - fi - # Make sure a worker is running on every iteration so a transient - # worker death does not stall the drain. - ensure_worker_running "$session_id" - sleep 0.2 - done - - local remaining - remaining=$(_queue_pending_count "$session_id") - log "WARN" "drain_queue timed out for session $session_id after ${timeout}s with $remaining job(s) still pending" - return 1 -} - -# True if any jobs are in this session's pending/ or processing/ dir. -# Args: session_id -_queue_has_jobs() { - local session_id="$1" - [ "$(_queue_pending_count "$session_id")" -gt 0 ] || \ - [ "$(_queue_processing_count "$session_id")" -gt 0 ] -} - -# Args: session_id -_queue_pending_count() { - local session_id="$1" - local sdir - sdir=$(session_queue_dir "$session_id") - local n - n=$(find "$sdir/pending" -maxdepth 1 -name '*.json' -type f 2>/dev/null | wc -l) - echo "${n// /}" -} - -# Args: session_id -_queue_processing_count() { - local session_id="$1" - local sdir - sdir=$(session_queue_dir "$session_id") - local n - n=$(find "$sdir/processing" -maxdepth 1 -name '*.json' -type f 2>/dev/null | wc -l) - echo "${n// /}" -} - -# Sweep $QUEUE_DIR for session dirs that look crashed (worker.lock mtime -# older than BRAINTRUST_WORKER_STALE_SECS, or lock missing while pending -# files exist). For each stale session: kill the lock-holder PID if still -# around, sweep processing/ back to pending/, and respawn a worker to -# drain anything that's left. Empty stale dirs are removed. -# -# Skips the session_id passed as an argument (the current session), if any. -# -# Args: [current_session_id] -sweep_dead_sessions() { - is_truthy "$BRAINTRUST_SYNC_QUEUE" && return 0 - - local current_session="${1:-}" - local stale_secs="$BRAINTRUST_WORKER_STALE_SECS" - local now - now=$(date +%s) - - [ -d "$QUEUE_DIR" ] || return 0 - - local entry sid sdir lock_file pid lock_mtime age pending processing - for entry in "$QUEUE_DIR"/*; do - [ -d "$entry" ] || continue - sid=$(basename "$entry") - [ "$sid" = "$current_session" ] && continue - - sdir="$entry" - lock_file="$sdir/worker.lock" - pending=$(_queue_pending_count "$sid") - processing=$(_queue_processing_count "$sid") - - if [ -f "$lock_file" ]; then - # _file_mtime returns 0 if it can't stat the file - lock_mtime=$(_file_mtime "$lock_file") - age=$(( now - lock_mtime )) - if [ "$age" -lt "$stale_secs" ]; then - # Looks alive (or recently was). Leave it alone. - continue - fi - # Stale lock - try to kill the holder (harmless if already gone). - pid=$(cat "$lock_file" 2>/dev/null) - if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then - log "WARN" "Sweeping dead session $sid: killing stale worker pid=$pid (lock age=${age}s)" - kill "$pid" 2>/dev/null || true - else - log "INFO" "Sweeping dead session $sid (lock age=${age}s, pid=$pid not running)" - fi - rm -f "$lock_file" - elif [ "$pending" -eq 0 ] && [ "$processing" -eq 0 ]; then - # No lock and no jobs - clean leftover empty dir. - rmdir "$sdir/pending" "$sdir/processing" 2>/dev/null || true - rmdir "$sdir" 2>/dev/null || true - continue - fi - - # Recover any in-flight jobs by moving them back to pending. - local f - for f in "$sdir"/processing/*.json; do - [ -e "$f" ] || continue - mv "$f" "$sdir/pending/$(basename "$f")" 2>/dev/null || true - done - - pending=$(_queue_pending_count "$sid") - if [ "$pending" -gt 0 ]; then - log "INFO" "Recovering $pending orphaned span(s) from crashed session $sid" - ensure_worker_running "$sid" - else - # Nothing left to do. Tidy up. - rmdir "$sdir/pending" "$sdir/processing" 2>/dev/null || true - rmdir "$sdir" 2>/dev/null || true - fi - done -} - -# Stat a file's mtime in epoch seconds, portable across macOS (BSD stat) -# and Linux (GNU stat). Returns 0 if the file can't be stat'd. -_file_mtime() { - local f="$1" - [ -f "$f" ] || { echo 0; return; } - stat -f '%m' "$f" 2>/dev/null || stat -c '%Y' "$f" 2>/dev/null || echo 0 -} - -### -# Per-session state management -# Each session has its own state file: $SESSION_STATE_DIR/{session_id}.json -# This eliminates race conditions between sessions entirely. -### - -# Get the state file path for a session -get_session_state_file() { - local session_id="$1" - echo "$SESSION_STATE_DIR/${session_id}.json" -} - -# Get a value from session state -get_session_state() { - local session_id="$1" - local key="$2" - local state_file - state_file=$(get_session_state_file "$session_id") - # Use --arg + bracket lookup so keys containing dashes/dots/etc work - [ -f "$state_file" ] && jq -r --arg k "$key" '.[$k] // empty' "$state_file" 2>/dev/null || echo "" -} - -# Set a value in session state -set_session_state() { - local session_id="$1" - local key="$2" - local value="$3" - local state_file state - state_file=$(get_session_state_file "$session_id") - state=$([ -f "$state_file" ] && cat "$state_file" || echo '{}') - state=$(echo "$state" | jq --arg k "$key" --arg v "$value" '.[$k] = $v') - echo "$state" > "$state_file" -} - -# Atomic check-and-set for session state - returns 0 if set, 1 if already exists -# Uses mkdir as an atomic lock for the specific session -check_and_set_session_state() { - local session_id="$1" - local key="$2" - local value="$3" - local state_file lock_dir - state_file=$(get_session_state_file "$session_id") - lock_dir="${state_file}.lock" - - # Try to acquire lock for this specific session - if ! mkdir "$lock_dir" 2>/dev/null; then - # Another process is initializing this session, wait briefly and check - sleep 0.1 - local existing - existing=$(get_session_state "$session_id" "$key") - if [ -n "$existing" ]; then - echo "$existing" - return 1 - fi - # Lock was released but key still not set - try again - rmdir "$lock_dir" 2>/dev/null || true - if ! mkdir "$lock_dir" 2>/dev/null; then - # Still can't get lock, just check and return - existing=$(get_session_state "$session_id" "$key") - if [ -n "$existing" ]; then - echo "$existing" - return 1 - fi - fi - fi - - # We have the lock - check if key already exists - local existing - existing=$(get_session_state "$session_id" "$key") - if [ -n "$existing" ]; then - rmdir "$lock_dir" 2>/dev/null || true - echo "$existing" - return 1 - fi - - # Set the value - set_session_state "$session_id" "$key" "$value" - rmdir "$lock_dir" 2>/dev/null || true - return 0 -} - -# Clean up old session state files (call periodically or from session_stop) -cleanup_old_sessions() { - local max_age_hours="${1:-24}" - local max_age_minutes=$((max_age_hours * 60)) - find "$SESSION_STATE_DIR" -name "*.json" -mmin "+$max_age_minutes" -delete 2>/dev/null || true - find "$SESSION_STATE_DIR" -name "*.lock" -mmin "+5" -delete 2>/dev/null || true -} - -# Generate a UUID -generate_uuid() { - uuidgen | tr '[:upper:]' '[:lower:]' -} - -# Get current ISO timestamp -get_timestamp() { - date -u +"%Y-%m-%dT%H:%M:%S.000Z" -} - -# Get system info for metadata -get_hostname() { - hostname 2>/dev/null || echo "unknown" -} - -get_username() { - whoami 2>/dev/null || echo "unknown" -} - -get_os() { - uname -s 2>/dev/null || echo "unknown" -} - -redact_git_remote_url() { - local remote="$1" - [ -z "$remote" ] && return 0 - - case "$remote" in - *://*@*) - local scheme="${remote%%://*}" - local rest="${remote#*://}" - echo "${scheme}://${rest#*@}" - ;; - *) - echo "$remote" - ;; - esac -} - -git_metadata_json() { - local cwd="$1" - if [ -z "$cwd" ]; then - echo '{}' - return 0 - fi - - local origin branch commit - origin=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" remote get-url origin 2>/dev/null || true) - branch=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" symbolic-ref --quiet --short HEAD 2>/dev/null || true) - commit=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" rev-parse HEAD 2>/dev/null || true) - - origin=$(redact_git_remote_url "$origin") - - jq -cn \ - --arg origin "$origin" \ - --arg branch "$branch" \ - --arg commit "$commit" \ - '{ - git_origin_url: $origin, - git_branch: $branch, - git_commit_sha: $commit - } | with_entries(select(.value != ""))' -} - -# Version of this plugin, read from its plugin.json manifest. Cached after the -# first lookup. Returns "unknown" if it can't be read. -get_plugin_version() { - if [ -n "${_PLUGIN_VERSION:-}" ]; then - echo "$_PLUGIN_VERSION" - return - fi - local manifest="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/../.claude-plugin/plugin.json" - local v="" - [ -f "$manifest" ] && v=$(jq -r '.version // empty' "$manifest" 2>/dev/null) - _PLUGIN_VERSION="${v:-unknown}" - echo "$_PLUGIN_VERSION" -} - -# Version of the running Claude Code CLI. Prefers a version found in the -# session transcript (authoritative for the run that produced it); falls back -# to `claude --version`. Cached after the first lookup. Returns "unknown" if -# neither source is available. -# -# Args: [transcript_path] - optional transcript to read `.version` from. -get_claude_code_version() { - local transcript="${1:-}" - if [ -n "${_CC_VERSION:-}" ]; then - echo "$_CC_VERSION" - return - fi - local v="" - if [ -n "$transcript" ] && [ -f "$transcript" ]; then - v=$(jq -rc 'select(.version) | .version' "$transcript" 2>/dev/null | head -1) - fi - if [ -z "$v" ]; then - # e.g. "2.1.173 (Claude Code)" -> "2.1.173" - v=$(claude --version 2>/dev/null | awk '{print $1}') - fi - _CC_VERSION="${v:-unknown}" - echo "$_CC_VERSION" -} - -### -# Emit spans for a Claude Code transcript file (typically a sub-agent's own -# transcript), parented under a given span. This reproduces the same span -# structure the main conversation uses, so a sub-agent's Agent tool span -# subtree reads like a miniature conversation: -# -# Agent (tool, the parent) -# ├── (llm) - one model call (plan + tool_use) -# ├── (tool) - a tool the sub-agent invoked -# ├── (llm) - next model call (after the tool result) -# └── ... -# -# We walk the transcript chronologically, threading conversation history so -# each LLM span's `input` is the messages seen so far and its `output` is the -# assistant content plus OpenAI-style tool_calls. Each tool_result becomes a -# tool span. -# -# Token accounting mirrors stop_hook.sh: a single API response repeats across -# content-block lines sharing one requestId; input/cache are identical on each -# line (count once) while output_tokens streams cumulatively (take the max). -# -# Args: -# $1 transcript_file - path to the (sub-agent) transcript JSONL -# $2 session_id - session id (for enqueue) -# $3 project_id - project id (for enqueue) -# $4 root_span_id - root span id of the trace -# $5 parent_span_id - span the emitted spans should be children of -# -# Returns the number of LLM spans emitted on stdout (tool spans are not -# counted, to preserve the historical return-value contract). Best-effort: -# returns 0 and emits nothing if the file is missing or has no usable content. -emit_llm_spans_from_transcript() { - local transcript_file="$1" - local session_id="$2" - local project_id="$3" - local root_span_id="$4" - local parent_span_id="$5" - - [ -n "$transcript_file" ] && [ -f "$transcript_file" ] || { echo 0; return 0; } - - # Single jq pass: walk the transcript in order and emit an ordered NDJSON - # stream of "span directives", one per line. Each directive is either: - # {kind:"llm", ...metrics, input:, output:} - # {kind:"tool", name, input:, output:, ts} - # History is threaded so each llm directive carries the conversation as it - # stood when that call was made. Doing this in jq (rather than a bash loop) - # keeps multi-line text and tool JSON intact. - local directives - directives=$(jq -s -c ' - # Collapse assistant content-block lines of one response (same - # requestId) into a single logical message, taking max tokens and - # concatenating text / collecting tool_use blocks. - def assistant_calls: - [ .[] | select(.type=="assistant") | select(.message.usage != null) ] - | group_by(.requestId // .message.id) - | map({ - kind: "llm", - rid: (.[0].requestId // .[0].message.id), - ts: .[0].timestamp, - model: (.[0].message.model // "claude"), - input_tokens: ([ .[].message.usage.input_tokens // 0 ] | max), - output_tokens: ([ .[].message.usage.output_tokens // 0 ] | max), - cache_creation_tokens: ([ .[].message.usage.cache_creation_input_tokens // 0 ] | max), - cache_creation_5m_tokens: ([ .[].message.usage.cache_creation.ephemeral_5m_input_tokens // 0 ] | max), - cache_creation_1h_tokens: ([ .[].message.usage.cache_creation.ephemeral_1h_input_tokens // 0 ] | max), - cache_creation_has_split: any(.[]; (((.message.usage.cache_creation.ephemeral_5m_input_tokens // 0) + (.message.usage.cache_creation.ephemeral_1h_input_tokens // 0)) > 0)), - cache_read_tokens: ([ .[].message.usage.cache_read_input_tokens // 0 ] | max), - text: ( [ .[].message.content - | if type=="array" then [ .[]|select(.type=="text")|.text ]|join("\n") - elif type=="string" then . else "" end ] - | map(select(. != "")) | join("\n") ), - tool_calls: ( [ .[].message.content[]? - | select(.type=="tool_use") - | { id: .id, type: "function", - function: { name: .name, arguments: (.input|tojson) } } ] - | unique_by(.id) ) - }) - # group_by sorts by the grouping key (requestId / message.id), which - # is not guaranteed to match conversation order. Re-sort by timestamp - # so history threading and span emission follow chronological order. - | sort_by(.ts); - - # Tool results, keyed by the tool_use_id they answer. - def tool_results: - [ .[] - | select(.type=="user") - | (.message.content) as $c - | select(($c|type=="array") and ($c[0].type=="tool_result")) - | { kind: "tool", - tool_use_id: $c[0].tool_use_id, - ts: .timestamp, - output: ($c[0].content), - is_error: ($c[0].is_error // false) } ]; - - (assistant_calls) as $llms - | (tool_results) as $tools - # Index tool results by id so we can attach name/args from the matching - # tool_use and place them right after the llm call that issued them. - | ( reduce $tools[] as $t ({}; .[$t.tool_use_id] = $t) ) as $tool_by_id - # Build the ordered output: for each llm call, emit it (with input - # history threaded), then emit a tool span for each of its tool_calls - # that has a matching result. We thread { history, out } through a - # single reduce; the inner per-tool work is done with map/reduce - # expressions that update the accumulator directly (no nested `reduce - # (...) as $x`, which jq does not allow). - | reduce $llms[] as $call ( - { history: [], out: [] }; - # Assistant message object (content + optional tool_calls). - ( { role:"assistant", content:$call.text } - + ( if ($call.tool_calls|length)>0 then {tool_calls:$call.tool_calls} else {} end ) - ) as $assistant_msg - # The llm directive carries the current history as input. - | ( { - kind: "llm", - ts: $call.ts, - model: $call.model, - input_tokens: $call.input_tokens, - output_tokens: $call.output_tokens, - cache_creation_tokens: $call.cache_creation_tokens, - cache_creation_5m_tokens: $call.cache_creation_5m_tokens, - cache_creation_1h_tokens: $call.cache_creation_1h_tokens, - cache_creation_has_split: $call.cache_creation_has_split, - cache_read_tokens: $call.cache_read_tokens, - input: .history, - output: $assistant_msg - } ) as $llm_dir - # Resolved tool calls for this llm call (those with a matching - # result), in order. - | ( [ $call.tool_calls[] - | { tc: ., res: $tool_by_id[.id] } - | select(.res != null) ] ) as $resolved - # Tool directives to emit after the llm directive. - | ( [ $resolved[] | { - kind: "tool", - ts: .res.ts, - tool_use_id: .res.tool_use_id, - name: .tc.function.name, - input: (.tc.function.arguments), - output: (.res.output), - is_error: (.res.is_error // false) - } ] ) as $tool_dirs - # History additions: the assistant message, then each tool result. - | ( [ $assistant_msg ] - + [ $resolved[] | { role:"tool", tool_call_id:.tc.id, - content: (.res.output|tostring) } ] ) as $hist_add - | { - history: ( .history + $hist_add ), - out: ( .out + [ $llm_dir ] + $tool_dirs ) - } - ) - | .out[] - ' "$transcript_file" 2>/dev/null) - - [ -z "$directives" ] && { echo 0; return 0; } - - local emitted=0 - local dir - while IFS= read -r dir; do - [ -z "$dir" ] && continue - - local kind ts epoch span_id - kind=$(echo "$dir" | jq -r '.kind') - ts=$(echo "$dir" | jq -r '.ts // empty') - epoch=$(_iso_to_epoch "$ts") - span_id=$(generate_uuid) - - local event - if [ "$kind" = "llm" ]; then - # LLM span: input is the threaded conversation history, output is - # the assistant message (content + tool_calls). Build in one jq - # call so multi-line text survives. - event=$(echo "$dir" | jq -c \ - --arg id "$span_id" \ - --arg root_span_id "$root_span_id" \ - --arg parent "$parent_span_id" \ - --argjson epoch "$epoch" \ - '{ - id: $id, - span_id: $id, - root_span_id: $root_span_id, - span_parents: [$parent], - created: (.ts // (now|todate)), - input: .input, - output: .output, - metrics: ({ - start: $epoch, end: $epoch, - prompt_tokens: ( - .input_tokens - + .cache_read_tokens - + ( - if .cache_creation_has_split then - (.cache_creation_5m_tokens + .cache_creation_1h_tokens) - else - .cache_creation_tokens - end - ) - ), - completion_tokens: .output_tokens, - tokens: ( - .input_tokens - + .cache_read_tokens - + ( - if .cache_creation_has_split then - (.cache_creation_5m_tokens + .cache_creation_1h_tokens) - else - .cache_creation_tokens - end - ) - + .output_tokens - ), - prompt_cached_tokens: .cache_read_tokens - } + ( - if .cache_creation_has_split then - { - prompt_cache_creation_5m_tokens: .cache_creation_5m_tokens, - prompt_cache_creation_1h_tokens: .cache_creation_1h_tokens - } - else - {prompt_cache_creation_tokens: .cache_creation_tokens} - end - )), - metadata: { model: .model }, - span_attributes: { name: .model, type: "llm" } - }') - else - # Tool span: mirror post_tool_use.sh shape (name + tool metadata). - local tool_name - tool_name=$(echo "$dir" | jq -r '.name // "tool"') - local span_name - span_name=$(_subagent_tool_span_name "$tool_name" "$(echo "$dir" | jq -c '.input')") - event=$(echo "$dir" | jq -c \ - --arg id "$span_id" \ - --arg root_span_id "$root_span_id" \ - --arg parent "$parent_span_id" \ - --arg name "$span_name" \ - --arg tool "$tool_name" \ - --argjson epoch "$epoch" \ - '{ - id: $id, - span_id: $id, - root_span_id: $root_span_id, - span_parents: [$parent], - created: (.ts // (now|todate)), - input: (.input | (try fromjson catch .)), - output: .output, - metrics: { start: $epoch, end: $epoch }, - metadata: { - tool_name: $tool, - tool_call_id: .tool_use_id, - tool_approval: "approved" - }, - span_attributes: { name: $name, type: "tool" } - } - + (if .is_error then {error: (.output|tostring)} else {} end)') - fi - - if [ -n "$event" ] && enqueue_span "$session_id" "$project_id" "$event"; then - [ "$kind" = "llm" ] && emitted=$((emitted + 1)) - fi - done <<< "$directives" - - echo "$emitted" - return 0 -} - -# Derive a tool span display name the same way post_tool_use.sh does, so -# sub-agent tool spans read consistently with top-level tool spans. -# Args: tool_name, tool_input_json -_subagent_tool_span_name() { - local tool_name="$1" - local tool_input="$2" - case "$tool_name" in - Read|Write|Edit|MultiEdit) - local fp - fp=$(echo "$tool_input" | jq -r '(. | (try fromjson catch .)) | (.file_path // .path // empty)' 2>/dev/null) - [ -n "$fp" ] && echo "$tool_name: $(basename "$fp")" || echo "$tool_name" - ;; - Bash|Terminal) - local cmd - cmd=$(echo "$tool_input" | jq -r '(. | (try fromjson catch .)) | (.command // empty)' 2>/dev/null | head -c 50) - echo "Terminal: ${cmd:-command}" - ;; - mcp__*) - echo "$tool_name" | sed 's/mcp__/MCP: /' | sed 's/__/ - /' - ;; - *) - echo "$tool_name" - ;; - esac -} - -# Convert an ISO-8601 timestamp (UTC, e.g. 2026-06-11T03:01:33.000Z) to a -# Unix epoch. Falls back to the current time when parsing fails. Shared by -# transcript-parsing code that needs span start/end metrics. -_iso_to_epoch() { - local ts="$1" - [ -z "$ts" ] && { date +%s; return; } - local clean_ts="${ts%.*}" # strip .xxxZ - clean_ts="${clean_ts}+0000" # treat as UTC - date -j -f "%Y-%m-%dT%H:%M:%S%z" "$clean_ts" "+%s" 2>/dev/null || \ - date -d "$ts" "+%s" 2>/dev/null || \ - date +%s -} diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/hooks.json b/src/plugins/claude/content/plugins/trace-claude-code/hooks/hooks.json index a6a9e9b..ae7e201 100644 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/hooks.json +++ b/src/plugins/claude/content/plugins/trace-claude-code/hooks/hooks.json @@ -5,7 +5,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session_start.sh", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -16,7 +16,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_submit.sh", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -28,7 +28,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_use.sh", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -39,7 +39,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/stop_hook.sh", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -50,7 +50,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session_end.sh", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -61,7 +61,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh Setup", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -72,7 +72,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_expansion.sh", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -84,7 +84,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh PreToolUse", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -96,7 +96,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh PermissionRequest", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -108,7 +108,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/permission_denied.sh", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -120,7 +120,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_use_failure.sh", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -131,7 +131,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh PostToolBatch", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -143,7 +143,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh Notification", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -154,7 +154,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh MessageDisplay", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -166,7 +166,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh SubagentStart", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -178,7 +178,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh SubagentStop", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -189,7 +189,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh TaskCreated", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -200,7 +200,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh TaskCompleted", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -212,7 +212,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh StopFailure", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -223,7 +223,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh TeammateIdle", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -235,7 +235,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh InstructionsLoaded", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -247,7 +247,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh ConfigChange", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -258,7 +258,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh CwdChanged", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -269,7 +269,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh WorktreeCreate", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -280,7 +280,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh WorktreeRemove", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -292,7 +292,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh PreCompact", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -304,7 +304,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh PostCompact", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -316,7 +316,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh Elicitation", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -328,7 +328,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh ElicitationResult", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] @@ -340,7 +340,7 @@ "hooks": [ { "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh FileChanged", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/claude-hook.sh\"", "async": false } ] diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/permission_denied.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/permission_denied.sh deleted file mode 100644 index a9195e5..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/permission_denied.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -### -# PermissionDenied Hook - Creates a denied tool span when tied to a tool request -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -debug "PermissionDenied hook triggered" - -tracing_enabled || { debug "Tracing disabled"; exit 0; } -check_requirements || exit 0 - -INPUT=$(cat) -record_hook_input "permission_denied" "$INPUT" -debug "PermissionDenied input: $(echo "$INPUT" | jq -c '.' 2>/dev/null | head -c 500)" - -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .tool // empty' 2>/dev/null) -TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // .input // {}' 2>/dev/null) -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) -TOOL_CALL_ID=$(echo "$INPUT" | jq -r '.tool_use_id // empty' 2>/dev/null) -PERMISSION_ID=$(echo "$INPUT" | jq -r '.permission_id // .permission.id // empty' 2>/dev/null) -PERMISSION_TYPE=$(echo "$INPUT" | jq -r '.permission_type // .permission.type // empty' 2>/dev/null) -PERMISSION_TITLE=$(echo "$INPUT" | jq -r '.permission_title // .permission.title // empty' 2>/dev/null) - -[ -z "$TOOL_NAME" ] && { debug "No tool name, skipping"; exit 0; } -[ -z "$SESSION_ID" ] && { debug "No session ID, skipping"; exit 0; } - -ROOT_SPAN_ID=$(get_session_state "$SESSION_ID" "root_span_id") -PROJECT_ID=$(get_session_state "$SESSION_ID" "project_id") -TURN_SPAN_ID=$(get_session_state "$SESSION_ID" "current_turn_span_id") - -if [ -z "$CC_EXPERIMENT_ID" ]; then - CC_EXPERIMENT_ID=$(get_session_state "$SESSION_ID" "experiment_id") - export CC_EXPERIMENT_ID -fi - -if [ -z "$TURN_SPAN_ID" ] || [ -z "$PROJECT_ID" ]; then - debug "No current turn for session $SESSION_ID, skipping denied tool trace" - exit 0 -fi - -SPAN_ID=$(generate_uuid) -TIMESTAMP=$(get_timestamp) -TOOL_TIME=$(date +%s) - -case "$TOOL_NAME" in - Bash|Terminal) - CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty' 2>/dev/null | head -c 50) - SPAN_NAME="Terminal: ${CMD:-command}" - ;; - *) - SPAN_NAME="$TOOL_NAME" - ;; -esac - -EVENT=$(jq -n \ - --arg id "$SPAN_ID" \ - --arg root_span_id "$ROOT_SPAN_ID" \ - --arg parent "$TURN_SPAN_ID" \ - --arg created "$TIMESTAMP" \ - --argjson input "$TOOL_INPUT" \ - --arg name "$SPAN_NAME" \ - --arg tool "$TOOL_NAME" \ - --arg tool_call_id "$TOOL_CALL_ID" \ - --arg permission_id "$PERMISSION_ID" \ - --arg permission_type "$PERMISSION_TYPE" \ - --arg permission_title "$PERMISSION_TITLE" \ - --argjson start_time "$TOOL_TIME" \ - --argjson end_time "$TOOL_TIME" \ - '{ - id: $id, - span_id: $id, - root_span_id: $root_span_id, - span_parents: [$parent], - created: $created, - input: $input, - metrics: {start: $start_time, end: $end_time}, - metadata: ({ - tool_name: $tool, - tool_approval: "denied" - } - + (if $tool_call_id != "" then {tool_call_id: $tool_call_id} else {} end) - + (if $permission_id != "" then {permission_id: $permission_id} else {} end) - + (if $permission_type != "" then {permission_type: $permission_type} else {} end) - + (if $permission_title != "" then {permission_title: $permission_title} else {} end)), - span_attributes: {name: $name, type: "tool"} - }') - -enqueue_span "$SESSION_ID" "$PROJECT_ID" "$EVENT" || { log "ERROR" "Failed to enqueue denied tool span"; exit 0; } - -log "INFO" "Denied tool: $SPAN_NAME (turn=$TURN_SPAN_ID)" -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/post_tool_use.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/post_tool_use.sh deleted file mode 100755 index 911ce44..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/post_tool_use.sh +++ /dev/null @@ -1,217 +0,0 @@ -#!/bin/bash -### -# PostToolUse Hook - Creates a tool span as child of current Turn -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -debug "PostToolUse hook triggered" - -tracing_enabled || { debug "Tracing disabled"; exit 0; } -check_requirements || exit 0 - -# Read input from stdin -INPUT=$(cat) -record_hook_input "post_tool_use" "$INPUT" -debug "PostToolUse input: $(echo "$INPUT" | jq -c '.' 2>/dev/null | head -c 500)" - -# Extract tool info -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) -TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) -TOOL_OUTPUT=$(echo "$INPUT" | jq -c '.tool_response // .output // {}' 2>/dev/null) -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) -TOOL_CALL_ID=$(echo "$INPUT" | jq -r '.tool_use_id // empty' 2>/dev/null) -TOOL_FAILED=$(echo "$INPUT" | jq -r ' - if ((.tool_response.interrupted // false) == true - or (.tool_response.is_error // false) == true - or (.tool_response.isError // false) == true - or (.tool_response.status // "") == "error" - or (.tool_response.status // "") == "failed" - or (.tool_response.error != null)) then - true - else - false - end -' 2>/dev/null) -if [ "$TOOL_FAILED" != "true" ]; then - TOOL_FAILED=false -fi -TOOL_ERROR=$(echo "$INPUT" | jq -r ' - .tool_response.error - // .tool_response.stderr - // .tool_response.message - // .tool_response.output - // "Tool execution failed" -' 2>/dev/null | head -n 1) - -# Skip if no tool name -[ -z "$TOOL_NAME" ] && { debug "No tool name, skipping"; exit 0; } -[ -z "$SESSION_ID" ] && { debug "No session ID, skipping"; exit 0; } - -# Get session info -ROOT_SPAN_ID=$(get_session_state "$SESSION_ID" "root_span_id") -PROJECT_ID=$(get_session_state "$SESSION_ID" "project_id") -TURN_SPAN_ID=$(get_session_state "$SESSION_ID" "current_turn_span_id") - -# Load experiment_id from session state if not already set -if [ -z "$CC_EXPERIMENT_ID" ]; then - CC_EXPERIMENT_ID=$(get_session_state "$SESSION_ID" "experiment_id") - export CC_EXPERIMENT_ID -fi - -# If no turn span exists, tools are orphaned - skip -if [ -z "$TURN_SPAN_ID" ] || [ -z "$PROJECT_ID" ]; then - debug "No current turn for session $SESSION_ID, skipping tool trace" - exit 0 -fi - -# Increment tool count for this turn -TOOL_COUNT=$(get_session_state "$SESSION_ID" "current_turn_tool_count") -TOOL_COUNT=${TOOL_COUNT:-0} -TOOL_COUNT=$((TOOL_COUNT + 1)) -set_session_state "$SESSION_ID" "current_turn_tool_count" "$TOOL_COUNT" - -# Generate span ID -SPAN_ID=$(generate_uuid) -TIMESTAMP=$(get_timestamp) -TOOL_TIME=$(date +%s) - -# Determine span name based on tool -METADATA_TOOL_NAME="$TOOL_NAME" -IS_SKILL_TOOL=false -SKILL_NAME="" -SKILL_LOAD_TRIGGER="" -case "$TOOL_NAME" in - Skill) - IS_SKILL_TOOL=true - SKILL_NAME=$(echo "$TOOL_INPUT" | jq -r '.name // .skill // .skill_name // .skillName // empty' 2>/dev/null) - EXPLICIT_SKILL_NAMES=$(get_session_state "$SESSION_ID" "current_turn_explicit_skill_names") - if [ -n "$SKILL_NAME" ] && [ -n "$EXPLICIT_SKILL_NAMES" ] && \ - echo "$EXPLICIT_SKILL_NAMES" | jq -e --arg name "$SKILL_NAME" 'index($name) != null' >/dev/null 2>&1; then - SKILL_LOAD_TRIGGER="explicit" - fi - if [ -n "$SKILL_NAME" ]; then - SPAN_NAME="skill: $SKILL_NAME" - else - SPAN_NAME="skill" - fi - ;; - Read|Write|Edit|MultiEdit) - FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .path // empty' 2>/dev/null) - if [ -n "$FILE_PATH" ]; then - SPAN_NAME="$TOOL_NAME: $(basename "$FILE_PATH")" - else - SPAN_NAME="$TOOL_NAME" - fi - ;; - Bash|Terminal) - CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty' 2>/dev/null | head -c 50) - SPAN_NAME="Terminal: ${CMD:-command}" - ;; - mcp__*) - SPAN_NAME=$(echo "$TOOL_NAME" | sed 's/mcp__/MCP: /' | sed 's/__/ - /') - ;; - *) - SPAN_NAME="$TOOL_NAME" - ;; -esac - -# Build the event - tool is child of Turn -EVENT=$(jq -n \ - --arg id "$SPAN_ID" \ - --arg span_id "$SPAN_ID" \ - --arg root_span_id "$ROOT_SPAN_ID" \ - --arg parent "$TURN_SPAN_ID" \ - --arg created "$TIMESTAMP" \ - --arg tool "$TOOL_NAME" \ - --argjson input "$TOOL_INPUT" \ - --argjson output "$TOOL_OUTPUT" \ - --arg name "$SPAN_NAME" \ - --arg metadata_tool "$METADATA_TOOL_NAME" \ - --arg tool_call_id "$TOOL_CALL_ID" \ - --arg tool_error "$TOOL_ERROR" \ - --argjson tool_failed "$TOOL_FAILED" \ - --arg skill_name "$SKILL_NAME" \ - --arg skill_load_trigger "$SKILL_LOAD_TRIGGER" \ - --argjson is_skill_tool "$IS_SKILL_TOOL" \ - --argjson start_time "$TOOL_TIME" \ - --argjson end_time "$TOOL_TIME" \ - '{ - id: $id, - span_id: $span_id, - root_span_id: $root_span_id, - span_parents: [$parent], - created: $created, - input: $input, - output: $output, - metrics: { - start: $start_time, - end: $end_time - }, - metadata: ({ - tool_name: $metadata_tool, - tool_approval: "approved" - } - + (if $tool_call_id != "" then {tool_call_id: $tool_call_id} else {} end) - + (if $is_skill_tool then { - tool_kind: "skill", - skill_name: (if $skill_name != "" then $skill_name else null end) - } else {} end) - + (if $skill_load_trigger != "" then {skill_load_trigger: $skill_load_trigger} else {} end)), - span_attributes: { - name: $name, - type: "tool" - } - } - + (if $tool_failed then {error: $tool_error} else {} end)') - -enqueue_span "$SESSION_ID" "$PROJECT_ID" "$EVENT" || { log "ERROR" "Failed to enqueue tool span"; exit 0; } - -log "INFO" "Tool: $SPAN_NAME (turn=$TURN_SPAN_ID)" - -# For Agent (sub-agent) tool calls, surface the sub-agent's own model calls -# (e.g. claude-haiku-4-5) as LLM spans nested under this Agent tool span. -# Claude Code writes each sub-agent's conversation to its own transcript at -# //subagents/agent-.jsonl -# which we derive from the main transcript_path + the agentId in the tool -# response. We do this in PostToolUse (not SubagentStop) because SubagentStop -# fires *before* PostToolUse, so the Agent tool span does not exist yet then. -if [ "$TOOL_NAME" = "Agent" ]; then - AGENT_ID=$(echo "$TOOL_OUTPUT" | jq -r '.agentId // .agent_id // empty' 2>/dev/null) - MAIN_TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) - - if [ -n "$AGENT_ID" ] && [ -n "$MAIN_TRANSCRIPT" ]; then - TRANSCRIPT_DIR=$(dirname "$MAIN_TRANSCRIPT") - SESSION_BASENAME=$(basename "$MAIN_TRANSCRIPT" .jsonl) - AGENT_FILE_NAME="agent-${AGENT_ID}.jsonl" - - # Candidate locations, in priority order: - # 1. Live layout: //subagents/agent-.jsonl - # 2. Replay/flat layout: /agent-.jsonl (record_hook_input - # snapshots agent transcripts flat next to the main one, and - # replay rewrites transcript_path into that flat transcripts/ dir) - AGENT_TRANSCRIPT="" - for candidate in \ - "$TRANSCRIPT_DIR/$SESSION_BASENAME/subagents/$AGENT_FILE_NAME" \ - "$TRANSCRIPT_DIR/$AGENT_FILE_NAME"; do - if [ -f "$candidate" ]; then - AGENT_TRANSCRIPT="$candidate" - break - fi - done - - if [ -n "$AGENT_TRANSCRIPT" ]; then - N_LLM=$(emit_llm_spans_from_transcript \ - "$AGENT_TRANSCRIPT" "$SESSION_ID" "$PROJECT_ID" \ - "$ROOT_SPAN_ID" "$SPAN_ID") - log "INFO" "Sub-agent $AGENT_ID: emitted ${N_LLM:-0} LLM spans under $SPAN_NAME" - else - debug "Agent transcript not found for agent_id=$AGENT_ID (looked under $TRANSCRIPT_DIR)" - fi - fi -fi - -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/post_tool_use_failure.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/post_tool_use_failure.sh deleted file mode 100644 index 27df716..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/post_tool_use_failure.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash -### -# PostToolUseFailure Hook - Creates a failed tool span as child of current Turn -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -debug "PostToolUseFailure hook triggered" - -tracing_enabled || { debug "Tracing disabled"; exit 0; } -check_requirements || exit 0 - -INPUT=$(cat) -record_hook_input "post_tool_use_failure" "$INPUT" -debug "PostToolUseFailure input: $(echo "$INPUT" | jq -c '.' 2>/dev/null | head -c 500)" - -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .tool // empty' 2>/dev/null) -TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // .input // {}' 2>/dev/null) -TOOL_OUTPUT=$(echo "$INPUT" | jq -c '.tool_response // .output // {}' 2>/dev/null) -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) -TOOL_CALL_ID=$(echo "$INPUT" | jq -r '.tool_use_id // empty' 2>/dev/null) -TOOL_ERROR=$(echo "$INPUT" | jq -r ' - .error - // .message - // .tool_response.error - // .tool_response.stderr - // .tool_response.message - // "Tool execution failed" -' 2>/dev/null | head -n 1) - -[ -z "$TOOL_NAME" ] && { debug "No tool name, skipping"; exit 0; } -[ -z "$SESSION_ID" ] && { debug "No session ID, skipping"; exit 0; } - -ROOT_SPAN_ID=$(get_session_state "$SESSION_ID" "root_span_id") -PROJECT_ID=$(get_session_state "$SESSION_ID" "project_id") -TURN_SPAN_ID=$(get_session_state "$SESSION_ID" "current_turn_span_id") - -if [ -z "$CC_EXPERIMENT_ID" ]; then - CC_EXPERIMENT_ID=$(get_session_state "$SESSION_ID" "experiment_id") - export CC_EXPERIMENT_ID -fi - -if [ -z "$TURN_SPAN_ID" ] || [ -z "$PROJECT_ID" ]; then - debug "No current turn for session $SESSION_ID, skipping failed tool trace" - exit 0 -fi - -TOOL_COUNT=$(get_session_state "$SESSION_ID" "current_turn_tool_count") -TOOL_COUNT=${TOOL_COUNT:-0} -TOOL_COUNT=$((TOOL_COUNT + 1)) -set_session_state "$SESSION_ID" "current_turn_tool_count" "$TOOL_COUNT" - -SPAN_ID=$(generate_uuid) -TIMESTAMP=$(get_timestamp) -TOOL_TIME=$(date +%s) - -case "$TOOL_NAME" in - Read|Write|Edit|MultiEdit) - FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .path // empty' 2>/dev/null) - if [ -n "$FILE_PATH" ]; then - SPAN_NAME="$TOOL_NAME: $(basename "$FILE_PATH")" - else - SPAN_NAME="$TOOL_NAME" - fi - ;; - Bash|Terminal) - CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty' 2>/dev/null | head -c 50) - SPAN_NAME="Terminal: ${CMD:-command}" - ;; - mcp__*) - SPAN_NAME=$(echo "$TOOL_NAME" | sed 's/mcp__/MCP: /' | sed 's/__/ - /') - ;; - *) - SPAN_NAME="$TOOL_NAME" - ;; -esac - -EVENT=$(jq -n \ - --arg id "$SPAN_ID" \ - --arg root_span_id "$ROOT_SPAN_ID" \ - --arg parent "$TURN_SPAN_ID" \ - --arg created "$TIMESTAMP" \ - --argjson input "$TOOL_INPUT" \ - --argjson output "$TOOL_OUTPUT" \ - --arg name "$SPAN_NAME" \ - --arg tool "$TOOL_NAME" \ - --arg tool_call_id "$TOOL_CALL_ID" \ - --arg tool_error "$TOOL_ERROR" \ - --argjson start_time "$TOOL_TIME" \ - --argjson end_time "$TOOL_TIME" \ - '{ - id: $id, - span_id: $id, - root_span_id: $root_span_id, - span_parents: [$parent], - created: $created, - input: $input, - output: $output, - error: $tool_error, - metrics: {start: $start_time, end: $end_time}, - metadata: ({ - tool_name: $tool, - tool_approval: "approved" - } + (if $tool_call_id != "" then {tool_call_id: $tool_call_id} else {} end)), - span_attributes: {name: $name, type: "tool"} - }') - -enqueue_span "$SESSION_ID" "$PROJECT_ID" "$EVENT" || { log "ERROR" "Failed to enqueue failed tool span"; exit 0; } - -log "INFO" "Failed tool: $SPAN_NAME (turn=$TURN_SPAN_ID)" -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/record_event.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/record_event.sh deleted file mode 100755 index 30b36d9..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/record_event.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -### -# Record-only hook - captures a hook event's stdin payload for fixtures, -# and otherwise does nothing. -# -# This script is registered for every Claude Code hook event that the -# plugin does not otherwise act on (PreToolUse, SubagentStart/Stop, -# PreCompact/PostCompact, etc.). Its sole purpose is observability: when -# BRAINTRUST_RECORD_DIR is set it appends the event to the recording so we -# can see exactly what data Claude Code makes available at each lifecycle -# point. When recording is off it is a near-instant no-op. -# -# The event name is passed as the first argument because not every hook -# payload self-identifies the event, and we want the recording to label -# each event unambiguously. -# -# Usage (from hooks.json): -# "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/record_event.sh PreToolUse" -# -# This hook NEVER blocks Claude Code and NEVER fails the event: it always -# exits 0, even on internal errors, so adding it everywhere is safe. -### - -# Note: intentionally no `set -e`. A record-only hook must never abort a -# Claude Code event, so we swallow all errors and always exit 0. - -# Fast path: if recording is off there is nothing to do. Avoid even -# reading stdin so the hook is as close to a no-op as possible. -[ -z "${BRAINTRUST_RECORD_DIR:-}" ] && exit 0 - -EVENT_NAME="${1:-unknown_event}" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=common.sh -source "$SCRIPT_DIR/common.sh" 2>/dev/null || exit 0 - -# Read the event payload from stdin and record it under the event's name. -INPUT=$(cat 2>/dev/null) -record_hook_input "$EVENT_NAME" "$INPUT" 2>/dev/null || true - -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/session_end.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/session_end.sh deleted file mode 100755 index 0a35b58..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/session_end.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash -### -# SessionEnd Hook - drains pending spans and shuts down this session's -# background worker. The only blocking step in the plugin's hook chain; -# Claude Code waits for this to return before it exits. -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -debug "SessionEnd hook triggered" - -tracing_enabled || { debug "Tracing disabled"; exit 0; } -check_requirements || exit 0 - -# Read input from stdin -INPUT=$(cat) -record_hook_input "session_end" "$INPUT" -debug "SessionEnd input: $(echo "$INPUT" | jq -c '.' 2>/dev/null | head -c 500)" - -# Extract session ID. Claude Code always sends one; if it doesn't, there's -# nothing we can drain (per-session queues are keyed by session id). -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) -[ -z "$SESSION_ID" ] && { debug "No session ID in payload, skipping"; exit 0; } - -# Log a one-line session summary for observability. State may be partial -# if the session never had a turn/tool, so default to 0. -TURN_COUNT=$(get_session_state "$SESSION_ID" "turn_count") -TOOL_COUNT=$(get_session_state "$SESSION_ID" "tool_count") -log "INFO" "Session ended: $SESSION_ID (turns=${TURN_COUNT:-0}, tools=${TOOL_COUNT:-0})" - -# Block until this session's worker has flushed all pending spans. This -# is the critical step that prevents spans from being lost when Claude -# Code exits. drain_queue is bounded by BRAINTRUST_DRAIN_TIMEOUT. -drain_queue "$SESSION_ID" || log "WARN" "Some spans may not have been flushed before session end" - -# Shut down this session's worker and clean up its queue dir. No-op in -# sync mode since there's no worker to stop. -if ! is_truthy "$BRAINTRUST_SYNC_QUEUE"; then - SDIR=$(session_queue_dir "$SESSION_ID") - LOCK_FILE="$SDIR/worker.lock" - - # Removing the lock file is the worker's exit signal: it checks - # ownership at the top of every loop and self-exits when the lock - # is gone. We avoid `kill` whenever possible because the PID we - # read from the lock could have been reused by an unrelated process - # between the read and the kill (the worker may have already exited - # after seeing the missing lock). - if [ -f "$LOCK_FILE" ]; then - WORKER_PID=$(cat "$LOCK_FILE" 2>/dev/null) - rm -f "$LOCK_FILE" - - if [ -n "$WORKER_PID" ]; then - # Poll up to 1s for the worker to self-exit. Loop period is - # ~200ms so one cycle is usually enough. - for _ in 1 2 3 4 5 6 7 8 9 10; do - kill -0 "$WORKER_PID" 2>/dev/null || break - sleep 0.1 - done - - # Still alive? Only escalate to SIGTERM if the process really - # is still our worker - verify by inspecting its command line. - if kill -0 "$WORKER_PID" 2>/dev/null; then - WORKER_CMD=$(ps -p "$WORKER_PID" -o command= 2>/dev/null || true) - if [[ "$WORKER_CMD" == *worker.sh* ]] && [[ "$WORKER_CMD" == *"$SESSION_ID"* ]]; then - kill "$WORKER_PID" 2>/dev/null || true - else - debug "Not killing pid $WORKER_PID; command line does not look like our worker: $WORKER_CMD" - fi - fi - fi - fi - - # Remove the now-empty session queue dir. rmdir is silent on - # non-empty dirs, which is the right behavior if a late job slipped - # in after the drain. - rmdir "$SDIR/pending" "$SDIR/processing" 2>/dev/null || true - rmdir "$SDIR" 2>/dev/null || true -fi - -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/session_start.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/session_start.sh deleted file mode 100755 index a3eedac..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/session_start.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/bin/bash -### -# SessionStart Hook - Creates the root trace span when a Claude Code session begins -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -debug "SessionStart hook triggered" -debug "TRACE_TO_BRAINTRUST=$TRACE_TO_BRAINTRUST" - -tracing_enabled || { debug "Tracing disabled"; exit 0; } -check_requirements || exit 0 - -# Read input from stdin -INPUT=$(cat) -record_hook_input "session_start" "$INPUT" -debug "SessionStart input: $INPUT" - -# Extract session ID from input -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) - -if [ -z "$SESSION_ID" ]; then - # Generate a session ID if not provided - SESSION_ID=$(generate_uuid) - debug "Generated session ID: $SESSION_ID" -fi - -# Clean up any queue dirs left behind by previous Claude Code sessions -# that crashed (no SessionEnd hook fired). Sweeps any dir whose worker -# lock is stale or missing-with-leftover-jobs, recovering or removing -# as appropriate. Skips our own session's dir. -sweep_dead_sessions "$SESSION_ID" - -# Determine mode and get appropriate IDs -if is_experiment_mode; then - debug "Experiment mode: CC_EXPERIMENT_ID=$CC_EXPERIMENT_ID" - # In experiment mode, we still get project_id for state management - # but spans are inserted to the experiment endpoint - PROJECT_ID=$(get_project_id "$PROJECT") || PROJECT_ID="experiment-mode" - log "INFO" "Tracing to experiment: $CC_EXPERIMENT_ID" -else - # Get project ID for project_logs mode - PROJECT_ID=$(get_project_id "$PROJECT") || { log "ERROR" "Aborting session_start: could not resolve project '$PROJECT' (see prior error)"; exit 0; } - debug "Using project: $PROJECT (id: $PROJECT_ID)" -fi - -# Create the session span -# If CC_PARENT_SPAN_ID is set, this session becomes a child of an existing trace -if [ -n "$CC_PARENT_SPAN_ID" ]; then - ROOT_SPAN_ID="$CC_ROOT_SPAN_ID" - debug "Attaching to parent span: $CC_PARENT_SPAN_ID (root: $ROOT_SPAN_ID)" -else - ROOT_SPAN_ID="$SESSION_ID" -fi -SPAN_ID="$SESSION_ID" -TIMESTAMP=$(get_timestamp) - -# Atomically check if we already have a root span for this session and set it if not -# This prevents race conditions when session_start is called multiple times -if ! check_and_set_session_state "$SESSION_ID" "root_span_id" "$ROOT_SPAN_ID"; then - EXISTING_ROOT=$(get_session_state "$SESSION_ID" "root_span_id") - debug "Session already has root span (race avoided): $EXISTING_ROOT" - exit 0 -fi -debug "Claimed session root span: $ROOT_SPAN_ID" - -# Extract workspace info if available -WORKSPACE=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) -WORKSPACE_NAME=$(basename "$WORKSPACE" 2>/dev/null || echo "Claude Code") - -# Get system info -HOSTNAME=$(get_hostname) -USERNAME=$(get_username) -OS=$(get_os) - -# Version info for observability: this plugin's version and the Claude Code -# CLI version that produced the session. -TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) -PLUGIN_VERSION=$(get_plugin_version) -CLAUDE_CODE_VERSION=$(get_claude_code_version "$TRANSCRIPT_PATH") -GIT_METADATA=$(git_metadata_json "$WORKSPACE") - -EVENT=$(jq -n \ - --arg id "$SPAN_ID" \ - --arg span_id "$SPAN_ID" \ - --arg root_span_id "$ROOT_SPAN_ID" \ - --arg created "$TIMESTAMP" \ - --arg session "$SESSION_ID" \ - --arg workspace "$WORKSPACE_NAME" \ - --arg cwd "$WORKSPACE" \ - --arg hostname "$HOSTNAME" \ - --arg username "$USERNAME" \ - --arg os "$OS" \ - --arg plugin_version "$PLUGIN_VERSION" \ - --arg claude_code_version "$CLAUDE_CODE_VERSION" \ - --argjson git_metadata "$GIT_METADATA" \ - '{ - id: $id, - span_id: $span_id, - root_span_id: $root_span_id, - created: $created, - input: ("Session: " + $workspace), - metadata: ({ - session_id: $session, - workspace: $cwd, - hostname: $hostname, - username: $username, - os: $os, - source: "claude-code", - trace_claude_code_version: $plugin_version, - claude_code_version: $claude_code_version - } + $git_metadata), - span_attributes: { - name: ("Claude Code: " + $workspace), - type: "task" - } - }') - -# Add span_parents if attaching to an existing trace -if [ -n "$CC_PARENT_SPAN_ID" ]; then - debug "Setting span_parents to: $CC_PARENT_SPAN_ID" - EVENT=$(echo "$EVENT" | jq --arg parent "$CC_PARENT_SPAN_ID" '. + {span_parents: [$parent]}') -fi - -enqueue_span "$SESSION_ID" "$PROJECT_ID" "$EVENT" || { log "ERROR" "Failed to enqueue session root span"; exit 0; } - -# Save remaining session state (root_span_id was already set atomically above) -set_session_state "$SESSION_ID" "session_span_id" "$SPAN_ID" -set_session_state "$SESSION_ID" "project_id" "$PROJECT_ID" -set_session_state "$SESSION_ID" "turn_count" "0" -set_session_state "$SESSION_ID" "tool_count" "0" -set_session_state "$SESSION_ID" "started" "$TIMESTAMP" - -# Store experiment_id if in experiment mode -if is_experiment_mode; then - set_session_state "$SESSION_ID" "experiment_id" "$CC_EXPERIMENT_ID" - log "INFO" "Created session root: $SESSION_ID workspace=$WORKSPACE_NAME (experiment=$CC_EXPERIMENT_ID)" -else - log "INFO" "Created session root: $SESSION_ID workspace=$WORKSPACE_NAME (project=$PROJECT)" -fi - -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/stop_hook.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/stop_hook.sh deleted file mode 100755 index 3493764..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/stop_hook.sh +++ /dev/null @@ -1,529 +0,0 @@ -#!/bin/bash -### -# Stop Hook - Creates LLM spans for each model call within the Turn -# -# Structure: -# Session (task) -# ├── Turn 1 (task) - created by UserPromptSubmit -# │ ├── claude-sonnet... (llm) - first model call (plan + tool_use) -# │ ├── Tool 1 (tool) - created by PostToolUse -# │ ├── Tool 2 (tool) - created by PostToolUse -# │ └── claude-sonnet... (llm) - second model call (after tools) -# └── Turn 2 (task) -# └── ... -# -# Each assistant message block = one LLM call -# -# Token accounting note / known ceiling: -# Token counts are derived entirely from the transcript. For every request -# that the transcript records, our totals match Claude Code's /usage exactly. -# The exception is Claude Code's *internal background* model calls - chiefly -# automatic session-title generation (and conversation summarization). These -# are billed in /usage but the transcript stores only their result (e.g. an -# `ai-title` line) with NO requestId, model, or usage, and no hook payload -# carries their tokens. They are therefore unrecoverable, so an interactive -# session's traced totals can read slightly below /usage (a small amount of -# opus cache-read tokens). Non-interactive (-p) runs don't make these calls -# and reconcile exactly. See README "token accounting". -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -debug "Stop hook triggered" - -tracing_enabled || { debug "Tracing disabled"; exit 0; } -check_requirements || exit 0 - -# Read input from stdin -INPUT=$(cat) -record_hook_input "stop_hook" "$INPUT" -debug "Stop input: $(echo "$INPUT" | jq -c '.' 2>/dev/null | head -c 500)" - -# Get session ID -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) - -if [ -z "$SESSION_ID" ]; then - TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) - if [ -n "$TRANSCRIPT_PATH" ]; then - SESSION_ID=$(basename "$TRANSCRIPT_PATH" .jsonl) - fi -fi - -[ -z "$SESSION_ID" ] && { debug "No session ID"; exit 0; } - -# The Stop event includes Claude's final assistant message directly in the -# payload, so we don't have to reconstruct it from the transcript. -# (See https://docs.claude.com/en/docs/claude-code/hooks -> Stop input) -LAST_ASSISTANT_MESSAGE=$(echo "$INPUT" | jq -r '.last_assistant_message // empty' 2>/dev/null) - -# Get session state -ROOT_SPAN_ID=$(get_session_state "$SESSION_ID" "root_span_id") -PROJECT_ID=$(get_session_state "$SESSION_ID" "project_id") -TURN_SPAN_ID=$(get_session_state "$SESSION_ID" "current_turn_span_id") -TURN_START=$(get_session_state "$SESSION_ID" "current_turn_start") - -# Load experiment_id from session state if not already set -if [ -z "$CC_EXPERIMENT_ID" ]; then - CC_EXPERIMENT_ID=$(get_session_state "$SESSION_ID" "experiment_id") - export CC_EXPERIMENT_ID -fi - -if [ -z "$TURN_SPAN_ID" ] || [ -z "$PROJECT_ID" ]; then - debug "No current turn to finalize" - exit 0 -fi - -# Find the conversation file -CONV_FILE=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) -if [ -z "$CONV_FILE" ] || [ ! -f "$CONV_FILE" ]; then - SESSIONS_DIR="$HOME/.claude/projects" - CONV_FILE=$(find "$SESSIONS_DIR" -name "${SESSION_ID}.jsonl" -type f 2>/dev/null | head -1) -fi - -[ -z "$CONV_FILE" ] || [ ! -f "$CONV_FILE" ] && { debug "No conversation file"; exit 0; } - -debug "Processing transcript: $CONV_FILE" - -# Get last processed line for this turn -TURN_LAST_LINE=$(get_session_state "$SESSION_ID" "turn_last_line") -TURN_LAST_LINE=${TURN_LAST_LINE:-0} - -TOTAL_LINES=$(wc -l < "$CONV_FILE" | tr -d ' ') - -# Process the transcript to find LLM calls -# An LLM call = assistant message(s) that follow a user message or tool_result -LLM_CALLS_CREATED=0 -CURRENT_OUTPUT_TEXT="" -CURRENT_TOOL_CALLS="[]" -CURRENT_MODEL="" -CURRENT_PROMPT_TOKENS=0 -CURRENT_COMPLETION_TOKENS=0 -CURRENT_CACHE_CREATION_TOKENS=0 -CURRENT_CACHE_CREATION_5M_TOKENS=0 -CURRENT_CACHE_CREATION_1H_TOKENS=0 -CURRENT_CACHE_CREATION_MISSING_SPLIT=false -CURRENT_CACHE_READ_TOKENS=0 -CURRENT_START_TIMESTAMP="" # ISO timestamp when this LLM call started -CURRENT_END_TIMESTAMP="" # ISO timestamp when this LLM call ended -LINE_NUM=0 - -# Claude Code writes one transcript line per content block (thinking, text, -# each tool_use) tagged with the same `requestId` for a single API response. -# Within a response, input/cache usage is repeated identically on every line, -# but `output_tokens` is reported CUMULATIVELY as the response streams (early -# lines hold partials, the final line holds the true total). A response can -# also straddle a tool_result boundary (the same requestId reappears after -# tool output). -# -# To count each response correctly we therefore: -# - add input/cache exactly once per requestId (first sighting), and -# - track the running MAX output per requestId, adding only the delta when a -# larger value appears, so the total reflects the final (max) output. -# -# SEEN_REQUEST_IDS holds requestIds whose input/cache have been counted. -# REQUEST_OUTPUT_MAX maps "=" so we can add deltas. -SEEN_REQUEST_IDS=" " -REQUEST_OUTPUT_MAX=" " - -# Note: we deliberately do NOT write aggregate token metrics onto the Turn -# span. Token metrics live only on the leaf LLM spans (main-conversation and -# sub-agent), and Braintrust rolls those up to parent spans for display. -# Writing our own Turn-level sums here would be both redundant and incomplete -# (it would miss sub-agent tokens, which are emitted outside this loop). - -# Accumulated conversation history (JSON array of messages) -CONVERSATION_HISTORY="[]" - -# Add message to conversation history -add_to_history() { - local role="$1" - local content="$2" - local tool_call_id="$3" - local tool_calls="$4" - - if [ "$role" = "tool" ]; then - CONVERSATION_HISTORY=$(echo "$CONVERSATION_HISTORY" | jq --arg role "$role" --arg content "$content" --arg id "$tool_call_id" \ - '. += [{role: $role, tool_call_id: $id, content: $content}]') - elif [ -n "$tool_calls" ] && [ "$tool_calls" != "[]" ]; then - CONVERSATION_HISTORY=$(echo "$CONVERSATION_HISTORY" | jq --arg role "$role" --arg content "$content" --argjson tc "$tool_calls" \ - '. += [{role: $role, content: $content, tool_calls: $tc}]') - else - CONVERSATION_HISTORY=$(echo "$CONVERSATION_HISTORY" | jq --arg role "$role" --arg content "$content" \ - '. += [{role: $role, content: $content}]') - fi -} - -create_llm_span() { - local output_text="$1" - local model="$2" - local prompt_tokens="$3" - local completion_tokens="$4" - local start_ts="$5" # ISO timestamp - local end_ts="$6" # ISO timestamp - local tool_calls_json="${7:-[]}" - local input_history="$8" # JSON array of conversation history - local cache_creation_tokens="${9:-0}" - local cache_read_tokens="${10:-0}" - local cache_creation_5m_tokens="${11:-0}" - local cache_creation_1h_tokens="${12:-0}" - local cache_creation_missing_split="${13:-false}" - - # Need either text or tool_calls - [ -z "$output_text" ] && [ "$tool_calls_json" = "[]" ] && return - - local span_id=$(generate_uuid) - local use_cache_creation_split=false - if [ "$cache_creation_missing_split" != "true" ]; then - if [ "$cache_creation_5m_tokens" -gt 0 ] 2>/dev/null \ - || [ "$cache_creation_1h_tokens" -gt 0 ] 2>/dev/null; then - use_cache_creation_split=true - fi - fi - - local effective_cache_creation_tokens="$cache_creation_tokens" - if [ "$use_cache_creation_split" = "true" ]; then - effective_cache_creation_tokens=$((cache_creation_5m_tokens + cache_creation_1h_tokens)) - fi - local bt_prompt_tokens=$((prompt_tokens + cache_read_tokens + effective_cache_creation_tokens)) - local total_tokens=$((bt_prompt_tokens + completion_tokens)) - local start_time=$(_iso_to_epoch "$start_ts") - local end_time=$(_iso_to_epoch "$end_ts") - - # Input is the conversation history up to this point - local input_json="$input_history" - - # Format output - include tool_calls if present - local output_json - local has_tool_calls=$(echo "$tool_calls_json" | jq 'length > 0' 2>/dev/null) - if [ "$has_tool_calls" = "true" ]; then - output_json=$(jq -n \ - --arg content "${output_text:-}" \ - --argjson tool_calls "$tool_calls_json" \ - '{role: "assistant", content: $content, tool_calls: $tool_calls}') - else - output_json=$(jq -n --arg content "$output_text" '{role: "assistant", content: $content}') - fi - - local event=$(jq -n \ - --arg id "$span_id" \ - --arg span_id "$span_id" \ - --arg root_span_id "$ROOT_SPAN_ID" \ - --arg parent "$TURN_SPAN_ID" \ - --arg created "${start_ts:-$(get_timestamp)}" \ - --argjson input "$input_json" \ - --argjson output "$output_json" \ - --arg model "${model:-claude}" \ - --argjson prompt_tokens "$prompt_tokens" \ - --argjson bt_prompt_tokens "$bt_prompt_tokens" \ - --argjson completion_tokens "$completion_tokens" \ - --argjson tokens "$total_tokens" \ - --argjson cache_creation_tokens "$cache_creation_tokens" \ - --argjson cache_read_tokens "$cache_read_tokens" \ - --argjson cache_creation_5m_tokens "$cache_creation_5m_tokens" \ - --argjson cache_creation_1h_tokens "$cache_creation_1h_tokens" \ - --argjson use_cache_creation_split "$use_cache_creation_split" \ - --argjson start_time "$start_time" \ - --argjson end_time "$end_time" \ - '{ - id: $id, - span_id: $span_id, - root_span_id: $root_span_id, - span_parents: [$parent], - created: $created, - input: $input, - output: $output, - metrics: ({ - start: $start_time, - end: $end_time, - prompt_tokens: $bt_prompt_tokens, - completion_tokens: $completion_tokens, - tokens: $tokens, - prompt_cached_tokens: $cache_read_tokens - } + ( - if $use_cache_creation_split then - { - prompt_cache_creation_5m_tokens: $cache_creation_5m_tokens, - prompt_cache_creation_1h_tokens: $cache_creation_1h_tokens - } - else - {prompt_cache_creation_tokens: $cache_creation_tokens} - end - )), - metadata: { - model: $model - }, - span_attributes: { - name: $model, - type: "llm" - } - }') - - if enqueue_span "$SESSION_ID" "$PROJECT_ID" "$event"; then - LLM_CALLS_CREATED=$((LLM_CALLS_CREATED + 1)) - log "INFO" "LLM span: $model tokens=$total_tokens (turn=$TURN_SPAN_ID)" - fi -} - -# Flush the pending assistant segment at a boundary (tool_result, real user -# message, or end of transcript). -# -# A single API response (one requestId) can span multiple tool_result -# boundaries: it emits a tool_use, gets a tool_result, then emits MORE -# tool_use blocks under the SAME requestId. Input/cache for that requestId -# are counted once and output is tracked as a running max, so every segment -# AFTER the first carries zero new tokens. Emitting a span for those -# continuation segments would create misleading all-zero-token LLM spans and -# split one logical response across several spans. -# -# To match the per-requestId grouping the sub-agent path uses, we only emit -# an LLM span when the pending segment actually accrued token metrics (its -# first sighting). Continuation segments are still recorded into the -# conversation history (so tool calls keep their context) but do not produce -# their own span. -flush_pending_llm() { - [ -n "$CURRENT_OUTPUT_TEXT" ] || [ "$CURRENT_TOOL_CALLS" != "[]" ] || return 0 - - if [ "$CURRENT_PROMPT_TOKENS" -gt 0 ] 2>/dev/null \ - || [ "$CURRENT_COMPLETION_TOKENS" -gt 0 ] 2>/dev/null \ - || [ "$CURRENT_CACHE_CREATION_TOKENS" -gt 0 ] 2>/dev/null \ - || [ "$CURRENT_CACHE_CREATION_5M_TOKENS" -gt 0 ] 2>/dev/null \ - || [ "$CURRENT_CACHE_CREATION_1H_TOKENS" -gt 0 ] 2>/dev/null \ - || [ "$CURRENT_CACHE_READ_TOKENS" -gt 0 ] 2>/dev/null; then - create_llm_span "$CURRENT_OUTPUT_TEXT" "$CURRENT_MODEL" "$CURRENT_PROMPT_TOKENS" "$CURRENT_COMPLETION_TOKENS" "$CURRENT_START_TIMESTAMP" "$CURRENT_END_TIMESTAMP" "$CURRENT_TOOL_CALLS" "$CONVERSATION_HISTORY" "$CURRENT_CACHE_CREATION_TOKENS" "$CURRENT_CACHE_READ_TOKENS" "$CURRENT_CACHE_CREATION_5M_TOKENS" "$CURRENT_CACHE_CREATION_1H_TOKENS" "$CURRENT_CACHE_CREATION_MISSING_SPLIT" - fi - - # Always thread the assistant turn into history, whether or not a span - # was emitted, so subsequent tool results / messages keep their context. - add_to_history "assistant" "$CURRENT_OUTPUT_TEXT" "" "$CURRENT_TOOL_CALLS" -} - -while IFS= read -r line; do - LINE_NUM=$((LINE_NUM + 1)) - [ "$LINE_NUM" -le "$TURN_LAST_LINE" ] && continue - [ -z "$line" ] && continue - - MSG_TYPE=$(echo "$line" | jq -r '.type // empty' 2>/dev/null) - MSG_TIMESTAMP=$(echo "$line" | jq -r '.timestamp // empty' 2>/dev/null) - - if [ "$MSG_TYPE" = "user" ]; then - # Check if tool_result or real user message - CONTENT=$(echo "$line" | jq -r '.message.content // empty' 2>/dev/null) - IS_TOOL_RESULT=$(echo "$CONTENT" | jq -e '.[0].type == "tool_result"' >/dev/null 2>&1 && echo "true" || echo "false") - - if [ "$IS_TOOL_RESULT" = "true" ]; then - # Tool result - flush any pending assistant segment first. This - # emits an LLM span only if the segment accrued tokens (a fresh - # requestId); continuation segments of an already-counted - # requestId are folded into history without a separate span. - flush_pending_llm - - # Extract tool result content and tool_use_id - TOOL_RESULT_CONTENT=$(echo "$CONTENT" | jq -r '.[0].content // "tool result"' 2>/dev/null) - TOOL_USE_ID=$(echo "$CONTENT" | jq -r '.[0].tool_use_id // ""' 2>/dev/null) - - # Add tool result to conversation history - add_to_history "tool" "$TOOL_RESULT_CONTENT" "$TOOL_USE_ID" "" - - # Reset for next LLM call - DON'T set start timestamp yet - # The next assistant message timestamp will be the actual LLM start - CURRENT_OUTPUT_TEXT="" - CURRENT_TOOL_CALLS="[]" - CURRENT_MODEL="" - CURRENT_PROMPT_TOKENS=0 - CURRENT_COMPLETION_TOKENS=0 - CURRENT_CACHE_CREATION_TOKENS=0 - CURRENT_CACHE_CREATION_5M_TOKENS=0 - CURRENT_CACHE_CREATION_1H_TOKENS=0 - CURRENT_CACHE_CREATION_MISSING_SPLIT=false - CURRENT_CACHE_READ_TOKENS=0 - CURRENT_START_TIMESTAMP="" # Will be set from first assistant message - CURRENT_END_TIMESTAMP="" - else - # Real user message - flush any pending assistant segment first. - flush_pending_llm - - # Add user message to conversation history - add_to_history "user" "$CONTENT" "" "" - - # Reset for next LLM call - CURRENT_OUTPUT_TEXT="" - CURRENT_TOOL_CALLS="[]" - CURRENT_MODEL="" - CURRENT_PROMPT_TOKENS=0 - CURRENT_COMPLETION_TOKENS=0 - CURRENT_CACHE_CREATION_TOKENS=0 - CURRENT_CACHE_CREATION_5M_TOKENS=0 - CURRENT_CACHE_CREATION_1H_TOKENS=0 - CURRENT_CACHE_CREATION_MISSING_SPLIT=false - CURRENT_CACHE_READ_TOKENS=0 - CURRENT_START_TIMESTAMP="$MSG_TIMESTAMP" - CURRENT_END_TIMESTAMP="" - fi - - elif [ "$MSG_TYPE" = "assistant" ]; then - # Extract text content - TEXT=$(echo "$line" | jq -r ' - .message.content - | if type == "array" then - [.[] | select(.type == "text") | .text] | join("\n") - elif type == "string" then - . - else - empty - end - ' 2>/dev/null) - - # Extract full tool_use objects for tool_calls - TOOL_CALLS_JSON=$(echo "$line" | jq -c ' - .message.content - | if type == "array" then - [.[] | select(.type == "tool_use") | { - id: .id, - type: "function", - function: { - name: .name, - arguments: (.input | tojson) - } - }] - else - [] - end - ' 2>/dev/null) - - # Check if we have tool calls - HAS_TOOL_CALLS=$(echo "$TOOL_CALLS_JSON" | jq 'length > 0' 2>/dev/null) - - # Set start timestamp from first assistant message of this LLM call - [ -z "$CURRENT_START_TIMESTAMP" ] && CURRENT_START_TIMESTAMP="$MSG_TIMESTAMP" - - if [ -n "$TEXT" ]; then - if [ -n "$CURRENT_OUTPUT_TEXT" ]; then - CURRENT_OUTPUT_TEXT="$CURRENT_OUTPUT_TEXT"$'\n'"$TEXT" - else - CURRENT_OUTPUT_TEXT="$TEXT" - fi - CURRENT_END_TIMESTAMP="$MSG_TIMESTAMP" - fi - - if [ "$HAS_TOOL_CALLS" = "true" ]; then - CURRENT_TOOL_CALLS="$TOOL_CALLS_JSON" - CURRENT_END_TIMESTAMP="$MSG_TIMESTAMP" - fi - - # Extract model - MODEL=$(echo "$line" | jq -r '.message.model // empty' 2>/dev/null) - [ -n "$MODEL" ] && CURRENT_MODEL="$MODEL" - - # Extract tokens. A single API response repeats across multiple - # content-block lines sharing one requestId. Input/cache are identical - # on every line (count once); output_tokens streams cumulatively - # (track the running max and add only deltas). Lines without a - # requestId (rare) fall back to message id; if neither is present we - # treat each line as its own request. - REQUEST_ID=$(echo "$line" | jq -r '.requestId // .message.id // empty' 2>/dev/null) - - USAGE=$(echo "$line" | jq -c '.message.usage // {}' 2>/dev/null) - if [ "$USAGE" != "{}" ] && [ -n "$USAGE" ]; then - INPUT_TOKENS=$(echo "$USAGE" | jq -r '.input_tokens // 0' 2>/dev/null) - OUTPUT_TOKENS=$(echo "$USAGE" | jq -r '.output_tokens // 0' 2>/dev/null) - CACHE_CREATION=$(echo "$USAGE" | jq -r '.cache_creation_input_tokens // 0' 2>/dev/null) - CACHE_READ=$(echo "$USAGE" | jq -r '.cache_read_input_tokens // 0' 2>/dev/null) - CACHE_CREATION_5M=$(echo "$USAGE" | jq -r '.cache_creation.ephemeral_5m_input_tokens // 0' 2>/dev/null) - CACHE_CREATION_1H=$(echo "$USAGE" | jq -r '.cache_creation.ephemeral_1h_input_tokens // 0' 2>/dev/null) - HAS_CACHE_CREATION_SPLIT=$(echo "$USAGE" | jq -r 'if ((.cache_creation? // null) | type) == "object" then "true" else "false" end' 2>/dev/null) - [ "$INPUT_TOKENS" = "null" ] && INPUT_TOKENS=0 - [ "$OUTPUT_TOKENS" = "null" ] && OUTPUT_TOKENS=0 - [ "$CACHE_CREATION" = "null" ] && CACHE_CREATION=0 - [ "$CACHE_READ" = "null" ] && CACHE_READ=0 - [ "$CACHE_CREATION_5M" = "null" ] && CACHE_CREATION_5M=0 - [ "$CACHE_CREATION_1H" = "null" ] && CACHE_CREATION_1H=0 - [ "$HAS_CACHE_CREATION_SPLIT" = "null" ] && HAS_CACHE_CREATION_SPLIT=false - - # Determine whether input/cache for this requestId were already - # counted, and fetch the prior max output for delta accounting. - local_first_sighting=true - prior_output=0 - if [ -n "$REQUEST_ID" ]; then - case "$SEEN_REQUEST_IDS" in - *" $REQUEST_ID "*) local_first_sighting=false ;; - *) SEEN_REQUEST_IDS="${SEEN_REQUEST_IDS}${REQUEST_ID} " ;; - esac - # Look up the prior max output recorded for this requestId. - case "$REQUEST_OUTPUT_MAX" in - *" ${REQUEST_ID}="*) - prior_output=${REQUEST_OUTPUT_MAX#*" ${REQUEST_ID}="} - prior_output=${prior_output%% *} - ;; - esac - fi - - # Input + cache: count once per requestId (constant across lines). - if [ "$local_first_sighting" = "true" ]; then - [ "$INPUT_TOKENS" -gt 0 ] 2>/dev/null && CURRENT_PROMPT_TOKENS=$((CURRENT_PROMPT_TOKENS + INPUT_TOKENS)) - [ "$CACHE_CREATION" -gt 0 ] 2>/dev/null && CURRENT_CACHE_CREATION_TOKENS=$((CURRENT_CACHE_CREATION_TOKENS + CACHE_CREATION)) - [ "$CACHE_READ" -gt 0 ] 2>/dev/null && CURRENT_CACHE_READ_TOKENS=$((CURRENT_CACHE_READ_TOKENS + CACHE_READ)) - [ "$CACHE_CREATION_5M" -gt 0 ] 2>/dev/null && CURRENT_CACHE_CREATION_5M_TOKENS=$((CURRENT_CACHE_CREATION_5M_TOKENS + CACHE_CREATION_5M)) - [ "$CACHE_CREATION_1H" -gt 0 ] 2>/dev/null && CURRENT_CACHE_CREATION_1H_TOKENS=$((CURRENT_CACHE_CREATION_1H_TOKENS + CACHE_CREATION_1H)) - if [ "$CACHE_CREATION" -gt 0 ] 2>/dev/null && [ "$HAS_CACHE_CREATION_SPLIT" != "true" ]; then - CURRENT_CACHE_CREATION_MISSING_SPLIT=true - fi - fi - - # Output: add only the increase over this requestId's prior max, - # so the running total converges on the final (largest) value. - if [ -z "$REQUEST_ID" ]; then - # No requestId: treat as a standalone response. - [ "$OUTPUT_TOKENS" -gt 0 ] 2>/dev/null && CURRENT_COMPLETION_TOKENS=$((CURRENT_COMPLETION_TOKENS + OUTPUT_TOKENS)) - elif [ "$OUTPUT_TOKENS" -gt "$prior_output" ] 2>/dev/null; then - CURRENT_COMPLETION_TOKENS=$((CURRENT_COMPLETION_TOKENS + OUTPUT_TOKENS - prior_output)) - # Record the new max for this requestId. Use an intermediate - # variable for the pattern: nesting double-quotes inside the - # ${var//pat/repl} expansion does NOT produce the intended - # pattern and instead corrupts REQUEST_OUTPUT_MAX, so on the - # next sighting the prior max can't be found and the full - # output gets re-added as a bogus delta. - _rom_pat=" ${REQUEST_ID}=${prior_output} " - REQUEST_OUTPUT_MAX="${REQUEST_OUTPUT_MAX//$_rom_pat/ }" - REQUEST_OUTPUT_MAX="${REQUEST_OUTPUT_MAX}${REQUEST_ID}=${OUTPUT_TOKENS} " - fi - fi - fi -done < "$CONV_FILE" - -# Save final LLM call (same zero-token suppression as the boundary flushes). -flush_pending_llm - -# Update the Turn span with its end time and Claude's final response via a -# merge write. The merge keeps the existing fields (input, name, type) set -# when user_prompt_submit.sh created the Turn span. We intentionally do NOT -# write token metrics here: token metrics live only on the leaf LLM spans, -# and Braintrust aggregates them onto parents (Turn, session) for display. -END_TIME=$(date +%s) - -TURN_UPDATE=$(jq -n \ - --arg id "$TURN_SPAN_ID" \ - --arg output "$LAST_ASSISTANT_MESSAGE" \ - --argjson end_time "$END_TIME" \ - '{ - id: $id, - _is_merge: true, - output: $output, - metrics: { - end: $end_time - } - }') - -enqueue_span "$SESSION_ID" "$PROJECT_ID" "$TURN_UPDATE" || true - -# Update state -set_session_state "$SESSION_ID" "turn_last_line" "$TOTAL_LINES" -set_session_state "$SESSION_ID" "current_turn_span_id" "" -set_session_state "$SESSION_ID" "current_turn_explicit_skill_names" "[]" - -[ "$LLM_CALLS_CREATED" -gt 0 ] && log "INFO" "Created $LLM_CALLS_CREATED LLM spans for turn" -log "INFO" "Turn finalized (end=$END_TIME)" - -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/user_prompt_expansion.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/user_prompt_expansion.sh deleted file mode 100644 index 8e9ed75..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/user_prompt_expansion.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/bash -### -# UserPromptExpansion Hook - Captures explicit Claude slash skill requests. -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -tracing_enabled || exit 0 -check_requirements || exit 0 - -INPUT=$(cat) -record_hook_input "UserPromptExpansion" "$INPUT" - -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) -[ -z "$SESSION_ID" ] && exit 0 - -EXPANSION_TYPE=$(echo "$INPUT" | jq -r '.expansion_type // .type // empty' 2>/dev/null) -if [ -n "$EXPANSION_TYPE" ] && [ "$EXPANSION_TYPE" != "slash_command" ]; then - exit 0 -fi - -_normalize_skill_name() { - echo "$1" | sed 's#^/##' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | sed 's/[),.;:]*$//' -} - -_skill_listing_contains() { - local transcript="$1" - local name="$2" - [ -n "$transcript" ] && [ -f "$transcript" ] || return 1 - jq -e --arg name "$name" ' - select(.attachment.type == "skill_listing") - | .attachment.names[]? - | select(. == $name) - ' "$transcript" >/dev/null 2>&1 -} - -SKILL_NAME=$(echo "$INPUT" | jq -r ' - .skill_name - // .skillName - // .skill.name - // .skill - // empty -' 2>/dev/null) - -if [ -z "$SKILL_NAME" ]; then - COMMAND_NAME=$(echo "$INPUT" | jq -r '.command_name // .command // .slash_command // .name // empty' 2>/dev/null) - COMMAND_NAME=$(_normalize_skill_name "$COMMAND_NAME") - TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) - if [ -n "$COMMAND_NAME" ] && _skill_listing_contains "$TRANSCRIPT_PATH" "$COMMAND_NAME"; then - SKILL_NAME="$COMMAND_NAME" - fi -fi - -SKILL_NAME=$(_normalize_skill_name "$SKILL_NAME") -[ -z "$SKILL_NAME" ] && exit 0 - -EXISTING=$(get_session_state "$SESSION_ID" "current_turn_explicit_skill_names") -[ -z "$EXISTING" ] && EXISTING="[]" - -NAMES_JSON=$(jq -nc --argjson existing "$EXISTING" --arg name "$SKILL_NAME" ' - ($existing + [$name]) - | reduce .[] as $name ([]; if index($name) then . else . + [$name] end) -' 2>/dev/null || jq -nc --arg name "$SKILL_NAME" '[$name]') - -set_session_state "$SESSION_ID" "current_turn_explicit_skill_names" "$NAMES_JSON" - -TURN_SPAN_ID=$(get_session_state "$SESSION_ID" "current_turn_span_id") -PROJECT_ID=$(get_session_state "$SESSION_ID" "project_id") -[ -n "$TURN_SPAN_ID" ] && [ -n "$PROJECT_ID" ] || exit 0 - -ROOT_SPAN_ID=$(get_session_state "$SESSION_ID" "root_span_id") - -EVENT=$(jq -n \ - --arg id "$TURN_SPAN_ID" \ - --argjson names "$NAMES_JSON" \ - '{ - id: $id, - _is_merge: true, - metadata: { - loaded_skill_names: $names, - loaded_skills: ($names | map({name: .})) - } - }') - -enqueue_span "$SESSION_ID" "$PROJECT_ID" "$EVENT" || true - -log "INFO" "Explicit skill request: $SKILL_NAME (turn=$TURN_SPAN_ID root=$ROOT_SPAN_ID)" - -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/user_prompt_submit.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/user_prompt_submit.sh deleted file mode 100755 index 75060b3..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/user_prompt_submit.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/bin/bash -### -# UserPromptSubmit Hook - Creates a Turn container span when user submits a prompt -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -debug "UserPromptSubmit hook triggered" - -tracing_enabled || { debug "Tracing disabled"; exit 0; } -check_requirements || exit 0 - -# Read input from stdin -INPUT=$(cat) -record_hook_input "user_prompt_submit" "$INPUT" -debug "UserPromptSubmit input: $(echo "$INPUT" | jq -c '.' 2>/dev/null | head -c 500)" - -# Extract session ID and prompt -SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) -PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null) - -[ -z "$SESSION_ID" ] && { debug "No session ID"; exit 0; } - -# Get session info -ROOT_SPAN_ID=$(get_session_state "$SESSION_ID" "root_span_id") -SESSION_SPAN_ID=$(get_session_state "$SESSION_ID" "session_span_id") -PROJECT_ID=$(get_session_state "$SESSION_ID" "project_id") - -# Load experiment_id from session state if not already set -if [ -z "$CC_EXPERIMENT_ID" ]; then - CC_EXPERIMENT_ID=$(get_session_state "$SESSION_ID" "experiment_id") - export CC_EXPERIMENT_ID -fi - -# If no session root exists yet, we'll create it -if [ -z "$ROOT_SPAN_ID" ] || [ -z "$PROJECT_ID" ]; then - PROJECT_ID=$(get_project_id "$PROJECT") || { log "ERROR" "Aborting user_prompt_submit: could not resolve project '$PROJECT' (see prior error)"; exit 0; } - ROOT_SPAN_ID="$SESSION_ID" - - # Get workspace name from cwd - CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) - WORKSPACE_NAME=$(basename "$CWD" 2>/dev/null || echo "workspace") - - TIMESTAMP=$(get_timestamp) - HOSTNAME=$(get_hostname) - USERNAME=$(get_username) - OS=$(get_os) - - # Version info for observability (mirrors session_start.sh). - TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null) - PLUGIN_VERSION=$(get_plugin_version) - CLAUDE_CODE_VERSION=$(get_claude_code_version "$TRANSCRIPT_PATH") - GIT_METADATA=$(git_metadata_json "$CWD") - - EVENT=$(jq -n \ - --arg id "$ROOT_SPAN_ID" \ - --arg span_id "$ROOT_SPAN_ID" \ - --arg root_span_id "$ROOT_SPAN_ID" \ - --arg created "$TIMESTAMP" \ - --arg session "$SESSION_ID" \ - --arg workspace "$WORKSPACE_NAME" \ - --arg hostname "$HOSTNAME" \ - --arg username "$USERNAME" \ - --arg os "$OS" \ - --arg plugin_version "$PLUGIN_VERSION" \ - --arg claude_code_version "$CLAUDE_CODE_VERSION" \ - --argjson git_metadata "$GIT_METADATA" \ - '{ - id: $id, - span_id: $span_id, - root_span_id: $root_span_id, - created: $created, - input: ("Session: " + $workspace), - metadata: ({ - session_id: $session, - workspace: $workspace, - hostname: $hostname, - username: $username, - os: $os, - source: "claude-code", - trace_claude_code_version: $plugin_version, - claude_code_version: $claude_code_version - } + $git_metadata), - span_attributes: { - name: ("Claude Code: " + $workspace), - type: "task" - } - }') - - enqueue_span "$SESSION_ID" "$PROJECT_ID" "$EVENT" || true - set_session_state "$SESSION_ID" "root_span_id" "$ROOT_SPAN_ID" - set_session_state "$SESSION_ID" "session_span_id" "$ROOT_SPAN_ID" - set_session_state "$SESSION_ID" "project_id" "$PROJECT_ID" - SESSION_SPAN_ID="$ROOT_SPAN_ID" - log "INFO" "Created session root: $SESSION_ID" -fi - -# Increment turn count and create Turn span -TURN_COUNT=$(get_session_state "$SESSION_ID" "turn_count") -TURN_COUNT=${TURN_COUNT:-0} -TURN_COUNT=$((TURN_COUNT + 1)) - -TURN_SPAN_ID=$(generate_uuid) -TIMESTAMP=$(get_timestamp) -START_TIME=$(date +%s) - -EXPLICIT_SKILL_NAMES=$(get_session_state "$SESSION_ID" "current_turn_explicit_skill_names") -[ -z "$EXPLICIT_SKILL_NAMES" ] && EXPLICIT_SKILL_NAMES="[]" - -# Truncate prompt for display (first 100 chars) -PROMPT_PREVIEW="${PROMPT:0:100}" -[ ${#PROMPT} -gt 100 ] && PROMPT_PREVIEW="${PROMPT_PREVIEW}..." - -# Create Turn container span (parent is the session span, not the root) -EVENT=$(jq -n \ - --arg id "$TURN_SPAN_ID" \ - --arg span_id "$TURN_SPAN_ID" \ - --arg root_span_id "$ROOT_SPAN_ID" \ - --arg session_span_id "$SESSION_SPAN_ID" \ - --arg created "$TIMESTAMP" \ - --arg prompt "$PROMPT" \ - --argjson explicit_skill_names "$EXPLICIT_SKILL_NAMES" \ - --argjson turn "$TURN_COUNT" \ - --argjson start_time "$START_TIME" \ - '{ - id: $id, - span_id: $span_id, - root_span_id: $root_span_id, - span_parents: [$session_span_id], - created: $created, - input: $prompt, - metrics: { - start: $start_time - }, - metadata: (if ($explicit_skill_names | length) > 0 then { - loaded_skill_names: $explicit_skill_names, - loaded_skills: ($explicit_skill_names | map({name: .})) - } else {} end), - span_attributes: { - name: ("Turn " + ($turn | tostring)), - type: "task" - } - }') - -enqueue_span "$SESSION_ID" "$PROJECT_ID" "$EVENT" || { log "ERROR" "Failed to enqueue turn span"; exit 0; } - -# Save turn state -set_session_state "$SESSION_ID" "turn_count" "$TURN_COUNT" -set_session_state "$SESSION_ID" "current_turn_span_id" "$TURN_SPAN_ID" -set_session_state "$SESSION_ID" "current_turn_start" "$START_TIME" -set_session_state "$SESSION_ID" "current_turn_tool_count" "0" - -log "INFO" "Turn $TURN_COUNT started: $TURN_SPAN_ID" - -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/hooks/worker.sh b/src/plugins/claude/content/plugins/trace-claude-code/hooks/worker.sh deleted file mode 100644 index 175df43..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/hooks/worker.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/bin/bash -### -# Background queue worker for a single Claude Code session. -# -# Spawned by ensure_worker_running() in common.sh. Drains the session's -# pending/ dir in FIFO order. The worker: -# 1. Acquires an exclusive lock by writing its PID to worker.lock. -# 2. Refreshes the lock file mtime on every loop iteration so a future -# sweep can distinguish "alive" from "crashed". -# 3. Picks the oldest pending/*.json file. -# 4. Atomically renames it into processing/. -# 5. Runs _http_insert_span() on it. -# 6. Deletes the file on success, or logs and drops on failure. -# -# Lifecycle: the worker runs until its lock file is removed (clean -# shutdown by session_end.sh) or it loses the lock (some other entity -# took over, e.g. crash recovery). There is no idle timeout: the worker -# lives as long as its Claude Code session is alive. -# -# Args: session_id (positional, required) -### - -set -u - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=common.sh -source "$SCRIPT_DIR/common.sh" - -SESSION_ID="${1:-}" -if [ -z "$SESSION_ID" ]; then - log "ERROR" "worker.sh started without a session_id argument" - exit 2 -fi - -SDIR=$(session_queue_dir "$SESSION_ID") -LOCK_FILE="$SDIR/worker.lock" - -# Ensure the session subtree exists. ensure_worker_running normally does -# this for us, but it's cheap and makes the worker robust if invoked -# manually (e.g. during crash recovery). -mkdir -p "$SDIR/pending" "$SDIR/processing" - -# If another worker holds the lock, exit. (We trust ensure_worker_running -# to have checked, but races happen.) -if [ -f "$LOCK_FILE" ]; then - other_pid=$(cat "$LOCK_FILE" 2>/dev/null) - if [ -n "$other_pid" ] && [ "$other_pid" != "$$" ] && kill -0 "$other_pid" 2>/dev/null; then - debug "Worker for session $SESSION_ID: another worker (pid=$other_pid) holds the lock; exiting" - exit 0 - fi -fi - -# Claim the lock. We use noclobber to make this somewhat atomic against -# other workers racing to claim the same session. -if ! { set -C; echo "$$" > "$LOCK_FILE"; } 2>/dev/null; then - # Lost the race - someone else just took the lock. - debug "Worker for session $SESSION_ID: lost lock race; exiting" - exit 0 -fi - -cleanup_worker() { - # Only remove the lock if it's still ours. Crash recovery may have - # already removed and replaced it. - if [ -f "$LOCK_FILE" ]; then - local owner - owner=$(cat "$LOCK_FILE" 2>/dev/null) - if [ "$owner" = "$$" ]; then - rm -f "$LOCK_FILE" - fi - fi - debug "Worker $$ for session $SESSION_ID exiting" -} -trap cleanup_worker EXIT - -# Sweep any in-flight files that a previous worker left behind. We're now -# the owner of this session's queue, so it's safe to recover them. -for f in "$SDIR"/processing/*.json; do - [ -e "$f" ] || continue - mv "$f" "$SDIR/pending/$(basename "$f")" 2>/dev/null || true -done - -debug "Worker $$ started for session $SESSION_ID" - -# Main loop. Runs until the lock file disappears (clean shutdown signal -# from session_end.sh). -while true; do - # Heartbeat: keep the lock file's mtime fresh so the sweep doesn't - # mistake us for a crashed worker. - touch "$LOCK_FILE" 2>/dev/null || { - debug "Worker $$ for session $SESSION_ID: lock file disappeared; exiting" - break - } - - # Verify we still own the lock. If something else claimed it (e.g. a - # crash-recovery sweep killed us and respawned), exit so the new - # worker can take over without contention. - owner=$(cat "$LOCK_FILE" 2>/dev/null) - if [ "$owner" != "$$" ]; then - debug "Worker $$ for session $SESSION_ID: lock now owned by pid=$owner; exiting" - # Don't remove the lock - it's not ours anymore. - trap - EXIT - exit 0 - fi - - # Pick the oldest pending job (sorted lexically; filenames sort by - # epoch-ns timestamp so this is effectively FIFO). - job_file=$(find "$SDIR/pending" -maxdepth 1 -name '*.json' -type f 2>/dev/null \ - | sort | head -n1) - - if [ -z "$job_file" ]; then - sleep 0.2 - continue - fi - - # Atomically claim the job by moving it to processing/. - processing_file="$SDIR/processing/$(basename "$job_file")" - if ! mv "$job_file" "$processing_file" 2>/dev/null; then - # Another mover beat us to it (shouldn't happen with a single - # worker per session, but harmless). Retry. - continue - fi - - job=$(cat "$processing_file" 2>/dev/null) - if [ -z "$job" ]; then - log "WARN" "Empty job file in session $SESSION_ID: $(basename "$processing_file") - discarding" - rm -f "$processing_file" - continue - fi - - if _process_job_inline "$job"; then - rm -f "$processing_file" - else - log "ERROR" "Worker for session $SESSION_ID dropping failed job: $(basename "$processing_file")" - rm -f "$processing_file" - fi -done - -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/setup.sh b/src/plugins/claude/content/plugins/trace-claude-code/setup.sh index da5c4f3..a0615cc 100755 --- a/src/plugins/claude/content/plugins/trace-claude-code/setup.sh +++ b/src/plugins/claude/content/plugins/trace-claude-code/setup.sh @@ -1,214 +1,77 @@ #!/bin/bash -### -# Setup script for Braintrust Claude Code tracing -# Run this in any project directory to enable comprehensive tracing -### +# Configure Claude Code to use the shared Braintrust tracing daemon. -set -e +set -euo pipefail -echo "🧠 Braintrust Claude Code Tracing Setup" -echo "========================================" -echo "" -echo "This script will configure Claude Code to trace conversations to Braintrust." -echo "Traces include: sessions, conversation turns, and tool calls." -echo "" +echo "Braintrust Claude Code tracing setup" +echo -# Get the directory where this script lives SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HOOKS_DIR="$SCRIPT_DIR/hooks" - -# Verify hooks exist -for hook in common.sh session_start.sh post_tool_use.sh stop_hook.sh session_end.sh; do - if [ ! -f "$HOOKS_DIR/$hook" ]; then - echo "❌ Error: Missing hook script: $HOOKS_DIR/$hook" - exit 1 - fi -done - -# Check for required tools -for cmd in jq curl uuidgen; do - if ! command -v "$cmd" &> /dev/null; then - echo "❌ Error: $cmd is required but not installed" - if [[ "$OSTYPE" == "darwin"* ]]; then - if [ "$cmd" = "uuidgen" ]; then - echo " uuidgen should be pre-installed on macOS" - else - echo " Install with: brew install $cmd" - fi - else - if [ "$cmd" = "uuidgen" ]; then - echo " Install with: sudo apt-get install uuid-runtime" - else - echo " Install with: sudo apt-get install $cmd" - fi - fi - exit 1 - fi -done - -# Load API key from .env files (check current dir and parents) -load_env() { - local dir="$PWD" - while [ "$dir" != "/" ]; do - if [ -f "$dir/.env" ]; then - # Source the .env file safely (only export lines) - while IFS= read -r line || [ -n "$line" ]; do - # Skip comments and empty lines - [[ "$line" =~ ^#.*$ ]] && continue - [[ -z "$line" ]] && continue - # Export valid variable assignments - if [[ "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then - export "${line?}" - fi - done < "$dir/.env" - echo " Found .env at: $dir/.env" - return 0 - fi - dir="$(dirname "$dir")" - done - return 1 +[ -f "$SCRIPT_DIR/bin/claude-hook.sh" ] || { + echo "Missing bin/claude-hook.sh" >&2 + exit 1 +} +command -v jq >/dev/null 2>&1 || { + echo "jq is required to update the shared daemon config" >&2 + exit 1 } -# Try to load from .env -EXISTING_KEY="" -if load_env 2>/dev/null; then - EXISTING_KEY="${BRAINTRUST_API_KEY:-}" -fi - -# Prompt for API key (with default from .env if available) -if [ -n "$EXISTING_KEY" ]; then - echo "Found BRAINTRUST_API_KEY in .env" - echo "Press Enter to use it, or enter a different key:" - read -r -p "> " INPUT_KEY - BRAINTRUST_API_KEY="${INPUT_KEY:-$EXISTING_KEY}" -else - echo "Enter your Braintrust API key (starts with 'sk-'):" - echo " Get one at: https://www.braintrust.dev/app/settings/api-keys" - read -r -p "> " BRAINTRUST_API_KEY +BT_BIN="$(command -v bt 2>/dev/null || true)" +LOCAL_BT="${XDG_BIN_HOME:-$HOME/.local/bin}/bt" +if ! [ -n "$BT_BIN" ] || ! "$BT_BIN" agents hook --help >/dev/null 2>&1; then + if [ -x "$LOCAL_BT" ] && "$LOCAL_BT" agents hook --help >/dev/null 2>&1; then + BT_BIN="$LOCAL_BT" + else + command -v curl >/dev/null 2>&1 || { + echo "curl is required to install or upgrade bt" >&2 + exit 1 + } + echo "Installing the daemon-capable bt CLI..." + curl -fsSL https://bt.dev/cli/install.sh | bash + BT_BIN="$LOCAL_BT" + fi fi -if [ -z "$BRAINTRUST_API_KEY" ]; then - echo "❌ API key is required" +"$BT_BIN" agents hook --help >/dev/null 2>&1 || { + echo "The installed bt CLI does not support 'bt agents hook'." >&2 exit 1 -fi +} -# Validate API key format -if [[ ! "$BRAINTRUST_API_KEY" =~ ^sk- ]]; then - echo "⚠️ Warning: API key doesn't start with 'sk-'. Continuing anyway..." +if [ -z "${BRAINTRUST_API_KEY:-}" ] && ! "$BT_BIN" status --json >/dev/null 2>&1; then + echo "Authenticate with Braintrust:" + "$BT_BIN" auth login fi -# Prompt for project name -echo "" -echo "Enter the Braintrust project name for traces (default: claude-code):" -read -r -p "> " PROJECT_NAME +read -r -p "Shared Braintrust project for coding-agent traces [claude-code]: " PROJECT_NAME PROJECT_NAME="${PROJECT_NAME:-claude-code}" -# Prompt for debug mode -echo "" -echo "Enable debug logging? (y/N):" -read -r -p "> " ENABLE_DEBUG -if [[ "$ENABLE_DEBUG" =~ ^[Yy] ]]; then - DEBUG_VALUE="true" +if [ -n "${BT_DAEMON_CONFIG:-}" ]; then + SETTINGS_FILE="$BT_DAEMON_CONFIG" +elif [ -n "${BT_DAEMON_DATA_DIR:-}" ]; then + SETTINGS_FILE="$BT_DAEMON_DATA_DIR/config.json" +elif [ -n "${XDG_STATE_HOME:-}" ]; then + SETTINGS_FILE="$XDG_STATE_HOME/braintrust/bt-daemon/config.json" else - DEBUG_VALUE="false" + SETTINGS_FILE="$HOME/.braintrust/state/bt-daemon/config.json" fi -# Create .claude directory if needed -mkdir -p .claude - -# Build environment config -ENV_CONFIG=$(jq -n \ - --arg key "$BRAINTRUST_API_KEY" \ - --arg proj "$PROJECT_NAME" \ - --arg debug "$DEBUG_VALUE" \ - '{ - "TRACE_TO_BRAINTRUST": "true", - "BRAINTRUST_API_KEY": $key, - "BRAINTRUST_CC_PROJECT": $proj, - "BRAINTRUST_CC_DEBUG": $debug - }') - -# Check if settings.local.json exists -SETTINGS_FILE=".claude/settings.local.json" +mkdir -p "$(dirname "$SETTINGS_FILE")" +chmod 700 "$(dirname "$SETTINGS_FILE")" 2>/dev/null || true if [ -f "$SETTINGS_FILE" ]; then - echo "" - echo "Found existing $SETTINGS_FILE" - - # Read existing settings and merge - EXISTING=$(cat "$SETTINGS_FILE") - - UPDATED=$(echo "$EXISTING" | jq \ - --argjson env "$ENV_CONFIG" \ - '.env = (.env // {}) + $env') - - echo "$UPDATED" > "$SETTINGS_FILE" -else - # Create new settings file - jq -n \ - --argjson env "$ENV_CONFIG" \ - '{env: $env}' > "$SETTINGS_FILE" -fi - -echo "" -echo "✅ Setup complete!" -echo "" -echo "Configuration saved to: $SETTINGS_FILE" -echo "" -echo "Hooks configured:" -echo " • SessionStart - Creates trace root when session begins" -echo " • UserPromptSubmit - Creates Turn container for each user message" -echo " • PostToolUse - Captures tool calls as children of Turn" -echo " • Stop - Creates LLM span and finalizes Turn" -echo " • SessionEnd - Finalizes trace when session ends" -echo "" -echo "Settings:" -echo " • Project: $PROJECT_NAME" -echo " • Debug: $DEBUG_VALUE" -echo "" -echo "Next steps:" -echo " 1. Start Claude Code in this directory: claude" -echo " 2. Have a conversation" -echo " 3. View traces at: https://www.braintrust.dev/app/$PROJECT_NAME/logs" -echo "" -echo "To view hook logs:" -echo " tail -f ~/.claude/state/braintrust_hook.log" -echo "" - -# Test API connection and discover API URL -echo "Testing API connection..." - -# Discover API URL via login endpoint -APP_URL="${BRAINTRUST_APP_URL:-https://www.braintrust.dev}" -LOGIN_RESPONSE=$(curl -sf -X POST -H "Authorization: Bearer $BRAINTRUST_API_KEY" "$APP_URL/api/apikey/login" 2>/dev/null) || true - -ORG_NAME="${BRAINTRUST_ORG_NAME:-}" -if [ -n "$ORG_NAME" ]; then - # Filter by org name if specified - API_URL=$(echo "$LOGIN_RESPONSE" | jq -r --arg name "$ORG_NAME" \ - '.org_info[] | select(.name == $name) | .api_url // empty' 2>/dev/null | head -1) + jq --arg project "$PROJECT_NAME" ' + . + {traceToBraintrust: true, project: $project} + | del(.apiKey, .apiUrl, .appUrl, .token, .credentials, .auth) + ' "$SETTINGS_FILE" >"$SETTINGS_FILE.tmp" else - # Use first org - API_URL=$(echo "$LOGIN_RESPONSE" | jq -r '.org_info[0].api_url // empty' 2>/dev/null) -fi - -if [ -z "$API_URL" ]; then - # Fall back to default if login didn't return an API URL - API_URL="https://api.braintrust.dev" + jq -n --arg project "$PROJECT_NAME" '{ + traceToBraintrust: true, + project: $project + }' >"$SETTINGS_FILE.tmp" fi +mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE" +chmod 600 "$SETTINGS_FILE" 2>/dev/null || true -echo " Using API URL: $API_URL" - -RESPONSE=$(curl -s -w "\n%{http_code}" -X GET \ - -H "Authorization: Bearer $BRAINTRUST_API_KEY" \ - "$API_URL/v1/project?project_name=$(echo "$PROJECT_NAME" | jq -sRr @uri)" 2>&1) - -HTTP_CODE=$(echo "$RESPONSE" | tail -n1) - -if [ "$HTTP_CODE" = "200" ]; then - echo "✅ API connection successful - project exists" -elif [ "$HTTP_CODE" = "404" ]; then - echo "✅ API connection successful - project will be created on first trace" -else - echo "⚠️ API connection issue (HTTP $HTTP_CODE)" - echo " Check your API key and try again" -fi +echo +echo "Tracing enabled for every daemon-backed coding-agent plugin in $SETTINGS_FILE" +echo "Shared project: $PROJECT_NAME" +echo "Daemon status: $BT_BIN agents status" diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/assert.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/assert.sh deleted file mode 100644 index 410ceec..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/assert.sh +++ /dev/null @@ -1,207 +0,0 @@ -#!/bin/bash -### -# Assertion utilities for trace-claude-code tests. -# -# Test structure: -# describe "thing under test" -# it "behaves like X" -# -# assert_eq "$got" "$want" -# end_it -# -# Each test file should source this and helpers/harness.sh, then declare -# its tests. The runner aggregates pass/fail counts in environment vars -# so multiple test files share a single tally. -### - -# Color codes (disable if NO_COLOR or non-tty stdout) -if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then - C_RED=$'\033[31m' - C_GREEN=$'\033[32m' - C_YELLOW=$'\033[33m' - C_BOLD=$'\033[1m' - C_DIM=$'\033[2m' - C_RESET=$'\033[0m' -else - C_RED="" - C_GREEN="" - C_YELLOW="" - C_BOLD="" - C_DIM="" - C_RESET="" -fi - -# Test state - exported so subshells inherit, but counters are only meaningful -# at the top-level shell (we manage them via files for cross-process aggregation). -export TESTS_RUN_FILE="${TESTS_RUN_FILE:-/tmp/braintrust_test_run_$$}" -export TESTS_FAIL_FILE="${TESTS_FAIL_FILE:-/tmp/braintrust_test_fail_$$}" - -# Per-test ephemeral state (not exported across processes) -CURRENT_DESCRIBE="" -CURRENT_IT="" -CURRENT_TEST_FAILED=0 -CURRENT_TEST_FAILURES=() - -_init_counters() { - : > "$TESTS_RUN_FILE" - : > "$TESTS_FAIL_FILE" -} - -_incr_run() { - echo "x" >> "$TESTS_RUN_FILE" -} - -_incr_fail() { - echo "x" >> "$TESTS_FAIL_FILE" -} - -tests_total() { - [ -f "$TESTS_RUN_FILE" ] && wc -l < "$TESTS_RUN_FILE" | tr -d ' ' || echo 0 -} - -tests_failed() { - [ -f "$TESTS_FAIL_FILE" ] && wc -l < "$TESTS_FAIL_FILE" | tr -d ' ' || echo 0 -} - -describe() { - CURRENT_DESCRIBE="$1" - printf '\n%s%s%s\n' "$C_BOLD" "$CURRENT_DESCRIBE" "$C_RESET" -} - -# Run a single test case. -# -# Usage: -# it "does the thing" test_body_function_name -# -# The body function is invoked between setup_test_env and teardown_test_env -# (when those functions are defined). Inside the body you may use `local` -# variables freely. Use assert_* helpers to record failures; the test only -# fails if at least one assertion failed. -it() { - CURRENT_IT="$1" - local body_fn="$2" - CURRENT_TEST_FAILED=0 - CURRENT_TEST_FAILURES=() - _incr_run - - if declare -F setup_test_env >/dev/null; then - setup_test_env - fi - - # Run the test body in this shell so failures from assert_* propagate - # into our CURRENT_TEST_FAILURES array. Suppress `set -e` style aborts - # by using `|| true` if the body returns non-zero - we don't want the - # whole test file to exit just because one assertion failed. - "$body_fn" || true - - if [ "$CURRENT_TEST_FAILED" -eq 0 ]; then - printf ' %s✓%s %s\n' "$C_GREEN" "$C_RESET" "$CURRENT_IT" - else - _incr_fail - printf ' %s✗%s %s\n' "$C_RED" "$C_RESET" "$CURRENT_IT" - for msg in "${CURRENT_TEST_FAILURES[@]}"; do - printf ' %s%s%s\n' "$C_RED" "$msg" "$C_RESET" - done - fi - - if declare -F teardown_test_env >/dev/null; then - teardown_test_env - fi - - CURRENT_IT="" - CURRENT_TEST_FAILED=0 - CURRENT_TEST_FAILURES=() -} - -# Mark current test as failed and append a message -_fail() { - CURRENT_TEST_FAILED=1 - CURRENT_TEST_FAILURES+=("$1") -} - -assert_eq() { - local got="$1" want="$2" msg="${3:-}" - if [ "$got" != "$want" ]; then - _fail "${msg:-assert_eq}: expected '$want', got '$got'" - return 1 - fi - return 0 -} - -assert_ne() { - local got="$1" not_want="$2" msg="${3:-}" - if [ "$got" = "$not_want" ]; then - _fail "${msg:-assert_ne}: expected NOT '$not_want', got '$got'" - return 1 - fi - return 0 -} - -assert_match() { - local got="$1" pattern="$2" msg="${3:-}" - if ! [[ "$got" =~ $pattern ]]; then - _fail "${msg:-assert_match}: '$got' does not match /$pattern/" - return 1 - fi - return 0 -} - -assert_contains() { - local haystack="$1" needle="$2" msg="${3:-}" - if [[ "$haystack" != *"$needle"* ]]; then - _fail "${msg:-assert_contains}: '$haystack' does not contain '$needle'" - return 1 - fi - return 0 -} - -assert_not_contains() { - local haystack="$1" needle="$2" msg="${3:-}" - if [[ "$haystack" == *"$needle"* ]]; then - _fail "${msg:-assert_not_contains}: '$haystack' unexpectedly contains '$needle'" - return 1 - fi - return 0 -} - -assert_success() { - local status="$1" msg="${2:-}" - if [ "$status" -ne 0 ]; then - _fail "${msg:-assert_success}: expected exit 0, got $status" - return 1 - fi - return 0 -} - -assert_failure() { - local status="$1" msg="${2:-}" - if [ "$status" -eq 0 ]; then - _fail "${msg:-assert_failure}: expected non-zero exit, got 0" - return 1 - fi - return 0 -} - -assert_file_exists() { - local path="$1" msg="${2:-}" - if [ ! -f "$path" ]; then - _fail "${msg:-assert_file_exists}: file '$path' does not exist" - return 1 - fi - return 0 -} - -# Explicit failure -fail() { - _fail "${1:-explicit fail}" - return 1 -} - -# Skip the current test without failing it. Prints a note; the test body -# should `return 0` immediately after calling this. The test still counts as -# run/passed (it makes no assertions), which keeps the suite green when an -# optional fixture is absent. -skip() { - printf ' %s(skipped) %s%s\n' "$C_YELLOW" "${1:-no reason given}" "$C_RESET" - return 0 -} diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/curl_stub.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/curl_stub.sh deleted file mode 100644 index 185b5bc..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/curl_stub.sh +++ /dev/null @@ -1,224 +0,0 @@ -#!/bin/bash -### -# curl stub for tests. -# -# Defines a shell function `curl` that overrides the binary in any code -# sourced/exec'd after this file is loaded. The stub: -# 1. Parses curl args to extract METHOD, URL, and the -d/--data body. -# 2. Appends a single NDJSON line to $CAPTURED_REQUESTS describing the call. -# 3. Looks up a canned response based on URL pattern matching. -# 4. Emits the response body to stdout, followed by a newline + status code -# if the call included -w "%{http_code}" (matching real curl behavior). -# -# Configuring responses in a test: -# -# stub_response_for "*/api/apikey/login" 200 '{"org_info":[{"api_url":"https://api.test.invalid"}]}' -# stub_response_for "*/v1/project*" 200 '{"id":"proj_abc"}' -# stub_response_for "*/insert" 200 '{"row_ids":["row_xyz"]}' -# -# Patterns are bash glob patterns (case-style matching). The first matching -# pattern wins. If no pattern matches, the stub returns 200 with an empty -# JSON object; the failing test will surface that as a missing expectation. -# -# To simulate auth failure: -# stub_response_for "*" 401 "Invalid API Key" -### - -# Arrays storing (pattern, status, body) triplets in parallel. -# Bash 3 (default on macOS) lacks associative arrays for this, so we use -# parallel indexed arrays. -_CURL_STUB_PATTERNS=() -_CURL_STUB_STATUSES=() -_CURL_STUB_BODIES=() - -_curl_stub_reset() { - _CURL_STUB_PATTERNS=() - _CURL_STUB_STATUSES=() - _CURL_STUB_BODIES=() - # Re-export so subprocesses see the cleared state (they get a copy at exec). - _curl_stub_export -} - -stub_response_for() { - local pattern="$1" - local status="$2" - local body="$3" - _CURL_STUB_PATTERNS+=("$pattern") - _CURL_STUB_STATUSES+=("$status") - _CURL_STUB_BODIES+=("$body") - _curl_stub_export -} - -# Encode the stub config into a single env var so child processes can rebuild -# the lookup. Format: NDJSON, one line per stub. -_curl_stub_export() { - local i out="" - for i in "${!_CURL_STUB_PATTERNS[@]}"; do - out+=$(printf '%s\t%s\t%s\n' "${_CURL_STUB_PATTERNS[i]}" "${_CURL_STUB_STATUSES[i]}" "${_CURL_STUB_BODIES[i]}") - out+=$'\n' - done - export _CURL_STUB_CONFIG="$out" -} - -# Rebuild local arrays from the env var (used in subprocesses that inherit -# the env but not the bash arrays). -_curl_stub_import() { - _CURL_STUB_PATTERNS=() - _CURL_STUB_STATUSES=() - _CURL_STUB_BODIES=() - [ -z "${_CURL_STUB_CONFIG:-}" ] && return 0 - local line pattern status body - while IFS=$'\t' read -r pattern status body; do - [ -z "$pattern" ] && continue - _CURL_STUB_PATTERNS+=("$pattern") - _CURL_STUB_STATUSES+=("$status") - _CURL_STUB_BODIES+=("$body") - done <<< "$_CURL_STUB_CONFIG" -} - -# The curl stub itself. Designed to handle the curl invocation patterns -# used in common.sh (see top of file for the catalogue). -curl() { - # Rebuild stub config in case we're in a subprocess that inherited only env. - _curl_stub_import - - local method="GET" - local url="" - local data="" - local want_http_code=0 - local arg - - # Parse args. We handle the flags actually used by common.sh: - # -s silent (ignore) - # -f fail on error (ignore - we control status) - # -X METHOD method - # -H HEADER header (ignore - we don't assert on headers) - # -d DATA request body - # -w FORMAT write-out format; we check for %{http_code} - # URL positional - while [ $# -gt 0 ]; do - arg="$1" - case "$arg" in - -s|--silent) shift ;; - -f|--fail) shift ;; - -X|--request) method="$2"; shift 2 ;; - -H|--header) shift 2 ;; - -d|--data|--data-raw|--data-binary) data="$2"; shift 2 ;; - -w|--write-out) - if [[ "$2" == *"%{http_code}"* ]]; then - want_http_code=1 - fi - shift 2 - ;; - -o|--output) shift 2 ;; - -L|--location) shift ;; - --) shift; break ;; - -*) - # Unknown flag — try to skip it without consuming the next arg. - # This is best-effort; if it requires a value we may misparse. - shift - ;; - *) - # Treat as URL if we don't have one yet - if [ -z "$url" ]; then - url="$arg" - fi - shift - ;; - esac - done - - # Default to POST if -d was provided but -X wasn't (matches curl behavior) - if [ -n "$data" ] && [ "$method" = "GET" ]; then - method="POST" - fi - - # Record the request to the capture file as NDJSON. - if [ -n "${CAPTURED_REQUESTS:-}" ]; then - # Try to record data as parsed JSON when possible, otherwise as a string. - local data_field - if [ -z "$data" ]; then - data_field='null' - elif echo "$data" | jq -e . >/dev/null 2>&1; then - data_field=$(echo "$data" | jq -c .) - else - data_field=$(jq -nc --arg d "$data" '$d') - fi - jq -nc \ - --arg method "$method" \ - --arg url "$url" \ - --argjson data "$data_field" \ - '{method: $method, url: $url, body: $data}' >> "$CAPTURED_REQUESTS" - fi - - # Find a matching response - local status=200 - local body='{}' - local i matched=0 - for i in "${!_CURL_STUB_PATTERNS[@]}"; do - # shellcheck disable=SC2053 - case "$url" in - ${_CURL_STUB_PATTERNS[i]}) - status="${_CURL_STUB_STATUSES[i]}" - body="${_CURL_STUB_BODIES[i]}" - matched=1 - break - ;; - esac - done - - # Default behavior on no match: 200 with empty object body. - # Tests that care should configure stubs. - - # Emit response. - printf '%s' "$body" - if [ "$want_http_code" -eq 1 ]; then - printf '\n%s' "$status" - fi - - # Exit status: real curl returns non-zero on connection errors etc. - # We always return 0 - the HTTP status code communicates HTTP errors. - return 0 -} - -# Inspection helpers for tests. - -# Print all captured requests as pretty JSON (one object per request, joined). -captured_requests() { - [ -f "${CAPTURED_REQUESTS:-/dev/null}" ] || return 0 - jq -s . "$CAPTURED_REQUESTS" -} - -# Print all captured requests that match a URL glob, as a JSON array. -captured_requests_matching() { - local pattern="$1" - [ -f "${CAPTURED_REQUESTS:-/dev/null}" ] || { echo '[]'; return 0; } - jq -s --arg p "$pattern" ' - [ .[] | select(.url | test($p)) ] - ' "$CAPTURED_REQUESTS" -} - -# Count captured requests matching a URL glob. -captured_request_count() { - local pattern="${1:-.*}" - [ -f "${CAPTURED_REQUESTS:-/dev/null}" ] || { echo 0; return 0; } - jq -s --arg p "$pattern" ' - [ .[] | select(.url | test($p)) ] | length - ' "$CAPTURED_REQUESTS" -} - -# Extract all spans from /insert calls as a flat JSON array. -captured_spans() { - [ -f "${CAPTURED_REQUESTS:-/dev/null}" ] || { echo '[]'; return 0; } - jq -s ' - [ .[] | select(.url | test("/insert$")) | .body.events[]? ] - ' "$CAPTURED_REQUESTS" -} - -# Export helper functions so subprocesses (hook scripts run via run_hook) -# inherit them - the stubbed curl() function calls _curl_stub_import on -# every invocation to rebuild its lookup table from $_CURL_STUB_CONFIG. -export -f _curl_stub_reset -export -f _curl_stub_export -export -f _curl_stub_import -export -f curl diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/fixtures.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/fixtures.sh deleted file mode 100644 index cd1e442..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/fixtures.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/bin/bash -### -# Fixture builders for Claude Code hook payloads. -# -# Each builder returns a JSON object on stdout that matches the shape -# Claude Code passes to the corresponding hook script over stdin. -# -# Usage in tests: -# payload=$(fixture_session_start "sess-1" "/tmp/proj") -# run_hook session_start.sh "$payload" -### - -# SessionStart payload -# -# Args: [session_id] [cwd] -fixture_session_start() { - local session_id="${1:-test-session}" - local cwd="${2:-/tmp/test-workspace}" - jq -nc \ - --arg s "$session_id" \ - --arg c "$cwd" \ - '{session_id: $s, cwd: $c}' -} - -# UserPromptSubmit payload -# -# Args: session_id prompt [cwd] -fixture_user_prompt() { - local session_id="$1" - local prompt="$2" - local cwd="${3:-/tmp/test-workspace}" - jq -nc \ - --arg s "$session_id" \ - --arg p "$prompt" \ - --arg c "$cwd" \ - '{session_id: $s, prompt: $p, cwd: $c}' -} - -# PostToolUse payload -# -# Args: session_id tool_name tool_input_json tool_response_json -fixture_post_tool_use() { - local session_id="$1" - local tool_name="$2" - local tool_input="$3" # JSON object - local tool_response="$4" # JSON object - jq -nc \ - --arg s "$session_id" \ - --arg t "$tool_name" \ - --argjson i "$tool_input" \ - --argjson r "$tool_response" \ - '{session_id: $s, tool_name: $t, tool_input: $i, tool_response: $r}' -} - -# PostToolUseFailure payload -# -# Args: session_id tool_name tool_input_json error [tool_response_json] -fixture_post_tool_use_failure() { - local session_id="$1" - local tool_name="$2" - local tool_input="$3" # JSON object - local error="$4" - local tool_response="${5:-}" - [ -z "$tool_response" ] && tool_response="{}" - jq -nc \ - --arg s "$session_id" \ - --arg t "$tool_name" \ - --arg e "$error" \ - --argjson i "$tool_input" \ - --argjson r "$tool_response" \ - '{session_id: $s, tool_name: $t, tool_input: $i, tool_response: $r, error: $e}' -} - -# PermissionDenied payload -# -# Args: session_id tool_name tool_input_json [tool_use_id] -fixture_permission_denied() { - local session_id="$1" - local tool_name="$2" - local tool_input="$3" # JSON object - local tool_use_id="${4:-}" - jq -nc \ - --arg s "$session_id" \ - --arg t "$tool_name" \ - --arg tuid "$tool_use_id" \ - --argjson i "$tool_input" \ - '{session_id: $s, tool_name: $t, tool_input: $i} + (if $tuid != "" then {tool_use_id: $tuid} else {} end)' -} - -# Stop payload - includes the transcript path and optionally the -# last assistant message that Claude Code provides in real Stop events. -# -# Args: session_id transcript_path [last_assistant_message] -fixture_stop() { - local session_id="$1" - local transcript_path="$2" - local last_msg="${3:-}" - jq -nc \ - --arg s "$session_id" \ - --arg t "$transcript_path" \ - --arg m "$last_msg" \ - '{session_id: $s, transcript_path: $t, last_assistant_message: $m}' -} - -# SessionEnd payload -# -# Args: session_id -fixture_session_end() { - local session_id="$1" - jq -nc --arg s "$session_id" '{session_id: $s}' -} - -# Convenience: tool input/output JSON builders -fixture_tool_input_bash() { - local command="$1" - jq -nc --arg c "$command" '{command: $c}' -} - -fixture_tool_input_read() { - local file_path="$1" - jq -nc --arg p "$file_path" '{file_path: $p}' -} - -fixture_tool_response_text() { - local output="$1" - jq -nc --arg o "$output" '{output: $o}' -} diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/harness.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/harness.sh deleted file mode 100644 index eef6359..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/harness.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -### -# Test harness: provides a clean, isolated environment for each test. -# -# Each test gets: -# - A fresh temp directory used as $HOME so common.sh writes to it -# - Env vars set to defaults safe for testing (e.g. BRAINTRUST_API_KEY) -# - common.sh sourced so its functions are available -# - The stubbed `curl` function loaded so no real network calls are made -# - A capture file ($CAPTURED_REQUESTS) recording every HTTP request -# -# Usage in a test file: -# source helpers/assert.sh -# source helpers/harness.sh -# -# describe "my function" -# it "does the thing" -# setup_test_env -# ... call functions ... -# assert_eq "$got" "$want" -# end_it -### - -# Locate the plugin root and hooks directory based on this file's path -TEST_HELPERS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TEST_DIR="$(dirname "$TEST_HELPERS_DIR")" -PLUGIN_DIR="$(dirname "$TEST_DIR")" -HOOKS_DIR="$PLUGIN_DIR/hooks" - -export TEST_HELPERS_DIR TEST_DIR PLUGIN_DIR HOOKS_DIR - -# Source the curl stub - defines curl() as a shell function that overrides -# the binary. Tests can configure responses via stub_response_for. -source "$TEST_HELPERS_DIR/curl_stub.sh" - -# Source fixture builders and span-tree helpers so tests don't have to -# source them individually. These are pure helpers; no state. -source "$TEST_HELPERS_DIR/fixtures.sh" -source "$TEST_HELPERS_DIR/span_tree.sh" -source "$TEST_HELPERS_DIR/replay.sh" - -# Per-test temp dir; reset on every setup_test_env call. -TEST_TMP="" - -setup_test_env() { - # Fresh isolated home for this test - TEST_TMP=$(mktemp -d "${TMPDIR:-/tmp}/braintrust-test.XXXXXX") - export HOME="$TEST_TMP" - - # Defaults that make common.sh happy - export BRAINTRUST_API_KEY="test-api-key" - export BRAINTRUST_APP_URL="https://app.test.invalid" - export BRAINTRUST_API_URL="https://api.test.invalid" - export TRACE_TO_BRAINTRUST=true - export DEBUG=false - # Run the span queue inline so tests are deterministic - we don't - # want to spawn background workers or wait on file watchers. - export BRAINTRUST_SYNC_QUEUE=true - # If a test opts into async mode and a worker is left behind, make - # sure drain_queue calls don't block tests for the full default. - export BRAINTRUST_DRAIN_TIMEOUT=5 - - # Capture file for HTTP requests - export CAPTURED_REQUESTS="$TEST_TMP/captured_requests.ndjson" - : > "$CAPTURED_REQUESTS" - - # Reset stub response configuration - _curl_stub_reset - - # Clear cached state from a previous test in the same shell. - # common.sh caches API URL in _RESOLVED_API_URL; clearing avoids - # cross-test pollution. - unset _RESOLVED_API_URL - - # Source common.sh so its functions are available in this shell. - # common.sh uses $HOME, so this must happen AFTER setting HOME. - # shellcheck source=/dev/null - source "$HOOKS_DIR/common.sh" - - # After sourcing common.sh, re-export the stub curl so it shadows the - # binary for any subprocesses spawned by hook scripts. - export -f curl -} - -teardown_test_env() { - if [ -n "$TEST_TMP" ] && [ -d "$TEST_TMP" ]; then - rm -rf "$TEST_TMP" - fi - TEST_TMP="" -} - -# Run a hook script as a subprocess with the given stdin payload. -# Returns the exit code; stdout/stderr are captured into the named variables. -# -# Usage: -# run_hook session_start.sh "$payload" -# echo "$HOOK_STDOUT" "$HOOK_STDERR" "$HOOK_STATUS" -run_hook() { - local hook="$1" - local payload="$2" - local out err - local tmpout tmperr - tmpout=$(mktemp) - tmperr=$(mktemp) - - # Pass the env explicitly so the subprocess sees our stub curl - # (export -f propagates over `env` invocations in bash). - echo "$payload" | bash "$HOOKS_DIR/$hook" >"$tmpout" 2>"$tmperr" - HOOK_STATUS=$? - HOOK_STDOUT=$(cat "$tmpout") - HOOK_STDERR=$(cat "$tmperr") - rm -f "$tmpout" "$tmperr" - return $HOOK_STATUS -} - -# Convenience: print the contents of the hook log file (common.sh writes -# to $HOME/.claude/state/braintrust_hook.log when sourced). -hook_log() { - local f="$HOME/.claude/state/braintrust_hook.log" - [ -f "$f" ] && cat "$f" || true -} diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/replay.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/replay.sh deleted file mode 100644 index d2e7381..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/replay.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/bin/bash -### -# Replay helper: drive hooks from a recorded session fixture. -# -# A session fixture is a directory produced by setting BRAINTRUST_RECORD_DIR -# during a real Claude Code session. It contains: -# -# events.ndjson - one JSON record per hook invocation, in order: -# {ts, hook, payload}. The `hook` field holds -# Claude Code's event name (e.g. "PostToolUse"). -# transcripts/.jsonl - any transcript files referenced by a Stop event -# -# Usage in a test: -# -# replay_session "$TEST_DIR/fixtures/sessions/my-session" -# assert_eq "$(span_count_by_type tool)" "5" -### - -# Replay every event in a session fixture exactly the way Claude Code -# would: for each recorded event, look up its handlers in hooks.json and -# run each registered command with the payload piped to stdin. Replay is -# intentionally dumb - it does not know or care which hooks "do something". -# Whatever is registered for an event runs; whatever isn't, doesn't. This -# means record-only events (PreToolUse, PreCompact, ...) replay through -# record_event.sh and simply no-op (recording is off during replay), while -# the acting hooks create their spans. -# -# Stop payloads have their transcript_path rewritten to point at the -# fixture's transcripts/ directory so the hook can find the file. -# -# Returns the number of events that matched at least one handler on stdout; -# returns non-zero if the fixture is missing or any hook returns non-zero. -# -# Requires $HOOKS_DIR and $PLUGIN_DIR (exported by helpers/harness.sh). -replay_session() { - local fixture_dir="$1" - local events_file="$fixture_dir/events.ndjson" - local hooks_json="${HOOKS_DIR:-}/hooks.json" - - if [ ! -f "$events_file" ]; then - echo "replay_session: fixture not found: $events_file" >&2 - return 1 - fi - if [ ! -f "$hooks_json" ]; then - echo "replay_session: hooks.json not found: $hooks_json" >&2 - return 1 - fi - - local replayed=0 - local line event payload - - # Read line-by-line. NDJSON, one event per line. - while IFS= read -r line; do - [ -z "$line" ] && continue - - event=$(echo "$line" | jq -r '.hook // empty') - payload=$(echo "$line" | jq -c '.payload // {}') - - [ -z "$event" ] && continue - - # Rewrite transcript paths so the replayed hook reads the bundled - # copies instead of the absolute paths from the original machine. - # Stop references the main transcript via `transcript_path`; - # SubagentStop references the sub-agent transcript via - # `agent_transcript_path`. Both are snapshotted flat into - # transcripts/ by record_hook_input and resolved here by basename. - local _field - for _field in transcript_path agent_transcript_path; do - local original_path basename_t replay_path - original_path=$(echo "$payload" | jq -r --arg f "$_field" '.[$f] // empty') - [ -z "$original_path" ] && continue - basename_t=$(basename "$original_path") - replay_path="$fixture_dir/transcripts/$basename_t" - if [ -f "$replay_path" ]; then - payload=$(echo "$payload" | jq -c \ - --arg f "$_field" --arg p "$replay_path" '.[$f] = $p') - fi - done - - # Look up every command registered for this event in hooks.json and - # run them in order, mirroring how Claude Code dispatches an event. - # We read the raw command strings (which embed ${CLAUDE_PLUGIN_ROOT}) - # and substitute the plugin root before executing. - local commands - commands=$(jq -r --arg e "$event" ' - .hooks[$e][]?.hooks[]? - | select(.type == "command") - | .command - ' "$hooks_json" 2>/dev/null) - - [ -z "$commands" ] && continue # no handler registered: nothing to do - - local ran_any=0 - local cmd - while IFS= read -r cmd; do - [ -z "$cmd" ] && continue - # Substitute the plugin-root placeholder with the real path. - cmd=${cmd//\$\{CLAUDE_PLUGIN_ROOT\}/$PLUGIN_DIR} - - # Run the command with the payload on stdin, exactly as Claude - # Code would. Errors fail the replay so tests catch them. - echo "$payload" | bash -c "$cmd" - local rc=$? - if [ "$rc" -ne 0 ]; then - echo "replay_session: '$event' handler exited $rc on event $((replayed + 1)): $cmd" >&2 - return "$rc" - fi - ran_any=1 - done <<< "$commands" - - [ "$ran_any" -eq 1 ] && replayed=$((replayed + 1)) - done < "$events_file" - - echo "$replayed" - return 0 -} - -# Print a summary of what's in a fixture (count of each hook type, etc.) -# Useful for debugging. -describe_fixture() { - local fixture_dir="$1" - local events_file="$fixture_dir/events.ndjson" - [ -f "$events_file" ] || { echo "(no events)"; return 1; } - - echo "Fixture: $fixture_dir" - echo " Events: $(wc -l < "$events_file" | tr -d ' ')" - echo " Hook counts:" - jq -r '.hook' "$events_file" | sort | uniq -c | awk '{printf " %s: %s\n", $2, $1}' - local n_transcripts=0 - if [ -d "$fixture_dir/transcripts" ]; then - n_transcripts=$(find "$fixture_dir/transcripts" -maxdepth 1 -name '*.jsonl' -type f 2>/dev/null | wc -l | tr -d ' ') - fi - echo " Transcripts: $n_transcripts" -} diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/span_tree.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/span_tree.sh deleted file mode 100644 index 13142a4..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/helpers/span_tree.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash -### -# Span query helpers - the bash equivalent of opencode-plugin's spansToTree(). -# -# These read the captured POST requests from $CAPTURED_REQUESTS, extract -# spans from /insert calls, and provide flat + relational queries for tests. -# -# All functions print JSON to stdout. Tests use `jq` to drill into results. -### - -# Flat array of all spans sent to any /insert endpoint, in arrival order. -all_spans() { - [ -f "${CAPTURED_REQUESTS:-/dev/null}" ] || { echo '[]'; return 0; } - jq -s ' - [ .[] - | select(.url | test("/insert$")) - | .body.events[]? - ] - ' "$CAPTURED_REQUESTS" -} - -# Total count of spans inserted. -span_count() { - all_spans | jq 'length' -} - -# Count of spans by .span_attributes.type (e.g. "task", "tool", "llm"). -span_count_by_type() { - local type="$1" - all_spans | jq --arg t "$type" ' - [ .[] | select(.span_attributes.type == $t) ] | length - ' -} - -# Return spans whose span_attributes.name matches the given regex. -# Spans without a name (e.g. merge updates that only touch metrics) are -# excluded. -spans_named() { - local pattern="$1" - all_spans | jq --arg p "$pattern" ' - [ .[] - | select(.span_attributes.name != null) - | select(.span_attributes.name | test($p)) - ] - ' -} - -# Return the first span matching a name regex (or `null`). -span_by_name() { - local pattern="$1" - spans_named "$pattern" | jq '.[0] // null' -} - -# Return the first span with a given .span_attributes.type (or `null`). -span_by_type() { - local type="$1" - all_spans | jq --arg t "$type" ' - [.[] | select(.span_attributes.type == $t)][0] // null - ' -} - -# Return the span with the given span_id (or `null`). -span_by_id() { - local id="$1" - all_spans | jq --arg i "$id" ' - [.[] | select(.span_id == $i)][0] // null - ' -} - -# Return all spans whose first parent matches the given span_id. -children_of() { - local parent_id="$1" - all_spans | jq --arg p "$parent_id" ' - [ .[] - | select(.span_parents and (.span_parents | length > 0) - and (.span_parents[0] == $p)) - ] - ' -} - -# True/false: does a span_id have a span_parents[0] equal to the given id? -is_child_of() { - local child_id="$1" - local parent_id="$2" - all_spans | jq --arg c "$child_id" --arg p "$parent_id" -e ' - any(.[]; .span_id == $c and (.span_parents // [])[0] == $p) - ' >/dev/null -} diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/reconcile_usage.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/reconcile_usage.sh deleted file mode 100755 index f6dd593..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/reconcile_usage.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/bin/bash -### -# reconcile_usage.sh - drive a real Claude Code session headlessly, then -# compare Claude Code's own reported token usage against what the plugin's -# transcript parsing extracts. This is a developer tool for closing token -# reconciliation gaps (e.g. getting opus totals to match). -# -# It runs `claude -p` with the dev plugin dir and BRAINTRUST_RECORD_DIR set, -# captures CC's authoritative per-model usage from `--output-format json` -# (the same numbers /usage shows), then re-derives per-model totals from the -# recorded transcripts (main + sub-agents) using the same rules the plugin -# uses: dedupe by requestId, take MAX output_tokens per request (it streams), -# count input/cache once per request. -# -# Usage: -# ./reconcile_usage.sh "your prompt here" -# ./reconcile_usage.sh # uses a default sub-agent prompt -# -# Env: -# CC_PROJECT Braintrust project (default: trace-cc-debug) -# KEEP_REC=1 keep the recording dir (otherwise printed but not deleted) -### - -set -u - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PLUGIN_DIR="$(dirname "$SCRIPT_DIR")" -CC_PROJECT="${CC_PROJECT:-trace-cc-debug}" - -PROMPT="${1:-Launch two Explore subagents to look at different parts of /tmp, then summarize.}" - -SID=$(uuidgen) -REC="${TMPDIR:-/tmp}/cc-reconcile-$SID" -mkdir -p "$REC" - -echo "==> Running Claude Code headlessly" -echo " session: $SID" -echo " record: $REC" -echo " prompt: $PROMPT" -echo - -SETTINGS=$(jq -nc \ - --arg p "$CC_PROJECT" \ - --arg r "$REC" \ - '{env:{TRACE_TO_BRAINTRUST:"true", BRAINTRUST_CC_PROJECT:$p, BRAINTRUST_RECORD_DIR:$r}}') - -# Capture CC's JSON result (includes modelUsage + session_id). -CC_JSON=$(cd "${TMPDIR:-/tmp}" && claude -p "$PROMPT" \ - --output-format json \ - --settings "$SETTINGS" \ - --plugin-dir "$PLUGIN_DIR" \ - --session-id "$SID" 2>/dev/null) - -if [ -z "$CC_JSON" ]; then - echo "ERROR: no output from claude -p" >&2 - exit 1 -fi - -# Resolve the session's transcript from the LIVE Claude projects dir rather -# than the recording's snapshot. In -p mode the plugin's Stop hook copies the -# transcript before the assistant reply is flushed to disk, so the recorded -# copy can be incomplete. The live file under ~/.claude/projects is complete by -# the time `claude -p` has returned. We pull its path (and any sub-agent -# transcripts) from the Stop event payload the plugin recorded. -SESSION_LC=$(echo "$CC_JSON" | jq -r '.session_id' | tr 'A-Z' 'a-z') -LIVE_MAIN=$(jq -rc 'select(.hook=="Stop")|.payload.transcript_path' "$REC/events.ndjson" 2>/dev/null | head -1) -LIVE_DIR="" -[ -n "$LIVE_MAIN" ] && LIVE_DIR=$(dirname "$LIVE_MAIN") - -echo "==> Claude Code reported usage (authoritative):" -echo "$CC_JSON" | jq -r ' - .modelUsage // {} - | to_entries[] - | " \(.key): input=\(.value.inputTokens) output=\(.value.outputTokens) cache_read=\(.value.cacheReadInputTokens) cache_write=\(.value.cacheCreationInputTokens) cost=$\(.value.costUSD)" -' -echo " total_cost=$(echo "$CC_JSON" | jq -r '.total_cost_usd // "?"')" -echo - -# ---- Derive plugin-side totals from the recorded transcripts ---- -# Normalize model names: CC's modelUsage uses e.g. "claude-opus-4-8[1m]", -# transcripts use "claude-opus-4-8". Strip the "[...]" suffix for comparison. -_strip_model() { sed -E 's/\[[^]]*\]$//'; } - -echo "==> Plugin-derived usage from the session transcripts:" -# Prefer the live transcript(s): the main file plus any sub-agent transcripts -# under /subagents/. Fall back to the recorded snapshot. -TRANSCRIPTS="" -if [ -n "$LIVE_MAIN" ] && [ -f "$LIVE_MAIN" ]; then - SESSION_BASE=$(basename "$LIVE_MAIN" .jsonl) - TRANSCRIPTS="$LIVE_MAIN" - if [ -d "$LIVE_DIR/$SESSION_BASE/subagents" ]; then - TRANSCRIPTS="$TRANSCRIPTS -$(find "$LIVE_DIR/$SESSION_BASE/subagents" -name 'agent-*.jsonl' -type f 2>/dev/null)" - fi -fi -[ -z "${TRANSCRIPTS//[[:space:]]/}" ] && TRANSCRIPTS=$(find "$REC/transcripts" -name '*.jsonl' -type f 2>/dev/null) -if [ -z "${TRANSCRIPTS//[[:space:]]/}" ]; then - echo " (no transcripts found)" -else - # Per model: dedupe assistant lines by requestId; max each token field. - # shellcheck disable=SC2086 - jq -rs ' - [ .[] - | select(.type=="assistant") - | select(.message.usage != null) - | { model:(.message.model // "claude"), - rid:(.requestId // .message.id), - inp:(.message.usage.input_tokens // 0), - out:(.message.usage.output_tokens // 0), - cr:(.message.usage.cache_read_input_tokens // 0), - cc:(.message.usage.cache_creation_input_tokens // 0) } - ] - | group_by(.model) - | map( - .[0].model as $m - | (group_by(.rid) - | { model:$m, - requests:length, - input:(map([.[].inp]|max)|add), - output:(map([.[].out]|max)|add), - cache_read:(map([.[].cr]|max)|add), - cache_write:(map([.[].cc]|max)|add) }) - ) - | .[] - | " \(.model): input=\(.input) output=\(.output) cache_read=\(.cache_read) cache_write=\(.cache_write) (\(.requests) requests)" - ' $TRANSCRIPTS -fi -echo - -# ---- Print a focused diff for each model CC reported ---- -echo "==> Diff (CC - plugin), per model:" -echo "$CC_JSON" | jq -r '.modelUsage // {} | to_entries[] | "\(.key)\t\(.value.inputTokens)\t\(.value.outputTokens)\t\(.value.cacheReadInputTokens)\t\(.value.cacheCreationInputTokens)"' \ -| while IFS=$'\t' read -r ccmodel ci co ccr ccw; do - model=$(echo "$ccmodel" | _strip_model) - # plugin totals for this model - ptot=$(jq -s --arg m "$model" ' - [ .[] | select(.type=="assistant") | select(.message.usage != null) - | select((.message.model // "") == $m) - | { rid:(.requestId // .message.id), - inp:(.message.usage.input_tokens // 0), - out:(.message.usage.output_tokens // 0), - cr:(.message.usage.cache_read_input_tokens // 0), - cc:(.message.usage.cache_creation_input_tokens // 0) } ] - | group_by(.rid) - | { input:(map([.[].inp]|max)|add // 0), - output:(map([.[].out]|max)|add // 0), - cache_read:(map([.[].cr]|max)|add // 0), - cache_write:(map([.[].cc]|max)|add // 0) } - ' $TRANSCRIPTS 2>/dev/null) - pi=$(echo "$ptot" | jq -r '.input // 0') - po=$(echo "$ptot" | jq -r '.output // 0') - pcr=$(echo "$ptot" | jq -r '.cache_read // 0') - pcw=$(echo "$ptot" | jq -r '.cache_write // 0') - printf ' %s\n' "$model" - printf ' input: cc=%-8s plugin=%-8s diff=%s\n' "$ci" "$pi" "$((ci - pi))" - printf ' output: cc=%-8s plugin=%-8s diff=%s\n' "$co" "$po" "$((co - po))" - printf ' cache_read: cc=%-8s plugin=%-8s diff=%s\n' "$ccr" "$pcr" "$((ccr - pcr))" - printf ' cache_write: cc=%-8s plugin=%-8s diff=%s\n' "$ccw" "$pcw" "$((ccw - pcw))" -done - -echo -echo "==> session_id: $(echo "$CC_JSON" | jq -r '.session_id')" -echo "==> recording: $REC" -if [ "${KEEP_REC:-0}" != "1" ]; then - echo " (set KEEP_REC=1 to preserve; leaving in place for inspection)" -fi diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/record_session.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/record_session.sh deleted file mode 100755 index 02aa0d7..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/record_session.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash -### -# record_session.sh - convenience wrapper for capturing a Claude Code -# session as a test fixture. -# -# Usage: -# ./record_session.sh -# -# This prints the env vars you need to set in your shell before running -# Claude Code. After the session, the fixture will be under -# test/fixtures/sessions// ready to be used by replay_session. -# -# Example: -# $ ./record_session.sh my-session -# # then in another terminal: -# $ export BRAINTRUST_RECORD_DIR=/path/printed/above -# $ claude -# # ... use claude normally ... -# # When done: -# $ ./record_session.sh --describe my-session -### - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -FIXTURES_DIR="$SCRIPT_DIR/fixtures/sessions" - -usage() { - cat < Prepare a new fixture and print instructions. - $0 --describe Show a summary of an existing fixture. - $0 --list List all existing fixtures. - -When recording, all you need to do is set BRAINTRUST_RECORD_DIR (printed by -this script) in your shell before running 'claude'. The hooks themselves -do the recording automatically. -USAGE - exit 1 -} - -case "${1:-}" in - "" | "-h" | "--help") usage ;; - "--list") - if [ ! -d "$FIXTURES_DIR" ]; then - echo "(no fixtures yet)" - exit 0 - fi - for d in "$FIXTURES_DIR"/*/; do - [ -d "$d" ] || continue - name=$(basename "$d") - events_file="$d/events.ndjson" - n_events=0 - if [ -f "$events_file" ]; then - n_events=$(wc -l < "$events_file" | tr -d ' ') - fi - printf ' %-30s %d events\n' "$name" "$n_events" - done - ;; - "--describe") - name="${2:-}" - [ -z "$name" ] && usage - # Load helpers and dispatch - # shellcheck source=helpers/replay.sh - source "$SCRIPT_DIR/helpers/replay.sh" - describe_fixture "$FIXTURES_DIR/$name" - ;; - *) - name="$1" - case "$name" in - -*) usage ;; - esac - dest="$FIXTURES_DIR/$name" - if [ -e "$dest" ]; then - echo "Fixture '$name' already exists at $dest" >&2 - echo "Remove it first if you want to re-record." >&2 - exit 1 - fi - mkdir -p "$dest" - cat < "$TESTS_RUN_FILE" -: > "$TESTS_FAIL_FILE" - -cleanup() { - rm -f "$TESTS_RUN_FILE" "$TESTS_FAIL_FILE" -} -trap cleanup EXIT - -# Resolve which test files to run. -declare -a TEST_FILES=() -if [ $# -gt 0 ]; then - for arg in "$@"; do - # Allow specifying with or without .sh, with or without test_ prefix - local_name="$arg" - case "$local_name" in - *.sh) ;; - *) local_name="${local_name}.sh" ;; - esac - case "$local_name" in - test_*) ;; - *) local_name="test_${local_name}" ;; - esac - if [ -f "$SCRIPT_DIR/$local_name" ]; then - TEST_FILES+=("$local_name") - else - echo "${C_RED}Test file not found: $local_name${C_RESET}" >&2 - exit 2 - fi - done -else - while IFS= read -r f; do - TEST_FILES+=("$f") - done < <(find "$SCRIPT_DIR" -maxdepth 1 -name 'test_*.sh' -type f | sort | xargs -n1 basename) -fi - -if [ ${#TEST_FILES[@]} -eq 0 ]; then - echo "${C_YELLOW}No test files found.${C_RESET}" - exit 0 -fi - -printf '%sRunning tests in %s%s\n' "$C_BOLD" "$SCRIPT_DIR" "$C_RESET" -printf '%s%d test file(s)%s\n' "$C_DIM" "${#TEST_FILES[@]}" "$C_RESET" - -START_TIME=$(date +%s) -FILES_WITH_FAILURES=0 - -for file in "${TEST_FILES[@]}"; do - printf '\n%s──── %s ────%s\n' "$C_BOLD" "$file" "$C_RESET" - # Run each test file in a fresh subshell so its globals don't leak. - if ! bash "$SCRIPT_DIR/$file"; then - FILES_WITH_FAILURES=$((FILES_WITH_FAILURES + 1)) - fi -done - -END_TIME=$(date +%s) -ELAPSED=$((END_TIME - START_TIME)) - -TOTAL=$(wc -l < "$TESTS_RUN_FILE" | tr -d ' ') -FAILED=$(wc -l < "$TESTS_FAIL_FILE" | tr -d ' ') -PASSED=$((TOTAL - FAILED)) - -printf '\n%s──── Summary ────%s\n' "$C_BOLD" "$C_RESET" -printf ' %sPassed:%s %d\n' "$C_GREEN" "$C_RESET" "$PASSED" -if [ "$FAILED" -gt 0 ]; then - printf ' %sFailed:%s %d\n' "$C_RED" "$C_RESET" "$FAILED" -else - printf ' Failed: 0\n' -fi -printf ' Total: %d\n' "$TOTAL" -printf ' Time: %ds\n' "$ELAPSED" - -if [ "$FAILED" -gt 0 ]; then - printf '\n%s✗ Tests failed%s\n' "$C_RED" "$C_RESET" - exit 1 -fi - -printf '\n%s✓ All tests passed%s\n' "$C_GREEN" "$C_RESET" -exit 0 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_common.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_common.sh deleted file mode 100755 index 03c317b..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_common.sh +++ /dev/null @@ -1,460 +0,0 @@ -#!/bin/bash -### -# Unit tests for utility functions in hooks/common.sh -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -# --------------------------------------------------------------------------- -describe "is_truthy" -# --------------------------------------------------------------------------- - -t_truthy_true() { is_truthy "true"; assert_eq "$?" "0"; } -t_truthy_TRUE() { is_truthy "TRUE"; assert_eq "$?" "0"; } -t_truthy_mixed() { is_truthy "tRuE"; assert_eq "$?" "0"; } -t_truthy_one() { is_truthy "1"; assert_eq "$?" "0"; } -t_truthy_yes() { is_truthy "yes"; assert_eq "$?" "0"; } -t_truthy_on() { is_truthy "on"; assert_eq "$?" "0"; } -t_truthy_false() { is_truthy "false"; assert_failure "$?"; } -t_truthy_zero() { is_truthy "0"; assert_failure "$?"; } -t_truthy_empty() { is_truthy ""; assert_failure "$?"; } -t_truthy_arbitrary() { is_truthy "maybe"; assert_failure "$?"; } - -it "returns 0 for 'true'" t_truthy_true -it "returns 0 for 'TRUE' (uppercase)" t_truthy_TRUE -it "returns 0 for 'tRuE' (mixed case)" t_truthy_mixed -it "returns 0 for '1'" t_truthy_one -it "returns 0 for 'yes'" t_truthy_yes -it "returns 0 for 'on'" t_truthy_on -it "returns non-zero for 'false'" t_truthy_false -it "returns non-zero for '0'" t_truthy_zero -it "returns non-zero for empty string" t_truthy_empty -it "returns non-zero for arbitrary string" t_truthy_arbitrary - -# --------------------------------------------------------------------------- -describe "tracing_enabled" -# --------------------------------------------------------------------------- - -t_tracing_true() { - TRACE_TO_BRAINTRUST=true - tracing_enabled - assert_eq "$?" "0" -} -t_tracing_false() { - TRACE_TO_BRAINTRUST=false - tracing_enabled - assert_failure "$?" -} -t_tracing_unset() { - unset TRACE_TO_BRAINTRUST - tracing_enabled - assert_failure "$?" -} - -it "follows TRACE_TO_BRAINTRUST=true" t_tracing_true -it "follows TRACE_TO_BRAINTRUST=false" t_tracing_false -it "returns non-zero when TRACE_TO_BRAINTRUST is unset" t_tracing_unset - -# --------------------------------------------------------------------------- -describe "check_requirements" -# --------------------------------------------------------------------------- - -t_check_req_ok() { - API_KEY="some-key" - check_requirements - assert_eq "$?" "0" -} -t_check_req_missing_key() { - API_KEY="" - check_requirements - assert_failure "$?" - local log - log=$(hook_log) - assert_contains "$log" "BRAINTRUST_API_KEY not set" -} - -it "passes when all binaries exist and API_KEY is set" t_check_req_ok -it "fails when API_KEY is empty" t_check_req_missing_key - -# --------------------------------------------------------------------------- -describe "get_cache_value / set_cache_value" -# --------------------------------------------------------------------------- - -t_cache_roundtrip() { - set_cache_value "my_key" "my_value" - local got - got=$(get_cache_value "my_key") - assert_eq "$got" "my_value" -} -t_cache_missing() { - local got - got=$(get_cache_value "never_set_key") - assert_eq "$got" "" -} -t_cache_overwrite() { - set_cache_value "k" "v1" - set_cache_value "k" "v2" - local got - got=$(get_cache_value "k") - assert_eq "$got" "v2" -} - -it "round-trips a value" t_cache_roundtrip -it "returns empty string when key is unset" t_cache_missing -it "overwrites an existing value" t_cache_overwrite - -# --------------------------------------------------------------------------- -describe "set_session_state / get_session_state" -# --------------------------------------------------------------------------- - -t_state_roundtrip() { - set_session_state "sess1" "name" "value-A" - local got - got=$(get_session_state "sess1" "name") - assert_eq "$got" "value-A" -} -t_state_isolation() { - set_session_state "sessA" "k" "valueA" - set_session_state "sessB" "k" "valueB" - local got_a got_b - got_a=$(get_session_state "sessA" "k") - got_b=$(get_session_state "sessB" "k") - assert_eq "$got_a" "valueA" - assert_eq "$got_b" "valueB" -} -t_state_missing() { - local got - got=$(get_session_state "sess_missing" "missing_key") - assert_eq "$got" "" -} -t_state_overwrite() { - set_session_state "sess" "k" "old" - set_session_state "sess" "k" "new" - local got - got=$(get_session_state "sess" "k") - assert_eq "$got" "new" -} - -it "round-trips a value within a single session" t_state_roundtrip -it "isolates state across distinct sessions" t_state_isolation -it "returns empty string for an unknown key" t_state_missing -it "supports overwriting an existing key" t_state_overwrite - -# --------------------------------------------------------------------------- -describe "check_and_set_session_state" -# --------------------------------------------------------------------------- - -t_check_set_new() { - check_and_set_session_state "sess" "first" "v" - local rc=$? - assert_eq "$rc" "0" - local got - got=$(get_session_state "sess" "first") - assert_eq "$got" "v" -} -t_check_set_existing() { - set_session_state "sess" "claimed" "original" - local out - out=$(check_and_set_session_state "sess" "claimed" "new-value") - local rc=$? - assert_eq "$rc" "1" - assert_eq "$out" "original" - local got - got=$(get_session_state "sess" "claimed") - assert_eq "$got" "original" -} - -it "sets and returns 0 when key is new" t_check_set_new -it "preserves existing value and returns 1 when key is already set" t_check_set_existing - -# --------------------------------------------------------------------------- -describe "is_experiment_mode" -# --------------------------------------------------------------------------- - -t_exp_set() { - CC_EXPERIMENT_ID="exp_abc" - is_experiment_mode - assert_eq "$?" "0" -} -t_exp_empty() { - CC_EXPERIMENT_ID="" - is_experiment_mode - assert_failure "$?" -} - -it "is true when CC_EXPERIMENT_ID is set" t_exp_set -it "is false when CC_EXPERIMENT_ID is empty" t_exp_empty - -# --------------------------------------------------------------------------- -describe "generate_uuid" -# --------------------------------------------------------------------------- - -t_uuid_format() { - local uuid - uuid=$(generate_uuid) - assert_match "$uuid" "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" -} -t_uuid_unique() { - local a b - a=$(generate_uuid) - b=$(generate_uuid) - assert_ne "$a" "$b" -} - -it "returns a non-empty lowercase UUID" t_uuid_format -it "returns unique values on subsequent calls" t_uuid_unique - -# --------------------------------------------------------------------------- -describe "git metadata helpers" -# --------------------------------------------------------------------------- - -_make_git_repo() { - local dir - dir=$(mktemp -d) - git -C "$dir" init >/dev/null 2>&1 - git -C "$dir" config user.email test@example.com - git -C "$dir" config user.name "Test User" - printf "hello\n" > "$dir/README.md" - git -C "$dir" add README.md - git -C "$dir" commit -m init >/dev/null 2>&1 - git -C "$dir" branch -M main - git -C "$dir" remote add origin "https://token@github.com/acme/app.git" - echo "$dir" -} - -t_git_remote_redaction() { - local redacted - redacted=$(redact_git_remote_url "https://token:secret@github.com/acme/app.git") - assert_eq "$redacted" "https://github.com/acme/app.git" - - local ssh - ssh=$(redact_git_remote_url "git@github.com:acme/app.git") - assert_eq "$ssh" "git@github.com:acme/app.git" -} - -t_git_metadata_json() { - local repo commit metadata - repo=$(_make_git_repo) - commit=$(git -C "$repo" rev-parse HEAD) - metadata=$(git_metadata_json "$repo") - - assert_eq "$(echo "$metadata" | jq -r '.git_origin_url')" "https://github.com/acme/app.git" - assert_eq "$(echo "$metadata" | jq -r '.git_branch')" "main" - assert_eq "$(echo "$metadata" | jq -r '.git_commit_sha')" "$commit" - - rm -rf "$repo" -} - -t_git_metadata_json_not_git() { - local dir metadata - dir=$(mktemp -d) - metadata=$(git_metadata_json "$dir") - assert_eq "$metadata" "{}" - rm -rf "$dir" -} - -it "redacts URL-style git remote credentials" t_git_remote_redaction -it "captures origin, branch, and commit" t_git_metadata_json -it "omits fields outside a git repo" t_git_metadata_json_not_git - -# --------------------------------------------------------------------------- -describe "record_hook_input: event labeling and transcript snapshots" -# --------------------------------------------------------------------------- - -# Helper: point recording at a fresh dir under the test's isolated HOME. -_rec_dir() { - echo "$HOME/recording" -} - -t_record_labels_by_event_name() { - # The recorded event should be labeled with the payload's - # hook_event_name (CamelCase), regardless of the name passed in. - export BRAINTRUST_RECORD_DIR="$(_rec_dir)" - local payload='{"session_id":"s1","hook_event_name":"PreToolUse","tool_name":"Bash"}' - record_hook_input "ignored_arg" "$payload" - - local label - label=$(jq -r '.hook' "$BRAINTRUST_RECORD_DIR/events.ndjson") - assert_eq "$label" "PreToolUse" "event labeled by hook_event_name" -} - -t_record_falls_back_to_arg_name() { - # When the payload has no hook_event_name, fall back to the passed name. - export BRAINTRUST_RECORD_DIR="$(_rec_dir)" - record_hook_input "CwdChanged" '{"session_id":"s1"}' - - local label - label=$(jq -r '.hook' "$BRAINTRUST_RECORD_DIR/events.ndjson") - assert_eq "$label" "CwdChanged" "falls back to caller-supplied name" -} - -t_record_copies_main_transcript_on_stop() { - # Regression guard: a Stop event must snapshot the main transcript into - # transcripts/. (This broke once when the copy guard still checked the - # old snake_case name after we switched to CamelCase labels.) - export BRAINTRUST_RECORD_DIR="$(_rec_dir)" - local transcript="$HOME/main.jsonl" - echo '{"type":"assistant"}' > "$transcript" - - local payload - payload=$(jq -nc --arg t "$transcript" \ - '{session_id:"s1", hook_event_name:"Stop", transcript_path:$t}') - record_hook_input "stop_hook" "$payload" - - assert_file_exists "$BRAINTRUST_RECORD_DIR/transcripts/main.jsonl" \ - "Stop should copy the main transcript" -} - -t_record_copies_agent_transcript_on_subagent_stop() { - # SubagentStop must snapshot the sub-agent's own transcript (which holds - # its model calls) before Claude Code can clean it up. - export BRAINTRUST_RECORD_DIR="$(_rec_dir)" - local agent_t="$HOME/agent-abc123.jsonl" - echo '{"type":"assistant"}' > "$agent_t" - - local payload - payload=$(jq -nc --arg t "$agent_t" \ - '{session_id:"s1", hook_event_name:"SubagentStop", agent_id:"abc123", agent_transcript_path:$t}') - record_hook_input "ignored" "$payload" - - assert_file_exists "$BRAINTRUST_RECORD_DIR/transcripts/agent-abc123.jsonl" \ - "SubagentStop should copy the agent transcript" -} - -t_record_off_is_noop() { - # With no BRAINTRUST_RECORD_DIR, recording must write nothing. - unset BRAINTRUST_RECORD_DIR - record_hook_input "Stop" '{"session_id":"s1","hook_event_name":"Stop"}' - # Nothing to assert beyond "no crash"; the absence of a recording dir - # means there is no file to inspect. Exit status should be success. - assert_success "$?" "record_hook_input is a no-op when recording is off" -} - -it "labels recorded events by hook_event_name" t_record_labels_by_event_name -it "falls back to the caller-supplied name" t_record_falls_back_to_arg_name -it "copies the main transcript on Stop" t_record_copies_main_transcript_on_stop -it "copies the agent transcript on SubagentStop" t_record_copies_agent_transcript_on_subagent_stop -it "is a no-op when recording is disabled" t_record_off_is_noop - -# --------------------------------------------------------------------------- -describe "emit_llm_spans_from_transcript: per-request LLM spans" -# --------------------------------------------------------------------------- - -# Write a tiny NDJSON transcript with two API requests. Request A spans two -# content-block lines sharing a requestId where output_tokens STREAMS (5 then -# 30) while input/cache stay constant - this exercises both the dedupe and the -# "take max output per request" logic. -_write_agent_transcript() { - local path="$1" - { - # Request A, line 1: partial output (5). - echo '{"type":"assistant","requestId":"reqA","timestamp":"2026-06-11T03:00:00.000Z","message":{"model":"claude-haiku-4-5","content":[{"type":"text","text":"hello"}],"usage":{"input_tokens":5,"output_tokens":5,"cache_creation_input_tokens":100,"cache_read_input_tokens":2000}}}' - # Request A, line 2: final cumulative output (30); input/cache identical. - echo '{"type":"assistant","requestId":"reqA","timestamp":"2026-06-11T03:00:00.000Z","message":{"model":"claude-haiku-4-5","content":[{"type":"tool_use","id":"t1","name":"Bash","input":{}}],"usage":{"input_tokens":5,"output_tokens":30,"cache_creation_input_tokens":100,"cache_read_input_tokens":2000}}}' - # A user/tool_result line in between (must be ignored). - echo '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t1","content":"ok"}]}}' - # Request B: single line. - echo '{"type":"assistant","requestId":"reqB","timestamp":"2026-06-11T03:00:05.000Z","message":{"model":"claude-haiku-4-5","content":[{"type":"text","text":"done"}],"usage":{"input_tokens":2,"output_tokens":20,"cache_creation_input_tokens":3,"cache_read_input_tokens":2500}}}' - } > "$path" -} - -t_emit_dedupes_by_request_id() { - local transcript="$HOME/agent.jsonl" - _write_agent_transcript "$transcript" - - # Capture emitted spans instead of enqueuing them. Compact each span to - # one line so the file is valid NDJSON regardless of jq pretty-printing. - local out="$HOME/emitted.ndjson" - : > "$out" - enqueue_span() { echo "$3" | jq -c '.' >> "$out"; return 0; } - - local n - n=$(emit_llm_spans_from_transcript "$transcript" "sess" "proj" "ROOT" "PARENT") - - # Return value counts LLM spans only: 2 unique requestIds -> 2 LLM spans. - assert_eq "$n" "2" "return value counts LLM spans" - - # Structure: 2 LLM spans + 1 tool span (request A's Bash tool_use, which - # has a matching tool_result). The third raw line is a tool_result, not a - # separate LLM call. - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm")]|length' "$out")" "2" "two LLM spans" - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="tool")]|length' "$out")" "1" "one tool span" - - # Output is the MAX per request: A=30 (not 5), B=20 -> 50. - # Braintrust prompt_tokens are inclusive for Anthropic: - # input 7 + cache_read 4500 + cache_creation 103 = 4610. - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm")|.metrics.completion_tokens]|add' "$out")" "50" "completion uses max per request" - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm")|.metrics.prompt_tokens]|add' "$out")" "4610" "prompt includes input and cache tokens" - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm")|.metrics.tokens]|add' "$out")" "4660" "total tokens includes inclusive prompt and completion" - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm")|.metrics.prompt_cached_tokens]|add' "$out")" "4500" "cache_read deduped" - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm")|.metrics.prompt_cache_creation_tokens]|add' "$out")" "103" "cache_creation deduped" - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm" and (.metrics | (has("cache_read_input_tokens") or has("cache_creation_input_tokens"))))]|length' "$out")" "0" "raw Anthropic cache metrics are not emitted" - - # All spans parented under PARENT. - assert_eq "$(jq -s 'all(.[]; .span_parents[0] == "PARENT")' "$out")" "true" "all parented under PARENT" - # LLM spans tagged with the model; tool span named after the tool. - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm")]|all(.span_attributes.name == "claude-haiku-4-5")' "$out")" "true" "model name on LLM spans" - assert_eq "$(jq -rs '[.[]|select(.span_attributes.type=="tool")][0].span_attributes.name' "$out")" "Terminal: command" "tool span named after the tool" - - # The second LLM span's input includes the prior assistant + tool history. - local hist_roles - hist_roles=$(jq -s '[.[]|select(.span_attributes.type=="llm")][1].input|map(.role)|join(",")' "$out") - assert_eq "$hist_roles" '"assistant,tool"' "second LLM span input carries conversation history" -} - -t_emit_missing_file_is_zero() { - enqueue_span() { return 0; } - local n - n=$(emit_llm_spans_from_transcript "$HOME/does-not-exist.jsonl" "s" "p" "R" "P") - assert_eq "$n" "0" "missing transcript emits nothing" -} - -# Write a transcript whose chronological order is the REVERSE of the -# requestId sort order: the first request ("reqZ") happens at t=00, the -# second ("reqA") at t=05. group_by sorts by id, so without an explicit -# re-sort the directives come out reqA-then-reqZ, scrambling the threaded -# conversation history. -_write_out_of_id_order_transcript() { - local path="$1" - { - # Chronologically FIRST (t=00) but sorts LAST by id. - echo '{"type":"assistant","requestId":"reqZ","timestamp":"2026-06-11T03:00:00.000Z","message":{"model":"claude-haiku-4-5","content":[{"type":"text","text":"first turn"}],"usage":{"input_tokens":5,"output_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' - # Chronologically SECOND (t=05) but sorts FIRST by id. - echo '{"type":"assistant","requestId":"reqA","timestamp":"2026-06-11T03:00:05.000Z","message":{"model":"claude-haiku-4-5","content":[{"type":"text","text":"second turn"}],"usage":{"input_tokens":8,"output_tokens":20,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' - } > "$path" -} - -t_emit_preserves_chronological_order() { - local transcript="$HOME/agent_order.jsonl" - _write_out_of_id_order_transcript "$transcript" - - local out="$HOME/emitted_order.ndjson" - : > "$out" - enqueue_span() { echo "$3" | jq -c '.' >> "$out"; return 0; } - - emit_llm_spans_from_transcript "$transcript" "sess" "proj" "ROOT" "PARENT" >/dev/null - - # Spans must be emitted in chronological order: "first turn" then - # "second turn", regardless of requestId sort order. - local first_text second_text - first_text=$(jq -rs '[.[]|select(.span_attributes.type=="llm")][0].output.content' "$out") - second_text=$(jq -rs '[.[]|select(.span_attributes.type=="llm")][1].output.content' "$out") - assert_eq "$first_text" "first turn" "first emitted LLM span is the chronologically-first turn" - assert_eq "$second_text" "second turn" "second emitted LLM span is the chronologically-second turn" - - # History threading must follow chronological order: the first turn has - # empty input history; the second turn carries the first turn's assistant - # message as input. - assert_eq "$(jq -s '[.[]|select(.span_attributes.type=="llm")][0].input|length' "$out")" "0" "first turn has empty input history" - local second_input_text - second_input_text=$(jq -rs '[.[]|select(.span_attributes.type=="llm")][1].input[0].content' "$out") - assert_eq "$second_input_text" "first turn" "second turn input carries the first turn as history" -} - -it "emits one LLM span per requestId (dedupes repeated usage)" t_emit_dedupes_by_request_id -it "emits nothing when the transcript file is missing" t_emit_missing_file_is_zero -it "preserves chronological order when requestIds don't sort chronologically" t_emit_preserves_chronological_order diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_fixture_replay.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_fixture_replay.sh deleted file mode 100755 index 746ef5c..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_fixture_replay.sh +++ /dev/null @@ -1,361 +0,0 @@ -#!/bin/bash -### -# Regression tests against the `test-fixture` session - a real Claude Code -# session captured via BRAINTRUST_RECORD_DIR and verified to look correct -# in the Braintrust UI. -# -# The fixture contains: -# - 1 session_start -# - 4 user_prompt_submit (4 turns) -# - 13 post_tool_use (tool calls across the 4 turns) -# - 4 stop_hook -# - 1 session_end -# - 1 transcript with 17 assistant messages (claude-opus-4-7) -# -# Tool breakdown (from the recorded payloads): -# Agent: 4, Bash: 5, TaskCreate: 1, ToolSearch: 1, WebFetch: 1, WebSearch: 1 -# -# Re-record with: -# ./record_session.sh test-fixture -# -# Each `it` test below replays the full fixture once. To keep wall time -# reasonable (~3s per replay), related assertions are bundled into a -# single test rather than split across many `it` blocks. -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -FIXTURE_DIR="$SCRIPT_DIR/fixtures/sessions/test-fixture" -SESSION_ID="4381b0d7-d67e-4187-bb2d-86a101d3b955" - -_setup_default_stubs() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_fixture"}' - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' -} - -# Count tool spans whose metadata.tool_name matches a given value. -_count_tool_spans_by_name() { - local name="$1" - all_spans | jq --arg n "$name" ' - [ .[] - | select(.span_attributes.type == "tool") - | select(.metadata.tool_name == $n) - ] | length - ' -} - -# --------------------------------------------------------------------------- -describe "test-fixture: replay end-to-end" -# --------------------------------------------------------------------------- - -t_replay_all_events() { - _setup_default_stubs - - local n - n=$(replay_session "$FIXTURE_DIR") - assert_success "$?" "every hook in the fixture should exit cleanly" - assert_eq "$n" "23" "expected 23 events replayed" -} - -it "replays all 23 hook events without errors" t_replay_all_events - -# --------------------------------------------------------------------------- -describe "test-fixture: top-level span counts" -# --------------------------------------------------------------------------- - -t_top_level_counts() { - _setup_default_stubs - replay_session "$FIXTURE_DIR" >/dev/null - - # Exactly one root session span - assert_eq "$(spans_named '^Claude Code: ' | jq 'length')" "1" \ - "expected exactly one root session span" - - # 4 user_prompt_submit events -> 4 Turn spans - assert_eq "$(spans_named '^Turn ' | jq 'length')" "4" \ - "expected 4 Turn spans (one per user_prompt_submit)" - - # The critical regression assertion: 13 post_tool_use -> 13 tool spans - # (no drops from async/race issues). - assert_eq "$(span_count_by_type tool)" "13" \ - "every post_tool_use must produce a tool span" - - # The transcript has 17 assistant messages; the stop_hook collapses - # them into one LLM span per LLM call. Should be at least one per - # turn (4) and at most 17. - local llms - llms=$(span_count_by_type llm) - if [ "$llms" -lt 4 ] || [ "$llms" -gt 17 ]; then - fail "expected 4-17 LLM spans; got $llms" - fi - - # The stop_hook writes a merge update for each Turn at end-of-turn, - # carrying the final output text and metrics. 4 turns -> 4 merges. - local merges - merges=$(all_spans | jq '[.[] | select(._is_merge == true)] | length') - assert_eq "$merges" "4" "expected 4 merge updates (one per Turn end)" -} - -it "session/turn/tool/llm span counts match the fixture" t_top_level_counts - -# --------------------------------------------------------------------------- -describe "test-fixture: tool span breakdown" -# --------------------------------------------------------------------------- - -t_tool_breakdown() { - _setup_default_stubs - replay_session "$FIXTURE_DIR" >/dev/null - - assert_eq "$(_count_tool_spans_by_name 'Agent')" "4" "Agent count" - assert_eq "$(_count_tool_spans_by_name 'Bash')" "5" "Bash count" - assert_eq "$(_count_tool_spans_by_name 'TaskCreate')" "1" "TaskCreate count" - assert_eq "$(_count_tool_spans_by_name 'ToolSearch')" "1" "ToolSearch count" - assert_eq "$(_count_tool_spans_by_name 'WebFetch')" "1" "WebFetch count" - assert_eq "$(_count_tool_spans_by_name 'WebSearch')" "1" "WebSearch count" -} - -it "tool spans match: 4 Agent / 5 Bash / 1 TaskCreate / 1 ToolSearch / 1 WebFetch / 1 WebSearch" \ - t_tool_breakdown - -# --------------------------------------------------------------------------- -describe "test-fixture: tree structure" -# --------------------------------------------------------------------------- - -t_tree_structure() { - _setup_default_stubs - replay_session "$FIXTURE_DIR" >/dev/null - - # 1. The root span's span_id should equal the recorded session id - local session_span - session_span=$(span_by_id "$SESSION_ID") - assert_ne "$session_span" "null" "session span with id=$SESSION_ID should exist" - - # 2. Every non-merge span shares the session root_span_id. - # (Merge updates omit root_span_id since they target an existing span - # by id - they only carry the fields being updated, like final output - # and metrics.) - local off_root - off_root=$(all_spans | jq --arg r "$SESSION_ID" ' - [ .[] - | select(._is_merge != true) - | select(.root_span_id != $r) - ] | length - ') - assert_eq "$off_root" "0" "all non-merge spans should share the session root_span_id" - - # 3. Every Turn span's first parent is the session id - local off_turn - off_turn=$(spans_named "^Turn " | jq --arg s "$SESSION_ID" ' - [ .[] | select((.span_parents // [])[0] != $s) ] | length - ') - assert_eq "$off_turn" "0" "all Turn spans should be parented to the session" - - # 4. Collect all turn ids; tools and llms must be children of some turn - local turn_ids - turn_ids=$(spans_named "^Turn " | jq -c '[.[].span_id]') - - local orphan_tools - orphan_tools=$(all_spans | jq --argjson ids "$turn_ids" ' - [ .[] - | select(.span_attributes.type == "tool") - | select(((.span_parents // [])[0]) as $p | ($ids | index($p)) == null) - ] | length - ') - assert_eq "$orphan_tools" "0" "every tool span should be a child of some Turn" - - local orphan_llms - orphan_llms=$(all_spans | jq --argjson ids "$turn_ids" ' - [ .[] - | select(.span_attributes.type == "llm") - | select(((.span_parents // [])[0]) as $p | ($ids | index($p)) == null) - ] | length - ') - assert_eq "$orphan_llms" "0" "every LLM span should be a child of some Turn" -} - -it "session > turn > tool/llm hierarchy is correct" t_tree_structure - -# --------------------------------------------------------------------------- -describe "test-fixture: LLM span content" -# --------------------------------------------------------------------------- - -t_llm_content() { - _setup_default_stubs - replay_session "$FIXTURE_DIR" >/dev/null - - # All LLM spans use the model the recorded transcript shows - local off_model - off_model=$(all_spans | jq ' - [ .[] - | select(.span_attributes.type == "llm") - | select(.metadata.model != "claude-opus-4-7") - ] | length - ') - assert_eq "$off_model" "0" "all LLM spans should be tagged with model=claude-opus-4-7" - - # All LLM spans have non-negative token counts - local bad_metrics - bad_metrics=$(all_spans | jq ' - [ .[] - | select(.span_attributes.type == "llm") - | select((.metrics.prompt_tokens // 0) < 0 - or (.metrics.completion_tokens // 0) < 0) - ] | length - ') - assert_eq "$bad_metrics" "0" "LLM spans should have non-negative token metrics" - - # At least one LLM span should report > 0 completion tokens - local with_output - with_output=$(all_spans | jq ' - [ .[] - | select(.span_attributes.type == "llm") - | select((.metrics.completion_tokens // 0) > 0) - ] | length - ') - if [ "$with_output" -lt 1 ]; then - fail "expected at least one LLM span with completion_tokens > 0; got $with_output" - fi -} - -it "LLM spans use claude-opus-4-7 and have sensible token metrics" t_llm_content - -# --------------------------------------------------------------------------- -describe "test-fixture: token totals dedupe by requestId" -# --------------------------------------------------------------------------- - -# Regression test for the double-counting bug: Claude Code writes one -# transcript line per content block (thinking, text, each tool_use) and -# every line for the same API response repeats the identical `usage` block, -# tagged with the same `requestId`. The stop_hook must count each response's -# usage exactly once, not once per content-block line. -# -# The fixture transcript has 30 assistant lines but only 7 unique -# requestIds. We assert the dedup by summing metrics across the LLM spans -# (token metrics live only on LLM spans now - the Turn merge carries no -# token totals; Braintrust aggregates them for display). Summing all LLM -# spans gives the whole-session totals, which must match the deduped -# expectation. Before the fix, output alone would be 425*4 + ... + 745*8 + -# ... = thousands too high. -t_token_dedupe_totals() { - _setup_default_stubs - replay_session "$FIXTURE_DIR" >/dev/null - - # Sum metrics across all LLM spans (this fixture has no sub-agents, so - # all LLM spans are the main-conversation calls). - local llms - llms=$(all_spans | jq '[.[] | select(.span_attributes.type == "llm")]') - - local total_prompt total_completion total_cache_creation total_cache_read total_tokens raw_metric_count - total_prompt=$(echo "$llms" | jq '[.[].metrics.prompt_tokens // 0] | add') - total_completion=$(echo "$llms" | jq '[.[].metrics.completion_tokens // 0] | add') - total_cache_creation=$(echo "$llms" | jq '[.[].metrics.prompt_cache_creation_tokens // 0, .[].metrics.prompt_cache_creation_5m_tokens // 0, .[].metrics.prompt_cache_creation_1h_tokens // 0] | add') - total_cache_read=$(echo "$llms" | jq '[.[].metrics.prompt_cached_tokens // 0] | add') - total_tokens=$(echo "$llms" | jq '[.[].metrics.tokens // 0] | add') - raw_metric_count=$(echo "$llms" | jq '[.[] | select(.metrics | (has("cache_creation_input_tokens") or has("cache_read_input_tokens")))] | length') - - # Expected = sum of usage over the 7 unique requestIds in the transcript. - assert_eq "$total_prompt" "187216" "prompt_tokens should include input and cache tokens deduped by requestId" - assert_eq "$total_completion" "1867" "completion_tokens should dedupe by requestId" - assert_eq "$total_cache_creation" "21064" "prompt cache creation tokens should dedupe by requestId" - assert_eq "$total_cache_read" "165784" "prompt_cached_tokens should dedupe by requestId" - assert_eq "$total_tokens" "189083" "tokens should include inclusive prompt and completion" - assert_eq "$raw_metric_count" "0" "raw Anthropic cache metrics should not be emitted" - - # Turn merge spans must NOT carry token metrics anymore. - local merge_tokens - merge_tokens=$(all_spans | jq '[.[] | select(._is_merge == true) | .metrics.prompt_tokens // empty] | length') - assert_eq "$merge_tokens" "0" "Turn merges carry no token metrics" -} - -it "LLM span token totals count each requestId once (no per-content-block double-count)" \ - t_token_dedupe_totals - -# --------------------------------------------------------------------------- -describe "subagent-compact: sub-agent LLM spans" -# --------------------------------------------------------------------------- - -# Real recorded session that launched Explore sub-agents. Each sub-agent made -# its own model calls and wrote its own transcript, which the recorder -# snapshotted into transcripts/agent-.jsonl. On replay, post_tool_use.sh -# (for the Agent tool) must parse those transcripts and emit one LLM span per -# sub-agent API request, nested under the corresponding Agent tool span. -# -# Rather than hard-coding token goldens (which would break every time the -# fixture is re-recorded), we DERIVE the expected span count and token totals -# directly from the fixture's own sub-agent transcripts, applying the same -# rules the production code does: dedupe by requestId, count output_tokens as -# the MAX per request (it streams cumulatively), and count input/cache once. -# The test then asserts the replayed spans match that derived expectation. -SUBAGENT_FIXTURE="$SCRIPT_DIR/fixtures/sessions/subagent-compact" - -# Compute expected {spans,input,output,cache_read,cache_creation} from the fixture's -# agent-*.jsonl transcripts. Prints a compact JSON object. -_expected_subagent_totals() { - jq -s ' - [ .[] - | select(.type=="assistant") - | select(.message.usage != null) - | { rid:(.requestId // .message.id), - in:(.message.usage.input_tokens // 0), - out:(.message.usage.output_tokens // 0), - cr:(.message.usage.cache_read_input_tokens // 0), - cc:(.message.usage.cache_creation_input_tokens // 0) } - ] - | group_by(.rid) - | { spans: length, - input: (map(.[0].in) | add), - output: (map([.[].out]|max) | add), - cache_read: (map(.[0].cr) | add), - cache_creation: (map(.[0].cc) | add) } - ' "$SUBAGENT_FIXTURE"/transcripts/agent-*.jsonl -} - -t_subagent_llm_spans() { - # Skip cleanly if the fixture hasn't been (re-)recorded yet. - if ! ls "$SUBAGENT_FIXTURE"/transcripts/agent-*.jsonl >/dev/null 2>&1; then - skip "subagent-compact fixture not present; record one to enable this test" - return 0 - fi - - _setup_default_stubs - replay_session "$SUBAGENT_FIXTURE" >/dev/null - - local expected - expected=$(_expected_subagent_totals) - local exp_spans exp_in exp_out exp_cr exp_cc exp_prompt exp_tokens - exp_spans=$(echo "$expected" | jq '.spans') - exp_in=$(echo "$expected" | jq '.input') - exp_out=$(echo "$expected" | jq '.output') - exp_cr=$(echo "$expected" | jq '.cache_read') - exp_cc=$(echo "$expected" | jq '.cache_creation') - exp_prompt=$((exp_in + exp_cr + exp_cc)) - exp_tokens=$((exp_prompt + exp_out)) - - # Collect the sub-agent LLM spans: llm spans whose parent is an Agent - # tool span (model-agnostic, since the sub-agent model may differ between - # recordings). - local agent_ids subagent_spans - agent_ids=$(all_spans | jq -c '[.[] | select(.span_attributes.type=="tool" and .span_attributes.name=="Agent") | .span_id]') - subagent_spans=$(all_spans | jq --argjson a "$agent_ids" \ - '[.[] | select(.span_attributes.type=="llm") | select(.span_parents[0] as $p | ($a | index($p)) != null)]') - - # Span count and deduped token totals match what the transcripts imply. - assert_eq "$(echo "$subagent_spans" | jq 'length')" "$exp_spans" "sub-agent span count matches transcripts" - assert_eq "$(echo "$subagent_spans" | jq '[.[].metrics.completion_tokens]|add')" "$exp_out" "sub-agent completion (max per request)" - assert_eq "$(echo "$subagent_spans" | jq '[.[].metrics.prompt_tokens]|add')" "$exp_prompt" "sub-agent prompt includes input and cache tokens" - assert_eq "$(echo "$subagent_spans" | jq '[.[].metrics.tokens]|add')" "$exp_tokens" "sub-agent total tokens" - assert_eq "$(echo "$subagent_spans" | jq '[.[].metrics.prompt_cached_tokens]|add')" "$exp_cr" "sub-agent cache_read total" - assert_eq "$(echo "$subagent_spans" | jq '[.[].metrics.prompt_cache_creation_tokens // 0, .[].metrics.prompt_cache_creation_5m_tokens // 0, .[].metrics.prompt_cache_creation_1h_tokens // 0] | add')" "$exp_cc" "sub-agent cache_creation total" - assert_eq "$(echo "$subagent_spans" | jq '[.[] | select(.metrics | (has("cache_creation_input_tokens") or has("cache_read_input_tokens")))] | length')" "0" "sub-agent raw Anthropic cache metrics absent" - - # Sanity: at least one sub-agent span was actually produced. - if [ "$exp_spans" -gt 0 ]; then - assert_eq "$(echo "$subagent_spans" | jq 'length > 0')" "true" "expected some sub-agent spans" - fi -} - -it "emits sub-agent spans under Agent tool spans matching the transcripts" t_subagent_llm_spans diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_full_pipeline.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_full_pipeline.sh deleted file mode 100755 index c1c62c8..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_full_pipeline.sh +++ /dev/null @@ -1,174 +0,0 @@ -#!/bin/bash -### -# Full pipeline end-to-end tests. -# -# These tests chain together SessionStart → UserPromptSubmit → PostToolUse... -# to assert that the resulting span tree has the expected structure. They -# are the bash equivalent of opencode-plugin's `assertEventsProduceTree` -# integration tests. -# -# These are the tests most likely to catch regressions like the missing- -# spans bug (where async hooks dropped spans due to state-file races or -# killed curl processes). -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -_setup_default_stubs() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_test"}' - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' -} - -# --------------------------------------------------------------------------- -describe "full pipeline: simple session with one turn and two tools" -# --------------------------------------------------------------------------- - -t_e2e_simple_session() { - _setup_default_stubs - local sid="e2e-sess-1" - - run_hook session_start.sh "$(fixture_session_start "$sid" "/tmp/proj-x")" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "$sid" "List files")" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "$sid" "Bash" \ - "$(fixture_tool_input_bash 'ls')" \ - "$(fixture_tool_response_text 'a.txt')")" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "$sid" "Read" \ - "$(fixture_tool_input_read /tmp/a.txt)" \ - "$(fixture_tool_response_text 'hello')")" - - # Expected total: 1 session + 1 turn + 2 tools = 4 spans - local total - total=$(span_count) - assert_eq "$total" "4" - - # Count by type - local task_count tool_count - task_count=$(span_count_by_type "task") - tool_count=$(span_count_by_type "tool") - assert_eq "$task_count" "2" "expected 2 task spans (session + turn)" - assert_eq "$tool_count" "2" "expected 2 tool spans" -} - -t_e2e_span_hierarchy() { - _setup_default_stubs - local sid="e2e-hier-1" - - run_hook session_start.sh "$(fixture_session_start "$sid" "/tmp/proj")" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "$sid" "go")" - run_hook post_tool_use.sh "$(fixture_post_tool_use "$sid" "Bash" \ - "$(fixture_tool_input_bash 'echo hi')" \ - "$(fixture_tool_response_text 'hi')")" - - # Session span: span_id == session_id, no parents - local session_span - session_span=$(span_by_name "^Claude Code: ") - local session_id_value - session_id_value=$(echo "$session_span" | jq -r '.span_id') - assert_eq "$session_id_value" "$sid" - - # Turn span: parent is session - local turn_span turn_id turn_parent - turn_span=$(span_by_name "^Turn 1$") - turn_id=$(echo "$turn_span" | jq -r '.span_id') - turn_parent=$(echo "$turn_span" | jq -r '.span_parents[0]') - assert_eq "$turn_parent" "$sid" - - # Tool span: parent is turn - local tool_span tool_parent - tool_span=$(span_by_type "tool") - tool_parent=$(echo "$tool_span" | jq -r '.span_parents[0]') - assert_eq "$tool_parent" "$turn_id" - - # All three share the same root_span_id (the session id) - local turn_root tool_root - turn_root=$(echo "$turn_span" | jq -r '.root_span_id') - tool_root=$(echo "$tool_span" | jq -r '.root_span_id') - assert_eq "$turn_root" "$sid" - assert_eq "$tool_root" "$sid" -} - -t_e2e_multi_turn() { - _setup_default_stubs - local sid="e2e-multi-1" - - run_hook session_start.sh "$(fixture_session_start "$sid" "/tmp/proj")" - - # Turn 1: one tool - run_hook user_prompt_submit.sh "$(fixture_user_prompt "$sid" "turn 1")" - run_hook post_tool_use.sh "$(fixture_post_tool_use "$sid" "Bash" \ - "$(fixture_tool_input_bash 'echo 1')" \ - "$(fixture_tool_response_text '1')")" - - # Turn 2: two tools - run_hook user_prompt_submit.sh "$(fixture_user_prompt "$sid" "turn 2")" - run_hook post_tool_use.sh "$(fixture_post_tool_use "$sid" "Read" \ - "$(fixture_tool_input_read /tmp/a)" \ - "$(fixture_tool_response_text 'a')")" - run_hook post_tool_use.sh "$(fixture_post_tool_use "$sid" "Read" \ - "$(fixture_tool_input_read /tmp/b)" \ - "$(fixture_tool_response_text 'b')")" - - # Total: 1 session + 2 turns + 3 tools = 6 spans - local total - total=$(span_count) - assert_eq "$total" "6" - - # Two turn spans - local turns - turns=$(spans_named "^Turn ") - local turn_count - turn_count=$(echo "$turns" | jq 'length') - assert_eq "$turn_count" "2" - - # Tool spans are split across turns - local turn1_id turn2_id - turn1_id=$(echo "$turns" | jq -r '.[0].span_id') - turn2_id=$(echo "$turns" | jq -r '.[1].span_id') - - local children_t1 children_t2 - children_t1=$(children_of "$turn1_id" | jq 'length') - children_t2=$(children_of "$turn2_id" | jq 'length') - - assert_eq "$children_t1" "1" "turn 1 should have 1 tool child" - assert_eq "$children_t2" "2" "turn 2 should have 2 tool children" -} - -it "produces session + turn + 2 tool spans (4 total)" t_e2e_simple_session -it "spans form correct session > turn > tool hierarchy" t_e2e_span_hierarchy -it "multiple turns produce correctly-parented spans" t_e2e_multi_turn - -# --------------------------------------------------------------------------- -describe "full pipeline: no tool spans dropped under sequential PostToolUse" -# --------------------------------------------------------------------------- - -# This is the regression test for the missing-spans class of bugs. We fire -# many PostToolUse hooks in rapid succession and assert that every one -# produced a tool span. With the previous async config, some of these -# would be silently dropped; with sync hooks they should all land. -t_e2e_no_drops_sequential() { - _setup_default_stubs - local sid="e2e-no-drops" - local N=20 - - run_hook session_start.sh "$(fixture_session_start "$sid" "/tmp/proj")" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "$sid" "many tools")" - - local i - for i in $(seq 1 $N); do - run_hook post_tool_use.sh "$(fixture_post_tool_use "$sid" "Bash" \ - "$(fixture_tool_input_bash "echo $i")" \ - "$(fixture_tool_response_text "$i")")" - done - - local tool_count - tool_count=$(span_count_by_type "tool") - assert_eq "$tool_count" "$N" "expected $N tool spans, none dropped" -} - -it "20 sequential PostToolUse hooks produce 20 tool spans" t_e2e_no_drops_sequential diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_insert_span.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_insert_span.sh deleted file mode 100755 index 1f615fe..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_insert_span.sh +++ /dev/null @@ -1,262 +0,0 @@ -#!/bin/bash -### -# Tests for _http_insert_span() and get_project_id() HTTP status handling. -# -# Exercises the curl_stub by configuring canned responses and asserting on -# the captured requests and the function's stdout/exit codes. -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -# A minimal valid span event JSON (matching the shape common.sh builds). -_test_event() { - jq -nc \ - --arg id "span-test-1" \ - --arg root "root-1" \ - '{ - id: $id, - span_id: $id, - root_span_id: $root, - input: "test input", - span_attributes: { name: "test span", type: "task" } - }' -} - -# --------------------------------------------------------------------------- -describe "_http_insert_span: success" -# --------------------------------------------------------------------------- - -t_insert_success_returns_row_id() { - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_abc"]}' - - local event row_id rc - event=$(_test_event) - row_id=$(_http_insert_span "proj_123" "$event") - rc=$? - - assert_success "$rc" - assert_eq "$row_id" "row_abc" - - local count - count=$(captured_request_count '/insert$') - assert_eq "$count" "1" -} - -t_insert_request_body_shape() { - stub_response_for "*/insert" 200 '{"row_ids":["row_xyz"]}' - - local event - event=$(_test_event) - _http_insert_span "proj_456" "$event" >/dev/null - - local body_events_len - body_events_len=$(jq -s '.[0].body.events | length' "$CAPTURED_REQUESTS") - assert_eq "$body_events_len" "1" - - local body_span_id - body_span_id=$(jq -s -r '.[0].body.events[0].span_id' "$CAPTURED_REQUESTS") - assert_eq "$body_span_id" "span-test-1" -} - -it "POSTs to the project_logs insert endpoint and returns the row id" t_insert_success_returns_row_id -it "includes the event in the request body wrapped under .events" t_insert_request_body_shape - -# --------------------------------------------------------------------------- -describe "_http_insert_span: failure modes" -# --------------------------------------------------------------------------- - -t_insert_401() { - stub_response_for "*/insert" 401 "Invalid API Key" - - local event rc - event=$(_test_event) - _http_insert_span "proj_123" "$event" >/dev/null - rc=$? - - assert_failure "$rc" - local log - log=$(hook_log) - assert_contains "$log" "Insert failed (HTTP 401)" -} - -t_insert_500() { - stub_response_for "*/insert" 500 "Internal Server Error" - - local event rc - event=$(_test_event) - _http_insert_span "proj_123" "$event" >/dev/null - rc=$? - - assert_failure "$rc" - local log - log=$(hook_log) - assert_contains "$log" "Insert failed (HTTP 500)" -} - -t_insert_no_api_key() { - API_KEY="" - - local event rc - event=$(_test_event) - _http_insert_span "proj_123" "$event" >/dev/null - rc=$? - - assert_failure "$rc" - local log - log=$(hook_log) - assert_contains "$log" "API_KEY is empty" -} - -t_insert_empty_row_ids() { - stub_response_for "*/insert" 200 '{"row_ids":[]}' - - local event rc - event=$(_test_event) - _http_insert_span "proj_123" "$event" >/dev/null - rc=$? - - assert_failure "$rc" -} - -it "returns non-zero on HTTP 401 and logs an error" t_insert_401 -it "returns non-zero on HTTP 500" t_insert_500 -it "returns non-zero when API_KEY is empty" t_insert_no_api_key -it "returns non-zero when the response has no row_ids" t_insert_empty_row_ids - -# --------------------------------------------------------------------------- -describe "_http_insert_span: experiment mode" -# --------------------------------------------------------------------------- - -t_insert_experiment_mode() { - stub_response_for "*/v1/experiment/exp_42/insert" 200 '{"row_ids":["row_e1"]}' - - CC_EXPERIMENT_ID="exp_42" - - local event row_id rc - event=$(_test_event) - row_id=$(_http_insert_span "proj_irrelevant" "$event") - rc=$? - - assert_success "$rc" - assert_eq "$row_id" "row_e1" - - local pl_count - pl_count=$(captured_request_count '/project_logs/') - assert_eq "$pl_count" "0" - - local exp_count - exp_count=$(captured_request_count '/v1/experiment/exp_42/insert') - assert_eq "$exp_count" "1" -} - -it "POSTs to /v1/experiment//insert when CC_EXPERIMENT_ID is set" t_insert_experiment_mode - -# --------------------------------------------------------------------------- -describe "get_project_id" -# --------------------------------------------------------------------------- - -t_get_project_existing() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_existing"}' - - local pid rc - pid=$(get_project_id "my-project") - rc=$? - - assert_success "$rc" - assert_eq "$pid" "proj_existing" -} - -t_get_project_create() { - stub_response_for "*/v1/project?project_name=*" 200 '{}' - stub_response_for "*/v1/project" 200 '{"id":"proj_created"}' - - local pid rc - pid=$(get_project_id "brand-new-project") - rc=$? - - assert_success "$rc" - assert_eq "$pid" "proj_created" -} - -t_get_project_cached() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_cached"}' - - get_project_id "cached-project" >/dev/null - local before - before=$(captured_request_count '.') - - local pid - pid=$(get_project_id "cached-project") - local after - after=$(captured_request_count '.') - - assert_eq "$pid" "proj_cached" - assert_eq "$before" "$after" -} - -t_get_project_401() { - stub_response_for "*/v1/project*" 401 "Invalid API Key" - - local pid rc - pid=$(get_project_id "doomed-project") - rc=$? - - assert_failure "$rc" - assert_eq "$pid" "" - - local log - log=$(hook_log) - assert_contains "$log" "authentication failed" - assert_contains "$log" "BRAINTRUST_API_KEY" -} - -t_get_project_403() { - stub_response_for "*/v1/project*" 403 "Forbidden" - - local pid rc - pid=$(get_project_id "forbidden-project") - rc=$? - - assert_failure "$rc" - assert_eq "$pid" "" - - local log - log=$(hook_log) - assert_contains "$log" "authentication failed" -} - -t_get_project_create_escapes_special_chars() { - # Project names can contain characters that would break a hand-rolled - # JSON literal: double quotes, backslashes, newlines. The create body - # must escape these correctly via jq. - stub_response_for "*/v1/project?project_name=*" 200 '{}' - stub_response_for "*/v1/project" 200 '{"id":"proj_created"}' - - local weird_name='my "quoted" \backslash project' - local pid rc - pid=$(get_project_id "$weird_name") - rc=$? - - assert_success "$rc" - assert_eq "$pid" "proj_created" - - # The POST body must be parseable JSON and the name field must round - # trip cleanly (including the embedded quotes and backslash). - local sent_name - sent_name=$(jq -s --arg url "/v1/project" -r ' - [.[] | select(.method == "POST") | select(.url | endswith($url))] - | .[-1].body.name - ' "$CAPTURED_REQUESTS") - assert_eq "$sent_name" "$weird_name" -} - -it "returns the existing project id on successful lookup" t_get_project_existing -it "creates a new project when lookup returns no id" t_get_project_create -it "escapes special characters in the project create body" t_get_project_create_escapes_special_chars -it "caches the project id so the second call makes no HTTP request" t_get_project_cached -it "returns non-zero and logs an auth error on HTTP 401" t_get_project_401 -it "returns non-zero and logs an auth error on HTTP 403" t_get_project_403 diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_post_tool_use.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_post_tool_use.sh deleted file mode 100755 index 50ac952..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_post_tool_use.sh +++ /dev/null @@ -1,322 +0,0 @@ -#!/bin/bash -### -# End-to-end tests for the PostToolUse hook. -# -# PostToolUse fires after each tool invocation by the assistant. It creates -# a "tool" span as a child of the current Turn span. If no current Turn is -# active (no UserPromptSubmit since last Stop), the tool span is skipped. -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -_setup_default_stubs() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_test"}' - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' -} - -# Setup helper: run session_start + user_prompt_submit, then clear capture. -_with_turn_started() { - local session_id="$1" - _setup_default_stubs - run_hook session_start.sh "$(fixture_session_start "$session_id" "/tmp/x")" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "$session_id" "do something")" - : > "$CAPTURED_REQUESTS" -} - -# --------------------------------------------------------------------------- -describe "post_tool_use.sh: with active turn" -# --------------------------------------------------------------------------- - -t_post_tool_creates_tool_span() { - _with_turn_started "sess-pt-1" - - local payload - payload=$(fixture_post_tool_use "sess-pt-1" "Bash" \ - "$(fixture_tool_input_bash 'ls -la')" \ - "$(fixture_tool_response_text 'a.txt\nb.txt')") - payload=$(echo "$payload" | jq -c '. + {tool_use_id: "toolu_success"}') - - run_hook post_tool_use.sh "$payload" - assert_success "$HOOK_STATUS" - - local count - count=$(span_count) - assert_eq "$count" "1" - - local tool_span - tool_span=$(span_by_type "tool") - assert_ne "$tool_span" "null" - - local name - name=$(echo "$tool_span" | jq -r '.span_attributes.name') - # Bash spans become "Terminal: " - assert_contains "$name" "Terminal" - assert_contains "$name" "ls -la" - assert_eq "$(echo "$tool_span" | jq -r '.metadata.tool_approval')" "approved" - assert_eq "$(echo "$tool_span" | jq -r '.metadata.tool_call_id')" "toolu_success" -} - -t_post_tool_failure_creates_error_span() { - _with_turn_started "sess-pt-fail" - - run_hook post_tool_use_failure.sh "$(fixture_post_tool_use_failure "sess-pt-fail" "Bash" \ - "$(fixture_tool_input_bash 'exit 1')" \ - "Exit code 1" \ - "$(fixture_tool_response_text 'failed')")" - assert_success "$HOOK_STATUS" - - local tool_span - tool_span=$(span_by_type "tool") - assert_eq "$(echo "$tool_span" | jq -r '.metadata.tool_approval')" "approved" - assert_eq "$(echo "$tool_span" | jq -r '.error')" "Exit code 1" -} - -t_permission_denied_creates_denied_span() { - _with_turn_started "sess-pt-denied" - - run_hook permission_denied.sh "$(fixture_permission_denied "sess-pt-denied" "Bash" \ - "$(fixture_tool_input_bash 'rm -rf /tmp/nope')" \ - "toolu_denied")" - assert_success "$HOOK_STATUS" - - local tool_span - tool_span=$(span_by_type "tool") - assert_eq "$(echo "$tool_span" | jq -r '.metadata.tool_approval')" "denied" - assert_eq "$(echo "$tool_span" | jq -r '.metadata.tool_call_id')" "toolu_denied" - assert_eq "$(echo "$tool_span" | jq 'has("output")')" "false" - assert_eq "$(echo "$tool_span" | jq 'has("error")')" "false" -} - -t_tool_span_is_child_of_turn() { - _with_turn_started "sess-pt-child" - - # Capture the current turn id from state - local turn_id - turn_id=$(get_session_state "sess-pt-child" "current_turn_span_id") - assert_ne "$turn_id" "" "expected a current turn span" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "sess-pt-child" "Read" \ - "$(fixture_tool_input_read /tmp/a.txt)" \ - "$(fixture_tool_response_text 'hello')")" - - local tool_span - tool_span=$(span_by_type "tool") - local parent - parent=$(echo "$tool_span" | jq -r '.span_parents[0]') - - assert_eq "$parent" "$turn_id" -} - -t_read_tool_span_name_includes_basename() { - _with_turn_started "sess-pt-read" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "sess-pt-read" "Read" \ - "$(fixture_tool_input_read /tmp/some/long/path/file.txt)" \ - "$(fixture_tool_response_text 'content')")" - - local tool_span name - tool_span=$(span_by_type "tool") - name=$(echo "$tool_span" | jq -r '.span_attributes.name') - assert_eq "$name" "Read: file.txt" -} - -t_skill_tool_span_marks_tool_kind() { - _with_turn_started "sess-pt-skill" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "sess-pt-skill" "Skill" \ - "$(jq -nc '{name: "review"}')" \ - "$(fixture_tool_response_text 'loaded')")" - - local tool_span name - tool_span=$(span_by_type "tool") - name=$(echo "$tool_span" | jq -r '.span_attributes.name') - assert_eq "$name" "skill: review" - assert_eq "$(echo "$tool_span" | jq -r '.metadata.tool_name')" "Skill" - assert_eq "$(echo "$tool_span" | jq -r '.metadata.tool_kind')" "skill" - assert_eq "$(echo "$tool_span" | jq -r '.metadata.skill_name')" "review" -} - -t_multiple_tools_in_turn() { - _with_turn_started "sess-pt-multi" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "sess-pt-multi" "Bash" \ - "$(fixture_tool_input_bash 'echo 1')" \ - "$(fixture_tool_response_text '1')")" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "sess-pt-multi" "Bash" \ - "$(fixture_tool_input_bash 'echo 2')" \ - "$(fixture_tool_response_text '2')")" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "sess-pt-multi" "Read" \ - "$(fixture_tool_input_read /tmp/x)" \ - "$(fixture_tool_response_text 'x')")" - - local tool_count - tool_count=$(span_count_by_type "tool") - assert_eq "$tool_count" "3" -} - -it "creates a tool span on PostToolUse" t_post_tool_creates_tool_span -it "creates an error tool span on PostToolUseFailure" t_post_tool_failure_creates_error_span -it "creates a denied tool span on PermissionDenied" t_permission_denied_creates_denied_span -it "tool span is a child of the current Turn span" t_tool_span_is_child_of_turn -it "Read tool span name includes file basename" t_read_tool_span_name_includes_basename -it "Skill tool span marks tool kind" t_skill_tool_span_marks_tool_kind -it "all tools in a turn produce distinct spans" t_multiple_tools_in_turn - -# --------------------------------------------------------------------------- -describe "post_tool_use.sh: without active turn" -# --------------------------------------------------------------------------- - -t_post_tool_skipped_without_turn() { - # No session_start or user_prompt_submit ran. The hook should bail out - # without creating any span (and without erroring). - _setup_default_stubs - - run_hook post_tool_use.sh "$(fixture_post_tool_use "sess-no-turn" "Bash" \ - "$(fixture_tool_input_bash 'ls')" \ - "$(fixture_tool_response_text 'output')")" - - assert_success "$HOOK_STATUS" - local count - count=$(span_count) - assert_eq "$count" "0" -} - -it "skips silently when no current turn is active" t_post_tool_skipped_without_turn - -# --------------------------------------------------------------------------- -describe "post_tool_use.sh: input validation" -# --------------------------------------------------------------------------- - -t_post_tool_no_tool_name() { - _with_turn_started "sess-no-name" - - # Payload without tool_name - local payload - payload=$(jq -nc --arg s "sess-no-name" '{session_id: $s}') - run_hook post_tool_use.sh "$payload" - - assert_success "$HOOK_STATUS" - local count - count=$(span_count) - assert_eq "$count" "0" -} - -t_post_tool_no_session_id() { - _setup_default_stubs - - local payload - payload=$(jq -nc --arg t "Bash" '{tool_name: $t, tool_input: {}, tool_response: {}}') - run_hook post_tool_use.sh "$payload" - - assert_success "$HOOK_STATUS" - local count - count=$(span_count) - assert_eq "$count" "0" -} - -it "skips silently when payload has no tool_name" t_post_tool_no_tool_name -it "skips silently when payload has no session_id" t_post_tool_no_session_id - -# --------------------------------------------------------------------------- -describe "post_tool_use.sh: Agent sub-agent LLM spans" -# --------------------------------------------------------------------------- - -# When the tool is an Agent (sub-agent), the hook should locate the -# sub-agent's transcript and emit its model calls as LLM spans nested under -# the Agent tool span. -t_agent_emits_subagent_llm_spans() { - _with_turn_started "sess-agent" - - # The Agent payload references the main transcript_path. The hook derives - # the sub-agent transcript from dirname(transcript_path) + agent-; - # we place a synthetic one in that flat fallback location. - local main_transcript="$TEST_TMP/88a535be.jsonl" - : > "$main_transcript" - local agent_id="aea5test" - local agent_transcript="$TEST_TMP/agent-${agent_id}.jsonl" - { - # r1: text + a tool_use (Bash) -> emits an LLM span and a tool span. - echo '{"type":"assistant","requestId":"r1","timestamp":"2026-06-11T03:00:00.000Z","message":{"model":"claude-haiku-4-5","content":[{"type":"text","text":"hi"},{"type":"tool_use","id":"tu1","name":"Bash","input":{"command":"ls -la"}}],"usage":{"input_tokens":5,"output_tokens":40,"cache_creation_input_tokens":10,"cache_read_input_tokens":1000}}}' - echo '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tu1","content":"Exit code 128","is_error":true}]}}' - # r2: final text answer -> emits a second LLM span (with history). - echo '{"type":"assistant","requestId":"r2","timestamp":"2026-06-11T03:00:02.000Z","message":{"model":"claude-haiku-4-5","content":[{"type":"text","text":"bye"}],"usage":{"input_tokens":3,"output_tokens":20,"cache_creation_input_tokens":5,"cache_read_input_tokens":1500}}}' - } > "$agent_transcript" - - # Build an Agent PostToolUse payload with transcript_path + tool_response.agentId. - local payload - payload=$(jq -nc \ - --arg s "sess-agent" \ - --arg tp "$main_transcript" \ - --arg aid "$agent_id" \ - '{ - session_id: $s, - transcript_path: $tp, - tool_name: "Agent", - tool_input: {description: "explore", subagent_type: "Explore"}, - tool_response: {agentId: $aid, content: "result"} - }') - - run_hook post_tool_use.sh "$payload" - assert_success "$HOOK_STATUS" - - # Tool spans: the Agent tool span itself + the sub-agent's Bash tool span. - assert_eq "$(span_count_by_type tool)" "2" "Agent span + sub-agent Bash tool span" - # Two sub-agent LLM spans (r1, r2). - assert_eq "$(span_count_by_type llm)" "2" "two sub-agent LLM spans" - - # The Agent tool span is the one named "Agent"; the sub-agent spans nest - # under it. - local agent_span_id - agent_span_id=$(all_spans | jq -r '[.[]|select(.span_attributes.type=="tool" and .span_attributes.name=="Agent")][0].span_id') - - # Every LLM span is a child of the Agent tool span. - local llm_parents_ok - llm_parents_ok=$(all_spans | jq --arg p "$agent_span_id" \ - 'all(.[]|select(.span_attributes.type=="llm"); .span_parents[0] == $p)') - assert_eq "$llm_parents_ok" "true" "LLM spans nested under the Agent tool span" - - # The sub-agent's Bash tool span also nests under the Agent tool span. - local subagent_tool - subagent_tool=$(all_spans | jq --arg p "$agent_span_id" \ - '[.[]|select(.span_attributes.type=="tool" and .span_parents[0]==$p)][0]') - assert_eq "$(echo "$subagent_tool" | jq -r '.span_attributes.name')" "Terminal: ls -la" "sub-agent tool span named after the Bash command" - assert_eq "$(echo "$subagent_tool" | jq -r '.metadata.tool_approval')" "approved" "sub-agent tool approval state" - assert_eq "$(echo "$subagent_tool" | jq -r '.metadata.tool_call_id')" "tu1" "sub-agent tool call id" - assert_eq "$(echo "$subagent_tool" | jq -r '.error')" "Exit code 128" "sub-agent tool error text" - - # Token totals match the deduped transcript. Braintrust prompt_tokens are - # inclusive for Anthropic: input 8 + cache_read 2500 + cache_creation 15 = 2523. - assert_eq "$(all_spans | jq '[.[]|select(.span_attributes.type=="llm")|.metrics.completion_tokens]|add')" "60" "completion summed" - assert_eq "$(all_spans | jq '[.[]|select(.span_attributes.type=="llm")|.metrics.prompt_tokens]|add')" "2523" "prompt includes input and cache tokens" - assert_eq "$(all_spans | jq '[.[]|select(.span_attributes.type=="llm")|.metrics.tokens]|add')" "2583" "total tokens includes inclusive prompt and completion" - assert_eq "$(all_spans | jq '[.[]|select(.span_attributes.type=="llm")|.metrics.prompt_cached_tokens]|add')" "2500" "cache_read summed" - assert_eq "$(all_spans | jq '[.[]|select(.span_attributes.type=="llm")|.metrics.prompt_cache_creation_tokens]|add')" "15" "cache_creation summed" - assert_eq "$(all_spans | jq '[.[]|select(.span_attributes.type=="llm" and (.metrics | (has("cache_read_input_tokens") or has("cache_creation_input_tokens"))))]|length')" "0" "raw Anthropic cache metrics are not emitted" - - # The LLM spans are tagged with the sub-agent's model. - assert_eq "$(all_spans | jq '[.[]|select(.span_attributes.type=="llm")]|all(.span_attributes.name=="claude-haiku-4-5")')" "true" "sub-agent model tagged" - - # The second LLM span's input carries the prior assistant + tool history. - assert_eq "$(all_spans | jq -r '[.[]|select(.span_attributes.type=="llm")][1].input|map(.role)|join(",")')" "assistant,tool" "sub-agent LLM input includes history" -} - -t_non_agent_tool_emits_no_llm_spans() { - # A normal (non-Agent) tool must not emit any LLM spans. - _with_turn_started "sess-noagent" - local payload - payload=$(fixture_post_tool_use "sess-noagent" "Bash" \ - "$(fixture_tool_input_bash 'ls')" \ - "$(fixture_tool_response_text 'out')") - run_hook post_tool_use.sh "$payload" - - assert_eq "$(span_count_by_type llm)" "0" "no LLM spans for non-Agent tools" -} - -it "emits sub-agent LLM spans under the Agent tool span" t_agent_emits_subagent_llm_spans -it "does not emit LLM spans for non-Agent tools" t_non_agent_tool_emits_no_llm_spans diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_queue.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_queue.sh deleted file mode 100755 index 2edb98b..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_queue.sh +++ /dev/null @@ -1,592 +0,0 @@ -#!/bin/bash -### -# Tests for the per-session span queue + background worker. -# -# Covers: -# - Sync mode: enqueue_span processes inline (default for tests) -# - Async mode: each session has its own pending/ and its own worker -# - drain_queue blocks until that session's queue is empty -# - sweep_dead_sessions cleans up crashed sessions -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -_test_event() { - local id="${1:-test-span-1}" - jq -nc \ - --arg id "$id" \ - '{ - id: $id, - span_id: $id, - root_span_id: $id, - input: "test", - span_attributes: { name: "test", type: "task" } - }' -} - -_setup_default_stubs() { - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_x"]}' - stub_response_for "*/v1/experiment/*/insert" 200 '{"row_ids":["row_x"]}' -} - -# Stop the worker for a session by removing its lock file (the in-loop -# heartbeat will fail to touch and the worker exits). Then poll until -# the PID is gone or we give up. -_stop_worker() { - local session_id="$1" - local lock_file - lock_file="$(session_queue_dir "$session_id")/worker.lock" - if [ -f "$lock_file" ]; then - local pid - pid=$(cat "$lock_file" 2>/dev/null) - rm -f "$lock_file" - if [ -n "$pid" ]; then - local i - for i in $(seq 1 50); do - kill -0 "$pid" 2>/dev/null || return 0 - sleep 0.1 - done - kill "$pid" 2>/dev/null || true - fi - fi -} - -# --------------------------------------------------------------------------- -describe "enqueue_span: sync mode (default in tests)" -# --------------------------------------------------------------------------- - -t_sync_enqueue_inserts_immediately() { - _setup_default_stubs - # Harness already sets BRAINTRUST_SYNC_QUEUE=true - - local event - event=$(_test_event) - enqueue_span "sess-sync" "proj_sync" "$event" - assert_success "$?" - - local count - count=$(span_count) - assert_eq "$count" "1" "expected the span to be inserted immediately" - - # No job files should be left in the queue. - # In sync mode the session dir is never created at all. - assert_eq "$(_queue_pending_count "sess-sync")" "0" - assert_eq "$(_queue_processing_count "sess-sync")" "0" -} - -t_sync_enqueue_does_not_spawn_worker() { - _setup_default_stubs - - enqueue_span "sess-sync2" "proj_sync" "$(_test_event)" >/dev/null - - # No worker lock should exist - local lock="$(session_queue_dir "sess-sync2")/worker.lock" - if [ -f "$lock" ]; then - fail "expected no worker lock in sync mode, got $lock" - fi -} - -t_sync_enqueue_uses_experiment_endpoint() { - _setup_default_stubs - CC_EXPERIMENT_ID="exp_sync" - - enqueue_span "sess-exp" "proj_irrelevant" "$(_test_event)" >/dev/null - - local pl_count exp_count - pl_count=$(captured_request_count '/project_logs/') - exp_count=$(captured_request_count '/v1/experiment/exp_sync/insert') - assert_eq "$pl_count" "0" - assert_eq "$exp_count" "1" -} - -t_sync_enqueue_requires_session_id() { - _setup_default_stubs - # Calling without a session_id should fail and log an error. - enqueue_span "" "proj_x" "$(_test_event)" >/dev/null 2>&1 - assert_failure "$?" - local log - log=$(hook_log) - assert_contains "$log" "without session_id" -} - -it "inserts the span immediately when BRAINTRUST_SYNC_QUEUE=true" t_sync_enqueue_inserts_immediately -it "does not spawn a background worker in sync mode" t_sync_enqueue_does_not_spawn_worker -it "honors CC_EXPERIMENT_ID and routes to experiment endpoint" t_sync_enqueue_uses_experiment_endpoint -it "returns an error if session_id is missing" t_sync_enqueue_requires_session_id - -# --------------------------------------------------------------------------- -describe "enqueue_span: async mode" -# --------------------------------------------------------------------------- - -t_async_enqueue_writes_job_file() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - # Disable worker spawn by hiding worker.sh so we can inspect pending/ - # before any draining can happen. - mv "$HOOKS_DIR/worker.sh" "$HOOKS_DIR/worker.sh.disabled" - - enqueue_span "sess-async" "proj_async" "$(_test_event)" - assert_success "$?" - - assert_eq "$(_queue_pending_count "sess-async")" "1" - - # The file should contain valid JSON with the right fields - local sdir job_file - sdir=$(session_queue_dir "sess-async") - job_file=$(find "$sdir/pending" -maxdepth 1 -name '*.json' -type f | head -1) - local proj_id event_id - proj_id=$(jq -r '.project_id' "$job_file") - event_id=$(jq -r '.event.id' "$job_file") - assert_eq "$proj_id" "proj_async" - assert_eq "$event_id" "test-span-1" - - mv "$HOOKS_DIR/worker.sh.disabled" "$HOOKS_DIR/worker.sh" -} - -t_async_drain_processes_pending_jobs() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - local i - for i in 1 2 3; do - enqueue_span "sess-drain" "proj_drain" "$(_test_event "span-$i")" - done - - drain_queue "sess-drain" 10 - assert_success "$?" - - assert_eq "$(_queue_pending_count "sess-drain")" "0" - assert_eq "$(_queue_processing_count "sess-drain")" "0" - assert_eq "$(span_count)" "3" - - _stop_worker "sess-drain" -} - -t_async_drain_timeout_returns_failure() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - # Disable worker spawn so the queue can never drain - mv "$HOOKS_DIR/worker.sh" "$HOOKS_DIR/worker.sh.disabled" - - enqueue_span "sess-stuck" "proj_stuck" "$(_test_event)" >/dev/null - - local start_time end_time elapsed - start_time=$(date +%s) - drain_queue "sess-stuck" 1 - local rc=$? - end_time=$(date +%s) - elapsed=$(( end_time - start_time )) - - assert_failure "$rc" "drain_queue should return non-zero on timeout" - if [ "$elapsed" -gt 3 ]; then - fail "drain_queue took ${elapsed}s; expected ~1s" - fi - - local log - log=$(hook_log) - assert_contains "$log" "drain_queue timed out" - - mv "$HOOKS_DIR/worker.sh.disabled" "$HOOKS_DIR/worker.sh" -} - -t_async_sessions_are_isolated() { - # Two sessions enqueueing concurrently should each get their own - # queue dir and their own worker. - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - enqueue_span "sess-A" "proj_a" "$(_test_event "a-1")" - enqueue_span "sess-B" "proj_b" "$(_test_event "b-1")" - - drain_queue "sess-A" 10 - drain_queue "sess-B" 10 - - assert_eq "$(_queue_pending_count "sess-A")" "0" - assert_eq "$(_queue_pending_count "sess-B")" "0" - assert_eq "$(span_count)" "2" - - # Two separate POSTs should have been made - local pl_count - pl_count=$(captured_request_count '/insert$') - assert_eq "$pl_count" "2" - - _stop_worker "sess-A" - _stop_worker "sess-B" -} - -it "writes a job file to pending/ when worker is offline" t_async_enqueue_writes_job_file -it "drain_queue processes all pending jobs" t_async_drain_processes_pending_jobs -it "drain_queue returns non-zero on timeout" t_async_drain_timeout_returns_failure -it "different sessions have isolated queues and workers" t_async_sessions_are_isolated - -# --------------------------------------------------------------------------- -describe "worker.sh" -# --------------------------------------------------------------------------- - -t_worker_drains_existing_jobs() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - # Pre-seed the queue with two job files (bypassing enqueue_span so we - # don't spawn a worker yet). - _ensure_session_queue "sess-seed" - local sdir - sdir=$(session_queue_dir "sess-seed") - local job1 job2 - job1=$(jq -nc --argjson e "$(_test_event "pre-1")" \ - '{type:"insert_span", project_id:"proj_w", experiment_id:"", event:$e}') - job2=$(jq -nc --argjson e "$(_test_event "pre-2")" \ - '{type:"insert_span", project_id:"proj_w", experiment_id:"", event:$e}') - echo "$job1" > "$sdir/pending/01-aaa.json" - echo "$job2" > "$sdir/pending/02-bbb.json" - - # Start the worker, give it time to drain, then remove its lock to - # signal shutdown. - bash "$HOOKS_DIR/worker.sh" sess-seed >/dev/null 2>&1 & - local worker_pid=$! - - # Poll for completion (up to 5s) - local i - for i in $(seq 1 50); do - if [ "$(_queue_pending_count "sess-seed")" = "0" ] && \ - [ "$(_queue_processing_count "sess-seed")" = "0" ]; then - break - fi - sleep 0.1 - done - - assert_eq "$(span_count)" "2" - assert_eq "$(_queue_pending_count "sess-seed")" "0" - - # Signal worker to exit by removing the lock, then wait briefly. - _stop_worker "sess-seed" - # Belt-and-suspenders: ensure background process is gone - kill "$worker_pid" 2>/dev/null || true -} - -t_worker_only_one_at_a_time_per_session() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - # Start the first worker for sess-W. - bash "$HOOKS_DIR/worker.sh" sess-W >/dev/null 2>&1 & - local first_pid=$! - - # Give it time to claim the lock - sleep 0.5 - - # Try to start a second worker for the same session - it should exit - # immediately (rc=0) and the first worker should still be alive. - bash "$HOOKS_DIR/worker.sh" sess-W - local rc=$? - assert_eq "$rc" "0" - - if ! kill -0 "$first_pid" 2>/dev/null; then - fail "first worker should still be alive" - fi - - _stop_worker "sess-W" -} - -t_worker_two_sessions_concurrent() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - bash "$HOOKS_DIR/worker.sh" sess-1 >/dev/null 2>&1 & - local pid1=$! - bash "$HOOKS_DIR/worker.sh" sess-2 >/dev/null 2>&1 & - local pid2=$! - - sleep 0.5 - - # Both workers should be alive (different sessions = different locks) - if ! kill -0 "$pid1" 2>/dev/null; then - fail "session 1 worker should be alive" - fi - if ! kill -0 "$pid2" 2>/dev/null; then - fail "session 2 worker should be alive" - fi - - # And each should hold its own lock - assert_file_exists "$(session_queue_dir sess-1)/worker.lock" - assert_file_exists "$(session_queue_dir sess-2)/worker.lock" - - _stop_worker "sess-1" - _stop_worker "sess-2" -} - -t_worker_heartbeat_refreshes_lock_mtime() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - bash "$HOOKS_DIR/worker.sh" sess-hb >/dev/null 2>&1 & - local pid=$! - - # Wait for the lock file to appear - local lock_file="$(session_queue_dir sess-hb)/worker.lock" - local i - for i in $(seq 1 50); do - [ -f "$lock_file" ] && break - sleep 0.1 - done - assert_file_exists "$lock_file" - - # Capture the initial mtime - local mtime1 - mtime1=$(_file_mtime "$lock_file") - - # Wait a bit longer than one heartbeat (0.2s loop) and re-check. - # Use a generous 1.5s to absorb scheduling jitter on slow CI. - sleep 1.5 - - local mtime2 - mtime2=$(_file_mtime "$lock_file") - - # mtime should have advanced - if [ "$mtime2" -le "$mtime1" ]; then - fail "expected lock mtime to advance: was=$mtime1 now=$mtime2" - fi - - _stop_worker "sess-hb" - kill "$pid" 2>/dev/null || true -} - -t_worker_survives_parent_exit() { - # Regression test for the core bug this whole refactor addresses: - # when a hook script enqueues a span and then exits, the background - # worker it spawned must keep running and drain the queue. Pre-refactor, - # `async: true` hooks were killed mid-curl and dropped spans. - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - local sid="sess-survives" - - # Run a child shell that enqueues a span and then exits. The child - # inherits our $HOME (which is the test's isolated tmp dir) and our - # stubbed curl + stub config. From the child's perspective this looks - # exactly like a hook script run by Claude Code: - # - sources common.sh - # - calls enqueue_span (which spawns a worker via nohup ... & disown) - # - exits. - # - # The worker has no idle-timeout mechanism; it lives until its lock - # file is removed (which only happens when session_end runs or when - # _stop_worker is invoked at the end of this test). - bash -c " - source '$HOOKS_DIR/common.sh' - event='$(_test_event 'survive-1')' - enqueue_span '$sid' 'proj_s' \"\$event\" - " - assert_success "$?" "child shell that enqueued should exit cleanly" - - # The child shell is gone. The worker should still be alive. - # Read its PID from the lock and assert. - local lock_file pid - lock_file="$(session_queue_dir "$sid")/worker.lock" - - # Worker may take a brief moment to claim its lock; wait up to 2s. - local i - for i in $(seq 1 20); do - [ -f "$lock_file" ] && break - sleep 0.1 - done - assert_file_exists "$lock_file" "worker should have claimed its lock" - - pid=$(cat "$lock_file" 2>/dev/null) - if [ -z "$pid" ]; then - fail "worker lock has no pid" - return 1 - fi - - # The PID must NOT be the long-gone child shell. It should be a - # detached process whose parent is init (pid 1) on Linux or launchd - # on macOS. Either way, the key assertion is: it's alive. - if ! kill -0 "$pid" 2>/dev/null; then - fail "worker pid $pid should still be alive after parent exit" - return 1 - fi - - # And it should have drained the job we enqueued. - local deadline=$(( $(date +%s) + 5 )) - while [ "$(date +%s)" -lt "$deadline" ]; do - [ "$(span_count)" -gt 0 ] && break - sleep 0.1 - done - assert_eq "$(span_count)" "1" "worker should have drained the enqueued job" - - _stop_worker "$sid" -} - -t_worker_survives_parent_exit_and_drains_more() { - # Stronger version: after the parent shell exits, we keep enqueuing - # jobs from the test process. The surviving worker should pick them up. - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - local sid="sess-survives-2" - - # Child enqueues one job and exits, spawning the worker. - bash -c " - source '$HOOKS_DIR/common.sh' - event='$(_test_event 'first')' - enqueue_span '$sid' 'proj_s' \"\$event\" - " - - # Wait for the worker to claim its lock so subsequent enqueues don't - # try to spawn a second worker. - local lock_file="$(session_queue_dir "$sid")/worker.lock" - local i - for i in $(seq 1 20); do - [ -f "$lock_file" ] && break - sleep 0.1 - done - - # From the test process, enqueue more jobs. Same session, so the - # already-running worker picks them up. - local n - for n in 2 3 4 5; do - enqueue_span "$sid" "proj_s" "$(_test_event "later-$n")" - done - - # Wait for the worker to drain all 5 jobs. - local deadline=$(( $(date +%s) + 5 )) - while [ "$(date +%s)" -lt "$deadline" ]; do - [ "$(span_count)" -ge 5 ] && break - sleep 0.1 - done - - assert_eq "$(span_count)" "5" "worker should have drained all 5 jobs across parent exit" - - _stop_worker "$sid" -} - -it "drains pre-existing jobs from pending/" t_worker_drains_existing_jobs -it "only one worker per session can hold the lock" t_worker_only_one_at_a_time_per_session -it "two sessions can have concurrent workers" t_worker_two_sessions_concurrent -it "worker refreshes its lock mtime on every iteration" t_worker_heartbeat_refreshes_lock_mtime -it "worker survives the spawning shell's exit" t_worker_survives_parent_exit -it "surviving worker drains jobs enqueued after parent exit" t_worker_survives_parent_exit_and_drains_more - -# --------------------------------------------------------------------------- -describe "drain_queue: edge cases" -# --------------------------------------------------------------------------- - -t_drain_empty_queue() { - export BRAINTRUST_SYNC_QUEUE=false - drain_queue "sess-empty" 1 - assert_success "$?" "draining an empty queue should succeed immediately" -} - -t_drain_sync_mode_noop() { - export BRAINTRUST_SYNC_QUEUE=true - drain_queue "sess-noop" 1 - assert_success "$?" "drain_queue is a no-op in sync mode" -} - -t_drain_requires_session_id() { - export BRAINTRUST_SYNC_QUEUE=false - drain_queue "" 1 - assert_failure "$?" "drain_queue without session_id should fail" -} - -it "succeeds immediately when the queue is empty" t_drain_empty_queue -it "is a no-op when BRAINTRUST_SYNC_QUEUE=true" t_drain_sync_mode_noop -it "returns an error if session_id is missing" t_drain_requires_session_id - -# --------------------------------------------------------------------------- -describe "sweep_dead_sessions" -# --------------------------------------------------------------------------- - -t_sweep_removes_empty_dead_session() { - export BRAINTRUST_SYNC_QUEUE=false - - # Create an empty session dir with no lock at all. - _ensure_session_queue "sess-empty-dead" - [ -d "$(session_queue_dir sess-empty-dead)/pending" ] || \ - fail "setup precondition: pending dir should exist" - - sweep_dead_sessions "current-session" - - # Empty dead dirs should be removed. - if [ -d "$(session_queue_dir sess-empty-dead)" ]; then - fail "expected empty dead session dir to be removed" - fi -} - -t_sweep_recovers_orphaned_jobs() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - # Create a session with leftover jobs but no live worker. - _ensure_session_queue "sess-crashed" - local sdir - sdir=$(session_queue_dir "sess-crashed") - local job - job=$(jq -nc --argjson e "$(_test_event "orphan-1")" \ - '{type:"insert_span", project_id:"proj_x", experiment_id:"", event:$e}') - echo "$job" > "$sdir/pending/01-orphan.json" - - # Sweep should recover the orphaned job by spawning a worker. - sweep_dead_sessions "current-session" - - # Wait for the recovery worker to drain - local i - for i in $(seq 1 50); do - [ "$(_queue_pending_count "sess-crashed")" = "0" ] && \ - [ "$(_queue_processing_count "sess-crashed")" = "0" ] && break - sleep 0.1 - done - - # The orphaned span should now have been POSTed. - assert_eq "$(span_count)" "1" - - _stop_worker "sess-crashed" -} - -t_sweep_skips_current_session() { - export BRAINTRUST_SYNC_QUEUE=false - - # Create what looks like a dead session for "active-sess" - empty dir - # with no lock. If sweep didn't skip the current session, it would - # rmdir this dir. - _ensure_session_queue "active-sess" - [ -d "$(session_queue_dir active-sess)/pending" ] || \ - fail "setup precondition: pending dir should exist" - - sweep_dead_sessions "active-sess" - - # The current session's dir should still exist. - if [ ! -d "$(session_queue_dir active-sess)" ]; then - fail "sweep should not have removed the current session's dir" - fi -} - -t_sweep_leaves_fresh_sessions_alone() { - _setup_default_stubs - export BRAINTRUST_SYNC_QUEUE=false - - # Spawn a worker for a "live" session - its heartbeat keeps the lock - # fresh, so sweep should leave it alone. - bash "$HOOKS_DIR/worker.sh" sess-live >/dev/null 2>&1 & - sleep 0.3 - - assert_file_exists "$(session_queue_dir sess-live)/worker.lock" - - sweep_dead_sessions "current-session" - - # The live session's lock should still exist. - assert_file_exists "$(session_queue_dir sess-live)/worker.lock" - - _stop_worker "sess-live" -} - -it "removes empty dead session dirs" t_sweep_removes_empty_dead_session -it "recovers orphaned jobs by spawning a recovery worker" t_sweep_recovers_orphaned_jobs -it "skips the current session" t_sweep_skips_current_session -it "leaves sessions with fresh heartbeats alone" t_sweep_leaves_fresh_sessions_alone diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_replay.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_replay.sh deleted file mode 100755 index 7a0a0db..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_replay.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash -### -# Tests for the replay helper. -# -# Replay-based tests are how you turn a real Claude Code session into a -# regression test: -# -# 1. Capture: set BRAINTRUST_RECORD_DIR to a fresh directory, run claude, -# let the session play out. Every hook invocation gets appended to -# $BRAINTRUST_RECORD_DIR/events.ndjson and any stop_hook transcripts -# are copied to $BRAINTRUST_RECORD_DIR/transcripts/. -# -# 2. Move: drop the captured directory under -# test/fixtures/sessions// -# -# 3. Replay: in a test, call replay_session "$FIXTURE_DIR" then assert -# on the resulting span tree using span_count_by_type, span_by_name, -# etc. -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -_setup_default_stubs() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_test"}' - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' -} - -# --------------------------------------------------------------------------- -describe "replay_session: example-simple fixture" -# --------------------------------------------------------------------------- - -t_replay_runs_all_events() { - _setup_default_stubs - - local n - n=$(replay_session "$SCRIPT_DIR/fixtures/sessions/example-simple") - assert_success "$?" "replay_session should succeed" - assert_eq "$n" "5" "expected 5 events replayed" -} - -t_replay_produces_expected_spans() { - _setup_default_stubs - - replay_session "$SCRIPT_DIR/fixtures/sessions/example-simple" >/dev/null - assert_success "$?" - - # The fixture has: session_start, user_prompt_submit, 2x post_tool_use, - # session_end. That should result in: - # - 1 session span (type=task, "Claude Code: ...") - # - 1 turn span (type=task, "Turn 1") - # - 2 tool spans (type=tool, "Terminal: ...", "Read: a.txt") - local task_count tool_count llm_count total - task_count=$(span_count_by_type "task") - tool_count=$(span_count_by_type "tool") - llm_count=$(span_count_by_type "llm") - total=$(span_count) - - assert_eq "$task_count" "2" "expected 2 task spans (session + turn)" - assert_eq "$tool_count" "2" "expected 2 tool spans" - assert_eq "$llm_count" "0" "no llm spans without a stop_hook" - assert_eq "$total" "4" -} - -t_replay_preserves_hierarchy() { - _setup_default_stubs - - replay_session "$SCRIPT_DIR/fixtures/sessions/example-simple" >/dev/null - - # Session id from the fixture - local sid="example-sess-001" - - # The session span's id is the session id - local session_span - session_span=$(span_by_id "$sid") - assert_ne "$session_span" "null" "expected session span to exist" - - # The turn span's parent is the session - local turn_span turn_parent - turn_span=$(span_by_name "^Turn 1$") - turn_parent=$(echo "$turn_span" | jq -r '.span_parents[0]') - assert_eq "$turn_parent" "$sid" - - # Each tool span's parent is the turn - local turn_id - turn_id=$(echo "$turn_span" | jq -r '.span_id') - local tool_children - tool_children=$(children_of "$turn_id" | jq 'length') - assert_eq "$tool_children" "2" "turn should have 2 tool children" -} - -it "replays all 5 hook events" t_replay_runs_all_events -it "produces session + turn + 2 tool spans" t_replay_produces_expected_spans -it "preserves session > turn > tool hierarchy" t_replay_preserves_hierarchy - -# --------------------------------------------------------------------------- -describe "replay_session: error handling" -# --------------------------------------------------------------------------- - -t_replay_missing_fixture() { - _setup_default_stubs - replay_session "/nonexistent/fixture/path" >/dev/null 2>&1 - assert_failure "$?" "replay_session should fail when fixture is missing" -} - -it "returns non-zero when the fixture directory does not exist" t_replay_missing_fixture - -# --------------------------------------------------------------------------- -describe "replay_session: record-only events run their no-op handler" -# --------------------------------------------------------------------------- - -# Replay is hooks.json-driven: every event runs whatever is registered for -# it. Record-only events (PreToolUse, PreCompact, InstructionsLoaded, ...) -# are registered to record_event.sh, which no-ops when recording is off. -# So they replay successfully and produce no spans, while the acting hooks -# create their spans. Replay must never fail just because an event is -# observability-only. -t_replay_runs_record_only_handlers() { - _setup_default_stubs - - # Build a fixture that interleaves record-only events among the acting - # hooks. All names are Claude Code's CamelCase event names. - local dir="$TEST_TMP/mixed-fixture" - mkdir -p "$dir/transcripts" - { - echo '{"ts":"t0","hook":"SessionStart","payload":{"session_id":"mix-1","cwd":"/tmp","hook_event_name":"SessionStart"}}' - echo '{"ts":"t1","hook":"InstructionsLoaded","payload":{"session_id":"mix-1","hook_event_name":"InstructionsLoaded"}}' - echo '{"ts":"t2","hook":"UserPromptSubmit","payload":{"session_id":"mix-1","prompt":"hi","cwd":"/tmp","hook_event_name":"UserPromptSubmit"}}' - echo '{"ts":"t3","hook":"PreToolUse","payload":{"session_id":"mix-1","tool_name":"Bash","hook_event_name":"PreToolUse"}}' - echo '{"ts":"t4","hook":"PostToolUse","payload":{"session_id":"mix-1","tool_name":"Bash","tool_input":{"command":"ls"},"tool_response":{"output":"x"},"hook_event_name":"PostToolUse"}}' - echo '{"ts":"t5","hook":"PreCompact","payload":{"session_id":"mix-1","trigger":"auto","hook_event_name":"PreCompact"}}' - echo '{"ts":"t6","hook":"SessionEnd","payload":{"session_id":"mix-1","hook_event_name":"SessionEnd"}}' - } > "$dir/events.ndjson" - - # Every event has a registered handler in hooks.json, so all 7 run. - local n - n=$(replay_session "$dir") - assert_success "$?" "replay should not fail on record-only events" - assert_eq "$n" "7" "all 7 events have a handler and should replay" - - # Only the acting hooks produce spans (session + turn + tool). The - # record-only events no-op, contributing nothing. - assert_eq "$(span_count_by_type task)" "2" "session + turn task spans" - assert_eq "$(span_count_by_type tool)" "1" "one tool span from PostToolUse" -} - -it "runs record-only event handlers as no-ops without failing" t_replay_runs_record_only_handlers diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_session_start.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_session_start.sh deleted file mode 100755 index 635e257..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_session_start.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/bin/bash -### -# End-to-end tests for the SessionStart hook. -# -# Each test: -# 1. Configures canned curl responses -# 2. Builds a hook payload using fixture_session_start() -# 3. Invokes the hook via run_hook -# 4. Asserts on the captured POST requests / resulting span data -# -# These tests are the bash equivalent of opencode-plugin's -# `assertEventsProduceTree` pattern. -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -# Canned response setup used by most tests. -_setup_default_stubs() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_test"}' - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' -} - -# --------------------------------------------------------------------------- -describe "session_start.sh: happy path" -# --------------------------------------------------------------------------- - -t_session_start_creates_one_span() { - _setup_default_stubs - - local payload - payload=$(fixture_session_start "sess-001" "/tmp/my-workspace") - - run_hook session_start.sh "$payload" - assert_success "$HOOK_STATUS" "hook should exit 0" - - local count - count=$(span_count) - assert_eq "$count" "1" "expected exactly one span inserted" -} - -t_session_start_span_shape() { - _setup_default_stubs - - local payload - payload=$(fixture_session_start "sess-002" "/tmp/cool-project") - run_hook session_start.sh "$payload" - - local span - span=$(span_by_type "task") - assert_ne "$span" "null" "expected a task span to exist" - - local name - name=$(echo "$span" | jq -r '.span_attributes.name') - assert_eq "$name" "Claude Code: cool-project" - - # span_id should equal the session_id; root_span_id should too - local span_id root_span_id - span_id=$(echo "$span" | jq -r '.span_id') - root_span_id=$(echo "$span" | jq -r '.root_span_id') - assert_eq "$span_id" "sess-002" - assert_eq "$root_span_id" "sess-002" -} - -t_session_start_metadata() { - _setup_default_stubs - - run_hook session_start.sh "$(fixture_session_start "sess-meta" "/tmp/x")" - - local span - span=$(span_by_type "task") - - local source - source=$(echo "$span" | jq -r '.metadata.source') - assert_eq "$source" "claude-code" - - local session_id - session_id=$(echo "$span" | jq -r '.metadata.session_id') - assert_eq "$session_id" "sess-meta" - - # Version attributes are present and non-empty. trace_claude_code_version - # comes from plugin.json; claude_code_version from the transcript or - # `claude --version` (falls back to "unknown" but must always be set). - local trace_version cc_version - trace_version=$(echo "$span" | jq -r '.metadata.trace_claude_code_version') - cc_version=$(echo "$span" | jq -r '.metadata.claude_code_version') - assert_ne "$trace_version" "null" "trace_claude_code_version should be set" - assert_ne "$trace_version" "" "trace_claude_code_version should be non-empty" - # It should match the manifest (e.g. a semver-ish string). - assert_match "$trace_version" "^[0-9]+\.[0-9]+\.[0-9]+" "trace_claude_code_version looks like a version" - assert_ne "$cc_version" "null" "claude_code_version should be set" - assert_ne "$cc_version" "" "claude_code_version should be non-empty" -} - -_make_git_repo() { - local dir - dir=$(mktemp -d) - git -C "$dir" init >/dev/null 2>&1 - git -C "$dir" config user.email test@example.com - git -C "$dir" config user.name "Test User" - printf "hello\n" > "$dir/README.md" - git -C "$dir" add README.md - git -C "$dir" commit -m init >/dev/null 2>&1 - git -C "$dir" branch -M main - git -C "$dir" remote add origin "https://token@github.com/acme/app.git" - echo "$dir" -} - -t_session_start_git_metadata() { - _setup_default_stubs - - local repo commit - repo=$(_make_git_repo) - commit=$(git -C "$repo" rev-parse HEAD) - - run_hook session_start.sh "$(fixture_session_start "sess-git" "$repo")" - - local span - span=$(span_by_type "task") - - assert_eq "$(echo "$span" | jq -r '.metadata.git_origin_url')" "https://github.com/acme/app.git" - assert_eq "$(echo "$span" | jq -r '.metadata.git_branch')" "main" - assert_eq "$(echo "$span" | jq -r '.metadata.git_commit_sha')" "$commit" - - rm -rf "$repo" -} - -t_session_start_writes_state() { - _setup_default_stubs - - run_hook session_start.sh "$(fixture_session_start "sess-state" "/tmp/x")" - - # The hook should persist root_span_id, session_span_id, project_id, etc. - # These are written by the parent shell of session_start via - # set_session_state. Reading them back is best done via the same helper - # so paths agree. - local root_id project_id turn_count - root_id=$(get_session_state "sess-state" "root_span_id") - project_id=$(get_session_state "sess-state" "project_id") - turn_count=$(get_session_state "sess-state" "turn_count") - - assert_eq "$root_id" "sess-state" - assert_eq "$project_id" "proj_test" - assert_eq "$turn_count" "0" -} - -it "creates exactly one root session span" t_session_start_creates_one_span -it "span has correct name, id, and type" t_session_start_span_shape -it "span includes claude-code metadata" t_session_start_metadata -it "span includes minimal git metadata" t_session_start_git_metadata -it "persists session state for later hooks" t_session_start_writes_state - -# --------------------------------------------------------------------------- -describe "session_start.sh: tracing disabled" -# --------------------------------------------------------------------------- - -t_session_start_disabled() { - _setup_default_stubs - export TRACE_TO_BRAINTRUST=false - - run_hook session_start.sh "$(fixture_session_start "sess-off" "/tmp/x")" - assert_success "$HOOK_STATUS" - - # No spans should have been inserted - local count - count=$(span_count) - assert_eq "$count" "0" -} - -it "is a no-op when TRACE_TO_BRAINTRUST is false" t_session_start_disabled - -# --------------------------------------------------------------------------- -describe "session_start.sh: missing API key" -# --------------------------------------------------------------------------- - -t_session_start_no_api_key() { - _setup_default_stubs - export BRAINTRUST_API_KEY="" - - run_hook session_start.sh "$(fixture_session_start "sess-noauth" "/tmp/x")" - # Hook exits 0 (graceful failure) - it should never block Claude Code. - assert_success "$HOOK_STATUS" - - local count - count=$(span_count) - assert_eq "$count" "0" - - local log - log=$(hook_log) - assert_contains "$log" "BRAINTRUST_API_KEY not set" -} - -it "exits gracefully and logs when BRAINTRUST_API_KEY is unset" t_session_start_no_api_key - -# --------------------------------------------------------------------------- -describe "session_start.sh: invalid API key" -# --------------------------------------------------------------------------- - -t_session_start_invalid_key() { - stub_response_for "*/v1/project?project_name=*" 401 "Invalid API Key" - - run_hook session_start.sh "$(fixture_session_start "sess-bad" "/tmp/x")" - # Hook exits 0 so Claude Code keeps running - assert_success "$HOOK_STATUS" - - local count - count=$(span_count) - assert_eq "$count" "0" "no spans should be sent when project lookup fails" - - local log - log=$(hook_log) - assert_contains "$log" "authentication failed" -} - -it "exits gracefully and logs an auth error on HTTP 401" t_session_start_invalid_key - -# --------------------------------------------------------------------------- -describe "session_start.sh: race protection" -# --------------------------------------------------------------------------- - -t_session_start_idempotent() { - _setup_default_stubs - - # Two SessionStart hooks for the same session should produce one span. - # The second invocation should detect the existing root_span_id via - # check_and_set_session_state and exit without inserting. - local payload - payload=$(fixture_session_start "sess-dup" "/tmp/x") - - run_hook session_start.sh "$payload" - assert_success "$HOOK_STATUS" - - run_hook session_start.sh "$payload" - assert_success "$HOOK_STATUS" - - local count - count=$(span_count) - assert_eq "$count" "1" "duplicate session_start should not double-insert" -} - -it "does not double-insert when called twice for the same session" t_session_start_idempotent diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_stop_hook.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_stop_hook.sh deleted file mode 100755 index 0fdae54..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_stop_hook.sh +++ /dev/null @@ -1,375 +0,0 @@ -#!/bin/bash -### -# End-to-end tests for the Stop hook. -# -# Stop fires when Claude finishes responding to a user turn. It: -# - Reads $LAST_ASSISTANT_MESSAGE from the hook input (Claude's final -# response text) and uses it as the Turn span's `output` field -# - Parses the conversation transcript to emit per-LLM-call spans and -# to aggregate turn-level token totals -# - Emits a TURN_UPDATE merge to populate the Turn span's output and -# metrics fields, finalizing the turn -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -_setup_default_stubs() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_test"}' - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' -} - -# Set up session + turn so stop_hook has a current turn to finalize. -_with_turn_started() { - local session_id="$1" - _setup_default_stubs - run_hook session_start.sh "$(fixture_session_start "$session_id" "/tmp/x")" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "$session_id" "do something")" - : > "$CAPTURED_REQUESTS" -} - -# Write a minimal empty transcript file. The hook will read it, find no -# assistant messages, and skip straight to emitting the TURN_UPDATE merge. -_empty_transcript() { - local path="$1" - : > "$path" - echo "$path" -} - -# --------------------------------------------------------------------------- -describe "stop_hook.sh: populates Turn output from last_assistant_message" -# --------------------------------------------------------------------------- - -t_stop_sets_turn_output() { - _with_turn_started "stop-out-1" - local transcript - transcript=$(_empty_transcript "$TEST_TMP/transcript.jsonl") - - local payload - payload=$(fixture_stop "stop-out-1" "$transcript" "Here is my answer.") - run_hook stop_hook.sh "$payload" - assert_success "$HOOK_STATUS" - - # The hook should have emitted one TURN_UPDATE merge span. Find it. - local span - span=$(all_spans | jq '.[] | select(._is_merge == true)' | jq -s '.[0]') - assert_ne "$span" "null" "expected a merge span to be emitted" - - local output - output=$(echo "$span" | jq -r '.output') - assert_eq "$output" "Here is my answer." -} - -t_stop_output_empty_when_message_missing() { - # If Claude Code doesn't supply last_assistant_message, the output - # field should be empty (rather than e.g. "null" or undefined). - _with_turn_started "stop-out-2" - local transcript - transcript=$(_empty_transcript "$TEST_TMP/transcript.jsonl") - - # Build a payload without last_assistant_message - local payload - payload=$(jq -nc --arg s "stop-out-2" --arg t "$transcript" \ - '{session_id: $s, transcript_path: $t}') - run_hook stop_hook.sh "$payload" - assert_success "$HOOK_STATUS" - - local span - span=$(all_spans | jq '.[] | select(._is_merge == true)' | jq -s '.[0]') - local output - output=$(echo "$span" | jq -r '.output') - assert_eq "$output" "" -} - -t_stop_turn_update_has_correct_id() { - # The merge should target the current_turn_span_id stored at - # user_prompt_submit time, not a freshly-generated id. - _with_turn_started "stop-id-1" - local turn_id - turn_id=$(get_session_state "stop-id-1" "current_turn_span_id") - assert_ne "$turn_id" "" - - local transcript - transcript=$(_empty_transcript "$TEST_TMP/transcript.jsonl") - run_hook stop_hook.sh "$(fixture_stop "stop-id-1" "$transcript" "msg")" - - local span - span=$(all_spans | jq '.[] | select(._is_merge == true)' | jq -s '.[0]') - local span_id - span_id=$(echo "$span" | jq -r '.id') - assert_eq "$span_id" "$turn_id" -} - -t_stop_merge_flag_is_set() { - # Sanity check that we send `_is_merge: true` so Braintrust doesn't - # try to create a brand-new span (which would orphan the original - # Turn span's children). - _with_turn_started "stop-merge-1" - local transcript - transcript=$(_empty_transcript "$TEST_TMP/transcript.jsonl") - run_hook stop_hook.sh "$(fixture_stop "stop-merge-1" "$transcript" "msg")" - - local merges - merges=$(all_spans | jq '[.[] | select(._is_merge == true)] | length') - assert_eq "$merges" "1" -} - -t_stop_merge_has_end_time_no_tokens() { - # The Turn merge should carry an end time but NO token metrics: token - # metrics live only on the leaf LLM spans, and Braintrust aggregates - # them onto parent spans for display. Writing Turn-level token sums here - # would be redundant and would miss sub-agent tokens. - _with_turn_started "stop-metrics-1" - local transcript - transcript=$(_empty_transcript "$TEST_TMP/transcript.jsonl") - run_hook stop_hook.sh "$(fixture_stop "stop-metrics-1" "$transcript" "ok")" - - local span - span=$(all_spans | jq '.[] | select(._is_merge == true)' | jq -s '.[0]') - - # end is a unix timestamp - should be a positive integer - local end_time - end_time=$(echo "$span" | jq -r '.metrics.end') - if [ "$end_time" -le 0 ] 2>/dev/null; then - fail "expected positive end time, got $end_time" - fi - - # No token metrics should be present on the merge. - assert_eq "$(echo "$span" | jq -r '.metrics.prompt_tokens // "absent"')" "absent" "no prompt_tokens on Turn merge" - assert_eq "$(echo "$span" | jq -r '.metrics.completion_tokens // "absent"')" "absent" "no completion_tokens on Turn merge" - assert_eq "$(echo "$span" | jq -r '.metrics.tokens // "absent"')" "absent" "no tokens on Turn merge" - assert_eq "$(echo "$span" | jq -r '.metrics.prompt_cached_tokens // "absent"')" "absent" "no cache_read on Turn merge" - assert_eq "$(echo "$span" | jq -r '.metrics.prompt_cache_creation_tokens // "absent"')" "absent" "no cache_creation on Turn merge" - assert_eq "$(echo "$span" | jq -r '.metrics.prompt_cache_creation_5m_tokens // "absent"')" "absent" "no cache_creation_5m on Turn merge" - assert_eq "$(echo "$span" | jq -r '.metrics.prompt_cache_creation_1h_tokens // "absent"')" "absent" "no cache_creation_1h on Turn merge" -} - -# --------------------------------------------------------------------------- -describe "stop_hook.sh: streaming output_tokens are not double-counted" -# --------------------------------------------------------------------------- - -# Build a single assistant transcript line for one requestId, carrying the -# given cumulative output_tokens in message.usage. input/cache are held -# constant across lines (as Claude Code reports them) so only the output -# delta logic is exercised. -_assistant_line() { - local request_id="$1" - local output_tokens="$2" - local text="$3" - jq -nc \ - --arg rid "$request_id" \ - --arg text "$text" \ - --argjson out "$output_tokens" \ - '{ - type: "assistant", - requestId: $rid, - timestamp: "2024-01-01T00:00:00.000Z", - message: { - model: "claude-test", - content: [{type: "text", text: $text}], - usage: { - input_tokens: 10, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - output_tokens: $out - } - } - }' -} - -t_stop_streaming_output_not_double_counted() { - # A single API response streams output_tokens cumulatively across its - # transcript lines (e.g. 5 -> 30 -> 40). The hook tracks the running max - # per requestId and should add only deltas, so the emitted LLM span's - # completion_tokens must equal the FINAL value (40), not the sum of all - # reported values (5 + 30 + 40 = 75) nor a partial double-count (70). - # - # This requires THREE OR MORE increasing lines on one requestId: the - # two-line case happens to be correct, which is why earlier fixtures - # (constant output_tokens per requestId) did not catch this. - _with_turn_started "stop-stream-1" - - local transcript="$TEST_TMP/transcript.jsonl" - { - _assistant_line "req_stream" 5 "partial one" - _assistant_line "req_stream" 30 "partial two" - _assistant_line "req_stream" 40 "final answer" - } > "$transcript" - - run_hook stop_hook.sh "$(fixture_stop "stop-stream-1" "$transcript" "final answer")" - assert_success "$HOOK_STATUS" - - # Exactly one LLM span should be emitted for this single response. - local llm_spans - llm_spans=$(all_spans | jq '[.[] | select(.span_attributes.type == "llm")]') - assert_eq "$(echo "$llm_spans" | jq 'length')" "1" "expected exactly one LLM span" - - local completion - completion=$(echo "$llm_spans" | jq -r '.[0].metrics.completion_tokens') - assert_eq "$completion" "40" "completion_tokens should be the final max, not a re-added full output" - - # Input is counted once per requestId, so prompt_tokens stays at 10. - local prompt - prompt=$(echo "$llm_spans" | jq -r '.[0].metrics.prompt_tokens') - assert_eq "$prompt" "10" "prompt_tokens should be counted once per requestId" -} - -# --------------------------------------------------------------------------- -describe "stop_hook.sh: one LLM span per requestId across tool_result boundaries" -# --------------------------------------------------------------------------- - -# Assistant line carrying a single tool_use block plus usage, for the given -# requestId. Output/input tokens are held constant across the requestId's -# lines (as Claude Code reports them for non-streamed usage). -_assistant_tool_use_line() { - local request_id="$1" - local tool_use_id="$2" - local tool_name="$3" - local output_tokens="$4" - jq -nc \ - --arg rid "$request_id" \ - --arg tuid "$tool_use_id" \ - --arg name "$tool_name" \ - --argjson out "$output_tokens" \ - '{ - type: "assistant", - requestId: $rid, - timestamp: "2024-01-01T00:00:00.000Z", - message: { - model: "claude-test", - content: [{type: "tool_use", id: $tuid, name: $name, input: {}}], - usage: { - input_tokens: 10, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - output_tokens: $out - } - } - }' -} - -# A user/tool_result line answering the given tool_use_id. -_tool_result_line() { - local tool_use_id="$1" - local result="$2" - jq -nc \ - --arg tuid "$tool_use_id" \ - --arg result "$result" \ - '{ - type: "user", - timestamp: "2024-01-01T00:00:00.000Z", - message: { - content: [{type: "tool_result", tool_use_id: $tuid, content: $result}] - } - }' -} - -t_stop_same_request_not_split_into_zero_token_spans() { - # A single API response (one requestId) can emit a tool_use, receive a - # tool_result, then emit MORE tool_use blocks under the SAME requestId - # before the next tool_result. The per-boundary span logic would emit a - # second LLM span for the continuation segment, but its input/cache were - # already counted and its output delta is zero - so that span carries - # all-zero token metrics and misattributes the response. - # - # Expectation: exactly one LLM span per requestId, and no LLM span with - # entirely zero token metrics. - _with_turn_started "stop-split-1" - - local transcript="$TEST_TMP/transcript.jsonl" - { - # First segment of req_A: tool_use #1 - _assistant_tool_use_line "req_A" "tool_1" "Bash" 20 - _tool_result_line "tool_1" "ok" - # Continuation of req_A after the tool_result: more tool_use blocks, - # same requestId, same (constant) usage. - _assistant_tool_use_line "req_A" "tool_2" "Bash" 20 - _tool_result_line "tool_2" "ok" - # A distinct follow-up response. - _assistant_line "req_B" 15 "all done" - } > "$transcript" - - run_hook stop_hook.sh "$(fixture_stop "stop-split-1" "$transcript" "all done")" - assert_success "$HOOK_STATUS" - - local llm_spans - llm_spans=$(all_spans | jq '[.[] | select(.span_attributes.type == "llm")]') - - # No LLM span should have all-zero token metrics. - local zero_token_spans - zero_token_spans=$(echo "$llm_spans" | jq '[ - .[] | select( - (.metrics.prompt_tokens // 0) == 0 - and (.metrics.completion_tokens // 0) == 0 - and (.metrics.prompt_cache_creation_tokens // 0) == 0 - and (.metrics.prompt_cache_creation_5m_tokens // 0) == 0 - and (.metrics.prompt_cache_creation_1h_tokens // 0) == 0 - and (.metrics.prompt_cached_tokens // 0) == 0 - ) - ] | length') - assert_eq "$zero_token_spans" "0" "no LLM span should carry all-zero token metrics" - - # Exactly two distinct LLM responses (req_A and req_B) -> two LLM spans. - assert_eq "$(echo "$llm_spans" | jq 'length')" "2" \ - "expected one LLM span per requestId (req_A, req_B)" - - # Session-level token totals must be preserved: req_A=10 in / 20 out, - # req_B=10 in / 15 out -> prompt 20, completion 35. - local total_prompt total_completion - total_prompt=$(echo "$llm_spans" | jq '[.[].metrics.prompt_tokens // 0] | add') - total_completion=$(echo "$llm_spans" | jq '[.[].metrics.completion_tokens // 0] | add') - assert_eq "$total_prompt" "20" "total prompt_tokens preserved across spans" - assert_eq "$total_completion" "35" "total completion_tokens preserved across spans" -} - -t_stop_emits_split_cache_metrics() { - _with_turn_started "stop-cache-split-1" - - local transcript="$TEST_TMP/transcript.jsonl" - jq -nc '{ - type: "assistant", - requestId: "req_cache", - timestamp: "2024-01-01T00:00:00.000Z", - message: { - model: "claude-test", - content: [{type: "text", text: "cached answer"}], - usage: { - input_tokens: 7, - cache_creation_input_tokens: 30, - cache_read_input_tokens: 100, - cache_creation: { - ephemeral_5m_input_tokens: 10, - ephemeral_1h_input_tokens: 20 - }, - output_tokens: 5 - } - } - }' > "$transcript" - - run_hook stop_hook.sh "$(fixture_stop "stop-cache-split-1" "$transcript" "cached answer")" - assert_success "$HOOK_STATUS" - - local llm - llm=$(all_spans | jq '[.[] | select(.span_attributes.type == "llm")][0]') - - assert_eq "$(echo "$llm" | jq -r '.metrics.prompt_tokens')" "137" "prompt includes input, cache read, and cache write" - assert_eq "$(echo "$llm" | jq -r '.metrics.tokens')" "142" "tokens includes inclusive prompt and completion" - assert_eq "$(echo "$llm" | jq -r '.metrics.prompt_cached_tokens')" "100" "cache read uses canonical metric" - assert_eq "$(echo "$llm" | jq -r '.metrics.prompt_cache_creation_5m_tokens')" "10" "5m cache write uses canonical metric" - assert_eq "$(echo "$llm" | jq -r '.metrics.prompt_cache_creation_1h_tokens')" "20" "1h cache write uses canonical metric" - assert_eq "$(echo "$llm" | jq -r '.metrics.prompt_cache_creation_tokens // "absent"')" "absent" "aggregate cache write omitted when split is present" - assert_eq "$(echo "$llm" | jq -r '.metrics.cache_creation_input_tokens // "absent"')" "absent" "raw cache creation omitted" - assert_eq "$(echo "$llm" | jq -r '.metrics.cache_read_input_tokens // "absent"')" "absent" "raw cache read omitted" -} - -it "writes last_assistant_message into the Turn span output" t_stop_sets_turn_output -it "leaves output empty when last_assistant_message missing" t_stop_output_empty_when_message_missing -it "targets the existing Turn span id via merge" t_stop_turn_update_has_correct_id -it "sets _is_merge=true on the update" t_stop_merge_flag_is_set -it "merge carries end time but no token metrics" t_stop_merge_has_end_time_no_tokens -it "does not double-count streaming output across 3+ lines" t_stop_streaming_output_not_double_counted -it "emits one LLM span per requestId across tool_result splits" t_stop_same_request_not_split_into_zero_token_spans -it "emits canonical split cache metrics" t_stop_emits_split_cache_metrics diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_user_prompt_expansion.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_user_prompt_expansion.sh deleted file mode 100644 index fba7445..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_user_prompt_expansion.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -### -# End-to-end tests for explicit skill capture from UserPromptExpansion. -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -_setup_default_stubs() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_test"}' - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' -} - -_skill_listing_transcript() { - local path="$1" - jq -nc '{attachment: {type: "skill_listing", names: ["review", "security-review"]}}' > "$path" -} - -_fixture_user_prompt_expansion() { - local session_id="$1" - local command_name="$2" - local transcript_path="$3" - jq -nc \ - --arg s "$session_id" \ - --arg c "$command_name" \ - --arg t "$transcript_path" \ - '{ - session_id: $s, - expansion_type: "slash_command", - command_name: $c, - transcript_path: $t - }' -} - -_with_started_session() { - local session_id="$1" - _setup_default_stubs - run_hook session_start.sh "$(fixture_session_start "$session_id" "/tmp/x")" -} - -_with_turn_started() { - local session_id="$1" - _with_started_session "$session_id" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "$session_id" "do something")" - : > "$CAPTURED_REQUESTS" -} - -# --------------------------------------------------------------------------- -describe "user_prompt_expansion.sh" -# --------------------------------------------------------------------------- - -t_expansion_merges_current_turn_metadata() { - _with_turn_started "sess-upe-merge" - local transcript="$TEST_TMP/transcript.jsonl" - _skill_listing_transcript "$transcript" - - run_hook user_prompt_expansion.sh "$(_fixture_user_prompt_expansion "sess-upe-merge" "/review" "$transcript")" - assert_success "$HOOK_STATUS" - - local span - span=$(all_spans | jq -c '.[0]') - assert_eq "$(echo "$span" | jq -r '._is_merge')" "true" - assert_eq "$(echo "$span" | jq -r '.metadata.loaded_skill_names[0]')" "review" - assert_eq "$(echo "$span" | jq -r '.metadata.loaded_skills[0].name')" "review" -} - -t_pending_expansion_is_added_to_next_turn() { - _with_started_session "sess-upe-pending" - local transcript="$TEST_TMP/transcript.jsonl" - _skill_listing_transcript "$transcript" - - run_hook user_prompt_expansion.sh "$(_fixture_user_prompt_expansion "sess-upe-pending" "/review" "$transcript")" - assert_success "$HOOK_STATUS" - : > "$CAPTURED_REQUESTS" - - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-upe-pending" "after expansion")" - assert_success "$HOOK_STATUS" - - local turn - turn=$(span_by_name "^Turn 1$") - assert_eq "$(echo "$turn" | jq -r '.metadata.loaded_skill_names[0]')" "review" - assert_eq "$(echo "$turn" | jq -r '.metadata.loaded_skills[0].name')" "review" -} - -t_matching_skill_tool_is_marked_explicit() { - _with_turn_started "sess-upe-tool" - local transcript="$TEST_TMP/transcript.jsonl" - _skill_listing_transcript "$transcript" - - run_hook user_prompt_expansion.sh "$(_fixture_user_prompt_expansion "sess-upe-tool" "/review" "$transcript")" - assert_success "$HOOK_STATUS" - : > "$CAPTURED_REQUESTS" - - run_hook post_tool_use.sh "$(fixture_post_tool_use "sess-upe-tool" "Skill" \ - "$(jq -nc '{name: "review"}')" \ - "$(fixture_tool_response_text 'loaded')")" - - local tool_span - tool_span=$(span_by_type "tool") - assert_eq "$(echo "$tool_span" | jq -r '.metadata.skill_load_trigger')" "explicit" -} - -t_non_skill_slash_command_is_ignored() { - _with_turn_started "sess-upe-ignore" - local transcript="$TEST_TMP/transcript.jsonl" - _skill_listing_transcript "$transcript" - - run_hook user_prompt_expansion.sh "$(_fixture_user_prompt_expansion "sess-upe-ignore" "/not-a-skill" "$transcript")" - assert_success "$HOOK_STATUS" - - assert_eq "$(span_count)" "0" -} - -it "merges explicit skill metadata onto the current turn" t_expansion_merges_current_turn_metadata -it "adds pending explicit skill metadata to the next turn" t_pending_expansion_is_added_to_next_turn -it "marks matching Skill tool spans as explicit" t_matching_skill_tool_is_marked_explicit -it "ignores slash commands not present in skill listings" t_non_skill_slash_command_is_ignored diff --git a/src/plugins/claude/content/plugins/trace-claude-code/test/test_user_prompt_submit.sh b/src/plugins/claude/content/plugins/trace-claude-code/test/test_user_prompt_submit.sh deleted file mode 100755 index 098300e..0000000 --- a/src/plugins/claude/content/plugins/trace-claude-code/test/test_user_prompt_submit.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/bin/bash -### -# End-to-end tests for the UserPromptSubmit hook. -# -# UserPromptSubmit is fired each time the user submits a prompt. It creates -# a "Turn N" child span under the session span. If the session root doesn't -# exist yet (e.g. session_start was missed), it back-fills one. -### - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=helpers/assert.sh -source "$SCRIPT_DIR/helpers/assert.sh" -# shellcheck source=helpers/harness.sh -source "$SCRIPT_DIR/helpers/harness.sh" - -_setup_default_stubs() { - stub_response_for "*/v1/project?project_name=*" 200 '{"id":"proj_test"}' - stub_response_for "*/v1/project_logs/*/insert" 200 '{"row_ids":["row_1"]}' -} - -# Helper: run session_start first, ignoring its inserts. -_with_started_session() { - local session_id="$1" - _setup_default_stubs - run_hook session_start.sh "$(fixture_session_start "$session_id" "/tmp/x")" - # Clear capture so subsequent tests see only the new hook's requests - : > "$CAPTURED_REQUESTS" -} - -# --------------------------------------------------------------------------- -describe "user_prompt_submit.sh: with existing session" -# --------------------------------------------------------------------------- - -t_prompt_creates_turn_span() { - _with_started_session "sess-turn-1" - - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-turn-1" "Hello!")" - assert_success "$HOOK_STATUS" - - local count - count=$(span_count) - assert_eq "$count" "1" "expected exactly one new span (the turn)" - - local turn - turn=$(span_by_type "task") - assert_ne "$turn" "null" - - local name - name=$(echo "$turn" | jq -r '.span_attributes.name') - assert_eq "$name" "Turn 1" - - local input - input=$(echo "$turn" | jq -r '.input') - assert_eq "$input" "Hello!" -} - -t_turn_is_child_of_session() { - _with_started_session "sess-parent" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-parent" "Hi")" - - local turn - turn=$(span_by_name "^Turn 1$") - local parent root - parent=$(echo "$turn" | jq -r '.span_parents[0]') - root=$(echo "$turn" | jq -r '.root_span_id') - - # Turn's parent is the session span; its root is the same session id. - assert_eq "$parent" "sess-parent" - assert_eq "$root" "sess-parent" -} - -t_subsequent_prompts_increment_turn_number() { - _with_started_session "sess-multi" - - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-multi" "first")" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-multi" "second")" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-multi" "third")" - - local count - count=$(span_count) - assert_eq "$count" "3" - - # Verify each turn got the right name - local t1 t2 t3 - t1=$(all_spans | jq -r '.[0].span_attributes.name') - t2=$(all_spans | jq -r '.[1].span_attributes.name') - t3=$(all_spans | jq -r '.[2].span_attributes.name') - assert_eq "$t1" "Turn 1" - assert_eq "$t2" "Turn 2" - assert_eq "$t3" "Turn 3" -} - -t_turn_state_stored() { - _with_started_session "sess-state-turn" - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-state-turn" "X")" - - local turn_span_id - turn_span_id=$(get_session_state "sess-state-turn" "current_turn_span_id") - assert_ne "$turn_span_id" "" "current_turn_span_id should be persisted" - - local turn_count - turn_count=$(get_session_state "sess-state-turn" "turn_count") - assert_eq "$turn_count" "1" -} - -it "creates a Turn span on prompt submit" t_prompt_creates_turn_span -it "Turn span is a child of the session span" t_turn_is_child_of_session -it "subsequent prompts increment the turn number" t_subsequent_prompts_increment_turn_number -it "persists current_turn_span_id to state" t_turn_state_stored - -# --------------------------------------------------------------------------- -describe "user_prompt_submit.sh: without prior session_start" -# --------------------------------------------------------------------------- - -t_prompt_backfills_session_root() { - # No session_start ran first. The hook should create both the session - # root span AND the Turn span. - _setup_default_stubs - - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-orphan" "Hello")" - assert_success "$HOOK_STATUS" - - local count - count=$(span_count) - assert_eq "$count" "2" "expected session root + turn (2 spans)" - - # We should have at least one span with type=task named "Claude Code: *" - local session_span - session_span=$(span_by_name "^Claude Code: ") - assert_ne "$session_span" "null" - - local turn_span - turn_span=$(span_by_name "^Turn 1$") - assert_ne "$turn_span" "null" -} - -it "back-fills a session root span if session_start was missed" t_prompt_backfills_session_root - -# --------------------------------------------------------------------------- -describe "user_prompt_submit.sh: tracing disabled" -# --------------------------------------------------------------------------- - -t_prompt_disabled() { - _setup_default_stubs - export TRACE_TO_BRAINTRUST=false - - run_hook user_prompt_submit.sh "$(fixture_user_prompt "sess-off" "x")" - assert_success "$HOOK_STATUS" - - local count - count=$(span_count) - assert_eq "$count" "0" -} - -it "is a no-op when TRACE_TO_BRAINTRUST is false" t_prompt_disabled diff --git a/src/plugins/claude/validate.sh b/src/plugins/claude/validate.sh index 7bb7940..f264c50 100755 --- a/src/plugins/claude/validate.sh +++ b/src/plugins/claude/validate.sh @@ -35,12 +35,36 @@ required=( "plugins/braintrust/skills/troubleshoot-braintrust-mcp/SKILL.md" "plugins/trace-claude-code/.claude-plugin/plugin.json" "plugins/trace-claude-code/hooks/hooks.json" + "plugins/trace-claude-code/bin/claude-hook.sh" + "plugins/trace-claude-code/bin/claude-hook.cmd" ) for rel in "${required[@]}"; do [[ -f "$TARGET_DIR/$rel" ]] || fail "missing $rel" case "$rel" in *.json) check_json "$TARGET_DIR/$rel";; esac done +grep -q "'agents','hook','--source','claude-code'" \ + "$TARGET_DIR/plugins/trace-claude-code/bin/claude-hook.cmd" \ + || fail "Claude Windows hook does not invoke bt agents" +grep -Fq 'call "%BT_HOOK_BIN%" agents hook --help' \ + "$TARGET_DIR/plugins/trace-claude-code/bin/claude-hook.cmd" \ + || fail "Claude Windows hook does not return from bt.cmd compatibility check" +grep -q 'agents hook --source claude-code' \ + "$TARGET_DIR/plugins/trace-claude-code/bin/claude-hook.sh" \ + || fail "Claude Unix hook does not invoke bt agents" + +# The Rust daemon is the only event processor. Shipping any of the legacy +# per-event shell processors would reintroduce two competing trace models. +legacy_hooks=( + common.sh session_start.sh user_prompt_submit.sh user_prompt_expansion.sh + post_tool_use.sh post_tool_use_failure.sh permission_denied.sh stop_hook.sh + session_end.sh worker.sh +) +for hook in "${legacy_hooks[@]}"; do + [[ ! -e "$TARGET_DIR/plugins/trace-claude-code/hooks/$hook" ]] \ + || fail "obsolete Claude processor was packaged: hooks/$hook" +done + # Every marketplace entry's source path must exist in the built tree. if command -v jq >/dev/null 2>&1; then while IFS= read -r p; do diff --git a/src/plugins/codex/build.sh b/src/plugins/codex/build.sh index 5646fd9..eb5f5fd 100755 --- a/src/plugins/codex/build.sh +++ b/src/plugins/codex/build.sh @@ -5,22 +5,10 @@ # The Codex marketplace consumes a repo whose ROOT is the marketplace: # .agents/plugins/marketplace.json marketplace manifest # plugins/braintrust-codex-plugin/ skills plugin (MCP + skills) -# plugins/trace-codex/ tracing plugin (TS event server + hooks) -# -# By default this is a content assembly (a straight copy of content/). The seam -# for injecting shared code later lives here. -# -# trace-codex ships its compiled hook binaries (bin/codex-hook--) -# committed in the dist repo; the launcher execs the host's binary directly (no -# download). Compiling those binaries is gated behind CODEX_BUILD_BINARIES=1 so -# plain content builds (make test) stay fast and need no JS toolchain. publish.sh -# sets the flag, so every real deploy ships fresh binaries. -# -# Env: -# CODEX_BUILD_BINARIES=1 compile + embed the trace-codex binaries (needs -# node + pnpm; pkg downloads base runtimes, so it can -# cross-compile all targets from one host). +# plugins/trace-codex/ thin hooks over the shared bt agents # +# The tracing launcher finds (or installs) `bt` and invokes +# `bt agents hook --source codex`; there is no per-plugin compiled binary. # Usage: build.sh (TARGET_DIR is created if missing) set -euo pipefail @@ -31,35 +19,4 @@ CONTENT_DIR="$SRC_DIR/content" mkdir -p "$TARGET_DIR" rsync -a --delete --exclude '.git' --exclude 'node_modules' "$CONTENT_DIR/" "$TARGET_DIR/" - -if [[ "${CODEX_BUILD_BINARIES:-}" == "1" ]]; then - tc="$TARGET_DIR/plugins/trace-codex" - echo "Compiling trace-codex hook binaries (all platforms)..." - ( cd "$tc" && pnpm install && pnpm run build ) - - # Ad-hoc sign the macOS binaries: unsigned arm64 Mach-O executables are - # SIGKILLed by the kernel on Apple Silicon. `codesign` exists only on macOS; - # `rcodesign` is the cross-platform fallback (Linux). Linux binaries need no - # signature. This is why the codex build runs on a macOS CI runner. - for f in "$tc"/bin/codex-hook-darwin-*; do - [ -f "$f" ] || continue - if command -v codesign >/dev/null 2>&1 && codesign -s - -f "$f" >/dev/null 2>&1; then - continue - fi - if command -v rcodesign >/dev/null 2>&1 && rcodesign sign "$f" >/dev/null 2>&1; then - continue - fi - echo "warning: could not sign $f (no codesign/rcodesign); it will be killed on Apple Silicon" >&2 - done - - # Keep only the compiled binaries; drop build intermediates. - rm -rf "$tc/node_modules" "$tc/dist" - # The plugin .gitignore excludes the binaries (correct for the monorepo, wrong - # for the dist repo). Un-ignore them so the deploy actually commits them. - if [[ -f "$tc/.gitignore" ]]; then - grep -v -E '^bin/codex-hook' "$tc/.gitignore" > "$tc/.gitignore.tmp" - mv "$tc/.gitignore.tmp" "$tc/.gitignore" - fi -fi - echo "Built codex dist into $TARGET_DIR (content from $CONTENT_DIR)." diff --git a/src/plugins/codex/content/AGENTS.md b/src/plugins/codex/content/AGENTS.md index 2e4bf58..a01f921 100644 --- a/src/plugins/codex/content/AGENTS.md +++ b/src/plugins/codex/content/AGENTS.md @@ -22,7 +22,8 @@ Key files for the tracing plugin (see [`plugins/trace-codex/AGENTS.md`](plugins/ - `plugins/trace-codex/AGENTS.md` — architecture and contributor guide for this plugin - `plugins/trace-codex/.codex-plugin/plugin.json` — plugin manifest - `plugins/trace-codex/hooks/hooks.json` — lifecycle hook config -- `plugins/trace-codex/src/` — the hook client + event server (compiled to `bin/codex-hook`) +- `plugins/trace-codex/bin/codex-hook.*` — fail-open shims that invoke the + shared daemon through `bt agents hook --source codex` ## Making changes diff --git a/src/plugins/codex/content/install.sh b/src/plugins/codex/content/install.sh index 402076d..84436e2 100755 --- a/src/plugins/codex/content/install.sh +++ b/src/plugins/codex/content/install.sh @@ -28,25 +28,6 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MARKETPLACE="braintrust-codex-plugins" -# Default port the trace-codex event server listens on (see plugins/trace-codex -# config.ts DEFAULT_PORT). Overridable via the same env var the plugin reads. -EVENT_SERVER_PORT="${BRAINTRUST_EVENT_SERVER_PORT:-52734}" - -# Ask any running trace-codex event server to shut down. After a re-install the -# cache holds a new build, but a server spawned from the OLD build may still be -# running; it would refuse the new version (version mismatch) and block tracing -# until its idle timeout. Shutting it down lets the next session boot a fresh -# one. No server running is the normal case, so failures are ignored. -shutdown_event_server() { - if ! command -v curl >/dev/null 2>&1; then - return 0 - fi - if curl -fsS --max-time 2 -X POST \ - "http://127.0.0.1:$EVENT_SERVER_PORT/shutdown" >/dev/null 2>&1; then - echo " shut down running event server on port $EVENT_SERVER_PORT." - fi -} - # Plugin folders to install. Default: every folder under plugins/. if [ "$#" -gt 0 ]; then PLUGINS=("$@") @@ -61,6 +42,10 @@ if ! command -v codex >/dev/null 2>&1; then echo "Error: 'codex' CLI not found. Install Codex CLI first." >&2 exit 1 fi +if ! command -v bt >/dev/null 2>&1 && [ ! -x "${XDG_BIN_HOME:-$HOME/.local/bin}/bt" ]; then + echo "Installing the bt CLI required by trace-codex..." + curl -fsSL https://bt.dev/cli/install.sh | bash +fi echo "Installing Braintrust Codex plugins from: $REPO_ROOT" echo "" @@ -78,24 +63,6 @@ read_json_field() { grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$1" | head -1 | sed 's/.*"\([^"]*\)"$/\1/' } -# Build a plugin's compiled assets if it has a build (build-on-install). -build_plugin() { - plugin_src="$1" - if [ ! -f "$plugin_src/package.json" ]; then - return 0 - fi - if ! grep -q '"build"' "$plugin_src/package.json"; then - return 0 - fi - if ! command -v pnpm >/dev/null 2>&1; then - echo "Error: '$folder' needs pnpm to build, but 'pnpm' was not found." >&2 - echo " Install pnpm from https://pnpm.io/installation and re-run ./install.sh" >&2 - exit 1 - fi - echo " building (pnpm)..." - ( cd "$plugin_src" && pnpm install --reporter=silent && BUILD_HOST_ONLY=1 pnpm run build ) -} - for folder in "${PLUGINS[@]}"; do plugin_src="$REPO_ROOT/plugins/$folder" manifest="$plugin_src/.codex-plugin/plugin.json" @@ -115,18 +82,10 @@ for folder in "${PLUGINS[@]}"; do fi echo "Installing '$name' (v$version) from plugins/$folder..." - # Build compiled assets first so the marketplace copy includes them. - build_plugin "$plugin_src" # Remove any prior install so the copy is re-synced from the current files. codex plugin remove "$name@$MARKETPLACE" >/dev/null 2>&1 || true codex plugin add "$name" --marketplace "$MARKETPLACE" >/dev/null echo " installed." - - # trace-codex runs a long-lived background event server. Stop any stale one - # left over from a previous build so it doesn't linger with the old version. - if [ "$folder" = "trace-codex" ]; then - shutdown_event_server - fi done echo "" diff --git a/src/plugins/codex/content/plugins/trace-codex/.codex-plugin/plugin.json b/src/plugins/codex/content/plugins/trace-codex/.codex-plugin/plugin.json index 92c893d..0657016 100644 --- a/src/plugins/codex/content/plugins/trace-codex/.codex-plugin/plugin.json +++ b/src/plugins/codex/content/plugins/trace-codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "trace-codex", - "version": "0.1.1", + "version": "0.2.0", "description": "Trace Codex sessions to Braintrust (session, turn, and tool spans).", "author": { "name": "Braintrust", diff --git a/src/plugins/codex/content/plugins/trace-codex/.gitignore b/src/plugins/codex/content/plugins/trace-codex/.gitignore deleted file mode 100644 index 17e411d..0000000 --- a/src/plugins/codex/content/plugins/trace-codex/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules/ -# tsup's intermediate bundle (input to pkg); regenerated by `pnpm run build`. -dist/ -# Built binaries are downloaded at runtime / built locally, never committed. -# The launcher scripts (bin/codex-hook.sh, bin/codex-hook.cmd) are tracked. -bin/codex-hook -bin/codex-hook-* -*.log -# Local user settings; the tracked template is config.json.example. -config.json diff --git a/src/plugins/codex/content/plugins/trace-codex/AGENTS.md b/src/plugins/codex/content/plugins/trace-codex/AGENTS.md index 1b51542..ab2cf87 100644 --- a/src/plugins/codex/content/plugins/trace-codex/AGENTS.md +++ b/src/plugins/codex/content/plugins/trace-codex/AGENTS.md @@ -1,208 +1,39 @@ -# AGENTS.md — trace-codex +# trace-codex -Guidelines for AI agents working in the `trace-codex` plugin. This is the -opt-in plugin that traces Codex sessions to Braintrust. It is **independent** -from the MCP/skills plugin (`braintrust-codex-plugin`) — do not merge behavior -between them. See the [repo AGENTS.md](../../AGENTS.md) for the monorepo -overview. - -## What this plugin is - -A long-lived, local **event server** plus a short-lived **hook client**, both -compiled into a single `codex-hook` binary. Codex fires lifecycle hooks; each -hook invocation runs the client, which forwards the event to the server, which -turns the stream of events into Braintrust spans (session → turn → tool). - -## Architecture - -Request/data flow: +This opt-in plugin is a thin hook shim over the shared Rust tracing daemon in +the `bt` CLI: +```text +Codex hook -> bin/codex-hook.sh|cmd -> bt agents hook --source codex + -> local socket or named pipe -> Rust translator -> Braintrust ``` -Codex hook → bin/codex-hook.sh → codex-hook (client mode) - → POST /enqueue → in-memory FIFO queue - → ProcessorRegistry (one EventProcessor per session) - → Braintrust spans -``` - -Key pieces: - -- `src/index.ts` — entry point. One binary, three modes: `serve` (the - background server), `hook` (default; read one event from stdin, enqueue it), - and `replay` (re-POST a recorded NDJSON session). -- `src/client/` — the hook client. Ensures a server is up (booting a detached - one if needed), POSTs events, and on a terminal event (`Stop`) calls `/flush` - so final spans are delivered before the process tree is torn down. -- `src/server/` — the HTTP server, the FIFO `EventQueue`, routes - (`/enqueue`, `/flush`, `/health`, `/shutdown`), and the idle watchdog. -- `src/processor/` — the generic `ProcessorRegistry` (LRU map of per-session - processors) and the `EventProcessor` interface. -- `src/agents/codex/` — Codex-specific: translate hook payloads into - `EnqueueEvent`s (`event-builder.ts`), build spans (`event-processor.ts`), and - read user settings (`settings.ts`). -- `src/braintrust/` — thin wrapper over the Braintrust SDK (span factory). - -The server is **single-version**: a client whose version doesn't match a running -server bails rather than talking to it. It shuts itself down after an idle -window (default 5 min) or on `/shutdown`. - -The design has three deliberate properties worth understanding before changing -anything: - -### 1. Agent-agnostic "event server" - -The core (`src/server/`, `src/processor/`, `src/braintrust/`) knows nothing -about Codex. It speaks a generic `EnqueueEvent` shape and routes events to a -per-session processor. All Codex-specific knowledge lives in `src/agents/codex/` -and is registered into the generic core at startup. Adding another agent (Claude -Code, opencode, etc.) means adding a sibling `src/agents//` module that -exports the same `Agent` shape — the server, queue, and registry should not need -to change. - -Keep this boundary clean: nothing under `src/server/`, `src/processor/`, or -`src/braintrust/` should reference Codex by name. The generic event server may -eventually be extracted into its own package, so treat any Codex-specific leak -into the generic layers as a bug, not a shortcut. - -### 2. Node-compiled, cross-platform binaries - -The hook command in `hooks.json` is a fixed, platform-agnostic string that -invokes `bin/codex-hook.sh` (or `.cmd` on Windows), **not** the binary directly. -That launcher resolves and runs the real, platform-specific `codex-hook` binary. -- `scripts/build.ts` runs a two-step build: `tsup` bundles `src/index.ts` into a - single self-contained CommonJS file (`dist/codex-hook.cjs`, inlining the - `braintrust` dependency), then `@yao-pkg/pkg` wraps that bundle plus a Node - runtime into a standalone executable per target (`darwin-arm64/x64`, - `linux-x64/arm64`; Windows slots in but isn't built yet). `BUILD_HOST_ONLY=1` - builds just the host target (used by the repo-root `install.sh` for local dev - installs). -- **Build the release on macOS.** pkg code-signs the darwin binaries via the - `codesign` utility (macOS-only), and Apple Silicon kills an unsigned arm64 - binary on launch. A macOS host cross-compiles the Linux targets too, so one - job produces all four signed/valid binaries (see `release.yaml`). -- **Re-exec caveat (pkg):** the hook spawns the server by re-executing - `process.execPath` with `serve`. pkg's child_process patch would otherwise make - the child act like `node` and treat `serve` as a script path, so - `spawn-server.ts` pre-sets `PKG_EXECPATH` to a non-exec-path sentinel to force - packaged-app mode. See the comment there. -- The compiled binaries are **large and not committed**. The launcher downloads - the matching binary from the plugin's GitHub release on first use and caches - it at `$PLUGIN_ROOT/bin/codex-hook`. Codex wipes `$PLUGIN_ROOT` on every - install/upgrade, so the cache self-invalidates and the next hook re-downloads - the right version. -- Plugin version is the single source of truth in - `.codex-plugin/plugin.json`; the launcher reads it to construct the release - tag (`trace-codex-v`). +The launchers must always fail open: log errors to stderr and exit successfully +so tracing cannot fail a Codex turn. They may discover or install `bt`, map +agent-specific environment compatibility variables, and forward invocation +metadata. They must not parse event sequences, construct spans, persist daemon +state, resolve credentials, or deliver rows. -**Hard rule:** the hook must never fail the Codex turn. The launcher and client -log to stderr and exit 0 on any error; the server swallows errors so a turn is -never blocked by tracing. +Codex state-machine logic belongs in +`bt-daemon/src/translate/codex.rs`. Shared lifecycle, journal, transport, and +Braintrust delivery logic belongs elsewhere in that crate. The daemon's shared +`config.json` is for non-secret tracing behavior only; authentication and +backend selection belong to `bt`. Parent/root attachment remains per invocation +because it describes the surrounding trace context rather than a global plugin +setting. -### 3. Hooks are non-blocking by default +Build and validate all plugin packages with: -Tracing must not degrade the experience of using the coding agent. Every hook -fires synchronously in Codex's path, so blocking work on a hook directly adds -latency to the user's session. The design therefore makes the common path -fire-and-forget: the client POSTs to `/enqueue` and returns immediately, while -the background server does the slow work (Braintrust SDK calls, flushes) off the -critical path. - -The terminal event is the one place where blocking is even an option, and it's -**opt-in**: - -- On a **terminal event** (`Stop`), the client calls `/flush` **only** when - `BRAINTRUST_FLUSH_ON_TURN_END` is set, blocking until the server confirms the - queue has drained and buffered spans reached Braintrust. The blocking mode - exists for short-lived hosts (e.g. a CI job that ends right after the last turn) - that would otherwise tear the process tree down before the final spans are - delivered. When enabled, the wait is bounded by a timeout (`/flush` gives up - rather than hanging the turn). By default the client does nothing on `Stop`: the - long-lived server flushes on its own when the queue goes idle, so in normal - interactive use no spans are lost and the turn isn't stalled by a flush request. - -When adding behavior, prefer enqueue-and-return. Reach for a blocking -request/await only when data would otherwise be lost, keep it off the -per-hook hot path where possible, and always bound it so a slow or stuck backend -can't stall the agent. - -### 4. Sessions can outlive the server (resume) - -The server is short-lived (idle shutdown after ~5 min, or the user closes and -later resumes). A Codex session, though, can span that gap. To avoid dropping -the tail of a trace — or worse, re-emitting it as duplicate/orphaned spans — the -Codex processor **persists its resumable state** and rehydrates it when a new -processor is created for the same session id. - -How it works: - -- The transcript (rollout JSONL) on disk is the durable source of truth for span - *content*. What a restart loses is the processor's in-memory bookkeeping: - transcript byte offsets, which spans are still open (and their identities), - the reconstructed conversation history, and the subagent/compaction side-maps. -- On every `flush()` (idle drain, eviction, terminal Stop, shutdown), the - processor writes a JSON snapshot of that bookkeeping to - `PLUGIN_DATA/state/.json` (`src/agents/codex/snapshot-store.ts`). - Span handles are stored as identities (`span_id`/`root_span_id`/`parents` plus - name/type), captured from the SDK's synchronous span getters. -- On its first event, a processor attempts a one-time restore for its session - id. If a compatible snapshot exists, it recreates each span handle bound to the - original id via `SpanFactory.rehydrateSpan` (Braintrust merges rows by - `span_id`, so further `log()`/`end()` calls continue the same trace). Restore - runs **before** the tracing-enabled master switch, because the snapshot also - carries the session's reporting config (a bare mid-session restart may have no - leading config event). -- The snapshot is **not** deleted when the root span ends. Codex's `Stop` hook is - per-**turn**, not per-session (there is no session-end hook), and we end the - root on the first `Stop` — but later turns still attach as children of that same - root, so a restart *between* turns must be able to resume. The snapshot - therefore persists past root-end; stale ones (sessions that never resume) are - reclaimed by a startup GC sweep that removes snapshots older than a TTL. - -Invariants to preserve when changing this: - -- **Persistence is Codex-owned**, not part of the generic event server: *where* a - plugin may persist state is the host agent's call (Codex gives us - `PLUGIN_DATA`). Keep the store and snapshot shape under `src/agents/codex/`. - The only generic addition is `SpanFactory.rehydrateSpan` (pure SDK behavior). -- **Never persist secrets.** The `apiKey` is stripped from the snapshot's - reporting config and re-resolved from env / the config event on resume (mirrors - the event recorder's redaction). -- **Never throw.** The store swallows and logs all I/O errors; a persistence or - restore failure must not break a turn — it just falls back to starting fresh. -- **Version-gate snapshots.** Each carries `pluginVersion` + a schema version; - a mismatch discards the snapshot. Bump `SNAPSHOT_SCHEMA_VERSION` in - `state-snapshot.ts` whenever the shape changes incompatibly. - -## Making changes - -- **Generic vs. agent code**: put agent-specific logic in `src/agents//`; - keep the server/processor/braintrust layers agent-agnostic. -- **Config**: two layers. The agent-specific layer (`src/agents/codex/settings.ts`) - reads the user's `config.json` from `PLUGIN_DATA`, maps its friendly camelCase - keys onto `BRAINTRUST_*` / `BRAINTRUST_EVENT_SERVER_*` env vars (the file wins - over env), and is run by the hook client before it boots the server — so the - spawned server inherits the resolved env. The generic layer (`src/config.ts`) - then reads **only** those env vars and never touches `config.json`. Keep it - that way: config-file parsing is deliberately agent-specific. User-facing - settings are documented in the [README](./README.md); update it (and the - `Settings` map in `settings.ts`) when adding one. -- **Hooks**: `hooks/hooks.json` lists the lifecycle events wired to the client. - Most are forwarded but not yet turned into spans (the processor no-ops the - ones it doesn't handle). -- **Build**: edit `scripts/build.ts` for targets/output; the launcher scripts - (`bin/codex-hook.sh`, `bin/codex-hook.cmd`) for download/exec behavior. - -## Commands +```bash +make test +``` -Run from `plugins/trace-codex/`: +Test the tracing implementation with: -- `pnpm test` — run the test suite with vitest (tests live next to sources as - `*.test.ts`). -- `pnpm run typecheck` — `tsc --noEmit`. -- `pnpm run lint` / `pnpm run check` — Biome lint / lint+format. -- `pnpm run build` — build all target binaries via tsup + pkg - (`BUILD_HOST_ONLY=1` for host only). -- `pnpm run dev` — run the server in watch mode (tsx). +```bash +cargo test --manifest-path bt-daemon/Cargo.toml --all-features +``` -Add or update tests alongside any behavior change; keep typecheck and lint -clean. +Keep `.sh` and `.cmd` behavior aligned. Add translator or pipeline tests for +behavioral changes, and update the plugin README and +`bt-daemon/config.json.example` when shared settings change. diff --git a/src/plugins/codex/content/plugins/trace-codex/Makefile b/src/plugins/codex/content/plugins/trace-codex/Makefile index b479a14..1965660 100644 --- a/src/plugins/codex/content/plugins/trace-codex/Makefile +++ b/src/plugins/codex/content/plugins/trace-codex/Makefile @@ -1,108 +1,9 @@ -# Makefile for the Braintrust Codex tracing plugin. -# -# Targets: -# make test Lint + typecheck + unit tests + a full build + live -# integration check. -# make lint Run the Biome linter/formatter check (no writes). -# make typecheck Run the TypeScript type checker. -# make unit Run only the unit tests (vitest). -# make build Compile the host binary (and per-platform binaries). -# make integration Build, boot the server via the hook binary, health-check, -# assert the version, then shut it down. -# make smoke End-to-end: run a real `codex exec` session with the -# installed plugin, traced to a local mock collector, and -# assert >=1 trace row. Requires codex, node, pnpm, and -# OPENAI_API_KEY; optionally SMOKE_VERSION to pin a release. -# make token-proxy Run the token-counting proxy (debugging aid) that sits -# between Codex and OpenAI and prints each call's token usage -# for comparison against the trace. Requires OPENAI_API_KEY. -# See scripts/token-proxy.ts for the config.toml snippet. -# make clean Remove build artifacts. - -SHELL := /bin/bash - -# Port used only by the integration test. Chosen from the IANA dynamic/private -# range (49152-65535) and deliberately different from the default (52734) so -# the test never collides with a real running server. -TEST_PORT ?= 54219 - -# Expected /health version, read from the plugin manifest (single source of truth). -EXPECTED_VERSION := $(shell sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' .codex-plugin/plugin.json | head -1) - -BIN := bin/codex-hook -BASE_URL := http://127.0.0.1:$(TEST_PORT) - -.PHONY: test lint typecheck unit build integration smoke token-proxy clean - -test: lint typecheck unit integration - -lint: - @echo "==> Linting (biome)" - pnpm run check - -typecheck: - @echo "==> Typechecking (tsc)" - pnpm run typecheck - -unit: - @echo "==> Running unit tests" - pnpm test - -build: - @echo "==> Building binaries" - BUILD_HOST_ONLY=1 pnpm run build - -integration: build - @echo "==> Integration test (port $(TEST_PORT), expecting version $(EXPECTED_VERSION))" - @set -euo pipefail; \ - export BRAINTRUST_EVENT_SERVER_PORT=$(TEST_PORT); \ - export BRAINTRUST_EVENT_SERVER_LOG_DIR="$$(mktemp -d)"; \ - cleanup() { curl -s -o /dev/null -X POST $(BASE_URL)/shutdown || true; }; \ - trap cleanup EXIT; \ - echo " -> invoking hook dispatcher (boots server in background)"; \ - echo '{"hook_event_name":"SessionStart","session_id":"make-test"}' | "$(BIN)" hook; \ - echo " -> waiting for /health on $(BASE_URL)"; \ - version=""; \ - for i in $$(seq 1 50); do \ - body="$$(curl -s --max-time 1 $(BASE_URL)/health || true)"; \ - if [ -n "$$body" ]; then version="$$body"; break; fi; \ - sleep 0.1; \ - done; \ - if [ -z "$$version" ]; then echo "FAIL: server never became healthy on $(BASE_URL)"; exit 1; fi; \ - echo " -> /health returned: $$version"; \ - expected='{"version":"$(EXPECTED_VERSION)"}'; \ - if [ "$$version" != "$$expected" ]; then \ - echo "FAIL: version mismatch"; echo " expected: $$expected"; echo " actual: $$version"; exit 1; \ - fi; \ - echo " -> version matches ($(EXPECTED_VERSION))"; \ - echo " -> calling /shutdown"; \ - code="$$(curl -s -o /dev/null -w '%{http_code}' -X POST $(BASE_URL)/shutdown)"; \ - if [ "$$code" != "200" ]; then echo "FAIL: /shutdown returned $$code (expected 200)"; exit 1; fi; \ - echo " -> /shutdown returned 200"; \ - echo " -> verifying server stopped"; \ - stopped=0; \ - for i in $$(seq 1 30); do \ - if ! curl -s -o /dev/null --max-time 1 $(BASE_URL)/health; then stopped=1; break; fi; \ - sleep 0.1; \ - done; \ - if [ "$$stopped" != "1" ]; then echo "FAIL: server still responding after shutdown"; exit 1; fi; \ - echo " -> server stopped cleanly"; \ - echo "PASS: integration test" - -# End-to-end smoke test. Assumes the plugin is installed in Codex (run -# `../../install.sh trace-codex` first for a local dev install). Set -# SMOKE_VERSION to pin the release whose binary the launcher downloads; leave it -# unset to use the installed plugin manifest version / local dev binary. -smoke: - @echo "==> Smoke test" - SMOKE_RELEASE_VERSION="$(SMOKE_VERSION)" sh scripts/smoke-test.sh - -# Token-counting proxy for verifying trace-codex's token accounting. Run it, then -# point Codex at it via a custom model_provider (see scripts/token-proxy.ts). -token-proxy: - @echo "==> Token proxy (Ctrl-C for the session total)" - pnpm exec tsx scripts/token-proxy.ts - -clean: - @echo "==> Cleaning" - rm -rf bin +.PHONY: test + +# Runtime validation for the thin shared-daemon shim. +test: + @sh -n bin/codex-hook.sh + @grep -q "'agents','hook','--source','codex'" bin/codex-hook.cmd + @grep -Fq 'call "%BT_HOOK_BIN%" agents hook --help' bin/codex-hook.cmd + @jq empty hooks/hooks.json .codex-plugin/plugin.json + @echo "trace-codex shim OK" diff --git a/src/plugins/codex/content/plugins/trace-codex/README.md b/src/plugins/codex/content/plugins/trace-codex/README.md index ba8ce15..3435f2f 100644 --- a/src/plugins/codex/content/plugins/trace-codex/README.md +++ b/src/plugins/codex/content/plugins/trace-codex/README.md @@ -1,141 +1,85 @@ -# Braintrust Codex Tracing Plugin +# Braintrust Codex tracing plugin -A [Codex plugin](https://developers.openai.com/codex/plugins) that wires Codex lifecycle hooks as a foundation for sending Codex sessions to Braintrust as traces. +An opt-in Codex plugin that forwards lifecycle events to the shared Braintrust +daemon built into the `bt` CLI. The daemon tails Codex rollout transcripts and +creates a session, turn, LLM, and tool trace without doing parsing or network +delivery in the blocking hook process. -## Quickstart +## Setup -```bash -codex plugin marketplace add braintrustdata/braintrust-codex-plugin -codex plugin add trace-codex@braintrust-codex-plugins -``` - -Create an API key in Braintrust under **Settings > API keys**. The key must be available in the environment where Codex runs. Either export it in your current shell before starting Codex: +Install and authenticate the current `bt` CLI: ```bash -export BRAINTRUST_API_KEY="" -# NOTE: tracing must be explicitly enabled -# upon first run, codex will prompt for plugin permissions -TRACE_TO_BRAINTRUST=true BRAINTRUST_PROJECT=my-coding-agent codex +curl -fsSL https://bt.dev/cli/install.sh | bash +bt auth login ``` -Or set it for only that Codex invocation: +Then install the plugin and enable tracing: ```bash -BRAINTRUST_API_KEY="" TRACE_TO_BRAINTRUST=true BRAINTRUST_PROJECT=my-coding-agent codex -``` - -To upgrade: +codex plugin marketplace add braintrustdata/braintrust-codex-plugin +codex plugin add trace-codex@braintrust-codex-plugins -```bash -codex plugin marketplace upgrade braintrust-codex-plugins +TRACE_TO_BRAINTRUST=true BRAINTRUST_PROJECT=my-coding-agent codex ``` -## Using the plugin in CI - -Tracing `codex exec` runs in CI works. A few notes: - -- **Block on flush.** Set `BRAINTRUST_FLUSH_ON_TURN_END=true` so the final spans are delivered before the job exits (a short-lived CI job can otherwise tear down before the background server's idle-drain flush fires). -- **Trust the hooks non-interactively.** Pass `codex exec --dangerously-bypass-hook-trust` to skip the one-time interactive hook-trust prompt. (Only for plugins you trust.) - -GitHub Actions example: - -```yaml -name: codex-traced -on: [workflow_dispatch] - -jobs: - run: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - - name: Install Codex CLI - run: npm install -g @openai/codex # or your preferred install method - - - name: Install the trace-codex plugin (pinned to a release tag) - # TODO: replace with desired version ---------------------------------vvvvvvvvvvvvvvvvvv - run: | - codex plugin marketplace add braintrustdata/braintrust-codex-plugin@trace-codex-v0.0.X - codex plugin add trace-codex@braintrust-codex-plugins - - - name: Run a traced Codex session - env: - TRACE_TO_BRAINTRUST: "true" - BRAINTRUST_API_KEY: ${{ secrets.BRAINTRUST_API_KEY }} - BRAINTRUST_PROJECT: my-coding-agent # <--- TODO: replace with your project name - BRAINTRUST_FLUSH_ON_TURN_END: "true" - run: | - codex exec \ - --skip-git-repo-check \ - --dangerously-bypass-hook-trust \ - --sandbox read-only \ - "summarize the changes in this repo" +The hook is fail-open: tracing errors are written to stderr and never fail a +Codex turn. If a daemon-capable `bt` is unavailable, the Unix launcher queues +the current event when possible and starts a best-effort background install. + +## Shared configuration + +All coding-agent plugins connected to the daemon use the same non-credential +JSON settings. Set `BT_DAEMON_CONFIG` to choose the file. Otherwise it is +`config.json` inside `BT_DAEMON_DATA_DIR`, or inside the daemon's default state +directory (`~/.braintrust/state/bt-daemon` on Unix and +`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows). + +```json +{ + "traceToBraintrust": true, + "project": "coding-agents", + "flushOnTurnEnd": false, + "additionalMetadata": { + "team": "platform" + } +} ``` -If your plugin release repo is private, also expose a token the launcher can use to download the binary (`GH_TOKEN` or `GITHUB_TOKEN`, or an authenticated `gh`). +For supported keys, a value in the shared file overrides its environment +fallback. Omitted keys retain the existing plugin environment behavior: -## Configuration +| Shared key | Environment fallback | Default | +|---|---|---:| +| `traceToBraintrust` | `TRACE_TO_BRAINTRUST` | `false` | +| `project` | `BRAINTRUST_PROJECT` / `bt` project configuration | resolved by `bt` | +| `flushOnTurnEnd` | `BRAINTRUST_FLUSH_ON_TURN_END` | `false` | +| `additionalMetadata` | `BRAINTRUST_ADDITIONAL_METADATA` | unset | -There are two ways to configure the plugin, environment variables and a config file. +Do not put API keys, auth tokens, organization credentials, or backend URLs in +this file. They are intentionally ignored: `bt` owns profile, OAuth, keychain, +token refresh, and backend resolution. -``` -cp ~/.codex/plugins/cache/braintrust-codex-plugins/trace-codex//config.json.example ~/.codex/plugins/data/trace-codex-braintrust-codex-plugins/config.json -# now edit config.json with your desired settings -``` +`CODEX_PARENT_SPAN_ID` and `CODEX_ROOT_SPAN_ID` remain per-process environment +variables because they attach this Codex invocation to an existing trace. -Every setting can be provided as a `config.json` key or as an environment variable; **`config.json` always overrides the environment variable**. An environment variable only takes effect for settings that `config.json` leaves unset. +Both `bin/codex-hook.sh` and `bin/codex-hook.cmd` forward events to `bt`. On +Windows, the daemon uses a local named pipe. -| `config.json` key | Environment variable | Default | Meaning | -|----------------------|---------------------------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------| -| `traceToBraintrust` | `TRACE_TO_BRAINTRUST` | `false` | Master switch. **When `false` or unset, no traces are reported** Set `true` to enable tracing. | -| `apiKey` | `BRAINTRUST_API_KEY` | _(unset)_ | Braintrust API key. | -| `project` | `BRAINTRUST_PROJECT` | _(unset)_ | Project to log traces into. | -| `apiUrl` | `BRAINTRUST_API_URL` | api.braintrust.dev | Braintrust API URL. | -| `additionalMetadata` | `BRAINTRUST_ADDITIONAL_METADATA` | _(unset)_ | JSON object of extra metadata merged into the root span. Standard keys (`session_id`, `model`, `project`, etc.) take precedence on conflict. | -| `parentSpanId` | `CODEX_PARENT_SPAN_ID` | _(unset)_ | Existing Braintrust span to attach the Codex session under. If `rootSpanId` is unset, this is also used as the trace root. | -| `rootSpanId` | `CODEX_ROOT_SPAN_ID` | _(unset)_ | Root span id for the existing trace. If `parentSpanId` is unset, this is also used as the parent. | -| `flushOnTurnEnd` | `BRAINTRUST_FLUSH_ON_TURN_END` | `false` | When `true`, the hook blocks at each turn's end (the `Stop` event) until the server confirms all spans are flushed. Use in programmatic/CI runs to guarantee traces are delivered before Codex exits. | -| `recordFile` | `BRAINTRUST_EVENT_SERVER_RECORD_FILE` | _(unset)_ | If set, record every event to this NDJSON file (for `replay`). | +## CI -### Add a Codex trace to an existing trace - -You can attach a Codex session to an existing Braintrust trace by passing `CODEX_PARENT_SPAN_ID`: +For short-lived jobs, enable `flushOnTurnEnd` in the shared file or set +`BRAINTRUST_FLUSH_ON_TURN_END=true`: ```bash -TRACE_TO_BRAINTRUST=true CODEX_PARENT_SPAN_ID=your-parent-span-id codex +BRAINTRUST_FLUSH_ON_TURN_END=true \ +TRACE_TO_BRAINTRUST=true \ +BRAINTRUST_PROJECT=ci-agents \ +codex exec --dangerously-bypass-hook-trust "summarize this repository" ``` -If the parent span is not the trace root, also pass `CODEX_ROOT_SPAN_ID`: +Inspect the daemon and its sessions with: ```bash -TRACE_TO_BRAINTRUST=true CODEX_PARENT_SPAN_ID=parent-span-id CODEX_ROOT_SPAN_ID=root-span-id codex -``` - -The Codex session and all its turns/tools will appear as children of your parent span in Braintrust. - -### Resuming sessions - -Note that when resuming a session, the original session's options will remain in effect. - -For example: - -```sh -TRACE_TO_BRAINTRUST=true codex # first session enables braintrust -TRACE_TO_BRAINTRUST=false codex resume abcde # the resumed session will still be traced because the original session was +bt agents status ``` - -The trace itself also survives the background server stopping. The server shuts down after the idle window (or on an explicit shutdown), but a session can outlive it — you might leave it open past the timeout, send another message after the server stopped, or `codex resume` later. The plugin snapshots each session's in-progress trace state under `$PLUGIN_DATA/state/` and restores it when the session continues, so later turns keep landing in the same trace instead of starting a new one. Stale snapshots age out automatically, and secrets (your API key) are never written to them. - -### Advanced Options - -Advanced plugin settings for debugging or developing the plugin - - -| `config.json` key | Environment variable | Default | Meaning | -|-----------------------|--------------------------------------------------|----------------|-------------------------------------------------| -| `port` | `BRAINTRUST_EVENT_SERVER_PORT` | `52734` | Loopback port for the server. | -| `idleTimeoutMs` | `BRAINTRUST_EVENT_SERVER_IDLE_TIMEOUT_MS` | `300000` | Idle shutdown window. | -| `idleCheckIntervalMs` | `BRAINTRUST_EVENT_SERVER_IDLE_CHECK_INTERVAL_MS` | `30000` | Idle watchdog cadence. | -| _(none)_ | `BRAINTRUST_EVENT_SERVER_LOG_DIR` | `$PLUGIN_DATA` | Directory for logs, pidfile, and `config.json`. | - -The config file is read by the hook client only at the moment it boots the background server (the running server keeps the config it started with). To pick up config changes, stop the server (or wait for it to idle out) so the next event re-boots it. diff --git a/src/plugins/codex/content/plugins/trace-codex/bin/codex-hook.cmd b/src/plugins/codex/content/plugins/trace-codex/bin/codex-hook.cmd index 9c1525c..cac6acd 100644 --- a/src/plugins/codex/content/plugins/trace-codex/bin/codex-hook.cmd +++ b/src/plugins/codex/content/plugins/trace-codex/bin/codex-hook.cmd @@ -1,9 +1,36 @@ @echo off -REM Windows launcher for the trace-codex hook binary. -REM -REM Windows is not supported yet. This stub exits 0 so it never fails a Codex -REM turn; tracing simply does nothing on Windows for now. When Windows support -REM lands, this will mirror codex-hook.sh: detect arch, download the matching -REM codex-hook.exe from the GitHub release into %PLUGIN_ROOT%\bin, and exec it. -echo trace-codex: Windows support coming soon; tracing disabled this session.>&2 +REM Thin, fail-open Codex hook shim for the shared Braintrust daemon. +REM Invokes: bt agents hook --source codex +setlocal EnableExtensions DisableDelayedExpansion + +set "BT_HOOK_BIN=" +for /f "delims=" %%B in ('where bt.exe 2^>nul') do if not defined BT_HOOK_BIN set "BT_HOOK_BIN=%%B" +if not defined BT_HOOK_BIN for /f "delims=" %%B in ('where bt.cmd 2^>nul') do if not defined BT_HOOK_BIN set "BT_HOOK_BIN=%%B" +if not defined BT_HOOK_BIN for /f "delims=" %%B in ('where bt 2^>nul') do if not defined BT_HOOK_BIN set "BT_HOOK_BIN=%%B" +if not defined BT_HOOK_BIN if exist "%USERPROFILE%\.local\bin\bt.exe" set "BT_HOOK_BIN=%USERPROFILE%\.local\bin\bt.exe" +if not defined BT_HOOK_BIN ( + echo trace-codex: bt CLI is unavailable; tracing disabled for this event.>&2 + exit /b 0 +) + +call "%BT_HOOK_BIN%" agents hook --help >nul 2>&1 +if errorlevel 1 ( + echo trace-codex: a daemon-capable bt CLI is unavailable; tracing disabled for this event.>&2 + exit /b 0 +) + +if not defined BRAINTRUST_DEFAULT_PROJECT if defined BRAINTRUST_PROJECT set "BRAINTRUST_DEFAULT_PROJECT=%BRAINTRUST_PROJECT%" + +set "BT_PLUGIN_JSON=%~dp0..\.codex-plugin\plugin.json" +powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ^ + "$ErrorActionPreference='SilentlyContinue';" ^ + "$a=@('agents','hook','--source','codex');" ^ + "if(Test-Path $env:BT_PLUGIN_JSON){$v=(Get-Content -Raw $env:BT_PLUGIN_JSON|ConvertFrom-Json).version;if($v){$a+=@('--source-version',[string]$v)}};" ^ + "if($env:BRAINTRUST_FLUSH_ON_TURN_END -match '^(?i:1|true|yes|on)$'){$a+='--flush-on-turn-end'};" ^ + "if($env:CODEX_PARENT_SPAN_ID){$a+=@('--parent-span-id',$env:CODEX_PARENT_SPAN_ID)};" ^ + "if($env:CODEX_ROOT_SPAN_ID){$a+=@('--root-span-id',$env:CODEX_ROOT_SPAN_ID)};" ^ + "if($env:BRAINTRUST_ADDITIONAL_METADATA){$a+=@('--additional-metadata',$env:BRAINTRUST_ADDITIONAL_METADATA)};" ^ + "& $env:BT_HOOK_BIN @a;" ^ + "if($LASTEXITCODE -ne 0){[Console]::Error.WriteLine('trace-codex: bt agents hook failed non-fatally')};exit 0" + exit /b 0 diff --git a/src/plugins/codex/content/plugins/trace-codex/bin/codex-hook.sh b/src/plugins/codex/content/plugins/trace-codex/bin/codex-hook.sh index 4998391..326e281 100755 --- a/src/plugins/codex/content/plugins/trace-codex/bin/codex-hook.sh +++ b/src/plugins/codex/content/plugins/trace-codex/bin/codex-hook.sh @@ -1,45 +1,79 @@ #!/bin/sh -# Launcher for the trace-codex hook binary. -# -# hooks.json invokes this script (a fixed, platform-agnostic command). The -# platform-specific binaries ship in this plugin under bin/ as -# codex-hook-- (committed to the distribution repo at release time); -# this launcher picks the one matching the host and execs it. -# -# (Older versions downloaded the binary from GitHub Releases to avoid committing -# it to VCS. The binaries now ship in the dist repo, so there is no network -# path and no version/tag parsing here.) -# -# Hard rule: never fail the Codex turn. Any problem logs to stderr and exits 0 -# (Codex treats a 0 exit with no stdout as success). +# Thin, fail-open Codex hook shim for the shared Braintrust daemon. set -u -log() { printf 'trace-codex launcher: %s\n' "$1" >&2; } +log() { printf 'trace-codex: %s\n' "$1" >&2; } +truthy() { + case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in + 1 | true | yes | on) return 0 ;; + *) return 1 ;; + esac +} +compatible_bt() { + [ -n "${1:-}" ] && [ -x "$1" ] && "$1" agents hook --help >/dev/null 2>&1 +} -# PLUGIN_ROOT is set by Codex to the installed plugin directory. Fall back to -# this script's own parent so the launcher is runnable standalone. -SCRIPT_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) -ROOT="${PLUGIN_ROOT:-$(dirname "$SCRIPT_DIR")}" +# Build the eventual bt invocation before checking availability so a first-run +# event can be replayed after the background installer completes. +ROOT="${PLUGIN_ROOT:-$(CDPATH= cd "$(dirname "$0")/.." && pwd)}" +version=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$ROOT/.codex-plugin/plugin.json" 2>/dev/null | head -1) -# Map uname output to our binary suffix (-). -os=$(uname -s 2>/dev/null || echo unknown) -arch=$(uname -m 2>/dev/null || echo unknown) -case "$os" in - Darwin) os_name=darwin ;; - Linux) os_name=linux ;; - *) log "unsupported OS '$os'; tracing disabled this session"; exit 0 ;; -esac -case "$arch" in - arm64 | aarch64) arch_name=arm64 ;; - x86_64 | amd64) arch_name=x64 ;; - *) log "unsupported arch '$arch'; tracing disabled this session"; exit 0 ;; -esac +set -- agents hook --source codex +[ -z "$version" ] || set -- "$@" --source-version "$version" +truthy "${BRAINTRUST_FLUSH_ON_TURN_END:-false}" && set -- "$@" --flush-on-turn-end +[ -z "${CODEX_PARENT_SPAN_ID:-}" ] || set -- "$@" --parent-span-id "$CODEX_PARENT_SPAN_ID" +[ -z "${CODEX_ROOT_SPAN_ID:-}" ] || set -- "$@" --root-span-id "$CODEX_ROOT_SPAN_ID" +[ -z "${BRAINTRUST_ADDITIONAL_METADATA:-}" ] \ + || set -- "$@" --additional-metadata "$BRAINTRUST_ADDITIONAL_METADATA" -BIN="$ROOT/bin/codex-hook-$os_name-$arch_name" -if [ ! -x "$BIN" ]; then - log "no binary at $BIN; tracing disabled this session" +BT_BIN=$(command -v bt 2>/dev/null || true) +LOCAL_BT="${XDG_BIN_HOME:-$HOME/.local/bin}/bt" +if ! compatible_bt "$BT_BIN" && compatible_bt "$LOCAL_BT"; then + BT_BIN="$LOCAL_BT" +fi +if ! compatible_bt "$BT_BIN"; then + BT_BIN="" +fi +if [ -z "$BT_BIN" ] && command -v curl >/dev/null 2>&1; then + # Never install synchronously in a blocking hook. Securely spool this event, + # then install and forward it in the detached child so a first SessionStart + # does not lose hook-only source/permission metadata. + umask 077 + pending=$(mktemp "${TMPDIR:-/tmp}/trace-codex-event.XXXXXX" 2>/dev/null || true) + if [ -n "$pending" ] && cat >"$pending"; then + nohup sh -c ' + pending=$1 + shift + trap '"'"'rm -f "$pending"'"'"' 0 + curl -fsSL --max-time 20 https://bt.dev/cli/install.sh | sh >/dev/null 2>&1 || exit 0 + bt_bin=$(command -v bt 2>/dev/null || true) + if [ -z "$bt_bin" ] && [ -x "${XDG_BIN_HOME:-$HOME/.local/bin}/bt" ]; then + bt_bin="${XDG_BIN_HOME:-$HOME/.local/bin}/bt" + fi + [ -z "$bt_bin" ] || "$bt_bin" "$@" <"$pending" >/dev/null 2>&1 + ' trace-codex-install "$pending" "$@" /dev/null 2>&1 & + log "a daemon-capable bt CLI is unavailable; queued this event behind a background install or upgrade" + else + [ -z "$pending" ] || rm -f "$pending" + nohup sh -c 'curl -fsSL --max-time 20 https://bt.dev/cli/install.sh | sh' \ + /dev/null 2>&1 & + log "a daemon-capable bt CLI is unavailable; started a background install or upgrade but could not queue this event" + fi + exit 0 +fi +if [ -z "$BT_BIN" ]; then + log "a daemon-capable bt CLI is unavailable; tracing disabled for this event" exit 0 fi -exec "$BIN" "$@" +# The standalone Codex plugin has historically exposed BRAINTRUST_PROJECT, +# while bt's global project option uses BRAINTRUST_DEFAULT_PROJECT. Preserve +# the documented plugin contract without overriding an explicit bt default. +if [ -z "${BRAINTRUST_DEFAULT_PROJECT:-}" ] && [ -n "${BRAINTRUST_PROJECT:-}" ]; then + export BRAINTRUST_DEFAULT_PROJECT="$BRAINTRUST_PROJECT" +fi + +"$BT_BIN" "$@" || log "bt agents hook failed non-fatally" +exit 0 diff --git a/src/plugins/codex/content/plugins/trace-codex/biome.json b/src/plugins/codex/content/plugins/trace-codex/biome.json deleted file mode 100644 index 4391881..0000000 --- a/src/plugins/codex/content/plugins/trace-codex/biome.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": true - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 100 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "semicolons": "always" - } - } -} diff --git a/src/plugins/codex/content/plugins/trace-codex/config.json.example b/src/plugins/codex/content/plugins/trace-codex/config.json.example deleted file mode 100644 index 5fba6fb..0000000 --- a/src/plugins/codex/content/plugins/trace-codex/config.json.example +++ /dev/null @@ -1,16 +0,0 @@ -{ - "_comment": "Copy this file to your plugin data dir as config.json (e.g. ~/.codex/plugins/data/trace-codex-/config.json) and fill in your values. All keys are optional. Environment variables override these values. See README.md for details.", - "traceToBraintrust": true, - "apiKey": "sk-...", - "apiUrl": "https://api.braintrust.dev", - "appUrl": "https://www.braintrust.dev", - "project": "my-codex-project", - "additionalMetadata": { "team": "platform", "env": "dev" }, - "parentSpanId": "", - "rootSpanId": "", - "flushOnTurnEnd": false, - "recordFile": "", - "port": 52734, - "idleTimeoutMs": 300000, - "idleCheckIntervalMs": 30000 -} diff --git a/src/plugins/codex/content/plugins/trace-codex/package.json b/src/plugins/codex/content/plugins/trace-codex/package.json deleted file mode 100644 index 5f6d0ab..0000000 --- a/src/plugins/codex/content/plugins/trace-codex/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "trace-codex", - "version": "0.1.0", - "private": true, - "description": "Background event server and hook client for tracing Codex sessions to Braintrust.", - "type": "module", - "scripts": { - "build": "tsx scripts/build.ts", - "dev": "tsx --watch src/index.ts serve", - "test": "vitest run", - "test:watch": "vitest", - "typecheck": "tsc --noEmit", - "lint": "biome lint src scripts", - "lint:fix": "biome lint --write src scripts", - "format": "biome format --write src scripts", - "check": "biome check src scripts" - }, - "keywords": [ - "braintrust", - "tracing", - "observability", - "codex", - "hooks" - ], - "author": "Braintrust", - "license": "MIT", - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@types/node": "^24.3.0", - "@yao-pkg/pkg": "^6.6.0", - "tsup": "^8.5.1", - "tsx": "^4.21.0", - "typescript": "^5.7.2", - "vitest": "^4.1.5" - }, - "dependencies": { - "braintrust": "^3.17.0" - } -} diff --git a/src/plugins/codex/content/plugins/trace-codex/pnpm-lock.yaml b/src/plugins/codex/content/plugins/trace-codex/pnpm-lock.yaml deleted file mode 100644 index f195faa..0000000 --- a/src/plugins/codex/content/plugins/trace-codex/pnpm-lock.yaml +++ /dev/null @@ -1,4059 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - braintrust: - specifier: ^3.17.0 - version: 3.23.1(zod@4.4.3) - devDependencies: - '@biomejs/biome': - specifier: ^2.3.11 - version: 2.5.4 - '@types/node': - specifier: ^24.3.0 - version: 24.13.3 - '@yao-pkg/pkg': - specifier: ^6.6.0 - version: 6.21.0 - tsup: - specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.20)(tsx@4.23.1)(typescript@5.9.3) - tsx: - specifier: ^4.21.0 - version: 4.23.1 - typescript: - specifier: ^5.7.2 - version: 5.9.3 - vitest: - specifier: ^4.1.5 - version: 4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(tsx@4.23.1)) - -packages: - - '@babel/code-frame@7.29.7': - resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.29.7': - resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.29.7': - resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.29.7': - resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/template@7.29.7': - resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} - engines: {node: '>=6.9.0'} - - '@biomejs/biome@2.5.4': - resolution: {integrity: sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.5.4': - resolution: {integrity: sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.5.4': - resolution: {integrity: sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.5.4': - resolution: {integrity: sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-arm64@2.5.4': - resolution: {integrity: sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-linux-x64-musl@2.5.4': - resolution: {integrity: sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-x64@2.5.4': - resolution: {integrity: sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-win32-arm64@2.5.4': - resolution: {integrity: sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.5.4': - resolution: {integrity: sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - - '@braintrust/bt-darwin-arm64@0.12.0': - resolution: {integrity: sha512-mY6VW/3VwcOQOGN8sYHS6F0xzHTFwgZcNlj7zlQttI6OXOCGt/bhonGIqd03QBhxmj0M31ymSS7TqSeCK/RYIQ==} - cpu: [arm64] - os: [darwin] - - '@braintrust/bt-darwin-x64@0.12.0': - resolution: {integrity: sha512-woyRyDv2DfCF8+von+3X9f1cddAbFnKLSfCbEkrBQVc0ZsAM60as8nehYv7DkafJF/67yKFx/sUraV65sukesQ==} - cpu: [x64] - os: [darwin] - - '@braintrust/bt-linux-arm64@0.12.0': - resolution: {integrity: sha512-J9/7f3EIMKmFmSSQHQnAQLpUgB8YJENrOlkABjlTprBgsIYVgz7tSH7yg2P3ur+2Jem7PpGnrDxHGh/UjRfjsg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@braintrust/bt-linux-x64-musl@0.12.0': - resolution: {integrity: sha512-KnLgENOoztBXcH+mLFJe4bYzi67kSOuELOQrm4Hler35GjgaBMI1fFLIUjKRPAmyT9VIhBMhy1ICfGEFLueMEQ==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@braintrust/bt-linux-x64@0.12.0': - resolution: {integrity: sha512-HJFbUl3HYYY1Ivw8OYBeIMcIsU9r7+fC9BzLWB5FdH7881ToKee2wbbLZssMCxFbMVeUxzEo+SzGD/1Z9Gk9CA==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@braintrust/bt-win32-arm64@0.12.0': - resolution: {integrity: sha512-+7PkdBAmiqEJsWteGHP/Zs62F75PkHPA8m/ej4TOz/mHGKulUU0bZibw91KRqIB7J7jGi5ItvCiqkiGaLKLnyw==} - cpu: [arm64] - os: [win32] - - '@braintrust/bt-win32-x64@0.12.0': - resolution: {integrity: sha512-Q0c+pVUGm82n/7N8QJ5s6j/G/Q0GFzqOaTipHjkmElLbAOOEcfRH/uljdtIPNBiEQcrzqirS0GmOgiFkeQ3Txg==} - cpu: [x64] - os: [win32] - - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@kwsites/file-exists@1.1.1': - resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} - - '@kwsites/promise-deferred@1.1.1': - resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} - - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@next/env@14.2.35': - resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - - '@roberts_lando/vfs@0.3.3': - resolution: {integrity: sha512-YjkxVSLw5WMZQoARaryRAjcxA+GbBzWMJdwYZX5oLUt9cC/gew9as4Dn7tcLzPp7BPoR221VpTZ+78TRPawnjg==} - engines: {node: '>= 22'} - - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@rollup/rollup-android-arm-eabi@4.62.2': - resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.62.2': - resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.62.2': - resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.62.2': - resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.62.2': - resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.62.2': - resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.62.2': - resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.62.2': - resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.62.2': - resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.62.2': - resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.62.2': - resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.62.2': - resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.62.2': - resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.62.2': - resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} - cpu: [x64] - os: [win32] - - '@simple-git/args-pathspec@1.0.3': - resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==} - - '@simple-git/argv-parser@1.1.1': - resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/node@24.13.3': - resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} - - '@vercel/functions@1.6.0': - resolution: {integrity: sha512-R6FKQrYT5MZs5IE1SqeCJWxMuBdHawFcCZboKKw8p7s+6/mcd55Gx6tWmyKnQTyrSEA04NH73Tc9CbqpEle8RA==} - engines: {node: '>= 16'} - peerDependencies: - '@aws-sdk/credential-provider-web-identity': '*' - peerDependenciesMeta: - '@aws-sdk/credential-provider-web-identity': - optional: true - - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - - '@yao-pkg/pkg-fetch@3.6.4': - resolution: {integrity: sha512-2JXvS9HbMudLlzEjSaJ7bLNnF/WlTv7iaTAJp2Tk8pBsgwCyL9p4rIN8cztHlyZF7qVzr1EaBjd7DQooFk0azQ==} - hasBin: true - - '@yao-pkg/pkg@6.21.0': - resolution: {integrity: sha512-dZl2C7rdwwEI4tv7WW+Cvnl+2K8OqHKUXfNQRq3mZCTC4degoFx1jA4d5wOn9d8lHrUy58xuEt1eJD0pTddW5w==} - engines: {node: '>=22.0.0'} - hasBin: true - - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - - acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - astring@1.9.0: - resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} - hasBin: true - - b4a@1.8.1: - resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} - peerDependencies: - react-native-b4a: '*' - peerDependenciesMeta: - react-native-b4a: - optional: true - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - bare-events@2.9.1: - resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} - peerDependencies: - bare-abort-controller: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - - bare-fs@4.7.4: - resolution: {integrity: sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==} - engines: {bare: '>=1.16.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-path@3.1.1: - resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} - - bare-stream@2.13.3: - resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} - peerDependencies: - bare-abort-controller: '*' - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - bare-buffer: - optional: true - bare-events: - optional: true - - bare-url@2.4.5: - resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} - engines: {node: '>=18'} - - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} - - braintrust@3.23.1: - resolution: {integrity: sha512-HeTZodgOyN1fwhCayFl8xGE+z98RjTI86P29g4Q/U0SL/787kpM4PTVWqRB1rZ8c0Jw3OalvDfaOL37gPSaXQw==} - hasBin: true - peerDependencies: - zod: ^3.25.34 || ^4.0 - - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - - cjs-module-lexer@2.2.0: - resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - - cli-progress@3.12.0: - resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} - engines: {node: '>=4'} - - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - - dc-browser@1.0.4: - resolution: {integrity: sha512-7oEtnzNlcE+hr4OvO3GR6Gndgw8BhW+wKOEwMqSleyY7N29jbAxzyW5BaJl7qBCw+6OIxfMWtY0T+6dxq8RWLw==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - duplexer2@0.1.4: - resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - - es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - events-universal@1.0.1: - resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} - - eventsource-parser@1.1.2: - resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} - engines: {node: '>=14.18'} - - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - - expect-type@1.4.0: - resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} - engines: {node: '>=12.0.0'} - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - - fast-uri@3.1.4: - resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - - fs-extra@11.3.1: - resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} - engines: {node: '>=14.14'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - iconv-lite@0.7.3: - resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} - engines: {node: '>=0.10.0'} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - into-stream@9.1.0: - resolution: {integrity: sha512-DRsRnQrbzdFjaQ1oe4C6/EIUymIOEix1qROEJTF9dbMq+M4Zrm6VaLp6SD/B9IsiEjPZuBSnWWFN+udajugdWA==} - engines: {node: '>=20'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - jsonfile@6.2.1: - resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - - meriyah@6.1.4: - resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} - engines: {node: '>=18.0.0'} - - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - - minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} - - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - - module-details-from-path@1.0.4: - resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - multistream@4.1.0: - resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} - - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - node-abi@3.94.0: - resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} - engines: {node: '>=10'} - - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - - postcss@8.5.20: - resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} - engines: {node: ^10 || ^12 || >=14} - - postject@1.0.0-alpha.6: - resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} - engines: {node: '>=14.0.0'} - hasBin: true - - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. - hasBin: true - - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} - engines: {node: '>=0.6'} - - range-parser@1.3.0: - resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} - hasBin: true - - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rollup@4.62.2: - resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - semifies@1.0.0: - resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} - - semver@7.8.5: - resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} - engines: {node: '>=10'} - hasBin: true - - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.1: - resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} - engines: {node: '>= 0.4'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - - simple-git@3.36.0: - resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - - stream-meter@1.0.4: - resolution: {integrity: sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==} - - streamx@2.28.0: - resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - tar-fs@2.1.5: - resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} - - tar-fs@3.1.3: - resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - - tar-stream@3.2.0: - resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} - - tar@7.5.20: - resolution: {integrity: sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==} - engines: {node: '>=18'} - - teex@1.0.1: - resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - - termi-link@1.1.0: - resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} - engines: {node: '>=12'} - - text-decoder@1.2.7: - resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} - engines: {node: '>=18'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} - engines: {node: '>=14.0.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - - tsx@4.23.1: - resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} - engines: {node: '>=18.0.0'} - hasBin: true - - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} - engines: {node: '>=20.18.1'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - unplugin@2.3.11: - resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} - engines: {node: '>=18.12.0'} - - unzipper@0.12.5: - resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} - hasBin: true - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - - yargs@16.2.2: - resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} - engines: {node: '>=10'} - - zod-to-json-schema@3.25.2: - resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} - peerDependencies: - zod: ^3.25.28 || ^4 - - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - -snapshots: - - '@babel/code-frame@7.29.7': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/generator@7.29.7': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-globals@7.29.7': {} - - '@babel/helper-string-parser@7.29.7': {} - - '@babel/helper-validator-identifier@7.29.7': {} - - '@babel/parser@7.29.7': - dependencies: - '@babel/types': 7.29.7 - - '@babel/template@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/traverse@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.7': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - - '@biomejs/biome@2.5.4': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.4 - '@biomejs/cli-darwin-x64': 2.5.4 - '@biomejs/cli-linux-arm64': 2.5.4 - '@biomejs/cli-linux-arm64-musl': 2.5.4 - '@biomejs/cli-linux-x64': 2.5.4 - '@biomejs/cli-linux-x64-musl': 2.5.4 - '@biomejs/cli-win32-arm64': 2.5.4 - '@biomejs/cli-win32-x64': 2.5.4 - - '@biomejs/cli-darwin-arm64@2.5.4': - optional: true - - '@biomejs/cli-darwin-x64@2.5.4': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.5.4': - optional: true - - '@biomejs/cli-linux-arm64@2.5.4': - optional: true - - '@biomejs/cli-linux-x64-musl@2.5.4': - optional: true - - '@biomejs/cli-linux-x64@2.5.4': - optional: true - - '@biomejs/cli-win32-arm64@2.5.4': - optional: true - - '@biomejs/cli-win32-x64@2.5.4': - optional: true - - '@braintrust/bt-darwin-arm64@0.12.0': - optional: true - - '@braintrust/bt-darwin-x64@0.12.0': - optional: true - - '@braintrust/bt-linux-arm64@0.12.0': - optional: true - - '@braintrust/bt-linux-x64-musl@0.12.0': - optional: true - - '@braintrust/bt-linux-x64@0.12.0': - optional: true - - '@braintrust/bt-win32-arm64@0.12.0': - optional: true - - '@braintrust/bt-win32-x64@0.12.0': - optional: true - - '@colors/colors@1.5.0': - optional: true - - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/aix-ppc64@0.28.0': - optional: true - - '@esbuild/aix-ppc64@0.28.1': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.28.0': - optional: true - - '@esbuild/android-arm64@0.28.1': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - - '@esbuild/android-arm@0.28.0': - optional: true - - '@esbuild/android-arm@0.28.1': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - - '@esbuild/android-x64@0.28.0': - optional: true - - '@esbuild/android-x64@0.28.1': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - - '@esbuild/darwin-arm64@0.28.0': - optional: true - - '@esbuild/darwin-arm64@0.28.1': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - - '@esbuild/darwin-x64@0.28.0': - optional: true - - '@esbuild/darwin-x64@0.28.1': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - - '@esbuild/freebsd-arm64@0.28.0': - optional: true - - '@esbuild/freebsd-arm64@0.28.1': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - - '@esbuild/freebsd-x64@0.28.0': - optional: true - - '@esbuild/freebsd-x64@0.28.1': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - - '@esbuild/linux-arm64@0.28.0': - optional: true - - '@esbuild/linux-arm64@0.28.1': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - - '@esbuild/linux-arm@0.28.0': - optional: true - - '@esbuild/linux-arm@0.28.1': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - - '@esbuild/linux-ia32@0.28.0': - optional: true - - '@esbuild/linux-ia32@0.28.1': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - - '@esbuild/linux-loong64@0.28.0': - optional: true - - '@esbuild/linux-loong64@0.28.1': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - - '@esbuild/linux-mips64el@0.28.0': - optional: true - - '@esbuild/linux-mips64el@0.28.1': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - - '@esbuild/linux-ppc64@0.28.0': - optional: true - - '@esbuild/linux-ppc64@0.28.1': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - - '@esbuild/linux-riscv64@0.28.0': - optional: true - - '@esbuild/linux-riscv64@0.28.1': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - - '@esbuild/linux-s390x@0.28.0': - optional: true - - '@esbuild/linux-s390x@0.28.1': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - - '@esbuild/linux-x64@0.28.0': - optional: true - - '@esbuild/linux-x64@0.28.1': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - - '@esbuild/netbsd-arm64@0.28.0': - optional: true - - '@esbuild/netbsd-arm64@0.28.1': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - - '@esbuild/netbsd-x64@0.28.0': - optional: true - - '@esbuild/netbsd-x64@0.28.1': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - - '@esbuild/openbsd-arm64@0.28.0': - optional: true - - '@esbuild/openbsd-arm64@0.28.1': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - - '@esbuild/openbsd-x64@0.28.0': - optional: true - - '@esbuild/openbsd-x64@0.28.1': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - - '@esbuild/openharmony-arm64@0.28.0': - optional: true - - '@esbuild/openharmony-arm64@0.28.1': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - - '@esbuild/sunos-x64@0.28.0': - optional: true - - '@esbuild/sunos-x64@0.28.1': - optional: true - - '@esbuild/win32-arm64@0.27.7': - optional: true - - '@esbuild/win32-arm64@0.28.0': - optional: true - - '@esbuild/win32-arm64@0.28.1': - optional: true - - '@esbuild/win32-ia32@0.27.7': - optional: true - - '@esbuild/win32-ia32@0.28.0': - optional: true - - '@esbuild/win32-ia32@0.28.1': - optional: true - - '@esbuild/win32-x64@0.27.7': - optional: true - - '@esbuild/win32-x64@0.28.0': - optional: true - - '@esbuild/win32-x64@0.28.1': - optional: true - - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.3 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@kwsites/file-exists@1.1.1': - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@kwsites/promise-deferred@1.1.1': {} - - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@next/env@14.2.35': {} - - '@oxc-project/types@0.139.0': {} - - '@roberts_lando/vfs@0.3.3': {} - - '@rolldown/binding-android-arm64@1.1.5': - optional: true - - '@rolldown/binding-darwin-arm64@1.1.5': - optional: true - - '@rolldown/binding-darwin-x64@1.1.5': - optional: true - - '@rolldown/binding-freebsd-x64@1.1.5': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.1.5': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-x64-musl@1.1.5': - optional: true - - '@rolldown/binding-openharmony-arm64@1.1.5': - optional: true - - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.1.5': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@rollup/rollup-android-arm-eabi@4.62.2': - optional: true - - '@rollup/rollup-android-arm64@4.62.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.62.2': - optional: true - - '@rollup/rollup-darwin-x64@4.62.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.62.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.62.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.62.2': - optional: true - - '@rollup/rollup-openbsd-x64@4.62.2': - optional: true - - '@rollup/rollup-openharmony-arm64@4.62.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.62.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.62.2': - optional: true - - '@simple-git/args-pathspec@1.0.3': {} - - '@simple-git/argv-parser@1.1.1': - dependencies: - '@simple-git/args-pathspec': 1.0.3 - - '@standard-schema/spec@1.1.0': {} - - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.9': {} - - '@types/node@24.13.3': - dependencies: - undici-types: 7.18.2 - - '@vercel/functions@1.6.0': {} - - '@vitest/expect@4.1.10': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - chai: 6.2.2 - tinyrainbow: 3.1.0 - - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(tsx@4.23.1))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(tsx@4.23.1) - - '@vitest/pretty-format@4.1.10': - dependencies: - tinyrainbow: 3.1.0 - - '@vitest/runner@4.1.10': - dependencies: - '@vitest/utils': 4.1.10 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.10': {} - - '@vitest/utils@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - - '@yao-pkg/pkg-fetch@3.6.4': - dependencies: - picocolors: 1.1.1 - progress: 2.0.3 - semver: 7.8.5 - tar-fs: 3.1.3 - undici: 7.28.0 - yargs: 16.2.2 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - '@yao-pkg/pkg@6.21.0': - dependencies: - '@babel/generator': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - '@roberts_lando/vfs': 0.3.3 - '@yao-pkg/pkg-fetch': 3.6.4 - esbuild: 0.28.1 - into-stream: 9.1.0 - multistream: 4.1.0 - picocolors: 1.1.1 - picomatch: 4.0.5 - postject: 1.0.0-alpha.6 - prebuild-install: 7.1.3 - resolve: 1.22.12 - resolve.exports: 2.0.3 - stream-meter: 1.0.4 - tar: 7.5.20 - tinyglobby: 0.2.17 - unzipper: 0.12.5 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color - - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - - acorn-import-attributes@1.9.5(acorn@8.17.0): - dependencies: - acorn: 8.17.0 - - acorn@8.17.0: {} - - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.4 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - any-promise@1.3.0: {} - - argparse@2.0.1: {} - - assertion-error@2.0.1: {} - - astring@1.9.0: {} - - b4a@1.8.1: {} - - balanced-match@4.0.4: {} - - bare-events@2.9.1: {} - - bare-fs@4.7.4: - dependencies: - bare-events: 2.9.1 - bare-path: 3.1.1 - bare-stream: 2.13.3(bare-events@2.9.1) - bare-url: 2.4.5 - fast-fifo: 1.3.2 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - bare-path@3.1.1: {} - - bare-stream@2.13.3(bare-events@2.9.1): - dependencies: - b4a: 1.8.1 - streamx: 2.28.0 - teex: 1.0.1 - optionalDependencies: - bare-events: 2.9.1 - transitivePeerDependencies: - - react-native-b4a - - bare-url@2.4.5: - dependencies: - bare-path: 3.1.1 - - base64-js@1.5.1: {} - - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - - bluebird@3.7.2: {} - - body-parser@2.3.0: - dependencies: - bytes: 3.1.2 - content-type: 2.0.0 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.3 - on-finished: 2.4.1 - qs: 6.15.3 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - - brace-expansion@5.0.7: - dependencies: - balanced-match: 4.0.4 - - braintrust@3.23.1(zod@4.4.3): - dependencies: - '@next/env': 14.2.35 - '@vercel/functions': 1.6.0 - acorn: 8.17.0 - acorn-import-attributes: 1.9.5(acorn@8.17.0) - ajv: 8.20.0 - argparse: 2.0.1 - astring: 1.9.0 - cjs-module-lexer: 2.2.0 - cli-progress: 3.12.0 - cli-table3: 0.6.5 - cors: 2.8.6 - dc-browser: 1.0.4 - dotenv: 16.6.1 - esbuild: 0.28.0 - esquery: 1.7.0 - eventsource-parser: 1.1.2 - express: 5.2.1 - http-errors: 2.0.1 - meriyah: 6.1.4 - minimatch: 10.2.5 - module-details-from-path: 1.0.4 - mustache: 4.2.0 - pluralize: 8.0.0 - semifies: 1.0.0 - simple-git: 3.36.0 - source-map: 0.7.6 - termi-link: 1.1.0 - unplugin: 2.3.11 - uuid: 11.1.1 - zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) - optionalDependencies: - '@braintrust/bt-darwin-arm64': 0.12.0 - '@braintrust/bt-darwin-x64': 0.12.0 - '@braintrust/bt-linux-arm64': 0.12.0 - '@braintrust/bt-linux-x64': 0.12.0 - '@braintrust/bt-linux-x64-musl': 0.12.0 - '@braintrust/bt-win32-arm64': 0.12.0 - '@braintrust/bt-win32-x64': 0.12.0 - transitivePeerDependencies: - - '@aws-sdk/credential-provider-web-identity' - - supports-color - - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - bundle-require@5.1.0(esbuild@0.27.7): - dependencies: - esbuild: 0.27.7 - load-tsconfig: 0.2.5 - - bytes@3.1.2: {} - - cac@6.7.14: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - chai@6.2.2: {} - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - chownr@1.1.4: {} - - chownr@3.0.0: {} - - cjs-module-lexer@2.2.0: {} - - cli-progress@3.12.0: - dependencies: - string-width: 4.2.3 - - cli-table3@0.6.5: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - - cliui@7.0.4: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - commander@4.1.1: {} - - commander@9.5.0: {} - - confbox@0.1.8: {} - - consola@3.4.2: {} - - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - - content-type@2.0.0: {} - - convert-source-map@2.0.0: {} - - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - core-util-is@1.0.3: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - - dc-browser@1.0.4: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - deep-extend@0.6.0: {} - - depd@2.0.0: {} - - detect-libc@2.1.2: {} - - dotenv@16.6.1: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - duplexer2@0.1.4: - dependencies: - readable-stream: 2.3.8 - - ee-first@1.1.1: {} - - emoji-regex@8.0.0: {} - - encodeurl@2.0.0: {} - - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-module-lexer@2.3.1: {} - - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - - esbuild@0.28.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 - - esbuild@0.28.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 - - escalade@3.2.0: {} - - escape-html@1.0.3: {} - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - etag@1.8.1: {} - - events-universal@1.0.1: - dependencies: - bare-events: 2.9.1 - transitivePeerDependencies: - - bare-abort-controller - - eventsource-parser@1.1.2: {} - - expand-template@2.0.3: {} - - expect-type@1.4.0: {} - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.3.0 - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.3 - range-parser: 1.3.0 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - fast-deep-equal@3.1.3: {} - - fast-fifo@1.3.2: {} - - fast-uri@3.1.4: {} - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.2 - rollup: 4.62.2 - - forwarded@0.2.0: {} - - fresh@2.0.0: {} - - fs-constants@1.0.0: {} - - fs-extra@11.3.1: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 2.0.1 - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - get-caller-file@2.0.5: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 - - github-from-package@0.0.0: {} - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - has-symbols@1.1.0: {} - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - iconv-lite@0.7.3: - dependencies: - safer-buffer: 2.1.2 - - ieee754@1.2.1: {} - - inherits@2.0.4: {} - - ini@1.3.8: {} - - into-stream@9.1.0: {} - - ipaddr.js@1.9.1: {} - - is-core-module@2.16.2: - dependencies: - hasown: 2.0.4 - - is-fullwidth-code-point@3.0.0: {} - - is-promise@4.0.0: {} - - isarray@1.0.0: {} - - joycon@3.1.1: {} - - js-tokens@4.0.0: {} - - jsesc@3.1.0: {} - - json-schema-traverse@1.0.0: {} - - jsonfile@6.2.1: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - math-intrinsics@1.1.0: {} - - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - - meriyah@6.1.4: {} - - mime-db@1.54.0: {} - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - - mimic-response@3.1.0: {} - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.7 - - minimist@1.2.8: {} - - minipass@7.1.3: {} - - minizlib@3.1.0: - dependencies: - minipass: 7.1.3 - - mkdirp-classic@0.5.3: {} - - mlly@1.8.2: - dependencies: - acorn: 8.17.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.4 - - module-details-from-path@1.0.4: {} - - ms@2.1.3: {} - - multistream@4.1.0: - dependencies: - once: 1.4.0 - readable-stream: 3.6.2 - - mustache@4.2.0: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.16: {} - - napi-build-utils@2.0.0: {} - - negotiator@1.0.0: {} - - node-abi@3.94.0: - dependencies: - semver: 7.8.5 - - node-int64@0.4.0: {} - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - obug@2.1.4: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - parseurl@1.3.3: {} - - path-parse@1.0.7: {} - - path-to-regexp@8.4.2: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@4.0.5: {} - - pirates@4.0.7: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - pluralize@8.0.0: {} - - postcss-load-config@6.0.1(postcss@8.5.20)(tsx@4.23.1): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - postcss: 8.5.20 - tsx: 4.23.1 - - postcss@8.5.20: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postject@1.0.0-alpha.6: - dependencies: - commander: 9.5.0 - - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.94.0 - pump: 3.0.4 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.5 - tunnel-agent: 0.6.0 - - process-nextick-args@2.0.1: {} - - progress@2.0.3: {} - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - - qs@6.15.3: - dependencies: - es-define-property: 1.0.1 - side-channel: 1.1.1 - - range-parser@1.3.0: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.3 - unpipe: 1.0.0 - - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readdirp@4.1.2: {} - - require-directory@2.1.1: {} - - require-from-string@2.0.2: {} - - resolve-from@5.0.0: {} - - resolve.exports@2.0.3: {} - - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - rolldown@1.1.5: - dependencies: - '@oxc-project/types': 0.139.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 - - rollup@4.62.2: - dependencies: - '@types/estree': 1.0.9 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.62.2 - '@rollup/rollup-android-arm64': 4.62.2 - '@rollup/rollup-darwin-arm64': 4.62.2 - '@rollup/rollup-darwin-x64': 4.62.2 - '@rollup/rollup-freebsd-arm64': 4.62.2 - '@rollup/rollup-freebsd-x64': 4.62.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 - '@rollup/rollup-linux-arm-musleabihf': 4.62.2 - '@rollup/rollup-linux-arm64-gnu': 4.62.2 - '@rollup/rollup-linux-arm64-musl': 4.62.2 - '@rollup/rollup-linux-loong64-gnu': 4.62.2 - '@rollup/rollup-linux-loong64-musl': 4.62.2 - '@rollup/rollup-linux-ppc64-gnu': 4.62.2 - '@rollup/rollup-linux-ppc64-musl': 4.62.2 - '@rollup/rollup-linux-riscv64-gnu': 4.62.2 - '@rollup/rollup-linux-riscv64-musl': 4.62.2 - '@rollup/rollup-linux-s390x-gnu': 4.62.2 - '@rollup/rollup-linux-x64-gnu': 4.62.2 - '@rollup/rollup-linux-x64-musl': 4.62.2 - '@rollup/rollup-openbsd-x64': 4.62.2 - '@rollup/rollup-openharmony-arm64': 4.62.2 - '@rollup/rollup-win32-arm64-msvc': 4.62.2 - '@rollup/rollup-win32-ia32-msvc': 4.62.2 - '@rollup/rollup-win32-x64-gnu': 4.62.2 - '@rollup/rollup-win32-x64-msvc': 4.62.2 - fsevents: 2.3.3 - - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - - safer-buffer@2.1.2: {} - - semifies@1.0.0: {} - - semver@7.8.5: {} - - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.3.0 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} - - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - siginfo@2.0.0: {} - - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - - simple-git@3.36.0: - dependencies: - '@kwsites/file-exists': 1.1.1 - '@kwsites/promise-deferred': 1.1.1 - '@simple-git/args-pathspec': 1.0.3 - '@simple-git/argv-parser': 1.1.1 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - source-map-js@1.2.1: {} - - source-map@0.7.6: {} - - stackback@0.0.2: {} - - statuses@2.0.2: {} - - std-env@4.2.0: {} - - stream-meter@1.0.4: - dependencies: - readable-stream: 2.3.8 - - streamx@2.28.0: - dependencies: - events-universal: 1.0.1 - fast-fifo: 1.3.2 - text-decoder: 1.2.7 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-json-comments@2.0.1: {} - - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.17 - ts-interface-checker: 0.1.13 - - supports-preserve-symlinks-flag@1.0.0: {} - - tar-fs@2.1.5: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.4 - tar-stream: 2.2.0 - - tar-fs@3.1.3: - dependencies: - pump: 3.0.4 - tar-stream: 3.2.0 - optionalDependencies: - bare-fs: 4.7.4 - bare-path: 3.1.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - - tar-stream@3.2.0: - dependencies: - b4a: 1.8.1 - bare-fs: 4.7.4 - fast-fifo: 1.3.2 - streamx: 2.28.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - tar@7.5.20: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.3 - minizlib: 3.1.0 - yallist: 5.0.0 - - teex@1.0.1: - dependencies: - streamx: 2.28.0 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - termi-link@1.1.0: {} - - text-decoder@1.2.7: - dependencies: - b4a: 1.8.1 - transitivePeerDependencies: - - react-native-b4a - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyexec@1.2.4: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tinyrainbow@3.1.0: {} - - toidentifier@1.0.1: {} - - tree-kill@1.2.2: {} - - ts-interface-checker@0.1.13: {} - - tslib@2.8.1: - optional: true - - tsup@8.5.1(postcss@8.5.20)(tsx@4.23.1)(typescript@5.9.3): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.7) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.7 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.20)(tsx@4.23.1) - resolve-from: 5.0.0 - rollup: 4.62.2 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.17 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.20 - typescript: 5.9.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tsx@4.23.1: - dependencies: - esbuild: 0.28.1 - optionalDependencies: - fsevents: 2.3.3 - - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - - typescript@5.9.3: {} - - ufo@1.6.4: {} - - undici-types@7.18.2: {} - - undici@7.28.0: {} - - universalify@2.0.1: {} - - unpipe@1.0.0: {} - - unplugin@2.3.11: - dependencies: - '@jridgewell/remapping': 2.3.5 - acorn: 8.17.0 - picomatch: 4.0.5 - webpack-virtual-modules: 0.6.2 - - unzipper@0.12.5: - dependencies: - bluebird: 3.7.2 - duplexer2: 0.1.4 - fs-extra: 11.3.1 - graceful-fs: 4.2.11 - node-int64: 0.4.0 - - util-deprecate@1.0.2: {} - - uuid@11.1.1: {} - - vary@1.1.2: {} - - vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(tsx@4.23.1): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.20 - rolldown: 1.1.5 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 24.13.3 - esbuild: 0.27.7 - fsevents: 2.3.3 - tsx: 4.23.1 - - vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(tsx@4.23.1)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(tsx@4.23.1)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(tsx@4.23.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.13.3 - transitivePeerDependencies: - - msw - - webpack-virtual-modules@0.6.2: {} - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrappy@1.0.2: {} - - y18n@5.0.8: {} - - yallist@5.0.0: {} - - yargs-parser@20.2.9: {} - - yargs@16.2.2: - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - - zod-to-json-schema@3.25.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - - zod@4.4.3: {} diff --git a/src/plugins/codex/content/plugins/trace-codex/pnpm-workspace.yaml b/src/plugins/codex/content/plugins/trace-codex/pnpm-workspace.yaml deleted file mode 100644 index ad55a33..0000000 --- a/src/plugins/codex/content/plugins/trace-codex/pnpm-workspace.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# pnpm settings live here (not package.json) as of pnpm 10+. This package is a -# single project, not a multi-package workspace; the file exists only to record -# a decision on dependency install/build scripts. pnpm 11 treats un-acknowledged -# build scripts as a hard error, which otherwise fails `pnpm run